File: matplotlib_cmaps.py

package info (click to toggle)
plotpy 2.8.2-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 13,364 kB
  • sloc: python: 37,833; cpp: 2,005; sh: 32; makefile: 3
file content (331 lines) | stat: -rw-r--r-- 9,059 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
from __future__ import annotations

import copy
import sys
from typing import Callable

import matplotlib.colors as pltc
import matplotlib.pyplot as plt
import numpy as np

from plotpy.mathutils.colormap import (
    DEFAULT_COLORMAPS,
    DEFAULT_COLORMAPS_PATH,
    CmapDictType,
    EditableColormap,
    save_colormaps,
)

PERCEPTUALLY_UNIFORM_CMAPS = ["viridis", "plasma", "inferno", "magma", "cividis"]
SEQUENTIAL_CMAPS = [
    "Greys",
    "Purples",
    "Blues",
    "Greens",
    "Oranges",
    "Reds",
    "YlOrBr",
    "YlOrRd",
    "OrRd",
    "PuRd",
    "RdPu",
    "BuPu",
    "GnBu",
    "PuBu",
    "YlGnBu",
    "PuBuGn",
    "BuGn",
    "YlGn",
]
SEQUENTIAL_CMAPS2 = [
    "binary",
    "gist_yarg",
    "gist_gray",
    "gray",
    "bone",
    "pink",
    "spring",
    "summer",
    "autumn",
    "winter",
    "cool",
    "Wistia",
    "hot",
    "afmhot",
    "gist_heat",
    "copper",
]
DIVERGING_CMAPS = [
    "PiYG",
    "PRGn",
    "BrBG",
    "PuOr",
    "RdGy",
    "RdBu",
    "RdYlBu",
    "RdYlGn",
    "Spectral",
    "coolwarm",
    "bwr",
    "seismic",
]
CYCLIC_CMAPS = ["twilight", "twilight_shifted", "hsv"]
QUALITATIVE_CMAPS = [
    "Pastel1",
    "Pastel2",
    "Paired",
    "Accent",
    "Dark2",
    "Set1",
    "Set2",
    "Set3",
    "tab10",
    "tab20",
    "tab20b",
    "tab20c",
]
MISCELLANEOUS_CMAPS = [
    "flag",
    "prism",
    "ocean",
    "gist_earth",
    "terrain",
    "gist_stern",
    "gnuplot",
    "gnuplot2",
    "CMRmap",
    "cubehelix",
    "brg",
    "gist_rainbow",
    "rainbow",
    "jet",
    "turbo",
    "nipy_spectral",
    "gist_ncar",
]

SORTED_MATPLOTLIB_COLORMAPS: list[str] = [
    *PERCEPTUALLY_UNIFORM_CMAPS,
    *SEQUENTIAL_CMAPS,
    *SEQUENTIAL_CMAPS2,
    *DIVERGING_CMAPS,
    *CYCLIC_CMAPS,
    *QUALITATIVE_CMAPS,
    *MISCELLANEOUS_CMAPS,
]


def rgb_colors_to_hex_list(
    colors: list[tuple[int, int, int]],
) -> list[tuple[float, str]]:
    """Convert a list of RGB colors to a list of tuples with the position of the color
    and the color in hex format. Positions evenly distributed between 0 and 1.

    Args:
        colors: list of RGB colors

    Returns:
        list of tuples with the position of the color and the color in hex format
    """
    return [(i / len(colors), pltc.to_hex(color)) for i, color in enumerate(colors)]


def _interpolate(
    val: float, vmin: tuple[float, float, float], vmax: tuple[float, float, float]
):
    """Interpolate between two level of a color.

    Args:
        val: value to interpolate
        vmin: R, G or B tuple from a matplotlib segmented colormap
        vmax: R, G or B tuple from matplotlib segmented colormap

    Returns:
        The interpolated R, G or B component
    """
    interp = (val - vmin[0]) / (vmax[0] - vmin[0])
    return (1 - interp) * vmin[1] + interp * vmax[2]


def std_segmented_cmap_to_hex_list(cmdata: dict[str, list[tuple[float, float, float]]]):
    """Convert a matplotlib segmented colormap to a list of tuples with the position of
    the color and the color in hex format.

    Args:
        cmdata: segmented colormap data

    Returns:
        list of tuples with the position of the color and the color in hex format
    """
    colors: list[tuple[float, str]] = []
    red = np.array(cmdata["red"])
    green = np.array(cmdata["green"])
    blue = np.array(cmdata["blue"])
    indices = sorted(set(red[:, 0]) | set(green[:, 0]) | set(blue[:, 0]))
    for i in indices:
        idxr = red[:, 0].searchsorted(i)
        idxg = green[:, 0].searchsorted(i)
        idxb = blue[:, 0].searchsorted(i)
        compr = _interpolate(i, red[idxr - 1], red[idxr])
        compg = _interpolate(i, green[idxg - 1], green[idxg])
        compb = _interpolate(i, blue[idxb - 1], blue[idxb])
        colors.append((i, pltc.to_hex((compr, compg, compb))))
    return colors


InterpFuncT = Callable[[np.ndarray], np.ndarray]


