"""Selection of a palette of colors"""
# 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
import inspect
import plotly.express as px
import base64
import PIL
from PIL import Image, ImageDraw
from io import BytesIO
try:
from . import settings
from . import selectImage
except:
import settings
import selectImage
#################################################################################################################
# Code repeated from colors.py
#################################################################################################################
# Utility: From (r,g,b) to '#RRGGBB'
def rgb2hex(rgb):
return '#{:02X}{:02X}{:02X}'.format(rgb[0], rgb[1], rgb[2])
# Utility: From '#rrggbb' to (r,g,b)
def hex2rgb(color):
if color[0] == '#':
color = color[1:]
rgb = (int(color[0:2],16), int(color[2:4],16), int(color[4:6],16))
return rgb
# Utility: From 'rgb(a,b,c)' to (r,g,b)
def text2rgb(color):
if color[0:4] == 'rgb(':
rgb = color[4:].replace(')','').split(',')
if len(rgb) >= 3:
return ((int(rgb[0]),int(rgb[1]),int(rgb[2])))
return (0,0,0)
# Utility: Convert a color string in '#rrggbb' or in 'rgb(...)' format into a tuple (r,g,b)
def string2rgb(s):
if s[0] == '#':
return hex2rgb(s)
elif s[0:4] == 'rgb(':
return text2rgb(s)
return (0,0,0)
# colorInterpolator class
class colorInterpolator:
# Initialization
def __init__(self, colorlist, minValue=0.0, maxValue=100.0):
self.minValue = minValue
self.maxValue = maxValue
self.colors = []
for color in colorlist:
self.colors.append(string2rgb(color))
self.palette = []
for i in range(len(self.colors)):
if i == 0:
self.palette.append(self.colors[i])
else:
c1 = self.colors[i-1]
c2 = self.colors[i]
for j in range(51):
w2 = float(j)/50.0
w1 = 1.0 - w2
r = int(c1[0]*w1 + c2[0]*w2)
g = int(c1[1]*w1 + c2[1]*w2)
b = int(c1[2]*w1 + c2[2]*w2)
self.palette.append((r,g,b))
# Return '#rrggbb' color linearly interpolated
def GetColor(self, value):
if value < self.minValue: value = self.minValue
if value > self.maxValue: value = self.maxValue
n = len(self.palette)
v = (value - self.minValue) / (self.maxValue - self.minValue)
idx = int(float((n-1)*v) + 0.5)
if idx < 0: idx = 0
if idx >= n: idx = n - 1
if idx >= 0 and idx < len(self.palette):
return rgb2hex(self.palette[idx])
return '#FFFFFF'
# repr
def __repr__(self):
s = [str(x) for x in self.colors]
return '-'.join(s)
# Utility: Given a list of colors, returns a PIL image displaying the palette
def paletteImage(colorlist, width=400, height=40, interpolate=True):
img = Image.new('RGBA', (width,height), (255, 255, 255, 0))
d = ImageDraw.Draw(img)
if interpolate:
ci = colorInterpolator(colorlist,0,width-1.0)
for x in range(width):
d.line([x,0,x,height], fill=ci.GetColor(x), width=1)
else:
n = len(colorlist)
if n > 0:
wcolor = float(width)/float(n)
x = 0.0
for c in colorlist:
d.rectangle([x,0.0,x+wcolor,height], fill=c)
x += wcolor
return img
# Utility: convert a PIL image into a string containing the image in base64
def image2Base64(img):
buffered = BytesIO()
img.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue()).decode('utf-8')
return 'data:image/png;base64,' + img_str
# Class palettePicker
[docs]class palettePicker():
"""
Selection of a palette of colors.
Parameters
----------
family : str, optional
Family of the palette, one of these values: ['carto', 'cmocean', 'cyclical', 'diverging', 'plotlyjs', 'qualitative', 'sequential', 'custom']. If family is 'custom' the user of the widget has to pass the list of palettes to display (default is 'sequential')
custompalettes: list of dicts containing tags: "name" and "colors", optional
Custom palette to be selected when the family is 'custom' (default is [])
label : str, optional
Label to display inside the selection widget (default is '')
interpolate : bool, optional
If True the colors are displayed as interpolated (default is True)
width : int, optional
Width of the widget in pixels (default is 400)
height : int, optional
Height of the widget in pixels (default is 34)
clearable : Bool, optional
If True the selection widget will show a -x- button to clear the selection (default is True)
color : str, optional
Color of the selection widget (default is settings.color_first)
onchange : function, optional
Python function to call when the user selects one of the palettes. The function will receive no parameters (default is None)
Examples
--------
Creation of a simple selection widget for the palettes::
from vois.vuetify import palettePicker
from ipywidgets import widgets
from IPython.display import display
output = widgets.Output()
def onchange():
with output:
print('changed!')
p = palettePicker.palettePicker(onchange=onchange)
display(p.draw())
display(output)
.. figure:: figures/palettePicker.png
:scale: 100 %
:alt: palettePicker widget
Example of a simple palette picker
Creation of a complex selection widget for the palettes that manages all the palette families::
from vois.vuetify import palettePicker, selectSingle, switch
import ipyvuetify as v
from ipywidgets import widgets
from IPython.display import display
output = widgets.Output()
# Utility: convert three integers to '#RRGGBB'
def RGB(r,g,b):
return '#{:02X}{:02X}{:02X}'.format(r, g, b)
# Custom palettes
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'
interpolate = True
p = None
def onchangePalette():
if not p is None:
with output:
print("Palette changed to", p.value, p.colors)
def onchangeFamily():
global family, interpolate
family = sel.value
if family == 'carto' or family == 'qualitative':
interpolate = False
sw.value = interpolate
else:
interpolate = True
sw.value = interpolate
p.updatePalettes(family,interpolate)
def onchangeInterpolate(flag):
global interpolate
interpolate = flag
p.updatePalettes(family,interpolate)
sel = selectSingle.selectSingle('Family:', families, selection=family,
width=160, onchange=onchangeFamily,
marginy=1, clearable=False)
sw = switch.switch(interpolate, "Interpolate",
onchange=onchangeInterpolate)
p = palettePicker.palettePicker(family=family, custompalettes=custompalettes,
label='Palette:', height=26, onchange=onchangePalette)
spacer = v.Html(tag='div',children=[' '], style_='width: 10px;')
display(widgets.HBox([sel.draw(), spacer, p.draw(), spacer, sw.draw()]))
display(output)
.. figure:: figures/palettePicker2.png
:scale: 100 %
:alt: palettePicker full widget
Example of a palette picker that also manages the palette families
"""
def __init__(self, family='sequential', label='', interpolate=True, width=400, height=34,
custompalettes=[], clearable=True,
color=settings.color_first, onchange=None):
self.family = family
self.custompalettes = custompalettes
self.label = label
self.interpolate = interpolate
self.width = width
self.height = height
self.clearable = clearable
self.color = color
self.onchange = onchange
self.index = -1
self.s = selectImage.selectImage(images=[], width="%dpx"%(self.width+165),
max_width=self.width, max_height=self.height,
color=self.color,
label=self.label,
outlined=True, margins="ma-0 mr-2",
clearable=self.clearable,
onchange=self.__internal_onchange)
self.updatePalettes(self.family, self.interpolate)
# Manage onchange on the selectImage widget
def __internal_onchange(self):
if self.index != self.s.value:
self.index = self.s.value
if self.onchange:
self.onchange()
# value property
@property
def value(self):
"""
Get/Set name of the selected palette.
Returns
--------
name : str
Name of the currently selected palette
Example
-------
Set and then get the current palette name::
picker.value = 'Viridis'
print(picker.value)
"""
index = self.s.value
if index >= 0 and index < len(self.images):
return self.images[index]['name']
else:
return ''
# Select one of the palette given its name
@value.setter
def value(self, name):
for img in self.images:
if img['name'] == name:
self.s.value = img['index']
return
self.s.value = -1
# colors property
@property
def colors(self):
"""
Get the colors of the selected palette.
Returns
--------
colorlist : list of strings in '#RRGGBB' format
List of colors of the selected palette
Example
-------
Get the selected palette colors::
print(picker.colors)
"""
index = self.s.value
if index >= 0 and index < len(self.images):
if self.family == 'custom':
colors = self.custompalettes[index]['colors']
else:
colors = eval('px.colors.' + self.family + '.' + self.images[index]['name'])
return [rgb2hex(text2rgb(x)) if x[0:4] == 'rgb(' else x for x in colors]
else:
return []
# Update the palettes
[docs] def updatePalettes(self, family='sequential', interpolate=True):
"""
Update the displayed palette by changing family and/or interpolation flag
Parameters
----------
family : str, optional
Family of the palette, one of these values: ['carto', 'cmocean', 'cyclical', 'diverging', 'plotlyjs', 'qualitative', 'sequential'] (default is 'sequential')
interpolate : bool, optional
If True the colors are displayed as interpolated (default is True)
"""
self.family = family
self.interpolate = interpolate
if self.family == 'custom':
self.images = [{"name": x['name'],
"image": image2Base64(paletteImage(x['colors'],
width=self.width, height=self.height,
interpolate=self.interpolate))} for x in self.custompalettes]
else:
colorscale_names = []
colorscale_names.extend([self.family + '.' + name for name, body
in inspect.getmembers(getattr(px.colors, self.family))
if isinstance(body, list) and not '_r' in name and not name == '__all__'])
self.images = [{"name": x.split('.')[-1],
"image": image2Base64(paletteImage(eval('px.colors.'+x),
width=self.width, height=self.height,
interpolate=self.interpolate))} for x in colorscale_names]
self.s.setImages(self.images)
self.__internal_onchange()
# Returns the vuetify object to display (the v.selectImage)
[docs] def draw(self):
"""Returns the ipyvuetify object to display (the internal v.selectImage)"""
return self.s