Source code for watershed_workflow.colors

from typing import List, Dict, Tuple, Union, Optional, Callable, Any, Sequence, Literal, TypedDict, cast

Segment = Tuple[float, ...]
Channel = Literal["red", "green", "blue", "alpha"]
SegmentData = Dict[Channel, Sequence[Segment]]

try:
    from matplotlib.colors import Color  # type: ignore
except ImportError:
    Color = Union[str,  # named color, hex, grayscale str, shorthand
                  Tuple[float, float, float],  # RGB
                  Tuple[float, float, float, float],  # RGBA
                  ]

import matplotlib
import matplotlib.pyplot as plt
import matplotlib.colors
import matplotlib.cm
import numpy as np
import collections
import random
import colorsys

#
# Lists of disparate color palettes
#
enumerated_palettes: Dict[int, List[Color]] = {
    1: [
        '#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#ffff33', '#a65628', '#f781bf',
        '#999999'
    ],
    2: [
        '#a6cee3', '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99', '#e31a1c', '#fdbf6f', '#ff7f00',
        '#cab2d6', '#6a3d9a', '#ffff99', '#b15928'
    ],
    3: ['#1b9e77', '#d95f02', '#7570b3', '#e7298a', '#66a61e', '#e6ab02', '#a6761d', '#666666'],
    4: [
        "#399283", "#d2b48b", "#7f3a63", "#f3c5fa", "#e0079b", "#474747", "#c00018", "#2e21d0",
        "#5be13e", "#bce091", "#ed8220", "#769d31", "#d0de20", "#cd6ec6", "#547eec", "#8bd0eb",
        "#333a9e", "#94721a", "#d17778", "#f3c011", "#1eefc9", "#8e3703", "#02531d", "#d62df6"
    ],
}


