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