Skip to content

Commit 64e895b

Browse files
committed
Merge remote-tracking branch 'Codra/develop-2.1_new-colormaps' into develop
2 parents eae737f + 4024c25 commit 64e895b

3 files changed

Lines changed: 12779 additions & 4367 deletions

File tree

colormaps/matplotlib_cmaps.py

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
from __future__ import annotations
2+
3+
import copy
4+
import sys
5+
from typing import Callable
6+
7+
import matplotlib.colors as pltc
8+
import matplotlib.pyplot as plt
9+
import numpy as np
10+
11+
from plotpy.mathutils.colormap import (
12+
DEFAULT_COLORMAPS,
13+
DEFAULT_COLORMAPS_PATH,
14+
CmapDictType,
15+
EditableColormap,
16+
save_colormaps,
17+
)
18+
19+
PERCEPTUALLY_UNIFORM_CMAPS = ["viridis", "plasma", "inferno", "magma", "cividis"]
20+
SEQUENTIAL_CMAPS = [
21+
"Greys",
22+
"Purples",
23+
"Blues",
24+
"Greens",
25+
"Oranges",
26+
"Reds",
27+
"YlOrBr",
28+
"YlOrRd",
29+
"OrRd",
30+
"PuRd",
31+
"RdPu",
32+
"BuPu",
33+
"GnBu",
34+
"PuBu",
35+
"YlGnBu",
36+
"PuBuGn",
37+
"BuGn",
38+
"YlGn",
39+
]
40+
SEQUENTIAL_CMAPS2 = [
41+
"binary",
42+
"gist_yarg",
43+
"gist_gray",
44+
"gray",
45+
"bone",
46+
"pink",
47+
"spring",
48+
"summer",
49+
"autumn",
50+
"winter",
51+
"cool",
52+
"Wistia",
53+
"hot",
54+
"afmhot",
55+
"gist_heat",
56+
"copper",
57+
]
58+
DIVERGING_CMAPS = [
59+
"PiYG",
60+
"PRGn",
61+
"BrBG",
62+
"PuOr",
63+
"RdGy",
64+
"RdBu",
65+
"RdYlBu",
66+
"RdYlGn",
67+
"Spectral",
68+
"coolwarm",
69+
"bwr",
70+
"seismic",
71+
]
72+
CYCLIC_CMAPS = ["twilight", "twilight_shifted", "hsv"]
73+
QUALITATIVE_CMAPS = [
74+
"Pastel1",
75+
"Pastel2",
76+
"Paired",
77+
"Accent",
78+
"Dark2",
79+
"Set1",
80+
"Set2",
81+
"Set3",
82+
"tab10",
83+
"tab20",
84+
"tab20b",
85+
"tab20c",
86+
]
87+
MISCELLANEOUS_CMAPS = [
88+
"flag",
89+
"prism",
90+
"ocean",
91+
"gist_earth",
92+
"terrain",
93+
"gist_stern",
94+
"gnuplot",
95+
"gnuplot2",
96+
"CMRmap",
97+
"cubehelix",
98+
"brg",
99+
"gist_rainbow",
100+
"rainbow",
101+
"jet",
102+
"turbo",
103+
"nipy_spectral",
104+
"gist_ncar",
105+
]
106+
107+
SORTED_MATPLOTLIB_COLORMAPS: list[str] = [
108+
*PERCEPTUALLY_UNIFORM_CMAPS,
109+
*SEQUENTIAL_CMAPS,
110+
*SEQUENTIAL_CMAPS2,
111+
*DIVERGING_CMAPS,
112+
*CYCLIC_CMAPS,
113+
*QUALITATIVE_CMAPS,
114+
*MISCELLANEOUS_CMAPS,
115+
]
116+
117+
118+
def rgb_colors_to_hex_list(
119+
colors: list[tuple[int, int, int]]
120+
) -> list[tuple[float, str]]:
121+
"""Convert a list of RGB colors to a list of tuples with the position of the color
122+
and the color in hex format. Positions evenly distributed between 0 and 1.
123+
124+
Args:
125+
colors: list of RGB colors
126+
127+
Returns:
128+
list of tuples with the position of the color and the color in hex format
129+
"""
130+
return [(i / len(colors), pltc.to_hex(color)) for i, color in enumerate(colors)]
131+
132+
133+
def _interpolate(
134+
val: float, vmin: tuple[float, float, float], vmax: tuple[float, float, float]
135+
):
136+
"""Interpolate between two level of a color.
137+
138+
Args:
139+
val: value to interpolate
140+
vmin: R, G or B tuple from a matplotlib segmented colormap
141+
vmax: R, G or B tuple from matplotlib segmented colormap
142+
143+
Returns:
144+
The interpolated R, G or B component
145+
"""
146+
interp = (val - vmin[0]) / (vmax[0] - vmin[0])
147+
return (1 - interp) * vmin[1] + interp * vmax[2]
148+
149+
150+
def std_segmented_cmap_to_hex_list(cmdata: dict[str, list[tuple[float, float, float]]]):
151+
"""Convert a matplotlib segmented colormap to a list of tuples with the position of
152+
the color and the color in hex format.
153+
154+
Args:
155+
cmdata: segmented colormap data
156+
157+
Returns:
158+
list of tuples with the position of the color and the color in hex format
159+
"""
160+
colors: list[tuple[float, str]] = []
161+
red = np.array(cmdata["red"])
162+
green = np.array(cmdata["green"])
163+
blue = np.array(cmdata["blue"])
164+
indices = sorted(set(red[:, 0]) | set(green[:, 0]) | set(blue[:, 0]))
165+
for i in indices:
166+
idxr = red[:, 0].searchsorted(i)
167+
idxg = green[:, 0].searchsorted(i)
168+
idxb = blue[:, 0].searchsorted(i)
169+
compr = _interpolate(i, red[idxr - 1], red[idxr])
170+
compg = _interpolate(i, green[idxg - 1], green[idxg])
171+
compb = _interpolate(i, blue[idxb - 1], blue[idxb])
172+
colors.append((i, pltc.to_hex((compr, compg, compb))))
173+
return colors
174+
175+
176+
InterpFuncT = Callable[[np.ndarray], np.ndarray]
177+
178+
179+
def func_segmented_cmap_to_hex_list(
180+
n: int,
181+
cmap: pltc.LinearSegmentedColormap,
182+
) -> list[tuple[float, str]]:
183+
"""Convert a matplotlib segmented colormap to a list of tuples with the position of
184+
the color and the color in hex format. The input colormap contains function for each
185+
color RGB component instead of a list of colors.
186+
187+
Args:
188+
n: number of colors to generate
189+
cmap: segmented colormap
190+
191+
Returns:
192+
list of tuples with the position of the color and the color in hex format
193+
"""
194+
colors = []
195+
arr = np.linspace(0, 1, n, dtype=float)
196+
colors = [(i, pltc.to_hex(rgba)) for i, rgba in zip(arr, cmap(arr))]
197+
return colors
198+
199+
200+
def continuous_to_descrete_cmap(cmap: EditableColormap) -> EditableColormap:
201+
"""Convert a continuous colormap to a descrete one.
202+
203+
Args:
204+
cmap: colormap to convert
205+
206+
Returns:
207+
descrete colormap
208+
"""
209+
raw_cmap: tuple[tuple[float, str], ...] = cmap.to_tuples()
210+
new_raw_cmap: list[tuple[float, str]] = [raw_cmap[0]]
211+
n = len(raw_cmap)
212+
coeff = (n - 1) / n
213+
for i, (pos, color) in enumerate(raw_cmap[1:]):
214+
prev_pos, prev_color = raw_cmap[i]
215+
curr_pos, curr_color = pos, color
216+
new_pos = curr_pos * coeff
217+
new_raw_cmap.append((new_pos, prev_color))
218+
new_raw_cmap.append((new_pos, curr_color))
219+
new_raw_cmap.append(raw_cmap[-1])
220+
221+
return EditableColormap.from_iterable(new_raw_cmap, name=cmap.name)
222+
223+
224+
def sort_mpl_colormaps(colormaps: CmapDictType) -> CmapDictType:
225+
"""Filter and sort input colormaps to follow the same order (by category) as in the
226+
matplotlib colormaps documentation. Colormaps not found in the matplotlib
227+
are filtered out.
228+
229+
Args:
230+
colormaps: Dictionnary of colormaps to extract and order
231+
232+
Returns:
233+
Filtered and sorted colormaps dictionnary
234+
"""
235+
ordered_colormaps: CmapDictType = {}
236+
lower_cmap_names = [cm.lower() for cm in SORTED_MATPLOTLIB_COLORMAPS]
237+
for lower_name in lower_cmap_names:
238+
cmap = colormaps.get(lower_name, None)
239+
if lower_name.endswith("_r"):
240+
continue
241+
if cmap is None:
242+
print(f"Colormap {lower_name} not found in input colormaps.")
243+
continue
244+
ordered_colormaps[lower_name] = cmap
245+
return ordered_colormaps
246+
247+
248+
def append_non_mpl_colormaps(mpl_colormaps: CmapDictType, colormaps: CmapDictType):
249+
"""Append colormaps not found in the matplotlib colormaps to the input colormaps.
250+
Mutate the input in place.
251+
252+
Args:
253+
mpl_colormaps: dictionnary of matplotlib colormaps. Mutated in place.
254+
colormaps: dictionnary of colormaps to append to the matplotlib colormaps
255+
"""
256+
colormap_names = set(SORTED_MATPLOTLIB_COLORMAPS)
257+
for colormap in colormaps.values():
258+
if colormap.name not in colormap_names:
259+
print(f"{colormap} not in matplotlib colormaps.")
260+
mpl_colormaps[colormap.name.lower()] = colormap
261+
262+
263+
def main(cmaps: CmapDictType, out_json_path: str = DEFAULT_COLORMAPS_PATH):
264+
265+
new_cmaps: dict[str, list[tuple[float, str]]] = {}
266+
267+
# Uniform colormaps with a .colors attribute that return a list of RGB colors
268+
cmaps_with_colors = [
269+
"magma",
270+
"viridis",
271+
"inferno",
272+
"plasma",
273+
"cividis",
274+
]
275+
276+
# Discrete colormaps, same as uniform colormaps but the colormap must be post
277+
# processed to become descrete
278+
descrete_cmaps = [
279+
"Pastel1",
280+
"Pastel2",
281+
"Paired",
282+
"Accent",
283+
"Dark2",
284+
"Set1",
285+
"Set2",
286+
"Set3",
287+
]
288+
289+
cmaps_with_colors.extend(descrete_cmaps)
290+
291+
# Colormaps with a _segmented_data attribute that contains the R, G and B components
292+
# as lists of tuples
293+
segmented_cmaps = [
294+
"coolwarm",
295+
"bwr",
296+
"seismic",
297+
]
298+
299+
# Colormaps with a _segmentdata attribute that contains the R, G and B components as
300+
# functions that return the color for a given position
301+
interp_cmaps = ["gnuplot2", "CMRmap", "rainbow", "turbo", "afmhot"]
302+
303+
for cm_name in cmaps_with_colors:
304+
cmap = plt.get_cmap(cm_name)
305+
new_cmaps[cm_name] = rgb_colors_to_hex_list(cmap.colors)
306+
307+
for cm_name in descrete_cmaps:
308+
cmap = EditableColormap.from_iterable(new_cmaps[cm_name], name=cm_name)
309+
new_cmaps[cm_name] = list(continuous_to_descrete_cmap(cmap).to_tuples())
310+
311+
for cm_name in segmented_cmaps:
312+
cmap = plt.get_cmap(cm_name)
313+
new_cmaps[cm_name] = std_segmented_cmap_to_hex_list(cmap._segmentdata)
314+
315+
n = 128
316+
317+
for cm_name in interp_cmaps:
318+
cmap = plt.get_cmap(cm_name)
319+
new_cmaps[cm_name] = func_segmented_cmap_to_hex_list(n, cmap)
320+
321+
for name, raw_cm in new_cmaps.items():
322+
cmaps[name.lower()] = EditableColormap.from_iterable(raw_cm, name=name)
323+
324+
ordered_cmaps = sort_mpl_colormaps(cmaps)
325+
append_non_mpl_colormaps(ordered_cmaps, cmaps)
326+
save_colormaps(out_json_path, ordered_cmaps)
327+
328+
329+
if __name__ == "__main__":
330+
cmaps = copy.deepcopy(DEFAULT_COLORMAPS)
331+
out_json = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_COLORMAPS_PATH
332+
main(cmaps, out_json)

0 commit comments

Comments
 (0)