"""Widget for the creation and editing of color palettes."""
# Author(s): Davide.De-Marchi@ec.europa.eu
# Copyright © European Union 2022-2023
#
# Licensed under the EUPL, Version 1.2 or as soon they will be approved by
# the European Commission subsequent versions of the EUPL (the "Licence");
#
# You may not use this work except in compliance with the Licence.
#
# You may obtain a copy of the Licence at:
# https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12
# Unless required by applicable law or agreed to in writing, software
# distributed under the Licence is distributed on an "AS IS"
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
# express or implied.
#
# See the Licence for the specific language governing permissions and
# limitations under the Licence.
import ipyvuetify as v
from ipywidgets import widgets, Layout
from IPython.display import display, HTML
import random
import base64
import json
try:
from . import settings
from . import sortableList
from . import palettePicker
from . import colorPicker
from . import switch
from . import tooltip
from . import dialogGeneric
from . import dialogWait
from . import dialogMessage
from . import selectSingle
from . import upload
except:
import settings
import sortableList
import palettePicker
import colorPicker
import switch
import tooltip
import dialogGeneric
import dialogWait
import dialogMessage
import selectSingle
import upload
# Utility to return items from a list of colors
[docs]def colorlist2Items(colors=[]):
"""
Utility function to convert a list of colors to a list of items to pass as parameter to the :py:class:`paletteEditor.paletteEditor` class
Parameters
----------
colorlist : list of str, optional
List of string representing colors in the format '#RRGGBB' (default is [])
Return
------
A list of items ready to be passed to the :py:class:`paletteEditor.paletteEditor` class constructor
"""
return [{ "value": 0, "class": "", "color": x } for x in colors]
# Utility: 3 integers to '#RRGGBB'
def RGB(r,g,b):
return '#{:02X}{:02X}{:02X}'.format(r, g, b)
# Class paletteEditor
[docs]class paletteEditor():
"""
Widget for the creation and editing of color palettes.
Parameters
----------
title : str, optional
Title of the palette (default is '')
items : list of dicts, optional
List of dicts containing "value", "class" and "color" (default is [])
interpolate : bool, optional
Flag to control the interpolation of the colorlist: if True the palette will be displayed by adding intermediate colors (default is True)
width : int, optional
Width of the widget in pixels (default is 420)
color : str, optional
Color used for the widget (default is the color_first defined in the settings.py module)
dark : bool, optional
Flag to invert the text and backcolor (default is the value of settings.dark_mode)
onchange : function, optional
Python function to call when the user changes the order of colors or removes a color. The function will receive no parameters as input (default is None)
buttonstooltip : bool, optional
If True, the buttons to mode, add, remove colors and assign values to colors will have a tooltip (default is True)
Example
-------
Example of a widget to create and edit a color palette::
from vois.vuetify import paletteEditor
from ipywidgets import widgets
from IPython.display import display
output = widgets.Output()
def onchange():
with output:
print('onchange!')
p = paletteEditor(items=[], width=450, onchange=onchange)
display(p.draw())
display(output)
.. figure:: figures/paletteEditor.png
:scale: 100 %
:alt: card widget
Example of a widget to create and edit a color palette
"""
# Initialization
def __init__(self, title='', items=[], interpolate=True, width=420, maxheightlist=600,
color=settings.color_first, dark=settings.dark_mode,
onchange=None, buttonstooltip=True):
self.width = width
self.color = color
self.dark = dark
self.onchange = onchange
self.buttonstooltip = buttonstooltip
self.outputdialog = widgets.Output(layout=Layout(width='0px', min_width='0px', height='0px'))
self.outputtoolbar = widgets.Output()
self.outputtitle = widgets.Output()
self.outputpalette = widgets.Output()
self.outputlist = widgets.Output()
self.outputpreview = widgets.Output(layout=Layout(width='480px', min_width='480px', height='50px'))
self.tfTitle = v.TextField(v_model=title, label="Title", color=self.color, class_="pa-0 ma-0 mt-3", style_="width: %dpx; max-width: %dpx"%(self.width,self.width))
self.sp = v.Html(tag='div', class_="pa-0 ma-0 mr-3", children=[''])
self.palette_text = ''
self.tf = v.TextField(v_model="Palette", label="Palette file name", autofocus=True,
color=self.color, class_="pa-0 ma-0 ml-4 mr-4")
custompalettes = [
{ "name": "Simple", "colors": ['#000000', '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF', '#FFFFFF']},
{ "name": "Dem", "colors": [RGB(255,255,170), RGB( 39,168, 39), RGB( 11,128, 64), RGB(255,255, 0), RGB(255,186, 3),
RGB(158, 30, 2), RGB(110, 40, 10), RGB(138, 94, 66), RGB(255,255,255)]},
{ "name": "NDVI", "colors": [RGB(120,69,25), RGB(255,178,74), RGB(255,237,166), RGB(173,232,94),
RGB(135,181,64), RGB(3,156,0), RGB(1,100,0), RGB(1,80,0)]}
]
families = ['carto', 'cmocean', 'cyclical', 'diverging', 'plotlyjs', 'qualitative', 'sequential', 'custom']
family = 'sequential'
self.selfam = selectSingle.selectSingle('Family:', families, selection=family, width=160, onchange=self.onchangeFamily, marginy=1, clearable=False)
self.pp = palettePicker.palettePicker(family=family, custompalettes=custompalettes, label='Palette:', height=26)
self.upload_file = upload.upload(accept="application/json", multiple=False, show_progress=False, onchange=self.on_upload_palette,
label='Palette file:', placeholder='Click to select the palette to upload', width='480px', margins="pa-0 ma-0 ml-4")
# Top buttons
self.new = v.Btn(icon=True, class_="mr-0", dark=self.dark, children=[v.Icon(children=['mdi-file-outline'])])
self.new.on_event('click', self.onnew)
self.upload = v.Btn(icon=True, class_="mr-0", dark=self.dark, children=[v.Icon(children=['mdi-folder-open-outline'])])
self.upload.on_event('click', self.onupload)
self.download = v.Btn(icon=True, class_="mr-0", dark=self.dark, children=[v.Icon(children=['mdi-content-save'])])
self.download.on_event('click', self.ondownload)
self.select = v.Btn(icon=True, class_="mr-0", dark=self.dark, children=[v.Icon(children=['mdi-folder-search-outline'])])
self.select.on_event('click', self.onselect)
self.revert = v.Btn(icon=True, class_="mr-0", dark=self.dark, children=[v.Icon(children=['mdi-autorenew'])])
self.revert.on_event('click', self.onrevert)
self.random = v.Btn(icon=True, class_="mr-0", dark=self.dark, children=[v.Icon(children=['mdi-palette'])])
self.random.on_event('click', self.onrandom)
with self.outputtoolbar:
display(v.Row(no_gutters=True, style_="min-width: 300px;", justify="start", class_="pa-0 ma-0",
children=[tooltip.tooltip("New palette",self.new),
tooltip.tooltip("Load a palette from local disk",self.upload),
tooltip.tooltip("Save current palette to local disk",self.download),
tooltip.tooltip("Select one of the pre-defined palettes",self.select),
tooltip.tooltip("Revert palette order",self.revert),
tooltip.tooltip("Assign random colors",self.random)]))
with self.outputtitle:
display(self.tfTitle)
# Bottom buttons
self.zero = v.Btn(icon=True, class_="mr-0", dark=self.dark, children=[v.Icon(children=['mdi-numeric-0'])])
self.zero.on_event('click', self.onzero)
self.asc = v.Btn(icon=True, class_="mr-0", dark=self.dark, children=[v.Icon(children=['mdi-numeric-1'])])
self.asc.on_event('click', self.onasc)
self.desc = v.Btn(icon=True, class_="mr-0", dark=self.dark, children=[v.Icon(children=['mdi-numeric-negative-1'])])
self.desc.on_event('click', self.ondesc)
self.sw = switch.switch(interpolate, "Interpolate", color=self.color, onchange=self.__internal_onchange)
self.s = sortableList.sortableList(items=items, width=self.width, maxheightlist=maxheightlist, outlined=False, dark=self.dark,
allowNew=True, itemNew=self.itemNew, itemContent=self.itemContent,
bottomContent=[#self.sp,self.sp,
tooltip.tooltip("Set all values to 0",self.zero),
tooltip.tooltip("Set increasing values starting from first color",self.asc),
tooltip.tooltip("Set decreasing values starting from last color", self.desc),
tooltip.tooltip("Interpolate colors",self.sw.draw())],
onchange=self.__internal_onchange,
buttonstooltip=self.buttonstooltip,
tooltipadd='Add new color', tooltipdown='Move color down',
tooltipup='Move color up', tooltipremove='Remove color')
self.updatePalette()
with self.outputlist:
display(self.s.draw())
# Change family in the palette picker
def onchangeFamily(self):
family = self.selfam.value
if family == 'carto' or family == 'qualitative':
interpolate = False
else:
interpolate = True
self.pp.updatePalettes(family,interpolate)
# New palette
def onnew(self, widget, event, data):
self.s.items = []
self.sw.value = True
self.tfTitle.v_model = ''
# Select
def onselect(self, widget, event, data):
def on_ok():
dlg = dialogWait.dialogWait(text='Loading palette...', output=self.outputdialog)
self.s.items = colorlist2Items(self.pp.colors)
self.sw.value = self.pp.interpolate
dlg.close()
content1 = v.Row(no_gutters=True, justify="start", class_="pa-0 ma-0 ml-6", children=[self.selfam.draw()])
content2 = v.Row(no_gutters=True, justify="start", class_="pa-0 ma-0 ml-6", children=[self.pp.draw()])
dlg = dialogGeneric.dialogGeneric(title='Palette selection', text='Please select one of the predefined palettes:',
show=True, addclosebuttons=False, width=600,
addokcancelbuttons=True, on_ok=on_ok,
fullscreen=False, content=[widgets.VBox([content1, content2])], output=self.outputdialog)
# Called when a .json file is selected for upload
def on_upload_palette(self, files):
self.outputpreview.clear_output(wait=False)
if len(files) > 0:
f = files[0]
self.palette_text = f['file_obj'].read().decode("utf-8")
try:
j = json.loads(self.palette_text)
if ("format" in j) and ("interpolate" in j) and ("items" in j) and (j["format"] == "BDAP palette 1.0"):
items = j["items"]
interpolate = j["interpolate"]
colors = [x["color"] for x in items]
with self.outputpreview:
display(v.Card(outlined=True, dark=self.dark, class_="pa-0 ma-0", style_='width: 400px; max-width: 400px; height: 40px; max-height: 40px;' ,
children=[v.Img(class_="pa-0 ma-1", src=palettePicker.image2Base64(palettePicker.paletteImage(colors, width=360, height=29, interpolate=interpolate)))]))
else:
self.upload_file.clear()
e = dialogMessage.dialogMessage(title='Error',
text='The uploaded file is not in the \"BDAP palette 1.0\" format',
addclosebuttons=False, show=True, width=400, output=self.outputdialog)
except:
self.upload_file.clear()
e = dialogMessage.dialogMessage(title='Error',
text='Cannot read the uploaded file!',
addclosebuttons=False, show=True, width=400, output=self.outputdialog)
else:
self.palette_text = ''
# Called when the file upload dialog is closed with the "OK" button
def on_upload_ok(self):
if len(self.palette_text) > 2:
dlg = dialogWait.dialogWait(text='Loading palette...', output=self.outputdialog)
try:
j = json.loads(self.palette_text)
if ("format" in j) and ("interpolate" in j) and ("items" in j) and (j["format"] == "BDAP palette 1.0"):
self.s.items, self.sw.value = j["items"], j["interpolate"]
if "title" in j: self.tfTitle.v_model = j["title"]
else: self.tfTitle.v_model = ''
else:
e = dialogMessage.dialogMessage(title='Error',
text='The uploaded file is not in the \"BDAP palette 1.0\" format',
addclosebuttons=False, show=True, width=400, output=self.outputdialog)
except:
e = dialogMessage.dialogMessage(title='Error',
text='Cannot read the uploaded file!',
addclosebuttons=False, show=True, width=400, output=self.outputdialog)
dlg.close()
self.palette_text = ''
self.upload_file.clear()
# Called when the file upload dialog is closed with the "cancel" button
def on_upload_cancel(self):
self.palette_text = ''
self.upload_file.clear()
# Upload
def onupload(self, widget, event, data):
self.outputpreview.clear_output()
dlg = dialogGeneric.dialogGeneric(title='Load a palette from local disk', text='', show=True,
addclosebuttons=False, width=520,
addokcancelbuttons=True, on_ok=self.on_upload_ok, on_cancel=self.on_upload_cancel,
fullscreen=False, content=[self.upload_file.draw(), self.outputpreview], output=self.outputdialog)
# Direct download of a .txt file containing a string
def downloadText(self, textobj, fileName="palette.json"):
string_bytes = textobj.encode("ascii","ignore")
base64_bytes = base64.b64encode(string_bytes)
base64_string = base64_bytes.decode("ascii")
self.outputdialog.clear_output()
with self.outputdialog:
display(HTML('<script>function downloadURI(uri, name) { var link = document.createElement("a"); link.download = name; link.href = uri; link.click();} downloadURI("data:application/octet-stream;charset=utf-8;base64,' + base64_string + '","' + fileName + '"); </script>'))
# Download
def ondownload(self, widget, event, data):
def on_ok():
items = self.s.items
j = { "format": "BDAP palette 1.0",
"interpolate": self.sw.value,
"items" : items,
"title" : self.tfTitle.v_model
}
txt = json.dumps(j)
filename = self.tf.v_model
if filename[-5:] != ".json": filename += ".json"
self.downloadText(txt, fileName=filename)
dlg = dialogGeneric.dialogGeneric(title='Save current palette to local disk', text='', show=True,
addclosebuttons=False, width=500,
addokcancelbuttons=True, on_ok=on_ok,
fullscreen=False, content=[self.tf], output=self.outputdialog)
# Revert palette order
def onrevert(self, widget, event, data):
dlg = dialogWait.dialogWait(text='Updating palette...', output=self.outputdialog)
self.s.items = list(reversed(self.s.items))
dlg.close()
# Assign random colors
def onrandom(self, widget, event, data):
dlg = dialogWait.dialogWait(text='Updating palette...', output=self.outputdialog)
items = self.s.items
for item in items:
item['color'] = "#" + ''.join([random.choice('0123456789ABCDEF') for j in range(6)])
self.s.items = items
dlg.close()
# Set all values to zero
def onzero(self, widget, event, data):
dlg = dialogWait.dialogWait(text='Updating palette...', output=self.outputdialog)
items = self.s.items
for item in items:
item['value'] = 0
self.s.items = items
dlg.close()
# Set all increasing values starting from first color
def onasc(self, widget, event, data):
dlg = dialogWait.dialogWait(text='Updating palette...', output=self.outputdialog)
items = self.s.items
if len(items) > 0:
start = items[0]['value']
for index, item in enumerate(items):
item['value'] = start + index
self.s.items = items
dlg.close()
# Set all decreasing values starting from last color
def ondesc(self, widget, event, data):
dlg = dialogWait.dialogWait(text='Updating palette...', output=self.outputdialog)
items = self.s.items
if len(items) > 0:
start = items[-1]['value']
for index, item in enumerate(items):
item['value'] = start - len(items) + 1 + index
self.s.items = items
dlg.close()
# Update the palette image
def updatePalette(self):
colors = []
for item in self.s.items:
colors.append(item['color'])
self.outputpalette.clear_output(wait=True)
with self.outputpalette:
display(v.Card(outlined=True, dark=self.dark, style_='width: %dpx; max-width: %dpx; height: 40px; max-height: 40px;' %(self.width,self.width),
children=[v.Img(class_="pa-0 ma-1", src=palettePicker.image2Base64(palettePicker.paletteImage(colors, width=self.width-20,
height=29, interpolate=self.sw.value)))]))
# Creation of a new item
def itemNew(self):
return { "value": 0, "class": "", "color": "#FF0000" }
# Content of an item
def itemContent(self, item, index):
def onvalue(widget, event, data):
item["value"] = int(data)
def onclass(widget, event, data):
item["class"] = data
def oncolor():
item["color"] = cp.color
self.updatePalette()
netw = self.width - 140
tfvalue = v.TextField(label='Value:', value=item['value'], color=self.color, type="number", dense=True, style_="max-width: %dpx"%(int(0.25*netw)), class_="pa-0 ma-0 mt-2")
tfvalue.on_event('input', onvalue)
tfclass = v.TextField(label='Class:', value=item['class'], color=self.color, dense=True, style_="max-width: %dpx"%(int(0.74*netw)), class_="pa-0 ma-0 mt-2")
tfclass.on_event('input', onclass)
cp = colorPicker.colorPicker(color=item['color'], dark=self.dark, width=30, show_swatches=False, onchange=oncolor)
sp = v.Html(tag='div', class_="pa-0 ma-0 mr-3", children=[''])
return [ v.Row(class_="pa-0 ma-0 ml-2", no_gutters=True, children=[tfvalue, sp, tfclass, sp, cp.draw()]) ]
# Manage onchange on the sortableList widget
def __internal_onchange(self, arg=None):
self.updatePalette()
if self.onchange:
self.onchange()
# Returns the vuetify object to display
[docs] def draw(self):
"""Returns the ipyvuetify object to display (a v.Html object displaying two output widgets)"""
return v.Html(tag='div',children=[widgets.VBox([v.Html(tag='div',children=[''], class_='pa-0 ma-0 mb-2'),
widgets.HBox([self.outputtoolbar,self.outputdialog]),self.outputtitle,self.outputpalette,self.outputlist])])
# colors property
@property
def colors(self):
"""
Get/set the colors of the palette.
Returns
--------
colorlist : list of strings in '#RRGGBB' format
List of colors of the palette
Example
-------
Get the edited palette colors::
print(editor.colors)
"""
return [x['color'] for x in self.s.items]
# Set the colors
@colors.setter
def colors(self, colorlist):
dlg = dialogWait.dialogWait(text='Updating palette...', output=self.outputdialog)
items = self.s.items
for index,item in enumerate(items):
item['color'] = colorlist[index%len(colorlist)]
self.s.items = items
dlg.close()
# items property
@property
def items(self):
"""
Get/set the items of the palette.
Returns
--------
items : list of dicts
List of dicts containing "value", "class" and "color"
Example
-------
Print the edited palette items::
print(editor.items)
"""
return self.s.items
# Set the items
@items.setter
def items(self, items):
dlg = dialogWait.dialogWait(text='Updating palette...', output=self.outputdialog)
self.s.items = items
dlg.close()
# title property
@property
def title(self):
"""
Get/set the title of the palette.
Returns
--------
t : str
Title of the palette
"""
return self.tfTitle.v_model
# Set the title
@title.setter
def title(self, t):
self.tfTitle.v_model = str(t)
# interpolate property
@property
def interpolate(self):
"""
Get/set the interpolate flag.
Returns
--------
flag : bool
Interpolate flag
"""
return self.sw.value
# Set the interpolate
@interpolate.setter
def interpolate(self, flag):
self.sw.value = flag
self.updatePalette()