Source code for dantro.plot.utils.color_mngr

"""Implements the :py:class:`.ColorManager` which simplifies working with
:py:class:`matplotlib.colors.Colormap` and related objects."""

import copy
import logging
from math import ceil, floor
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union

import matplotlib as mpl
import matplotlib.colors
import numpy as np
from matplotlib.colors import to_rgb

from ...tools import make_columns, parse_str_to_args_and_kwargs

log = logging.getLogger(__name__)

# -----------------------------------------------------------------------------

NORMS = {
    "Normalize": mpl.colors.Normalize,
    "BoundaryNorm": mpl.colors.BoundaryNorm,
    "CenteredNorm": mpl.colors.CenteredNorm,
    "NoNorm": mpl.colors.NoNorm,
    "LogNorm": mpl.colors.LogNorm,
    "PowerNorm": mpl.colors.PowerNorm,
    "SymLogNorm": mpl.colors.SymLogNorm,
    "TwoSlopeNorm": mpl.colors.TwoSlopeNorm,
    "FuncNorm": mpl.colors.FuncNorm,
}
"""matplotlib color normalizations supported by the :py:class:`.ColorManager`.
See the :py:mod:`matplotlib.colors` module for more information.
"""


# -----------------------------------------------------------------------------


[docs]class ColorManager: """Custom color manager which provides an interface to the :py:mod:`matplotlib.colors` module and aims to simplify working with colormaps, colorbars, and different normalizations. """ _NORMS_NOT_SUPPORTING_VMIN_VMAX: Tuple[str] = ( "BoundaryNorm", "CenteredNorm", ) """Names of norms that do *not* support getting passed the ``vmin`` and ``vmax`` arguments.""" _POSSIBLE_CMAP_KWARGS: Tuple[str] = ( "name", "colors", "segmentdata", "bad", "under", "over", "reversed", "N", "gamma", # # ColorManager-internal "placeholder_color", "continuous", "from_values", ) """Keyword arguments that are used by matplotlib or the ColorManager to construct colormaps. If using the implicit syntax for defining labels and colormap values, these can *not* be used for labels.""" _SNS_COLOR_PALETTE_PREFIX: str = "color_palette::" """If a colormap ``name`` starts with this string, will use :py:func:`seaborn.color_palette` to generate the colormap""" _SNS_DIVERGING_PALETTE_PREFIX: str = "diverging::" """If a colormap ``name`` starts with this string, will use :py:func:`seaborn.diverging_palette` to generate the colormap, parsing the remaining parts of the name into positional and keyword arguments.""" # .........................................................................
[docs] def __init__( self, *, cmap: Union[str, dict, list, mpl.colors.Colormap] = None, norm: Union[str, dict, mpl.colors.Normalize] = None, labels: Union[List[str], Dict[float, str]] = None, vmin: float = None, vmax: float = None, discretized: bool = None, ): """Initializes a :py:class:`.ColorManager` by building the colormap, the norm, and the colorbar labels. Refer to the :ref:`dedicated documentation page <color_mngr>` for examples and integration instructions. Args: cmap (Union[str, dict, list, matplotlib.colors.Colormap], optional): The colormap specification. If this is not already a :py:class:`matplotlib.colors.Colormap` instance, it will be parsed into a dict-like specification, which has the options as shown below. * If ``cmap`` is a string, it is turned into ``dict(name=cmap)``. * If ``cmap`` is a list (or tuple), it will be converted to ``dict(from_values=cmap)``, creating a segmented colormap. See below for more information. In dict form, the following arguments are available: ``name`` (str, optional): Name of a registered matplotlib colormap or None to use a default. For available colormap names, see `here <https://matplotlib.org/stable/gallery/color/colormap_reference.html>`_. Also **supports seaborn colormaps**. If the name starts with the :py:attr:`._SNS_COLOR_PALETTE_PREFIX` string, :py:func:`seaborn.color_palette` is used to generate the colormap. If starting with :py:attr:`._SNS_DIVERGING_PALETTE_PREFIX`, :py:func:`seaborn.diverging_palette` is invoked, using argument specified as part of the ``name``. This opens many possibilities, as shown in the `seaborn documentation <https://seaborn.pydata.org/tutorial/color_palettes.html>`_. For example: .. code-block:: text color_palette::YlOrBr color_palette::icefire color_palette::icefire_r # reversed color_palette::light:b # white -> blue color_palette::dark:b # black -> blue color_palette::light:#69d # custom color color_palette::light:#69d_r # ... reversed color_palette::dark:salmon_r # named, reversed color_palette::ch:s=-.2,r=.6 # cubehelix diverging::220,20 diverging::145,300,s=60 diverging::250, 30, l=65, center=dark Here, the ``ch:<key>=<val>,<key>=<val>`` syntax is used to create a :py:func:`seaborn.cubehelix_palette`. The same ``<arg>,<arg>,<key>=<val>,<key>=<val>`` syntax is used for the diverging palette. .. note:: When specifying these via YAML, make sure to put the string into single or double quotes to avoid it being interpreted as a YAML mapping. ``from_values`` (Union[dict, list], optional): Dict of colors keyed by bin-specifier. If given, ``name`` is ignored and a discrete colormap is created from the list of specified colors. The ``norm`` is then set to :py:class:`matplotlib.colors.BoundaryNorm`. The bins can be specified either by bin-centers (Scalar) or by bin-intervals (2-tuples). For the former, the deduced bin-edges are assumed halfway between the bin-centers. For the latter, the given intervals must be pairwise connected. In both cases, the bins must monotonically increase. If a list of colors is passed they are automatically assigned to the bin-centers ``[0, 1, 2, ...]``, potentially shifted depending on ``vmin`` and ``vmax``. Inferring these values is done in :py:meth:`_infer_pos_map`. Alternatively, a continuous, linearly interpolated colormap can be generated by setting the ``continuous`` flag, see below. This will construct a :py:class:`~matplotlib.colors.LinearSegmentedColormap`. In such a case, keys in ``from_values`` can only be scalar, bin *intervals* cannot be specified. ``continuous`` (bool, optional): If True, will interpret the ``from_values`` data as specifying points between which a linear interpolation is carried out. Will create a :py:class:`~matplotlib.colors.LinearSegmentedColormap`. ``under`` (Union[str, dict], optional): Passed on to :py:meth:`~matplotlib.colors.Colormap.set_under` ``over`` (Union[str, dict], optional): Passed on to :py:meth:`~matplotlib.colors.Colormap.set_over` ``bad`` (Union[str, dict], optional): Passed on to :py:meth:`~matplotlib.colors.Colormap.set_bad` ``placeholder_color`` (str, optional): ``None`` values in ``from_values`` are replaced with this color (default: white). ``reversed`` (bool, optional): If True, will reverse the colormap. ``labels_and_colors`` (dict, optional): This is a shorthand syntax for specifying colorbar labels and colors at the same time. Keys refer to labels, values to colors. The label positions and bounds are inferred using :py:meth:`_infer_pos_map` and are affected by ``vmin`` and ``vmax``. These may also be given implicitly via ``**kwargs`` (see below), but *not* at the same time! Effectively, the mapping is unpacked into two parts: The keys are used to specify the values of the ``labels`` dict (on the top-level); the values are used to specify the values of the ``cmap.from_values`` dict (see above). The keys are inferred from the length of the sequence and ``vmin`` and ``vmax``, expecting to map to an integer data positions. **Example:** .. code-block:: yaml cmap: empty: darkkhaki # -> 0 susceptible: forestgreen # -> 1 exposed: darkorange # ... infected: firebrick recovered: slategray deceased: black source: maroon inert: moccasin # -> 7 # can still set extremes here (should not appear) under: red over: red ``**kwargs`` (optional): Depending on the argument names, these are either passed to colormap instantiation *or* are used to specify the ``labels_and_colors`` mapping. For the latter, labels may not be named after arguments that are relevant for colormap initialization (:py:attr:`._POSSIBLE_CMAP_KWARGS`). norm (Union[str, dict, matplotlib.colors.Normalize], optional): The norm that is applied for the color-mapping. If it is a string, the matching norm in :py:mod:`matplotlib.colors` is created with default values. If it is a dict, the ``name`` entry specifies the norm and all further entries are passed to its constructor. Overwritten if a discrete colormap is specified via ``cmap.from_values``. labels (Union[List[str], Dict[float, str]], optional): Colorbar tick-labels keyed by tick position. If a list of labels is passed they are automatically assigned to the positions ``[0, 1, 2, ...]`` (if no ``vmin`` and ``vmax`` are given) or ``[vmin, vmin + 1, ..., vmax]`` otherwise. vmin (float, optional): The lower bound of the color-mapping. Not passed to :py:class:`matplotlib.colors.BoundaryNorm`, which does not support it. If given, this argument in combination with ``vmax`` needs to define an integer range that has the same number of values as needed for a colormap constructed from ``from_values`` or via the ``label -> color`` mapping. If ``discretized`` is set, this value will be set to ``ceil(vmin) - 0.5``. vmax (float, optional): The upper bound of the color-mapping. Not passed to :py:class:`matplotlib.colors.BoundaryNorm`, which does not support it. If given, this argument in combination with ``vmin`` needs to define an integer range that has the same number of values as needed for a colormap constructed from ``from_values`` or via the ``label -> color`` mapping. If ``discretized`` is set, this value will be set to ``floor(vmax) + 0.5``. discretized (bool, optional): If True, assumes that the data this colormap is to represent only has integer values and makes a number of changes to improve the overall visualization. For instance, if ``True``, the ``vmin`` and ``vmax`` values will be set to the appropriate half-integer such that tick positions are centered within the corresponding range. If ``None`` (default), will do this automatically if a colormap is constructed via ``from_values`` or via ``label -> color`` mapping. """ self._cmap = None self._norm = None self._labels = None self._vmin = vmin self._vmax = vmax self.discretized = discretized self._cmap_kwargs = None self._norm_kwargs = None cmap_kwargs = None norm_kwargs = dict(name=None) labels_infd = None # .. Parse and set the colormap ....................................... if isinstance(cmap, mpl.colors.Colormap): self._cmap = cmap else: if isinstance(cmap, str) or cmap is None: cmap_kwargs = dict(name=cmap) elif isinstance(cmap, (list, tuple)): cmap_kwargs = dict(from_values=copy.copy(cmap)) else: cmap_kwargs = copy.deepcopy(cmap) cmap_kwargs, norm_kwargs, labels_infd = self._parse_cmap_kwargs( **cmap_kwargs, _labels=labels, ) self._cmap_kwargs = cmap_kwargs self._cmap = self._create_cmap(**cmap_kwargs) # .. Parse and set the norm ........................................... # If Normalize instance is given, set it directly. Otherwise parse the # norm_kwargs below. if isinstance(norm, mpl.colors.Normalize): self._norm = norm else: if norm is not None: if isinstance(norm, str) or norm is None: norm_kwargs["name"] = norm else: norm_kwargs = copy.deepcopy(norm) norm_kwargs = self._parse_norm_kwargs(**norm_kwargs) self._norm_kwargs = norm_kwargs self._norm = self._create_norm(**norm_kwargs) # The norm should be regarded as the authority over vmin and vmax if self.norm.scaled(): self._vmin = self.norm.vmin self._vmax = self.norm.vmax # .. Labels ........................................................... self._labels = self._parse_cbar_labels( labels if labels is not None else labels_infd )
@property def cmap(self) -> mpl.colors.Colormap: """Returns the constructed colormap object""" return self._cmap @property def norm(self) -> mpl.colors.Normalize: """Returns the constructed normalization object""" return self._norm @property def labels(self) -> dict: """A dict or list of colorbar labels""" return self._labels @property def vmin(self) -> Optional[float]: """The ``vmin`` value of the colormap and norm""" return self._vmin @property def vmax(self) -> Optional[float]: """The ``vmax`` value of the colormap and norm""" return self._vmax # .........................................................................
[docs] def map_to_color(self, X: Union[float, np.ndarray]): """Maps the input data to color(s) by applying both norm and colormap. Args: X (Union[float, numpy.ndarray]): Data value(s) to convert to RGBA. Returns: Tuple of RGBA values if X is scalar, otherwise an array of RGBA values with a shape of ``X.shape + (4, )``. """ return self.cmap(self.norm(X))
[docs] def create_cbar( self, mappable: "matplotlib.cm.ScalarMappable", *, fig: "matplotlib.figure.Figure" = None, ax: "matplotlib.axes.Axes" = None, label: str = None, label_kwargs: dict = None, tick_params: dict = None, extend: str = "auto", **cbar_kwargs, ) -> "matplotlib.colorbar.Colorbar": """Creates a colorbar of a given mappable Args: mappable (matplotlib.cm.ScalarMappable): The mappable that is to be described by the colorbar. fig (matplotlib.figure.Figure, optional): The figure; if not given, will use the current figure as determined by :py:func:`~matplotlib.pyplot.gcf`. ax (matplotlib.axes.Axes, optional): The axes; if not given, will use the one given by :py:meth:`matplotlib.figure.Figure.gca`. label (str, optional): A label for the colorbar label_kwargs (dict, optional): Additional parameters passed to :py:meth:`matplotlib.colorbar.Colorbar.set_label` tick_params (dict, optional): Set colorbar tick parameters via the :py:meth:`matplotlib.axes.Axes.tick_params` method of the :py:class:`matplotlib.colorbar.Colorbar` axes. extend (str, optional): Whether to extend the colorbar axis to show the ``under`` and ``over`` values. If ``auto`` (default), will inspect whether the colormap has these values set and decide accordingly. Can also be set manually, possible values being ``neither``, ``min``, ``max``, and ``both``. **cbar_kwargs: Passed on to :py:meth:`matplotlib.figure.Figure.colorbar` Returns: matplotlib.colorbar.Colorbar: The created colorbar object """ import matplotlib.pyplot as plt if fig is None: fig = plt.gcf() if ax is None: ax = fig.gca() # Determine extend if extend == "auto": EXTEND_MAP = { (False, False): "neither", (False, True): "max", (True, False): "min", (True, True): "both", } extend = EXTEND_MAP[ ( self.cmap._rgba_under is not None, self.cmap._rgba_over is not None, ) ] # Create the colorbar and set its label, ticks, tick labels cb = fig.colorbar(mappable, ax=ax, extend=extend, **cbar_kwargs) if label: cb.set_label(label=label, **(label_kwargs if label_kwargs else {})) if self.labels is not None: cb.set_ticks(list(self.labels.keys())) cb.set_ticklabels(list(self.labels.values())) if tick_params: cb.ax.tick_params(**tick_params) return cb
# .........................................................................
[docs] def _parse_cmap_kwargs( self, *, _labels: Union[list, dict], name: str = None, continuous: bool = None, from_values: Union[list, dict] = None, placeholder_color: str = "w", labels_and_colors: dict = None, **kwargs, ) -> Tuple[dict, dict, dict]: """ Args: _labels (Union[list, dict]): The (top-level!) ``labels`` argument. While not being parsed here, it is needed for informative error messages. name (str, optional): Name of the colormap continuous (bool, optional): Whether to create a continuous or a discrete colormap. from_values (Union[dict, list], optional): The values from which to create the colormap. Keys are either given explicitly or inferred using :py:meth:`_infer_pos_map`. placeholder_color (str, optional): Color used when a value in ``from_values`` did not specify a value. **kwargs: combined keyword arguments for the colormap creation and shorthand entries for ``label -> color`` mapping. """ def parse_from_values( mapping: Union[Sequence[str], Dict[float, str]], *, cmap_kwargs: dict, norm_kwargs: dict, continuous: bool, ): """Populates ``cmap_kwargs`` and ``norm_kwargs`` from the given mapping of values to colors. """ if not isinstance(mapping, dict): mapping = self._infer_pos_map( mapping, vmin=self._vmin, vmax=self._vmax ) # Replace all None entries by the placeholder color. mapping = { k: (v if v is not None else placeholder_color) for k, v in mapping.items() } # Distinguish between continous and discrete case if continuous: # Get the colordict used to generate the continuous colormap, # complying to the interface of LinearSegmentedColormap: # For each of RGB, construct a sequence of (x, y0, y1) tuples # that define how that color changes. The colormap will then # interpolate between the color values. cdict = dict() for num, col in enumerate(("red", "green", "blue")): cdict[col] = [ ( x, to_rgb(_color)[num], to_rgb(_color)[num], ) for x, _color in mapping.items() ] cmap_kwargs["segmentdata"] = cdict cmap_kwargs["name"] = "LinearSegmentedColormap" log.remark("Configuring a linear colormap 'from values'. ") else: # Discrete case, potentially with binning cmap_kwargs["name"] = "ListedColormap" cmap_kwargs["colors"] = list(mapping.values()) norm_kwargs["name"] = "BoundaryNorm" norm_kwargs["ncolors"] = len(mapping) norm_kwargs["boundaries"] = self._parse_boundaries( list(mapping.keys()), set_vmin_vmax=True, discretized=(self.discretized or self.discretized is None), ) log.remark( "Configuring a discrete colormap 'from values'. " "Setting 'norm' to BoundaryNorm with %d colors.", norm_kwargs["ncolors"], ) return cmap_kwargs, norm_kwargs # ..................................................................... cmap_kwargs = dict(name=name) norm_kwargs = dict() # Filter out arguments that specify the colormap and those that may be # used to denote labels _labels_and_colors = { l: c for l, c in kwargs.items() if l not in self._POSSIBLE_CMAP_KWARGS } cmap_kwargs.update( {k: v for k, v in kwargs.items() if k not in _labels_and_colors} ) if _labels_and_colors: if labels_and_colors: raise ValueError( "The label -> color mapping needs to be given _either_ " "via the explicit `labels_and_colors` argument _or_ " "implicitly via the **kwargs, but got both!" ) labels_and_colors = _labels_and_colors # Basic checks if name and ( from_values or continuous is not None or labels_and_colors ): raise ValueError( "Cannot use argument `name` in combination with argument(s) " "`from_values`, `continuous` and/or the shorthand syntax for " "specifying labels and colors!\n" "Got:\n" f" name: {name}\n" f" from_values: {from_values}\n" f" continuous: {continuous}\n" f" **labels_and_colors: {labels_and_colors}\n" ) # May have used the implicit syntax with labels and colors specified # within the ``cmap`` argument. If so, translate these to the long-form # and explicit syntax. if labels_and_colors: if from_values or continuous is not None: raise ValueError( "Cannot use the shorthand syntax for specifying labels " "and colors in combination with the arguments " "`continuous`, `from_values`!\n" "Either remove those arguments or those that were " "interpreted as belonging to the label -> color mapping: " f"{', '.join(labels_and_colors)}" ) _labels = self._infer_pos_map( labels_and_colors.keys(), vmin=self._vmin, vmax=self._vmax, ) from_values = {k: labels_and_colors[l] for k, l in _labels.items()} # Parse configuration for custom color mapping from values, which can # be either discrete or continuous (interpolated between colors) if from_values: cmap_kwargs, norm_kwargs = parse_from_values( from_values, cmap_kwargs=cmap_kwargs, norm_kwargs=norm_kwargs, continuous=continuous, ) return cmap_kwargs, norm_kwargs, _labels
[docs] def _parse_norm_kwargs(self, *, name: str = None, **kws) -> dict: """Parses the norm arguments into a uniform shape""" norm_kwargs = dict(name=name, **kws) # Some norms accept no vmin/vmax argument if name not in self._NORMS_NOT_SUPPORTING_VMIN_VMAX: norm_kwargs["vmin"] = norm_kwargs.get("vmin", self._vmin) norm_kwargs["vmax"] = norm_kwargs.get("vmax", self._vmax) return norm_kwargs
[docs] def _parse_cbar_labels( self, labels: Union[None, Dict[float, str], Sequence[str]] ) -> Optional[Dict[float, str]]: """Parses the ``labels`` argument into a uniform shape""" def skip_label(l: str) -> bool: if isinstance(l, str) and not l.strip(): return True return False def format_label(l, *, pos: float) -> str: # can do stuff here in the future, e.g. formatting return l if labels is None: return None if not isinstance(labels, dict): labels = self._infer_pos_map( labels, vmin=self._vmin, vmax=self._vmax ) labels = { pos: format_label(l, pos=pos) for pos, l in labels.items() if not skip_label(l) } return copy.deepcopy(labels)
[docs] def _infer_pos_map( self, seq: Sequence[Any], *, vmin: int = None, vmax: int = None ) -> Dict[float, Any]: """Given a sequence, infers a mapping ``position -> value``, where the positions are numeric values and the values of the resulting dict are the ones from the given sequence. If ``vmin`` and ``vmax`` are given, they are used to help with inferring the values. *Note* that these arguments need to be explicitly passed. """ if vmin is None and vmax is not None: _vmax = floor(vmax) _vmin = _vmax - len(seq) + 1 else: _vmin = ceil(vmin) if vmin is not None else 0 _vmax = floor(vmax) if vmax is not None else _vmin + (len(seq) - 1) rg = range(_vmin, _vmax + 1) if len(rg) != len(seq): raise ValueError( "Failed to infer data positions for the given sequence! There " "was a mismatch between the length of the given data " f"sequence ({len(seq)}) and the inferred number of candidate " f"positions ({len(rg)}).\n" "To address this issue, check the `vmin` and `vmax` arguments " "and make sure that they allow an integer mapping (with both " "`vmin` and `vmax` included). Note that these arguments are " "ceiled and floored, respectively, to arrive at integers.\n" f" sequence: {list(seq)}\n" f" vmin: {vmin} \t-> {_vmin} (after ceil)\n" f" vmax: {vmax} \t-> {_vmax} (after floor)\n" f" positions: {str(rg)} -> {list(rg)}\n" ) return {i: v for i, v in zip(rg, seq)}
[docs] def _parse_boundaries( self, bins: Sequence, *, set_vmin_vmax: bool = False, discretized: bool = False, ) -> Tuple[float]: """Parses the boundaries for the :py:class:`~matplotlib.colors.BoundaryNorm`. Args: bins (Sequence): Either monotonically increasing sequence of bin centers or sequence of connected intervals (2-tuples). set_vmin_vmax (bool, optional): Description discretized (bool, optional): Description Returns: Tuple[float]: Monotonically increasing boundaries. Raises: ValueError: On disconnected intervals or decreasing boundaries. """ def from_intervals(intervals) -> list: """Extracts bin edges from sequence of connected intervals""" b = [intervals[0][0]] for low, up in intervals: if up < low: raise ValueError( "Received decreasing boundaries: " f"{up} < {low}\n" "Boundaries should be monotonically increasing. Got:\n" f" {intervals}" ) elif b[-1] != low: raise ValueError( "Received disconnected intervals: Upper " f"bound {b[-1]} and lower bound {low} of " "the proximate interval do not match.\n" f"Specified intervals:\n {intervals}" ) b.append(up) return b def from_centers(centers) -> list: """Calculates the bin edges as the halfway points between adjacent bin centers.""" centers = np.array(list(centers)) if len(centers) < 2: raise ValueError( "At least 2 bin centers must be given to " f"create a BoundaryNorm. Got: {centers}" ) halves = 0.5 * np.diff(centers) left = ( self.vmin if self.vmin is not None else (centers[0] - halves[0]) ) right = ( self.vmax if self.vmax is not None else (centers[-1] + halves[-1]) ) b = [left] + [c + h for c, h in zip(centers, halves)] + [right] return b # ..................................................................... if isinstance(bins[0], tuple): boundaries = from_intervals(bins) else: boundaries = from_centers(bins) # Correction for discretized values left = boundaries[0] right = boundaries[-1] if discretized: if (left % 1) != 0.5: boundaries[0] = ceil(left) - 0.5 if (right % 1) != 0.5: boundaries[-1] = floor(right) + 0.5 if set_vmin_vmax: self._vmin = left self._vmax = right return tuple(boundaries)
[docs] def _create_cmap( self, *, name: str = None, colors: list = None, segmentdata: dict = None, bad: Union[str, dict] = None, under: Union[str, dict] = None, over: Union[str, dict] = None, reversed: bool = False, N: int = None, gamma: float = 1.0, ) -> mpl.colors.Colormap: """Creates a colormap. Args: name (str, optional): The colormap name. Can either be the name of a registered colormap or ``ListedColormap``. ``None`` means that the default value from the RC parameters (``image.cmap``) is used. If the name starts with the :py:attr:`._SNS_COLOR_PALETTE_PREFIX`, the colormap can be created by :py:func:`seaborn.color_palette`. See `the seaborn docs <https://seaborn.pydata.org/tutorial/color_palettes.html>`_ for available options. colors (list, optional): Passed on to :py:class:`matplotlib.colors.ListedColormap`, ignored otherwise segmentdata (dict, optional): Description bad (Union[str, dict], optional): Set color to be used for masked values. under (Union[str, dict], optional): Set the color for low out-of-range values when ``norm.clip = False``. over (Union[str, dict], optional): Set the color for high out-of-range values when ``norm.clip = False``. reversed (bool, optional): Reverses the colormap N (int, optional): Passed on to :py:class:`matplotlib.colors.ListedColormap` or :py:class:`matplotlib.colors.LinearSegmentedColormap`, ignored otherwise. gamma (float, optional): Passed on to :py:class:`matplotlib.colors.LinearSegmentedColormap` Returns: matplotlib.colors.Colormap: The created colormap. Raises: ValueError: On invalid colormap name. """ import seaborn as sns SNS_CP_PREFIX = self._SNS_COLOR_PALETTE_PREFIX SNS_DIV_PREFIX = self._SNS_DIVERGING_PALETTE_PREFIX # Depending on the name, use different constructors if name == "ListedColormap": cmap = mpl.colors.ListedColormap(colors, name=name, N=N) elif name == "LinearSegmentedColormap": cmap = mpl.colors.LinearSegmentedColormap( name, segmentdata, N=(N if N is not None else 256), gamma=gamma, ) elif name is not None and name.startswith(SNS_CP_PREFIX): name = name[len(SNS_CP_PREFIX) :].strip() cmap = sns.color_palette(name, as_cmap=True) elif name is not None and name.startswith(SNS_DIV_PREFIX): # Parse strings like 'diverging::65,0,sep=12' into args and kwargs args, kwargs = parse_str_to_args_and_kwargs( name[len(SNS_DIV_PREFIX) :], sep="," ) try: cmap = sns.diverging_palette(*args, **kwargs, as_cmap=True) except Exception as exc: raise ValueError( "Failed constructing a seaborn diverging palette from the " f"given string-specification '{name}'! " f"Got a {type(exc).__name__}: {exc}\n\n" "Check that no arguments are missing and all given " "arguments are valid. The above string was parsed into " "the following positional and keyword arguments:\n" f" args: {args}\n" f" kwargs: {kwargs}\n" ) from exc else: if name is None: name = mpl.rcParams["image.cmap"] # Get the colormap from the ColormapRegistry try: cmap = mpl.colormaps[name] except KeyError as err: _avail = make_columns( sorted( [cm for cm in mpl.colormaps if not cm.endswith("_r")] ) ) raise ValueError( f"'{name}' is not a known colormap name!\n" f"Available named colormaps:\n{_avail}\n" "Additional ways to specify colormaps by name:\n" " - Add '_r' suffix to the name to reverse it\n" f" - Add '{SNS_CP_PREFIX}' prefix to define a seaborn " "color palette\n" f" - Add '{SNS_DIV_PREFIX}' prefix to specify a " "diverging seaborn color map\n\n" "See dantro ColorManager documentation for more." ) from err # Parse some parameters if isinstance(bad, str): bad = dict(color=bad) if isinstance(under, str): under = dict(color=under) if isinstance(over, str): over = dict(color=over) # Set bad, under, over if bad is not None: cmap.set_bad(**bad) if under is not None: cmap.set_under(**under) if over is not None: cmap.set_over(**over) # Optionally, reverse the colormap if reversed: cmap = cmap.reversed() return cmap
[docs] def _create_norm( self, name: str = None, **norm_kwargs ) -> "matplotlib.colors.Normalize": r"""Creates a norm. Args: name (str, optional): The norm name. Must name a :py:class:`matplotlib.colors.Normalize` instance (see `matplotlib.colors <https://matplotlib.org/api/colors_api.html>`_). ``None`` means that the base class, ``Normalize``, is used. **norm_kwargs: Passed on to the constructor of the norm. Returns: matplotlib.colors.Normalize: The created norm. Raises: ValueError: On invalid norm specification. """ if name is None: name = "Normalize" if name not in NORMS: available_norms = ", ".join(NORMS) raise ValueError( f"Received invalid norm specifier '{name}'! " f"Must be one of: {available_norms}" ) return NORMS[name](**norm_kwargs)
# -- Supporting functions -----------------------------------------------------
[docs]def parse_cmap_and_norm_kwargs( *, _key_map: dict = None, use_color_manager: bool = True, **kws ) -> dict: """A function that parses colormap-related keyword arguments and passes them through the :py:class:`.ColorManager`, making its functionality available in places that would otherwise not be able to use the expanded syntax of the color manager. .. note:: The resulting dict will only have the ``cmap`` and ``cbar`` kwargs (or their mapped equivalents) set from the color manager, all other arguments are simply passed through. In particular, this means that the ``labels`` feature of the color manager is *not* supported, because this function has no ability to set the colorbar. Args: _key_map (dict, optional): If custom keyword argument keys are expected as output, e.g. ``hue_cmap`` instead of ``cmap``, set the values to these custom names: ``{"cmap": "hue_cmap"}``. Expected keys are ``cmap``, ``norm``, ``vmin``, ``vmax``. If not set or partially not set, will use defaults. use_color_manager (bool, optional): If false, will simply pass through **kws: Keyword arguments to parse Returns: dict: The updated keyword arguments with ``cmap`` and ``norm`` (or equivalent keys according to ``_key_map``). """ if not use_color_manager: return kws _key = dict(cmap="cmap", norm="norm", vmin="vmin", vmax="vmax") if _key_map is not None: _key.update(_key_map) if _key["cmap"] not in kws and _key["norm"] not in kws: return kws # otherwise: Create a ColorManager cm = ColorManager( cmap=kws.get(_key["cmap"]), norm=kws.get(_key["norm"]), vmin=kws.get(_key["vmin"]), vmax=kws.get(_key["vmax"]), ) # Evaluate it, only setting keys if they were there before if kws.get(_key["cmap"]) is not None: kws[_key["cmap"]] = cm.cmap if kws.get(_key["norm"]) is not None: kws[_key["norm"]] = cm.norm return kws