|
| 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