[docs] def isNearlyGrey(color: Color, tolerance: float = 0.1) -> bool: """Determines whether a color is nearly grey. Parameters ---------- color : Color Color to test for greyness. tolerance : float, optional Tolerance for RGB component differences. Default is 0.1. Returns ------- bool True if color is nearly grey (RGB components within tolerance). """ # Ensure the color hash is valid r, g, b, a = matplotlib.colors.to_rgba(color) # Check if the RGB values are within the tolerance range return abs(r - g) <= tolerance and abs(g - b) <= tolerance and abs(b - r) <= tolerance
[docs] def measureBoldness(color: Color) -> float: """Calculate a vibrancy and boldness score for a given color. Parameters ---------- color : Color Color to measure boldness for. Returns ------- float A score representing how vibrant and bold the color is (0 to 100). """ r, g, b, a = matplotlib.colors.to_rgba(color) # Convert RGB to HSL for better vibrancy measurement h, l, s = colorsys.rgb_to_hls(r, g, b) # Calculate vibrancy score (saturation and brightness impact vibrancy) vibrancy = s * (1 - abs(2*l - 1)) # Calculate boldness score (intensity of RGB components) boldness = (r+g+b) / 3 # Combine vibrancy and boldness into a single score (weighted average) score = (0.6*vibrancy + 0.4*boldness) * 100 return round(score, 2)
# create a very big list of non-grey colors _my_not_random = random.Random(7) #2 xkcd_colors = [c for c in matplotlib.colors.XKCD_COLORS.values() if not isNearlyGrey(c)] _my_not_random.shuffle(xkcd_colors) _xkcd_by_bold = list(reversed(sorted(xkcd_colors, key=measureBoldness))) xkcd_bolds = _xkcd_by_bold[0:len(_xkcd_by_bold) // 4] _my_not_random.shuffle(xkcd_bolds) xkcd_muted = _xkcd_by_bold[len(_xkcd_by_bold) // 2:3 * len(_xkcd_by_bold) // 4] _my_not_random.shuffle(xkcd_muted) #random.shuffle(xkcd_colors) enumerated_palettes[5] = xkcd_colors # create a bigish list of greyish colors # this gives us way more unique colors to cycle through in plots matplotlib.rcParams['axes.prop_cycle'] = matplotlib.rcsetup.cycler(color=xkcd_bolds)
[docs] def enumerated_colors(count: int, palette: Union[int, List[Color]] = 1, chain: bool = True) -> List[Color]: """Gets an enumerated list of count independent colors. Parameters ---------- count : int Number of colors to return. palette : int or List[Color], optional Palette identifier (int) or explicit list of colors. Default is 1. chain : bool, optional If True, cycle through palette colors when count exceeds palette size. Default is True. Returns ------- List[Color] List of colors from the specified palette. Raises ------ ValueError If no enumerated palette of sufficient length exists and chain is False. """ if isinstance(palette, int): p = enumerated_palettes[palette] else: p = palette if count <= len(p): return p[0:count] elif chain: def chain_iter(p): while True: for c in p: yield c return [c for (i, c) in zip(range(count), chain_iter(p))] else: raise ValueError("No enumerated palettes of length {}.".format(count))
# black-zero jet is jet, but with the 0-value set to black, with an immediate jump to blue
[docs] def blackzerojet_cmap(data: np.ndarray) -> 'matplotlib.colors.LinearSegmentedColormap': """Create a jet colormap with zero values set to black. Parameters ---------- data : np.ndarray Data array used to determine color scaling. Returns ------- matplotlib.colors.LinearSegmentedColormap Custom colormap with zero values as black. """ blackzerojet_dict : SegmentData = { 'blue': [(0.0, 0.0, 0.0), (0.0, 0.0, 0.5), (0.1, 1.0, 1.0), (0.34, 1.0, 1.0), (0.65, 0.0, 0.0), (1.0, 0.0, 0.0)], 'green': [(0.0, 0.0, 0.0), (0.0, 0.0, 0.0), (0.125, 0.0, 0.0), (0.375, 1.0, 1.0), (0.64, 1.0, 1.0), (0.9, 0.0, 0.0), (1.0, 0.0, 0.0)], 'red': [(0.0, 0.0, 0.0), (0.0, 0.0, 0.0), (0.35, 0.0, 0.0), (0.66, 1.0, 1.0), (0.89, 1.0, 1.0), (1.0, 0.5, 0.5)] } minval = data[np.where(data > 0.)[0]].min() maxval = data[np.where(data > 0.)[0]].max() oneminval = .9 * minval / maxval for color in ['blue', 'green', 'red']: c = cast(Channel, color) blackzerojet_dict[c] = [(e[0] * (1-oneminval) + oneminval, e[1], e[2]) for e in blackzerojet_dict[c]] return matplotlib.colors.LinearSegmentedColormap('blackzerojet', blackzerojet_dict)
# ice color map
[docs] def ice_cmap() -> 'matplotlib.colors.LinearSegmentedColormap': """Create an ice-themed colormap. Returns ------- matplotlib.colors.LinearSegmentedColormap Ice-themed colormap from white to blue. """ x = np.linspace(0, 1, 7) b = np.array([1, 1, 1, 1, 1, 0.8, 0.6]) g = np.array([1, 0.993, 0.973, 0.94, 0.893, 0.667, 0.48]) r = np.array([1, 0.8, 0.6, 0.5, 0.2, 0., 0.]) bb = np.array([x, b, b]).transpose() gg = np.array([x, g, g]).transpose() rr = np.array([x, r, r]).transpose() ice_dict : SegmentData = { 'blue': [tuple(e) for e in bb], 'green': [tuple(e) for e in gg], 'red': [tuple(e) for e in rr], } return matplotlib.colors.LinearSegmentedColormap('ice', ice_dict)
# water color map
[docs] def water_cmap() -> 'matplotlib.colors.LinearSegmentedColormap': """Create a water-themed colormap. Returns ------- matplotlib.colors.LinearSegmentedColormap Water-themed colormap from white to dark blue. """ x = np.linspace(0, 1, 8) b = np.array([1.0, 1.0, 1.0, 1.0, 0.8, 0.6, 0.4, 0.2]) g = np.array([1.0, 0.8, 0.6, 0.4, 0.2, 0.0, 0.0, 0.0]) r = np.array([1.0, 0.7, 0.5, 0.3, 0.1, 0.0, 0.0, 0.0]) bb = np.array([x, b, b]).transpose() gg = np.array([x, g, g]).transpose() rr = np.array([x, r, r]).transpose() water_dict : SegmentData = { 'blue': [tuple(e) for e in bb], 'green': [tuple(e) for e in gg], 'red': [tuple(e) for e in rr], } return matplotlib.colors.LinearSegmentedColormap('water', water_dict)
# water color map
[docs] def gas_cmap() -> 'matplotlib.colors.LinearSegmentedColormap': """Create a gas-themed colormap. Returns ------- matplotlib.colors.LinearSegmentedColormap Gas-themed colormap from white to red. """ x = np.linspace(0, 1, 8) r = np.array([1.0, 1.0, 1.0, 1.0, 0.8, 0.6, 0.4, 0.2]) # g = np.array([1.0, 0.8, 0.6, 0.4, 0.2, 0.0, 0.0, 0.0]) b = np.array([1.0, 0.6, 0.4, 0.2, 0.0, 0.0, 0.0, 0.0]) g = np.array([1.0, 0.6, 0.4, 0.2, 0.0, 0.0, 0.0, 0.0]) bb = np.array([x, b, b]).transpose() gg = np.array([x, g, g]).transpose() rr = np.array([x, r, r]).transpose() gas_dict : SegmentData = { 'blue': [tuple(e) for e in bb], 'green': [tuple(e) for e in gg], 'red': [tuple(e) for e in rr], } return matplotlib.colors.LinearSegmentedColormap('gas', gas_dict)
# jet-by-index
[docs] def cm_mapper( vmin: float = 0., vmax: float = 1., cmap: Optional[Union[str, matplotlib.colors.Colormap]] = None, norm: Optional[matplotlib.colors.Normalize] = None, ) -> Callable[[float], Tuple[float, float, float, float]]: """Provide a function that maps scalars to colors in a given colormap. Parameters ---------- vmin, vmax : scalar Min and max scalars to be mapped. cmap : str or matplotlib.colors.Colormap instance The colormap to discretize. norm : optional, matplotlib Norm object A normalization. Returns ------- Function, cmap(scalar) -> (r,g,b,a) Example ------- .. code:: python # plot 4 lines x = np.arange(10) cm = cm_mapper(0,3,'jet') for i in range(4): plt.plot(x, x**i, color=cm(i)) """ if cmap is None: cmap = plt.get_cmap('jet') if norm is None: norm = matplotlib.colors.Normalize(vmin, vmax) sm = matplotlib.cm.ScalarMappable(norm, cmap) def mapper(value): return sm.to_rgba(value) return mapper
[docs] def cm_discrete( ncolors: int, cmap: Union[str, matplotlib.colors.Colormap] = plt.get_cmap('jet') ) -> 'matplotlib.colors.LinearSegmentedColormap': """Calculate a discrete colormap with N entries from the continuous colormap cmap. Parameters ---------- ncolors : int Number of colors. cmap : str or matplotlib.colors.Colormap instance, optional The colormap to discretize. Default is 'jet'. Returns ------- matplotlib.colors.LinearSegmentedColormap instance Example ------- .. code:: python # plot 4 lines x = np.arange(10) colors = cmap_discretize('jet', 4) for i in range(4): plt.plot(x, x**i, color=colors[i]) """ if isinstance(cmap, str): cmap = plt.get_cmap(cmap) colors_i = np.concatenate((np.linspace(0, 1., ncolors), (0., 0., 0., 0.))) colors_rgba = cmap(colors_i) indices = np.linspace(0, 1., ncolors + 1) cdict : SegmentData = { 'blue': [(indices[i], colors_rgba[i - 1, 0], colors_rgba[i, 0]) for i in range(ncolors + 1)], 'green': [(indices[i], colors_rgba[i - 1, 1], colors_rgba[i, 1]) for i in range(ncolors + 1)], 'red': [(indices[i], colors_rgba[i - 1, 2], colors_rgba[i, 2]) for i in range(ncolors + 1)], } # Return colormap object. return matplotlib.colors.LinearSegmentedColormap(cmap.name + "_%d"%ncolors, cdict, 1024)
[docs] def desaturate(color: Color, amount: float = 0.4, is_hsv: bool = False) -> np.ndarray: """Desaturate a color by reducing its saturation. Parameters ---------- color : Color or np.ndarray Color to desaturate. Can be in RGB or HSV format. amount : float, optional Amount of desaturation to apply (0-1). Default is 0.4. is_hsv : bool, optional If True, input color is in HSV format. Default is False (RGB). Returns ------- np.ndarray Desaturated color in RGB format. """ if not is_hsv: hsv = matplotlib.colors.rgb_to_hsv(matplotlib.colors.to_rgb(color)) else: hsv = color hsv[1] = max(0, hsv[1] - amount) return matplotlib.colors.hsv_to_rgb(hsv)
[docs] def darken(color: Color, fraction: float = 0.6) -> Tuple[float, float, float]: """Darken a color by reducing its brightness. Parameters ---------- color : Color Color to darken. fraction : float, optional Fraction of brightness to remove (0-1). Default is 0.6. Returns ------- Tuple[float, float, float] Darkened color in RGB format. """ rgb = np.array(matplotlib.colors.to_rgb(color)) return tuple(np.maximum(rgb - fraction*rgb, 0))
[docs] def lighten(color: Color, fraction: float = 0.6) -> Tuple[float, float, float]: """Lighten a color by increasing its brightness. Parameters ---------- color : Color Color to lighten. fraction : float, optional Fraction of brightness to add (0-1). Default is 0.6. Returns ------- Tuple[float, float, float] Lightened color in RGB format. """ rgb = np.array(matplotlib.colors.to_rgb(color)) return tuple(np.minimum(rgb + fraction * (1-rgb), 1))
[docs] def createIndexedColormap( indices: Union[List[int], np.ndarray], cmap: Optional[Union[str, matplotlib.colors.Colormap]] = None, ) -> Tuple[List[int], 'matplotlib.colors.ListedColormap', 'matplotlib.colors.BoundaryNorm', List[int], List[str]]: """Generates an indexed colormap and labels for imaging, e.g. soil indices. Parameters ---------- indices : iterable(int) Collection of indices that will be used in this colormap. cmap : optional, str Name of matplotlib colormap to sample. Returns ------- indices_out : list(int) The unique, sorted list of indices found. cmap : cmap-type A linestringed map for use with plots. norm : BoundaryNorm A norm for use in `plot_trisurf()` or other plotting methods to ensure correct NLCD colors. ticks : list(int) A list of tick locations for the requested indices. For use with `set_ticks()`. labels : list(str) A list of labels associated with the ticks. For use with `set_{x,y}ticklabels()`. """ indices = sorted(set(indices)) cm_values = None if cmap is None: for i, palette in enumerated_palettes.items(): if len(indices) < len(palette): cm_values = enumerated_colors(len(indices), palette=i) if cm_values is None: cmap = 'gist_rainbow' if isinstance(cmap, str): cmap = plt.get_cmap(cmap) if cm_values is None: cm = cm_mapper(0, len(indices) - 1, cmap) cm_values = [cm(i) for i in range(0, len(indices))] cmap = matplotlib.colors.ListedColormap(cm_values) ticks = indices + [indices[-1] + 1, ] norm = matplotlib.colors.BoundaryNorm(ticks, len(ticks) - 1) labels = [str(i) for i in indices] return indices, cmap, norm, ticks, labels
_doc_template = \ """Generates a colormap and labels for imaging with the {label} colors. Parameters ---------- indices : iterable(int), optional Collection of {label} indices that will be used in this colormap. If None (default), uses all {label} indices. Returns ------- indices_out : list(int) The unique, sorted list of indices found. cmap : cmap-type A linestringed map for use with plots. norm : BoundaryNorm A norm for use in `plot_trisurf()` or other plotting methods to ensure correct {label} colors. ticks : list(int) A list of tick locations for the requested indices. For use with `set_ticks()`. labels : list(str) A list of labels associated with the ticks. For use with `set_{{x,y}}ticklabels()`. formatted: bool, To make the labels formatted nicely (i.e. add newline in long label names) Example ------- Plot a triangulation given a set of {label} colors on those triangles. Given a triangluation `mesh_points, mesh_tris` and {label} color indices for each triangle (tri_{label_lower}): .. code-block:: indices, cmap, norm, ticks, labels = generate_{label_lower}_colormap(set(tri_{label_lower})) mp = ax.plot_trisurf(mesh_points[:,0], mesh_points[:,1], mesh_points[:,2], triangles=mesh_tris, color=tri_{label_lower}, cmap=cmap, norm=norm) cb = fig.colorbar(mp, orientation='horizontal') cb.set_ticks(ticks) cb.ax.set_xticklabels(labels, rotation=45) """ def _createColormapCreator( label: str, all_colors: Dict[int, Tuple[str, Color]] ) -> Callable[[Optional[List[int]], bool], Tuple[List[int], 'matplotlib.colors.ListedColormap', 'matplotlib.colors.BoundaryNorm', List[float], List[str]]]: """Create a colormap creator function for specific color sets. Parameters ---------- label : str Label for the colormap type (e.g., 'NLCD', 'MODIS'). all_colors : Dict[int, Tuple[str, Color]] Dictionary mapping indices to (name, color) tuples. Returns ------- Callable Function that creates colormaps for the specified color set. """ def _createColormap( indices: Optional[List[int]] = None, formatted: bool = False ) -> Tuple[List[int], 'matplotlib.colors.ListedColormap', 'matplotlib.colors.BoundaryNorm', List[float], List[str]]: """Create colormap for specified indices. Parameters ---------- indices : List[int], optional List of color indices to include. If None, uses all available. formatted : bool, optional If True, format long labels with line breaks. Default is False. Returns ------- Tuple[List[int], matplotlib.colors.ListedColormap, matplotlib.colors.BoundaryNorm, List[float], List[str]] Tuple containing indices, colormap, normalization, tick positions, and labels. """ if indices is None: indices = list(all_colors.keys()) indices = sorted(set(indices)) print("making colormap with:", indices) values = [all_colors[k][1] for k in indices] print("making colormap with colors:", values) cmap = matplotlib.colors.ListedColormap(values) ticks = [i - 0.5 for i in indices] + [indices[-1] + 0.5, ] norm = matplotlib.colors.BoundaryNorm(ticks, len(ticks) - 1) labels = [all_colors[k][0] for k in indices] if formatted: nlcd_labels_fw = [] for label in labels: label_fw = label if len(label) > 15: if ' ' in label: lsplit = label.split() if len(lsplit) == 2: label_fw = '\n'.join(lsplit) elif len(lsplit) == 4: label_fw = '\n'.join([' '.join(lsplit[0:2]), ' '.join(lsplit[2:])]) elif len(lsplit) == 3: if len(lsplit[0]) > len(lsplit[-1]): label_fw = '\n'.join([lsplit[0], ' '.join(lsplit[1:])]) else: label_fw = '\n'.join([' '.join(lsplit[:-1]), lsplit[-1]]) nlcd_labels_fw.append(label_fw) labels = nlcd_labels_fw return indices, cmap, norm, ticks, labels doc = _doc_template.format(label=label, label_lower=label.lower()) _createColormap.__doc__ = doc return _createColormap import watershed_workflow.sources.manager_nlcd createNLCDColormap = _createColormapCreator('NLCD', watershed_workflow.sources.manager_nlcd.colors) import watershed_workflow.sources.manager_modis_appeears createMODISColormap = _createColormapCreator( 'MODIS', watershed_workflow.sources.manager_modis_appeears.colors)
[docs] def createIndexedColorbar(ncolors: int, cmap: matplotlib.colors.Colormap, labels: Optional[List[str]] = None, **kwargs: Any) -> 'matplotlib.colorbar.Colorbar': """Add an indexed colorbar based on a given colormap. This sets ticks in the middle of each color range, adds the colorbar, and sets the labels if provided. Parameters ---------- ncolors : int Number of colors to display. cmap : matplotlib.colors.Colormap instance The colormap used in the image. labels : list Optional list of label strings that equal to the number of colors. If not provided, labels are set to range(ncolors). kwargs : dict Other arguments are passed on to the plt.colorbar() call, which can be used for things like fraction, pad, etc to control the location/spacing of the colorbar. Returns ------- colorbar : the colorbar object """ if labels is not None: assert (len(labels) == ncolors) cmap = cm_discrete(ncolors, cmap) mappable = matplotlib.cm.ScalarMappable(cmap=cmap) mappable.set_array([]) mappable.set_clim(-0.5, ncolors + 0.5) if 'fraction' not in kwargs: kwargs['fraction'] = 0.03 if 'pad' not in kwargs: kwargs['pad'] = 0.04 colorbar = plt.colorbar(mappable, **kwargs) ticks = np.linspace(0, ncolors, ncolors) colorbar.set_ticks(list(ticks)) # set tick locations # set tick labels if labels is not None: assert (len(labels) == len(ticks)) colorbar.set_ticklabels(labels) else: colorbar.set_ticklabels([str(i) for i in range(ncolors)]) return colorbar