def func_segmented_cmap_to_hex_list(
    n: int,
    cmap: pltc.LinearSegmentedColormap,
) -> list[tuple[float, str]]:
    """Convert a matplotlib segmented colormap to a list of tuples with the position of
    the color and the color in hex format. The input colormap contains function for each
    color RGB component instead of a list of colors.

    Args:
        n: number of colors to generate
        cmap: segmented colormap

    Returns:
        list of tuples with the position of the color and the color in hex format
    """
    colors = []
    arr = np.linspace(0, 1, n, dtype=float)
    colors = [(i, pltc.to_hex(rgba)) for i, rgba in zip(arr, cmap(arr))]
    return colors


def continuous_to_descrete_cmap(cmap: EditableColormap) -> EditableColormap:
    """Convert a continuous colormap to a descrete one.

    Args:
        cmap: colormap to convert

    Returns:
        descrete colormap
    """
    raw_cmap: tuple[tuple[float, str], ...] = cmap.to_tuples()
    new_raw_cmap: list[tuple[float, str]] = [raw_cmap[0]]
    n = len(raw_cmap)
    coeff = (n - 1) / n
    for i, (pos, color) in enumerate(raw_cmap[1:]):
        prev_pos, prev_color = raw_cmap[i]
        curr_pos, curr_color = pos, color
        new_pos = curr_pos * coeff
        new_raw_cmap.append((new_pos, prev_color))
        new_raw_cmap.append((new_pos, curr_color))
    new_raw_cmap.append(raw_cmap[-1])

    return EditableColormap.from_iterable(new_raw_cmap, name=cmap.name)


def sort_mpl_colormaps(colormaps: CmapDictType) -> CmapDictType:
    """Filter and sort input colormaps to follow the same order (by category) as in the
     matplotlib colormaps documentation. Colormaps not found in the matplotlib
     are filtered out.

    Args:
        colormaps: Dictionnary of colormaps to extract and order

    Returns:
        Filtered and sorted colormaps dictionnary
    """
    ordered_colormaps: CmapDictType = {}
    lower_cmap_names = [cm.lower() for cm in SORTED_MATPLOTLIB_COLORMAPS]
    for lower_name in lower_cmap_names:
        cmap = colormaps.get(lower_name, None)
        if lower_name.endswith("_r"):
            continue
        if cmap is None:
            print(f"Colormap {lower_name} not found in input colormaps.")
            continue
        ordered_colormaps[lower_name] = cmap
    return ordered_colormaps


def append_non_mpl_colormaps(mpl_colormaps: CmapDictType, colormaps: CmapDictType):
    """Append colormaps not found in the matplotlib colormaps to the input colormaps.
     Mutate the input in place.

    Args:
        mpl_colormaps: dictionnary of matplotlib colormaps. Mutated in place.
        colormaps: dictionnary of colormaps to append to the matplotlib colormaps
    """
    colormap_names = set(SORTED_MATPLOTLIB_COLORMAPS)
    for colormap in colormaps.values():
        if colormap.name not in colormap_names:
            print(f"{colormap} not in matplotlib colormaps.")
            mpl_colormaps[colormap.name.lower()] = colormap


def main(cmaps: CmapDictType, out_json_path: str = DEFAULT_COLORMAPS_PATH):
    new_cmaps: dict[str, list[tuple[float, str]]] = {}

    # Uniform colormaps with a .colors attribute that return a list of RGB colors
    cmaps_with_colors = [
        "magma",
        "viridis",
        "inferno",
        "plasma",
        "cividis",
    ]

    # Discrete colormaps, same as uniform colormaps but the colormap must be post
    # processed to become descrete
    descrete_cmaps = [
        "Pastel1",
        "Pastel2",
        "Paired",
        "Accent",
        "Dark2",
        "Set1",
        "Set2",
        "Set3",
    ]

    cmaps_with_colors.extend(descrete_cmaps)

    # Colormaps with a _segmented_data attribute that contains the R, G and B components
    # as lists of tuples
    segmented_cmaps = [
        "coolwarm",
        "bwr",
        "seismic",
    ]

    # Colormaps with a _segmentdata attribute that contains the R, G and B components as
    # functions that return the color for a given position
    interp_cmaps = ["gnuplot2", "CMRmap", "rainbow", "turbo", "afmhot"]

    for cm_name in cmaps_with_colors:
        cmap = plt.get_cmap(cm_name)
        new_cmaps[cm_name] = rgb_colors_to_hex_list(cmap.colors)

    for cm_name in descrete_cmaps:
        cmap = EditableColormap.from_iterable(new_cmaps[cm_name], name=cm_name)
        new_cmaps[cm_name] = list(continuous_to_descrete_cmap(cmap).to_tuples())

    for cm_name in segmented_cmaps:
        cmap = plt.get_cmap(cm_name)
        new_cmaps[cm_name] = std_segmented_cmap_to_hex_list(cmap._segmentdata)

    n = 128

    for cm_name in interp_cmaps:
        cmap = plt.get_cmap(cm_name)
        new_cmaps[cm_name] = func_segmented_cmap_to_hex_list(n, cmap)

    for name, raw_cm in new_cmaps.items():
        cmaps[name.lower()] = EditableColormap.from_iterable(raw_cm, name=name)

    ordered_cmaps = sort_mpl_colormaps(cmaps)
    append_non_mpl_colormaps(ordered_cmaps, cmaps)
    save_colormaps(out_json_path, ordered_cmaps)


if __name__ == "__main__":
    cmaps = copy.deepcopy(DEFAULT_COLORMAPS)
    out_json = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_COLORMAPS_PATH
    main(cmaps, out_json)