# -*- coding: utf-8 -*-
"""
Created on Thu Sep 5 15:55:24 2024
@author: Alexandre Kenshilik Coche
"""
#%% IMPORT
import numpy as np
import numbers
#%% DISCRETE COLORMAPS FROM PERSONAL CATALOGS
[docs]def discrete(sequence_name='ibm', alpha=1, black=True, alternate=True,
color_format='float'):
"""
Generate a standardized colorscale, based on predefined color maps.
Parameters
----------
sequence_name : {"trio", "duo", "uno", "ibm", "wong"}, optional, default "ibm"
A flag to choose the colorscale among the available ones.
- ``"trio"``: a 3x9-color scale (+ grays) based on 9 distinct hues.
- Two other colorscales can be derived from this one:
- ``"duo"``: only the dark and light variations of each hue are returned.
- ``"uno"``: only the middle variation of each hue is returned.
- ``"wong"``: a 9-color scale (+ black) extended from Wong, adapted for \
colorblindness.
- ``"ibm"``: a 6-color scale (+ black) extended from IBM colorscale, \
adapted for colorblindness.
alpha : None or float, optional, default 1
Transparency (from 0 to 1). If ``None``, colors are returned without
the 4th value.
black : bool, optional, default True
If ``False``, the black color (and related gray variations) are not
included in the colorscale.
alternate : bool, optional, default True
If ``True``, the colorscale is not in rainbow order.
color_format : {"float", "rbg_str", "rgba_tuple"}, optional, default "float"
The way to define colors:
- ``"float"``: ``[0.22, 0.5, 0.99, 0.85]``
- ``"rgba_str"``: ``"rgba(56.1, 127.5, 252.45, 0.82)"``
- ``"rgba_tuple"``: ``(56.1, 127.5, 252.45, 0.82)``
Returns
-------
Return a numpy.array where each row is a 1D-array [red, green, blue, alpha],
with values between 0 and 1, or corresponding list with values converted to
rgba tuples or strings.
"""
# ---- Initialization
# =============================================================================
# # OLD CATALOG
# _cmap_catalog = [
# [1.000, 0.500, 0.000, 0.9], # 0. ~ orange
# [0.980, 0.691, 0.168, 0.9], # 1. orange-jaune (pour *.html)
# [0.973, 0.392, 0.420, 0.9], # 2. orange-rose
# [0.847, 0.000, 0.035, 0.9], # 3. ~ rouge royal
# [0.471, 0.000, 0.118, 0.9], # 4. ~ blackred
# [1.000, 0.557, 0.827, 0.9], # 5. ~ rose bonbon
# [0.949, 0.000, 0.784, 0.9], # 6. ~ fuschia
# [0.655, 0.204, 0.886, 0.9], # 7. ~ pourpre
# [0.404, 0.059, 0.902, 0.9], # 8. ~ violet fugace
# [0.000, 0.000, 0.470, 0.9], # 9. ~ bleu marine - noir
# [0.000, 0.318, 0.910, 0.9], # 10. ~ bleu
# [0.000, 0.707, 0.973, 0.9], # 11. ~ bleu ciel
# [0.000, 0.757, 0.757, 0.9], # 12. ~ bleu-vert émeraude
# [0.625, 0.777, 0.027, 0.9], # 13. ~ vert
# [0.824, 0.867, 0.141, 0.9], # 14. ~ vert-jaune (ou l'inverse)
# [1.000, 0.784, 0.059, 0.9], # 15. ~ jaune-orangé
# [1.000, 0.941, 0.059, 0.9], # 16. jaune
# [0, 0, 0, 1], # 17. ~ noir
# [0.37, 0.37, 0.37, 1], # 18. ~ gris sombrero
# [0.70, 0.70, 0.70, 1], # 19. ~ gris clairero
# [0, 0, 0, 0.2], # 20. ~ noir transparent
# ]
# =============================================================================
# NEW CATALOG (more complete)
# CSS named colors are finally discarded (not distinct enough)
cmap_catalog = {
# oranges & yellows
'darkorange': [0.737, 0.416, 0.004, 1.0],
'orange': [0.996, 0.612, 0.016, 1.0],
'lightorange': [0.996, 0.820, 0.549, 1.0],
# yellows
'darkyellow': [0.816, 0.737, 0.004, 1.0],
'yellow': [0.996, 0.890, 0.114, 1.0],
'lightyellow': [0.998, 0.945, 0.725, 1.0],
# reds
'darkred': [0.659, 0.071, 0.106, 1.0],
'red': [0.953, 0.184, 0.286, 1.0],
'lightred': [0.961, 0.655, 0.682, 1.0],
# pinks
'darkpink': [0.737, 0.102, 0.620, 1.0],
'pink': [0.922, 0.278, 0.843, 1.0],
'lightpink': [0.998, 0.522, 0.988, 1.0],
# violets
'darkviolet': [0.435, 0.002, 0.820, 1.0],
'violet': [0.616, 0.396, 0.998, 1.0],
'lightviolet': [0.788, 0.718, 0.998, 1.0],
# blues
'darkblue': [0.188, 0.188, 0.639, 1.0],
'blue': [0.239, 0.525, 0.953, 1.0],
'lightblue': [0.278, 0.796, 0.988, 1.0],
# turquoise
'darkturquoise': [0.102, 0.565, 0.592, 1.0],
'turquoise': [0.102, 0.839, 0.820, 1.0],
'lightturquoise': [0.647, 0.976, 0.925, 1.0],
# greens
'darkgreen': [0.169, 0.494, 0.239, 1.0],
'green': [0.290, 0.788, 0.302, 1.0],
'lightgreen': [0.659, 0.929, 0.635, 1.0],
# olive
'darkolive': [0.416, 0.533, 0.212, 1.0],
'olive': [0.592, 0.769, 0.106, 1.0],
'lightolive': [0.835, 0.937, 0.165, 1.0],
# grays
'black': [0.000, 0.000, 0.000, 1.0],
'sombrero': [0.370, 0.370, 0.370, 1.0], # ~ css_dimgray
'darkgray': [0.663, 0.663, 0.663, 1.0], # ~ clairero
'lightgray': [0.827, 0.827, 0.827, 1.0],
# extended IBM colorscale (https://www.ibm.com/design/language/resources/color-library)
'skyblue': [0.188, 0.737, 0.945, 1.0],
'bleuet': [0.392, 0.561, 0.998, 1.0],
'majorette': [0.471, 0.369, 0.941, 1.0],
'crimson': [0.863, 0.149, 0.498, 1.0],
'tangerine': [0.996, 0.380, 0.002, 1.0],
'gold': [0.996, 0.690, 0.002, 1.0],
# extended Wong colorscale (https://doi.org/10.1038/nmeth.1618)
'purple_w': [0.741, 0.522, 0.998, 1.0],
'bordeau_w': [0.533, 0.133, 0.333, 1.0],
'red_w': [0.835, 0.369, 0.002, 1.0],
'orange_w': [0.902, 0.623, 0.002, 1.0],
'yellow_w': [0.941, 0.894, 0.259, 1.0],
'olive_w': [0.722, 0.824, 0.624, 1.0],
'green_w': [0.002, 0.620, 0.451, 1.0],
'blue_w': [0.471, 0.765, 0.929, 1.0],
'marine_w': [0.002, 0.365, 0.698, 1.0],
}
# ---- Pre-generation of combinations
# COMBINATIONS
combinations = {
'trio': np.array([
'black', 'sombrero', 'darkgray',
'darkyellow', 'yellow', 'lightyellow',
'darkorange', 'orange', 'lightorange',
'darkred', 'red', 'lightred',
'darkpink', 'pink', 'lightpink',
'darkviolet', 'violet', 'lightviolet',
'darkblue', 'blue', 'lightblue',
'darkturquoise', 'turquoise', 'lightturquoise',
'darkgreen', 'green', 'lightgreen',
'darkolive', 'olive', 'lightolive',
]),
'ibm': np.array([
'black', 'gold', 'tangerine', 'crimson',
'majorette', 'bleuet', 'skyblue',
]),
'wong': np.array([
'black', 'yellow_w', 'bordeau_w', 'purple_w',
'red_w', 'orange_w', 'green_w', 'olive_w',
'marine_w', 'blue_w',
]),
}
# Reorder if alternate == True
if alternate == True:
combinations['trio'] \
= np.reshape(
np.reshape(
combinations['trio'], (int(len(combinations['trio'])/3), 3)
)[[0, 9, 3, 6, 2, 5, 8, 4, 7, 1]],
(int(len(combinations['trio'])),)
)
combinations['ibm'] = combinations['ibm'][[0, 3, 6, 2, 5, 1, 4]]
combinations['wong'] = combinations['wong'][[0, 5, 9, 6, 1, 8, 4, 2, 3, 7]]
# Remove black colors if specified
if black == False:
combinations['trio'] = combinations['trio'][3:]
combinations['ibm'] = combinations['ibm'][1:]
combinations['wong'] = combinations['wong'][1:]
# Create derived combinations
combinations['duo'] \
= np.reshape(
np.reshape(
combinations['trio'], (int(len(combinations['trio'])/3), 3)
)[:, [0, 2]],
(int(len(combinations['trio'])/3*2),)
)
combinations['uno'] \
= np.reshape(
np.reshape(
combinations['trio'], (int(len(combinations['trio'])/3), 3)
)[:, 1],
(int(len(combinations['trio'])/3),)
)
# ---- Generation the color map
cmap = np.array([cmap_catalog[k] for k in combinations[sequence_name]])
# Apply or remove alpha
if alpha is None:
cmap = cmap[:, 0:-1]
else:
cmap[:, -1] = alpha
if alpha > 1:
print("Warning: alpha is usually expected to be within 0 and 1")
# Apply the color format
if color_format.split('_')[0] == 'rgba':
if alpha is None:
cmap = cmap*255
else:
cmap[:, :-1] = cmap[:, :-1]*255
cmap = cmap.tolist()
if color_format == 'rgba_tuple':
cmap = [tuple(cmap[i]) for i in range(0, len(cmap))]
elif color_format == 'rgba_str':
if alpha is not None:
cmap = ['rgba' + str(tuple(cmap[i])) for i in range(0, len(cmap))]
else:
cmap = ['rgb' + str(tuple(cmap[i])) for i in range(0, len(cmap))]
return cmap
#%% CONTINUOUS COLORSCALES FROM 2 OR 3 COLORS
[docs]def custom(n_steps, *args):
"""
args:
color1, color2, color3... en format [Rouge Vert Bleu Alpha]
(valeurs entre 0 et 1)
"""
if len(args) == 4:
n_steps_adjusted = int(n_steps/(len(args)-1))+1
color_cmap = np.zeros(shape = (n_steps, 4))
color_cmap[0:n_steps_adjusted, :] = custom_2_colors(n_steps_adjusted, args[0], args[1])
color_cmap[n_steps_adjusted:2*n_steps_adjusted-1, :] = custom_2_colors(n_steps_adjusted, args[1], args[2])
color_cmap[2*n_steps_adjusted-1:, :] = custom_2_colors(n_steps-(2*n_steps_adjusted-1)+1, args[2], args[3])
elif len(args) == 3:
n_steps_adjusted = int(n_steps/(len(args)-1))+1
color_cmap = np.zeros(shape = (n_steps, 4)) #zeros(floor(n_steps/(nargin-2))*(nargin-2)+1, 3)
color_cmap[0:n_steps_adjusted, :] = custom_2_colors(n_steps_adjusted, args[0], args[1])
color_cmap[n_steps_adjusted-1:, :] = custom_2_colors(n_steps-n_steps_adjusted+1, args[1], args[2])
elif len(args) == 2:
color_cmap = custom_2_colors(n_steps, args[0], args[1])
elif len(args) == 1:
color_cmap = custom_2_colors(n_steps, args[0], [1, 1, 1])
return color_cmap
[docs]def custom_2_colors(n_steps, first_color, last_color):
# Color format correction (add alpha value):
if len(first_color) == 3:
first_color.append(1)
if len(last_color) == 3:
last_color.append(1)
# Initialization
col_array = np.zeros(shape = (n_steps, 4))
col_array[0,:] = first_color
col_array[-1,:] = last_color
# Filling
for i in range(0,4):
incr = (last_color[i]-first_color[i])/(n_steps-1)
col_array[1:(n_steps-1), i] = first_color[
i] + np.dot(incr, list(range(1, (n_steps-1))))
return col_array
#%% CONVERSION TOOLS
[docs]def to_rgba_str(color):
"""
This function can convert a color variable of any format (``'float'``, ``'rgba_tuple'``,
``'rgba_str'``) and any shape (one color or several colors) into a color variable
in the ``'rgba_str'`` format, for example::
'rgb(239.95, 227.97, 66.045)'
or::
['rgb(239.95, 227.97, 66.045)',
'rgb(135.92, 33.915, 84.915)',
'rgb(188.95, 133.11, 254.49)',
'rgb(212.92, 94.095, 0.51)',
'rgb(120.105, 195.08, 236.895)']
Parameters
----------
color : list, numpy.array, tuple or str
Input color variable to convert.
Returns
-------
A color variable similar to the input, but in the ``'rgba_str'`` format.
"""
# ---- Handling lists
if isinstance(color, list):
color = color.copy() # to avoid rewriting on top of the same list
# If color contains a list of numerical values, it is converted to a
# np.array, which will be handled in the following part
if isinstance(color[0], numbers.Number):
color = np.array(color)
# Else if color contains a list of iterables, this to_rgba_str function
# is recursively called to convert each element of color
else:
color_str = []
for i in range(0, len(color)):
# color[i] = to_rgba_str(color[i])
color_str.append(to_rgba_str(color[i]))
return color_str
# ---- Handling np.arrays
if isinstance(color, np.ndarray):
color = color.copy() # to avoid rewriting on top of the same list
# If color contains a 1D-array, it is converted to a
# string rgba or rgb depending on the presence/absence of alpha
if isinstance(color[0], numbers.Number):
if len(color) == 4:
color[:-1] = color[:-1]*255
return 'rgba' + str(tuple(color))
elif len(color == 3):
color = color*255
return 'rgb' + str(tuple(color))
# Else if color contains a 2D-array, this to_rgba_str function
# is recursively called to convert each element of color
else:
color_str = []
for i in range(0, color.shape[0]):
# color[i] = to_rgba_str(color[i])
color_str.append(to_rgba_str(color[i]))
# return color.tolist()
return color_str
# ---- Handling other simplier cases
elif isinstance(color, tuple):
if len(color) == 4:
return 'rgba' + str(color)
elif len(color) == 3:
return 'rgb' + str(color)
elif isinstance(color, str):
return color