Coverage for /usr/lib/python3/dist-packages/matplotlib/colors.py: 44%
1021 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""
2A module for converting numbers or color arguments to *RGB* or *RGBA*.
4*RGB* and *RGBA* are sequences of, respectively, 3 or 4 floats in the
5range 0-1.
7This module includes functions and classes for color specification conversions,
8and for mapping numbers to colors in a 1-D array of colors called a colormap.
10Mapping data onto colors using a colormap typically involves two steps: a data
11array is first mapped onto the range 0-1 using a subclass of `Normalize`,
12then this number is mapped to a color using a subclass of `Colormap`. Two
13subclasses of `Colormap` provided here: `LinearSegmentedColormap`, which uses
14piecewise-linear interpolation to define colormaps, and `ListedColormap`, which
15makes a colormap from a list of colors.
17.. seealso::
19 :doc:`/tutorials/colors/colormap-manipulation` for examples of how to
20 make colormaps and
22 :doc:`/tutorials/colors/colormaps` for a list of built-in colormaps.
24 :doc:`/tutorials/colors/colormapnorms` for more details about data
25 normalization
27 More colormaps are available at palettable_.
29The module also provides functions for checking whether an object can be
30interpreted as a color (`is_color_like`), for converting such an object
31to an RGBA tuple (`to_rgba`) or to an HTML-like hex string in the
32"#rrggbb" format (`to_hex`), and a sequence of colors to an (n, 4)
33RGBA array (`to_rgba_array`). Caching is used for efficiency.
35Colors that Matplotlib recognizes are listed at
36:doc:`/tutorials/colors/colors`.
38.. _palettable: https://jiffyclub.github.io/palettable/
39.. _xkcd color survey: https://xkcd.com/color/rgb/
40"""
42import base64
43from collections.abc import Sized, Sequence, Mapping
44import functools
45import importlib
46import inspect
47import io
48import itertools
49from numbers import Number
50import re
51from PIL import Image
52from PIL.PngImagePlugin import PngInfo
54import matplotlib as mpl
55import numpy as np
56from matplotlib import _api, _cm, cbook, scale
57from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS
60class _ColorMapping(dict):
61 def __init__(self, mapping):
62 super().__init__(mapping)
63 self.cache = {}
65 def __setitem__(self, key, value):
66 super().__setitem__(key, value)
67 self.cache.clear()
69 def __delitem__(self, key):
70 super().__delitem__(key)
71 self.cache.clear()
74_colors_full_map = {}
75# Set by reverse priority order.
76_colors_full_map.update(XKCD_COLORS)
77_colors_full_map.update({k.replace('grey', 'gray'): v
78 for k, v in XKCD_COLORS.items()
79 if 'grey' in k})
80_colors_full_map.update(CSS4_COLORS)
81_colors_full_map.update(TABLEAU_COLORS)
82_colors_full_map.update({k.replace('gray', 'grey'): v
83 for k, v in TABLEAU_COLORS.items()
84 if 'gray' in k})
85_colors_full_map.update(BASE_COLORS)
86_colors_full_map = _ColorMapping(_colors_full_map)
88_REPR_PNG_SIZE = (512, 64)
91def get_named_colors_mapping():
92 """Return the global mapping of names to named colors."""
93 return _colors_full_map
96class ColorSequenceRegistry(Mapping):
97 r"""
98 Container for sequences of colors that are known to Matplotlib by name.
100 The universal registry instance is `matplotlib.color_sequences`. There
101 should be no need for users to instantiate `.ColorSequenceRegistry`
102 themselves.
104 Read access uses a dict-like interface mapping names to lists of colors::
106 import matplotlib as mpl
107 cmap = mpl.color_sequences['tab10']
109 The returned lists are copies, so that their modification does not change
110 the global definition of the color sequence.
112 Additional color sequences can be added via
113 `.ColorSequenceRegistry.register`::
115 mpl.color_sequences.register('rgb', ['r', 'g', 'b'])
116 """
118 _BUILTIN_COLOR_SEQUENCES = {
119 'tab10': _cm._tab10_data,
120 'tab20': _cm._tab20_data,
121 'tab20b': _cm._tab20b_data,
122 'tab20c': _cm._tab20c_data,
123 'Pastel1': _cm._Pastel1_data,
124 'Pastel2': _cm._Pastel2_data,
125 'Paired': _cm._Paired_data,
126 'Accent': _cm._Accent_data,
127 'Dark2': _cm._Dark2_data,
128 'Set1': _cm._Set1_data,
129 'Set2': _cm._Set1_data,
130 'Set3': _cm._Set1_data,
131 }
133 def __init__(self):
134 self._color_sequences = {**self._BUILTIN_COLOR_SEQUENCES}
136 def __getitem__(self, item):
137 try:
138 return list(self._color_sequences[item])
139 except KeyError:
140 raise KeyError(f"{item!r} is not a known color sequence name")
142 def __iter__(self):
143 return iter(self._color_sequences)
145 def __len__(self):
146 return len(self._color_sequences)
148 def __str__(self):
149 return ('ColorSequenceRegistry; available colormaps:\n' +
150 ', '.join(f"'{name}'" for name in self))
152 def register(self, name, color_list):
153 """
154 Register a new color sequence.
156 The color sequence registry stores a copy of the given *color_list*, so
157 that future changes to the original list do not affect the registered
158 color sequence. Think of this as the registry taking a snapshot
159 of *color_list* at registration.
161 Parameters
162 ----------
163 name : str
164 The name for the color sequence.
166 color_list : list of colors
167 An iterable returning valid Matplotlib colors when iterating over.
168 Note however that the returned color sequence will always be a
169 list regardless of the input type.
171 """
172 if name in self._BUILTIN_COLOR_SEQUENCES:
173 raise ValueError(f"{name!r} is a reserved name for a builtin "
174 "color sequence")
176 color_list = list(color_list) # force copy and coerce type to list
177 for color in color_list:
178 try:
179 to_rgba(color)
180 except ValueError:
181 raise ValueError(
182 f"{color!r} is not a valid color specification")
184 self._color_sequences[name] = color_list
186 def unregister(self, name):
187 """
188 Remove a sequence from the registry.
190 You cannot remove built-in color sequences.
192 If the name is not registered, returns with no error.
193 """
194 if name in self._BUILTIN_COLOR_SEQUENCES:
195 raise ValueError(
196 f"Cannot unregister builtin color sequence {name!r}")
197 self._color_sequences.pop(name, None)
200_color_sequences = ColorSequenceRegistry()
203def _sanitize_extrema(ex):
204 if ex is None:
205 return ex
206 try:
207 ret = ex.item()
208 except AttributeError:
209 ret = float(ex)
210 return ret
213def _is_nth_color(c):
214 """Return whether *c* can be interpreted as an item in the color cycle."""
215 return isinstance(c, str) and re.match(r"\AC[0-9]+\Z", c)
218def is_color_like(c):
219 """Return whether *c* can be interpreted as an RGB(A) color."""
220 # Special-case nth color syntax because it cannot be parsed during setup.
221 if _is_nth_color(c):
222 return True
223 try:
224 to_rgba(c)
225 except ValueError:
226 return False
227 else:
228 return True
231def _has_alpha_channel(c):
232 """Return whether *c* is a color with an alpha channel."""
233 # 4-element sequences are interpreted as r, g, b, a
234 return not isinstance(c, str) and len(c) == 4
237def _check_color_like(**kwargs):
238 """
239 For each *key, value* pair in *kwargs*, check that *value* is color-like.
240 """
241 for k, v in kwargs.items():
242 if not is_color_like(v):
243 raise ValueError(f"{v!r} is not a valid value for {k}")
246def same_color(c1, c2):
247 """
248 Return whether the colors *c1* and *c2* are the same.
250 *c1*, *c2* can be single colors or lists/arrays of colors.
251 """
252 c1 = to_rgba_array(c1)
253 c2 = to_rgba_array(c2)
254 n1 = max(c1.shape[0], 1) # 'none' results in shape (0, 4), but is 1-elem
255 n2 = max(c2.shape[0], 1) # 'none' results in shape (0, 4), but is 1-elem
257 if n1 != n2:
258 raise ValueError('Different number of elements passed.')
259 # The following shape test is needed to correctly handle comparisons with
260 # 'none', which results in a shape (0, 4) array and thus cannot be tested
261 # via value comparison.
262 return c1.shape == c2.shape and (c1 == c2).all()
265def to_rgba(c, alpha=None):
266 """
267 Convert *c* to an RGBA color.
269 Parameters
270 ----------
271 c : Matplotlib color or ``np.ma.masked``
273 alpha : float, optional
274 If *alpha* is given, force the alpha value of the returned RGBA tuple
275 to *alpha*.
277 If None, the alpha value from *c* is used. If *c* does not have an
278 alpha channel, then alpha defaults to 1.
280 *alpha* is ignored for the color value ``"none"`` (case-insensitive),
281 which always maps to ``(0, 0, 0, 0)``.
283 Returns
284 -------
285 tuple
286 Tuple of floats ``(r, g, b, a)``, where each channel (red, green, blue,
287 alpha) can assume values between 0 and 1.
288 """
289 # Special-case nth color syntax because it should not be cached.
290 if _is_nth_color(c):
291 prop_cycler = mpl.rcParams['axes.prop_cycle']
292 colors = prop_cycler.by_key().get('color', ['k'])
293 c = colors[int(c[1:]) % len(colors)]
294 try:
295 rgba = _colors_full_map.cache[c, alpha]
296 except (KeyError, TypeError): # Not in cache, or unhashable.
297 rgba = None
298 if rgba is None: # Suppress exception chaining of cache lookup failure.
299 rgba = _to_rgba_no_colorcycle(c, alpha)
300 try:
301 _colors_full_map.cache[c, alpha] = rgba
302 except TypeError:
303 pass
304 return rgba
307def _to_rgba_no_colorcycle(c, alpha=None):
308 """
309 Convert *c* to an RGBA color, with no support for color-cycle syntax.
311 If *alpha* is given, force the alpha value of the returned RGBA tuple
312 to *alpha*. Otherwise, the alpha value from *c* is used, if it has alpha
313 information, or defaults to 1.
315 *alpha* is ignored for the color value ``"none"`` (case-insensitive),
316 which always maps to ``(0, 0, 0, 0)``.
317 """
318 orig_c = c
319 if c is np.ma.masked:
320 return (0., 0., 0., 0.)
321 if isinstance(c, str):
322 if c.lower() == "none":
323 return (0., 0., 0., 0.)
324 # Named color.
325 try:
326 # This may turn c into a non-string, so we check again below.
327 c = _colors_full_map[c]
328 except KeyError:
329 if len(orig_c) != 1:
330 try:
331 c = _colors_full_map[c.lower()]
332 except KeyError:
333 pass
334 if isinstance(c, str):
335 # hex color in #rrggbb format.
336 match = re.match(r"\A#[a-fA-F0-9]{6}\Z", c)
337 if match:
338 return (tuple(int(n, 16) / 255
339 for n in [c[1:3], c[3:5], c[5:7]])
340 + (alpha if alpha is not None else 1.,))
341 # hex color in #rgb format, shorthand for #rrggbb.
342 match = re.match(r"\A#[a-fA-F0-9]{3}\Z", c)
343 if match:
344 return (tuple(int(n, 16) / 255
345 for n in [c[1]*2, c[2]*2, c[3]*2])
346 + (alpha if alpha is not None else 1.,))
347 # hex color with alpha in #rrggbbaa format.
348 match = re.match(r"\A#[a-fA-F0-9]{8}\Z", c)
349 if match:
350 color = [int(n, 16) / 255
351 for n in [c[1:3], c[3:5], c[5:7], c[7:9]]]
352 if alpha is not None:
353 color[-1] = alpha
354 return tuple(color)
355 # hex color with alpha in #rgba format, shorthand for #rrggbbaa.
356 match = re.match(r"\A#[a-fA-F0-9]{4}\Z", c)
357 if match:
358 color = [int(n, 16) / 255
359 for n in [c[1]*2, c[2]*2, c[3]*2, c[4]*2]]
360 if alpha is not None:
361 color[-1] = alpha
362 return tuple(color)
363 # string gray.
364 try:
365 c = float(c)
366 except ValueError:
367 pass
368 else:
369 if not (0 <= c <= 1):
370 raise ValueError(
371 f"Invalid string grayscale value {orig_c!r}. "
372 f"Value must be within 0-1 range")
373 return c, c, c, alpha if alpha is not None else 1.
374 raise ValueError(f"Invalid RGBA argument: {orig_c!r}")
375 # turn 2-D array into 1-D array
376 if isinstance(c, np.ndarray):
377 if c.ndim == 2 and c.shape[0] == 1:
378 c = c.reshape(-1)
379 # tuple color.
380 if not np.iterable(c):
381 raise ValueError(f"Invalid RGBA argument: {orig_c!r}")
382 if len(c) not in [3, 4]:
383 raise ValueError("RGBA sequence should have length 3 or 4")
384 if not all(isinstance(x, Number) for x in c):
385 # Checks that don't work: `map(float, ...)`, `np.array(..., float)` and
386 # `np.array(...).astype(float)` would all convert "0.5" to 0.5.
387 raise ValueError(f"Invalid RGBA argument: {orig_c!r}")
388 # Return a tuple to prevent the cached value from being modified.
389 c = tuple(map(float, c))
390 if len(c) == 3 and alpha is None:
391 alpha = 1
392 if alpha is not None:
393 c = c[:3] + (alpha,)
394 if any(elem < 0 or elem > 1 for elem in c):
395 raise ValueError("RGBA values should be within 0-1 range")
396 return c
399def to_rgba_array(c, alpha=None):
400 """
401 Convert *c* to a (n, 4) array of RGBA colors.
403 Parameters
404 ----------
405 c : Matplotlib color or array of colors
406 If *c* is a masked array, an ndarray is returned with a (0, 0, 0, 0)
407 row for each masked value or row in *c*.
409 alpha : float or sequence of floats, optional
410 If *alpha* is given, force the alpha value of the returned RGBA tuple
411 to *alpha*.
413 If None, the alpha value from *c* is used. If *c* does not have an
414 alpha channel, then alpha defaults to 1.
416 *alpha* is ignored for the color value ``"none"`` (case-insensitive),
417 which always maps to ``(0, 0, 0, 0)``.
419 If *alpha* is a sequence and *c* is a single color, *c* will be
420 repeated to match the length of *alpha*.
422 Returns
423 -------
424 array
425 (n, 4) array of RGBA colors, where each channel (red, green, blue,
426 alpha) can assume values between 0 and 1.
427 """
428 # Special-case inputs that are already arrays, for performance. (If the
429 # array has the wrong kind or shape, raise the error during one-at-a-time
430 # conversion.)
431 if np.iterable(alpha):
432 alpha = np.asarray(alpha).ravel()
433 if (isinstance(c, np.ndarray) and c.dtype.kind in "if"
434 and c.ndim == 2 and c.shape[1] in [3, 4]):
435 mask = c.mask.any(axis=1) if np.ma.is_masked(c) else None
436 c = np.ma.getdata(c)
437 if np.iterable(alpha):
438 if c.shape[0] == 1 and alpha.shape[0] > 1:
439 c = np.tile(c, (alpha.shape[0], 1))
440 elif c.shape[0] != alpha.shape[0]:
441 raise ValueError("The number of colors must match the number"
442 " of alpha values if there are more than one"
443 " of each.")
444 if c.shape[1] == 3:
445 result = np.column_stack([c, np.zeros(len(c))])
446 result[:, -1] = alpha if alpha is not None else 1.
447 elif c.shape[1] == 4:
448 result = c.copy()
449 if alpha is not None:
450 result[:, -1] = alpha
451 if mask is not None:
452 result[mask] = 0
453 if np.any((result < 0) | (result > 1)):
454 raise ValueError("RGBA values should be within 0-1 range")
455 return result
456 # Handle single values.
457 # Note that this occurs *after* handling inputs that are already arrays, as
458 # `to_rgba(c, alpha)` (below) is expensive for such inputs, due to the need
459 # to format the array in the ValueError message(!).
460 if cbook._str_lower_equal(c, "none"):
461 return np.zeros((0, 4), float)
462 try:
463 if np.iterable(alpha):
464 return np.array([to_rgba(c, a) for a in alpha], float)
465 else:
466 return np.array([to_rgba(c, alpha)], float)
467 except (ValueError, TypeError):
468 pass
470 if isinstance(c, str):
471 raise ValueError(f"{c!r} is not a valid color value.")
473 if len(c) == 0:
474 return np.zeros((0, 4), float)
476 # Quick path if the whole sequence can be directly converted to a numpy
477 # array in one shot.
478 if isinstance(c, Sequence):
479 lens = {len(cc) if isinstance(cc, (list, tuple)) else -1 for cc in c}
480 if lens == {3}:
481 rgba = np.column_stack([c, np.ones(len(c))])
482 elif lens == {4}:
483 rgba = np.array(c)
484 else:
485 rgba = np.array([to_rgba(cc) for cc in c])
486 else:
487 rgba = np.array([to_rgba(cc) for cc in c])
489 if alpha is not None:
490 rgba[:, 3] = alpha
491 return rgba
494def to_rgb(c):
495 """Convert *c* to an RGB color, silently dropping the alpha channel."""
496 return to_rgba(c)[:3]
499def to_hex(c, keep_alpha=False):
500 """
501 Convert *c* to a hex color.
503 Parameters
504 ----------
505 c : :doc:`color </tutorials/colors/colors>` or `numpy.ma.masked`
507 keep_alpha : bool, default: False
508 If False, use the ``#rrggbb`` format, otherwise use ``#rrggbbaa``.
510 Returns
511 -------
512 str
513 ``#rrggbb`` or ``#rrggbbaa`` hex color string
514 """
515 c = to_rgba(c)
516 if not keep_alpha:
517 c = c[:3]
518 return "#" + "".join(format(int(round(val * 255)), "02x") for val in c)
521### Backwards-compatible color-conversion API
524cnames = CSS4_COLORS
525hexColorPattern = re.compile(r"\A#[a-fA-F0-9]{6}\Z")
526rgb2hex = to_hex
527hex2color = to_rgb
530class ColorConverter:
531 """
532 A class only kept for backwards compatibility.
534 Its functionality is entirely provided by module-level functions.
535 """
536 colors = _colors_full_map
537 cache = _colors_full_map.cache
538 to_rgb = staticmethod(to_rgb)
539 to_rgba = staticmethod(to_rgba)
540 to_rgba_array = staticmethod(to_rgba_array)
543colorConverter = ColorConverter()
546### End of backwards-compatible color-conversion API
549def _create_lookup_table(N, data, gamma=1.0):
550 r"""
551 Create an *N* -element 1D lookup table.
553 This assumes a mapping :math:`f : [0, 1] \rightarrow [0, 1]`. The returned
554 data is an array of N values :math:`y = f(x)` where x is sampled from
555 [0, 1].
557 By default (*gamma* = 1) x is equidistantly sampled from [0, 1]. The
558 *gamma* correction factor :math:`\gamma` distorts this equidistant
559 sampling by :math:`x \rightarrow x^\gamma`.
561 Parameters
562 ----------
563 N : int
564 The number of elements of the created lookup table; at least 1.
566 data : (M, 3) array-like or callable
567 Defines the mapping :math:`f`.
569 If a (M, 3) array-like, the rows define values (x, y0, y1). The x
570 values must start with x=0, end with x=1, and all x values be in
571 increasing order.
573 A value between :math:`x_i` and :math:`x_{i+1}` is mapped to the range
574 :math:`y^1_{i-1} \ldots y^0_i` by linear interpolation.
576 For the simple case of a y-continuous mapping, y0 and y1 are identical.
578 The two values of y are to allow for discontinuous mapping functions.
579 E.g. a sawtooth with a period of 0.2 and an amplitude of 1 would be::
581 [(0, 1, 0), (0.2, 1, 0), (0.4, 1, 0), ..., [(1, 1, 0)]
583 In the special case of ``N == 1``, by convention the returned value
584 is y0 for x == 1.
586 If *data* is a callable, it must accept and return numpy arrays::
588 data(x : ndarray) -> ndarray
590 and map values between 0 - 1 to 0 - 1.
592 gamma : float
593 Gamma correction factor for input distribution x of the mapping.
595 See also https://en.wikipedia.org/wiki/Gamma_correction.
597 Returns
598 -------
599 array
600 The lookup table where ``lut[x * (N-1)]`` gives the closest value
601 for values of x between 0 and 1.
603 Notes
604 -----
605 This function is internally used for `.LinearSegmentedColormap`.
606 """
608 if callable(data):
609 xind = np.linspace(0, 1, N) ** gamma
610 lut = np.clip(np.array(data(xind), dtype=float), 0, 1)
611 return lut
613 try:
614 adata = np.array(data)
615 except Exception as err:
616 raise TypeError("data must be convertible to an array") from err
617 _api.check_shape((None, 3), data=adata)
619 x = adata[:, 0]
620 y0 = adata[:, 1]
621 y1 = adata[:, 2]
623 if x[0] != 0. or x[-1] != 1.0:
624 raise ValueError(
625 "data mapping points must start with x=0 and end with x=1")
626 if (np.diff(x) < 0).any():
627 raise ValueError("data mapping points must have x in increasing order")
628 # begin generation of lookup table
629 if N == 1:
630 # convention: use the y = f(x=1) value for a 1-element lookup table
631 lut = np.array(y0[-1])
632 else:
633 x = x * (N - 1)
634 xind = (N - 1) * np.linspace(0, 1, N) ** gamma
635 ind = np.searchsorted(x, xind)[1:-1]
637 distance = (xind[1:-1] - x[ind - 1]) / (x[ind] - x[ind - 1])
638 lut = np.concatenate([
639 [y1[0]],
640 distance * (y0[ind] - y1[ind - 1]) + y1[ind - 1],
641 [y0[-1]],
642 ])
643 # ensure that the lut is confined to values between 0 and 1 by clipping it
644 return np.clip(lut, 0.0, 1.0)
647class Colormap:
648 """
649 Baseclass for all scalar to RGBA mappings.
651 Typically, Colormap instances are used to convert data values (floats)
652 from the interval ``[0, 1]`` to the RGBA color that the respective
653 Colormap represents. For scaling of data into the ``[0, 1]`` interval see
654 `matplotlib.colors.Normalize`. Subclasses of `matplotlib.cm.ScalarMappable`
655 make heavy use of this ``data -> normalize -> map-to-color`` processing
656 chain.
657 """
659 def __init__(self, name, N=256):
660 """
661 Parameters
662 ----------
663 name : str
664 The name of the colormap.
665 N : int
666 The number of RGB quantization levels.
667 """
668 self.name = name
669 self.N = int(N) # ensure that N is always int
670 self._rgba_bad = (0.0, 0.0, 0.0, 0.0) # If bad, don't paint anything.
671 self._rgba_under = None
672 self._rgba_over = None
673 self._i_under = self.N
674 self._i_over = self.N + 1
675 self._i_bad = self.N + 2
676 self._isinit = False
677 #: When this colormap exists on a scalar mappable and colorbar_extend
678 #: is not False, colorbar creation will pick up ``colorbar_extend`` as
679 #: the default value for the ``extend`` keyword in the
680 #: `matplotlib.colorbar.Colorbar` constructor.
681 self.colorbar_extend = False
683 def __call__(self, X, alpha=None, bytes=False):
684 """
685 Parameters
686 ----------
687 X : float or int, ndarray or scalar
688 The data value(s) to convert to RGBA.
689 For floats, X should be in the interval ``[0.0, 1.0]`` to
690 return the RGBA values ``X*100`` percent along the Colormap line.
691 For integers, X should be in the interval ``[0, Colormap.N)`` to
692 return RGBA values *indexed* from the Colormap with index ``X``.
693 alpha : float or array-like or None
694 Alpha must be a scalar between 0 and 1, a sequence of such
695 floats with shape matching X, or None.
696 bytes : bool
697 If False (default), the returned RGBA values will be floats in the
698 interval ``[0, 1]`` otherwise they will be uint8s in the interval
699 ``[0, 255]``.
701 Returns
702 -------
703 Tuple of RGBA values if X is scalar, otherwise an array of
704 RGBA values with a shape of ``X.shape + (4, )``.
705 """
706 if not self._isinit:
707 self._init()
709 # Take the bad mask from a masked array, or in all other cases defer
710 # np.isnan() to after we have converted to an array.
711 mask_bad = X.mask if np.ma.is_masked(X) else None
712 xa = np.array(X, copy=True)
713 if mask_bad is None:
714 mask_bad = np.isnan(xa)
715 if not xa.dtype.isnative:
716 xa = xa.byteswap().newbyteorder() # Native byteorder is faster.
717 if xa.dtype.kind == "f":
718 with np.errstate(invalid="ignore"):
719 xa *= self.N
720 # Negative values are out of range, but astype(int) would
721 # truncate them towards zero.
722 xa[xa < 0] = -1
723 # xa == 1 (== N after multiplication) is not out of range.
724 xa[xa == self.N] = self.N - 1
725 # Avoid converting large positive values to negative integers.
726 np.clip(xa, -1, self.N, out=xa)
727 xa = xa.astype(int)
728 # Set the over-range indices before the under-range;
729 # otherwise the under-range values get converted to over-range.
730 xa[xa > self.N - 1] = self._i_over
731 xa[xa < 0] = self._i_under
732 xa[mask_bad] = self._i_bad
734 lut = self._lut
735 if bytes:
736 lut = (lut * 255).astype(np.uint8)
738 rgba = lut.take(xa, axis=0, mode='clip')
740 if alpha is not None:
741 alpha = np.clip(alpha, 0, 1)
742 if bytes:
743 alpha *= 255 # Will be cast to uint8 upon assignment.
744 if alpha.shape not in [(), xa.shape]:
745 raise ValueError(
746 f"alpha is array-like but its shape {alpha.shape} does "
747 f"not match that of X {xa.shape}")
748 rgba[..., -1] = alpha
750 # If the "bad" color is all zeros, then ignore alpha input.
751 if (lut[-1] == 0).all() and np.any(mask_bad):
752 if np.iterable(mask_bad) and mask_bad.shape == xa.shape:
753 rgba[mask_bad] = (0, 0, 0, 0)
754 else:
755 rgba[..., :] = (0, 0, 0, 0)
757 if not np.iterable(X):
758 rgba = tuple(rgba)
759 return rgba
761 def __copy__(self):
762 cls = self.__class__
763 cmapobject = cls.__new__(cls)
764 cmapobject.__dict__.update(self.__dict__)
765 if self._isinit:
766 cmapobject._lut = np.copy(self._lut)
767 return cmapobject
769 def __eq__(self, other):
770 if (not isinstance(other, Colormap) or self.name != other.name or
771 self.colorbar_extend != other.colorbar_extend):
772 return False
773 # To compare lookup tables the Colormaps have to be initialized
774 if not self._isinit:
775 self._init()
776 if not other._isinit:
777 other._init()
778 return np.array_equal(self._lut, other._lut)
780 def get_bad(self):
781 """Get the color for masked values."""
782 if not self._isinit:
783 self._init()
784 return np.array(self._lut[self._i_bad])
786 def set_bad(self, color='k', alpha=None):
787 """Set the color for masked values."""
788 self._rgba_bad = to_rgba(color, alpha)
789 if self._isinit:
790 self._set_extremes()
792 def get_under(self):
793 """Get the color for low out-of-range values."""
794 if not self._isinit:
795 self._init()
796 return np.array(self._lut[self._i_under])
798 def set_under(self, color='k', alpha=None):
799 """Set the color for low out-of-range values."""
800 self._rgba_under = to_rgba(color, alpha)
801 if self._isinit:
802 self._set_extremes()
804 def get_over(self):
805 """Get the color for high out-of-range values."""
806 if not self._isinit:
807 self._init()
808 return np.array(self._lut[self._i_over])
810 def set_over(self, color='k', alpha=None):
811 """Set the color for high out-of-range values."""
812 self._rgba_over = to_rgba(color, alpha)
813 if self._isinit:
814 self._set_extremes()
816 def set_extremes(self, *, bad=None, under=None, over=None):
817 """
818 Set the colors for masked (*bad*) values and, when ``norm.clip =
819 False``, low (*under*) and high (*over*) out-of-range values.
820 """
821 if bad is not None:
822 self.set_bad(bad)
823 if under is not None:
824 self.set_under(under)
825 if over is not None:
826 self.set_over(over)
828 def with_extremes(self, *, bad=None, under=None, over=None):
829 """
830 Return a copy of the colormap, for which the colors for masked (*bad*)
831 values and, when ``norm.clip = False``, low (*under*) and high (*over*)
832 out-of-range values, have been set accordingly.
833 """
834 new_cm = self.copy()
835 new_cm.set_extremes(bad=bad, under=under, over=over)
836 return new_cm
838 def _set_extremes(self):
839 if self._rgba_under:
840 self._lut[self._i_under] = self._rgba_under
841 else:
842 self._lut[self._i_under] = self._lut[0]
843 if self._rgba_over:
844 self._lut[self._i_over] = self._rgba_over
845 else:
846 self._lut[self._i_over] = self._lut[self.N - 1]
847 self._lut[self._i_bad] = self._rgba_bad
849 def _init(self):
850 """Generate the lookup table, ``self._lut``."""
851 raise NotImplementedError("Abstract class only")
853 def is_gray(self):
854 """Return whether the colormap is grayscale."""
855 if not self._isinit:
856 self._init()
857 return (np.all(self._lut[:, 0] == self._lut[:, 1]) and
858 np.all(self._lut[:, 0] == self._lut[:, 2]))
860 def resampled(self, lutsize):
861 """Return a new colormap with *lutsize* entries."""
862 if hasattr(self, '_resample'):
863 _api.warn_external(
864 "The ability to resample a color map is now public API "
865 f"However the class {type(self)} still only implements "
866 "the previous private _resample method. Please update "
867 "your class."
868 )
869 return self._resample(lutsize)
871 raise NotImplementedError()
873 def reversed(self, name=None):
874 """
875 Return a reversed instance of the Colormap.
877 .. note:: This function is not implemented for the base class.
879 Parameters
880 ----------
881 name : str, optional
882 The name for the reversed colormap. If None, the
883 name is set to ``self.name + "_r"``.
885 See Also
886 --------
887 LinearSegmentedColormap.reversed
888 ListedColormap.reversed
889 """
890 raise NotImplementedError()
892 def _repr_png_(self):
893 """Generate a PNG representation of the Colormap."""
894 X = np.tile(np.linspace(0, 1, _REPR_PNG_SIZE[0]),
895 (_REPR_PNG_SIZE[1], 1))
896 pixels = self(X, bytes=True)
897 png_bytes = io.BytesIO()
898 title = self.name + ' colormap'
899 author = f'Matplotlib v{mpl.__version__}, https://matplotlib.org'
900 pnginfo = PngInfo()
901 pnginfo.add_text('Title', title)
902 pnginfo.add_text('Description', title)
903 pnginfo.add_text('Author', author)
904 pnginfo.add_text('Software', author)
905 Image.fromarray(pixels).save(png_bytes, format='png', pnginfo=pnginfo)
906 return png_bytes.getvalue()
908 def _repr_html_(self):
909 """Generate an HTML representation of the Colormap."""
910 png_bytes = self._repr_png_()
911 png_base64 = base64.b64encode(png_bytes).decode('ascii')
912 def color_block(color):
913 hex_color = to_hex(color, keep_alpha=True)
914 return (f'<div title="{hex_color}" '
915 'style="display: inline-block; '
916 'width: 1em; height: 1em; '
917 'margin: 0; '
918 'vertical-align: middle; '
919 'border: 1px solid #555; '
920 f'background-color: {hex_color};"></div>')
922 return ('<div style="vertical-align: middle;">'
923 f'<strong>{self.name}</strong> '
924 '</div>'
925 '<div class="cmap"><img '
926 f'alt="{self.name} colormap" '
927 f'title="{self.name}" '
928 'style="border: 1px solid #555;" '
929 f'src="data:image/png;base64,{png_base64}"></div>'
930 '<div style="vertical-align: middle; '
931 f'max-width: {_REPR_PNG_SIZE[0]+2}px; '
932 'display: flex; justify-content: space-between;">'
933 '<div style="float: left;">'
934 f'{color_block(self.get_under())} under'
935 '</div>'
936 '<div style="margin: 0 auto; display: inline-block;">'
937 f'bad {color_block(self.get_bad())}'
938 '</div>'
939 '<div style="float: right;">'
940 f'over {color_block(self.get_over())}'
941 '</div>')
943 def copy(self):
944 """Return a copy of the colormap."""
945 return self.__copy__()
948class LinearSegmentedColormap(Colormap):
949 """
950 Colormap objects based on lookup tables using linear segments.
952 The lookup table is generated using linear interpolation for each
953 primary color, with the 0-1 domain divided into any number of
954 segments.
955 """
957 def __init__(self, name, segmentdata, N=256, gamma=1.0):
958 """
959 Create colormap from linear mapping segments
961 segmentdata argument is a dictionary with a red, green and blue
962 entries. Each entry should be a list of *x*, *y0*, *y1* tuples,
963 forming rows in a table. Entries for alpha are optional.
965 Example: suppose you want red to increase from 0 to 1 over
966 the bottom half, green to do the same over the middle half,
967 and blue over the top half. Then you would use::
969 cdict = {'red': [(0.0, 0.0, 0.0),
970 (0.5, 1.0, 1.0),
971 (1.0, 1.0, 1.0)],
973 'green': [(0.0, 0.0, 0.0),
974 (0.25, 0.0, 0.0),
975 (0.75, 1.0, 1.0),
976 (1.0, 1.0, 1.0)],
978 'blue': [(0.0, 0.0, 0.0),
979 (0.5, 0.0, 0.0),
980 (1.0, 1.0, 1.0)]}
982 Each row in the table for a given color is a sequence of
983 *x*, *y0*, *y1* tuples. In each sequence, *x* must increase
984 monotonically from 0 to 1. For any input value *z* falling
985 between *x[i]* and *x[i+1]*, the output value of a given color
986 will be linearly interpolated between *y1[i]* and *y0[i+1]*::
988 row i: x y0 y1
989 /
990 /
991 row i+1: x y0 y1
993 Hence y0 in the first row and y1 in the last row are never used.
995 See Also
996 --------
997 LinearSegmentedColormap.from_list
998 Static method; factory function for generating a smoothly-varying
999 LinearSegmentedColormap.
1000 """
1001 # True only if all colors in map are identical; needed for contouring.
1002 self.monochrome = False
1003 super().__init__(name, N)
1004 self._segmentdata = segmentdata
1005 self._gamma = gamma
1007 def _init(self):
1008 self._lut = np.ones((self.N + 3, 4), float)
1009 self._lut[:-3, 0] = _create_lookup_table(
1010 self.N, self._segmentdata['red'], self._gamma)
1011 self._lut[:-3, 1] = _create_lookup_table(
1012 self.N, self._segmentdata['green'], self._gamma)
1013 self._lut[:-3, 2] = _create_lookup_table(
1014 self.N, self._segmentdata['blue'], self._gamma)
1015 if 'alpha' in self._segmentdata:
1016 self._lut[:-3, 3] = _create_lookup_table(
1017 self.N, self._segmentdata['alpha'], 1)
1018 self._isinit = True
1019 self._set_extremes()
1021 def set_gamma(self, gamma):
1022 """Set a new gamma value and regenerate colormap."""
1023 self._gamma = gamma
1024 self._init()
1026 @staticmethod
1027 def from_list(name, colors, N=256, gamma=1.0):
1028 """
1029 Create a `LinearSegmentedColormap` from a list of colors.
1031 Parameters
1032 ----------
1033 name : str
1034 The name of the colormap.
1035 colors : array-like of colors or array-like of (value, color)
1036 If only colors are given, they are equidistantly mapped from the
1037 range :math:`[0, 1]`; i.e. 0 maps to ``colors[0]`` and 1 maps to
1038 ``colors[-1]``.
1039 If (value, color) pairs are given, the mapping is from *value*
1040 to *color*. This can be used to divide the range unevenly.
1041 N : int
1042 The number of RGB quantization levels.
1043 gamma : float
1044 """
1045 if not np.iterable(colors):
1046 raise ValueError('colors must be iterable')
1048 if (isinstance(colors[0], Sized) and len(colors[0]) == 2
1049 and not isinstance(colors[0], str)):
1050 # List of value, color pairs
1051 vals, colors = zip(*colors)
1052 else:
1053 vals = np.linspace(0, 1, len(colors))
1055 r, g, b, a = to_rgba_array(colors).T
1056 cdict = {
1057 "red": np.column_stack([vals, r, r]),
1058 "green": np.column_stack([vals, g, g]),
1059 "blue": np.column_stack([vals, b, b]),
1060 "alpha": np.column_stack([vals, a, a]),
1061 }
1063 return LinearSegmentedColormap(name, cdict, N, gamma)
1065 def resampled(self, lutsize):
1066 """Return a new colormap with *lutsize* entries."""
1067 new_cmap = LinearSegmentedColormap(self.name, self._segmentdata,
1068 lutsize)
1069 new_cmap._rgba_over = self._rgba_over
1070 new_cmap._rgba_under = self._rgba_under
1071 new_cmap._rgba_bad = self._rgba_bad
1072 return new_cmap
1074 # Helper ensuring picklability of the reversed cmap.
1075 @staticmethod
1076 def _reverser(func, x):
1077 return func(1 - x)
1079 def reversed(self, name=None):
1080 """
1081 Return a reversed instance of the Colormap.
1083 Parameters
1084 ----------
1085 name : str, optional
1086 The name for the reversed colormap. If None, the
1087 name is set to ``self.name + "_r"``.
1089 Returns
1090 -------
1091 LinearSegmentedColormap
1092 The reversed colormap.
1093 """
1094 if name is None:
1095 name = self.name + "_r"
1097 # Using a partial object keeps the cmap picklable.
1098 data_r = {key: (functools.partial(self._reverser, data)
1099 if callable(data) else
1100 [(1.0 - x, y1, y0) for x, y0, y1 in reversed(data)])
1101 for key, data in self._segmentdata.items()}
1103 new_cmap = LinearSegmentedColormap(name, data_r, self.N, self._gamma)
1104 # Reverse the over/under values too
1105 new_cmap._rgba_over = self._rgba_under
1106 new_cmap._rgba_under = self._rgba_over
1107 new_cmap._rgba_bad = self._rgba_bad
1108 return new_cmap
1111class ListedColormap(Colormap):
1112 """
1113 Colormap object generated from a list of colors.
1115 This may be most useful when indexing directly into a colormap,
1116 but it can also be used to generate special colormaps for ordinary
1117 mapping.
1119 Parameters
1120 ----------
1121 colors : list, array
1122 List of Matplotlib color specifications, or an equivalent Nx3 or Nx4
1123 floating point array (*N* RGB or RGBA values).
1124 name : str, optional
1125 String to identify the colormap.
1126 N : int, optional
1127 Number of entries in the map. The default is *None*, in which case
1128 there is one colormap entry for each element in the list of colors.
1129 If ::
1131 N < len(colors)
1133 the list will be truncated at *N*. If ::
1135 N > len(colors)
1137 the list will be extended by repetition.
1138 """
1139 def __init__(self, colors, name='from_list', N=None):
1140 self.monochrome = False # Are all colors identical? (for contour.py)
1141 if N is None:
1142 self.colors = colors
1143 N = len(colors)
1144 else:
1145 if isinstance(colors, str):
1146 self.colors = [colors] * N
1147 self.monochrome = True
1148 elif np.iterable(colors):
1149 if len(colors) == 1:
1150 self.monochrome = True
1151 self.colors = list(
1152 itertools.islice(itertools.cycle(colors), N))
1153 else:
1154 try:
1155 gray = float(colors)
1156 except TypeError:
1157 pass
1158 else:
1159 self.colors = [gray] * N
1160 self.monochrome = True
1161 super().__init__(name, N)
1163 def _init(self):
1164 self._lut = np.zeros((self.N + 3, 4), float)
1165 self._lut[:-3] = to_rgba_array(self.colors)
1166 self._isinit = True
1167 self._set_extremes()
1169 def resampled(self, lutsize):
1170 """Return a new colormap with *lutsize* entries."""
1171 colors = self(np.linspace(0, 1, lutsize))
1172 new_cmap = ListedColormap(colors, name=self.name)
1173 # Keep the over/under values too
1174 new_cmap._rgba_over = self._rgba_over
1175 new_cmap._rgba_under = self._rgba_under
1176 new_cmap._rgba_bad = self._rgba_bad
1177 return new_cmap
1179 def reversed(self, name=None):
1180 """
1181 Return a reversed instance of the Colormap.
1183 Parameters
1184 ----------
1185 name : str, optional
1186 The name for the reversed colormap. If None, the
1187 name is set to ``self.name + "_r"``.
1189 Returns
1190 -------
1191 ListedColormap
1192 A reversed instance of the colormap.
1193 """
1194 if name is None:
1195 name = self.name + "_r"
1197 colors_r = list(reversed(self.colors))
1198 new_cmap = ListedColormap(colors_r, name=name, N=self.N)
1199 # Reverse the over/under values too
1200 new_cmap._rgba_over = self._rgba_under
1201 new_cmap._rgba_under = self._rgba_over
1202 new_cmap._rgba_bad = self._rgba_bad
1203 return new_cmap
1206class Normalize:
1207 """
1208 A class which, when called, linearly normalizes data into the
1209 ``[0.0, 1.0]`` interval.
1210 """
1212 def __init__(self, vmin=None, vmax=None, clip=False):
1213 """
1214 Parameters
1215 ----------
1216 vmin, vmax : float or None
1217 If *vmin* and/or *vmax* is not given, they are initialized from the
1218 minimum and maximum value, respectively, of the first input
1219 processed; i.e., ``__call__(A)`` calls ``autoscale_None(A)``.
1221 clip : bool, default: False
1222 If ``True`` values falling outside the range ``[vmin, vmax]``,
1223 are mapped to 0 or 1, whichever is closer, and masked values are
1224 set to 1. If ``False`` masked values remain masked.
1226 Clipping silently defeats the purpose of setting the over, under,
1227 and masked colors in a colormap, so it is likely to lead to
1228 surprises; therefore the default is ``clip=False``.
1230 Notes
1231 -----
1232 Returns 0 if ``vmin == vmax``.
1233 """
1234 self._vmin = _sanitize_extrema(vmin)
1235 self._vmax = _sanitize_extrema(vmax)
1236 self._clip = clip
1237 self._scale = None
1238 self.callbacks = cbook.CallbackRegistry(signals=["changed"])
1240 @property
1241 def vmin(self):
1242 return self._vmin
1244 @vmin.setter
1245 def vmin(self, value):
1246 value = _sanitize_extrema(value)
1247 if value != self._vmin:
1248 self._vmin = value
1249 self._changed()
1251 @property
1252 def vmax(self):
1253 return self._vmax
1255 @vmax.setter
1256 def vmax(self, value):
1257 value = _sanitize_extrema(value)
1258 if value != self._vmax:
1259 self._vmax = value
1260 self._changed()
1262 @property
1263 def clip(self):
1264 return self._clip
1266 @clip.setter
1267 def clip(self, value):
1268 if value != self._clip:
1269 self._clip = value
1270 self._changed()
1272 def _changed(self):
1273 """
1274 Call this whenever the norm is changed to notify all the
1275 callback listeners to the 'changed' signal.
1276 """
1277 self.callbacks.process('changed')
1279 @staticmethod
1280 def process_value(value):
1281 """
1282 Homogenize the input *value* for easy and efficient normalization.
1284 *value* can be a scalar or sequence.
1286 Returns
1287 -------
1288 result : masked array
1289 Masked array with the same shape as *value*.
1290 is_scalar : bool
1291 Whether *value* is a scalar.
1293 Notes
1294 -----
1295 Float dtypes are preserved; integer types with two bytes or smaller are
1296 converted to np.float32, and larger types are converted to np.float64.
1297 Preserving float32 when possible, and using in-place operations,
1298 greatly improves speed for large arrays.
1299 """
1300 is_scalar = not np.iterable(value)
1301 if is_scalar:
1302 value = [value]
1303 dtype = np.min_scalar_type(value)
1304 if np.issubdtype(dtype, np.integer) or dtype.type is np.bool_:
1305 # bool_/int8/int16 -> float32; int32/int64 -> float64
1306 dtype = np.promote_types(dtype, np.float32)
1307 # ensure data passed in as an ndarray subclass are interpreted as
1308 # an ndarray. See issue #6622.
1309 mask = np.ma.getmask(value)
1310 data = np.asarray(value)
1311 result = np.ma.array(data, mask=mask, dtype=dtype, copy=True)
1312 return result, is_scalar
1314 def __call__(self, value, clip=None):
1315 """
1316 Normalize *value* data in the ``[vmin, vmax]`` interval into the
1317 ``[0.0, 1.0]`` interval and return it.
1319 Parameters
1320 ----------
1321 value
1322 Data to normalize.
1323 clip : bool
1324 If ``None``, defaults to ``self.clip`` (which defaults to
1325 ``False``).
1327 Notes
1328 -----
1329 If not already initialized, ``self.vmin`` and ``self.vmax`` are
1330 initialized using ``self.autoscale_None(value)``.
1331 """
1332 if clip is None:
1333 clip = self.clip
1335 result, is_scalar = self.process_value(value)
1337 if self.vmin is None or self.vmax is None:
1338 self.autoscale_None(result)
1339 # Convert at least to float, without losing precision.
1340 (vmin,), _ = self.process_value(self.vmin)
1341 (vmax,), _ = self.process_value(self.vmax)
1342 if vmin == vmax:
1343 result.fill(0) # Or should it be all masked? Or 0.5?
1344 elif vmin > vmax:
1345 raise ValueError("minvalue must be less than or equal to maxvalue")
1346 else:
1347 if clip:
1348 mask = np.ma.getmask(result)
1349 result = np.ma.array(np.clip(result.filled(vmax), vmin, vmax),
1350 mask=mask)
1351 # ma division is very slow; we can take a shortcut
1352 resdat = result.data
1353 resdat -= vmin
1354 resdat /= (vmax - vmin)
1355 result = np.ma.array(resdat, mask=result.mask, copy=False)
1356 if is_scalar:
1357 result = result[0]
1358 return result
1360 def inverse(self, value):
1361 if not self.scaled():
1362 raise ValueError("Not invertible until both vmin and vmax are set")
1363 (vmin,), _ = self.process_value(self.vmin)
1364 (vmax,), _ = self.process_value(self.vmax)
1366 if np.iterable(value):
1367 val = np.ma.asarray(value)
1368 return vmin + val * (vmax - vmin)
1369 else:
1370 return vmin + value * (vmax - vmin)
1372 def autoscale(self, A):
1373 """Set *vmin*, *vmax* to min, max of *A*."""
1374 self.vmin = self.vmax = None
1375 self.autoscale_None(A)
1377 def autoscale_None(self, A):
1378 """If vmin or vmax are not set, use the min/max of *A* to set them."""
1379 A = np.asanyarray(A)
1380 if self.vmin is None and A.size:
1381 self.vmin = A.min()
1382 if self.vmax is None and A.size:
1383 self.vmax = A.max()
1385 def scaled(self):
1386 """Return whether vmin and vmax are set."""
1387 return self.vmin is not None and self.vmax is not None
1390class TwoSlopeNorm(Normalize):
1391 def __init__(self, vcenter, vmin=None, vmax=None):
1392 """
1393 Normalize data with a set center.
1395 Useful when mapping data with an unequal rates of change around a
1396 conceptual center, e.g., data that range from -2 to 4, with 0 as
1397 the midpoint.
1399 Parameters
1400 ----------
1401 vcenter : float
1402 The data value that defines ``0.5`` in the normalization.
1403 vmin : float, optional
1404 The data value that defines ``0.0`` in the normalization.
1405 Defaults to the min value of the dataset.
1406 vmax : float, optional
1407 The data value that defines ``1.0`` in the normalization.
1408 Defaults to the max value of the dataset.
1410 Examples
1411 --------
1412 This maps data value -4000 to 0., 0 to 0.5, and +10000 to 1.0; data
1413 between is linearly interpolated::
1415 >>> import matplotlib.colors as mcolors
1416 >>> offset = mcolors.TwoSlopeNorm(vmin=-4000.,
1417 vcenter=0., vmax=10000)
1418 >>> data = [-4000., -2000., 0., 2500., 5000., 7500., 10000.]
1419 >>> offset(data)
1420 array([0., 0.25, 0.5, 0.625, 0.75, 0.875, 1.0])
1421 """
1423 super().__init__(vmin=vmin, vmax=vmax)
1424 self._vcenter = vcenter
1425 if vcenter is not None and vmax is not None and vcenter >= vmax:
1426 raise ValueError('vmin, vcenter, and vmax must be in '
1427 'ascending order')
1428 if vcenter is not None and vmin is not None and vcenter <= vmin:
1429 raise ValueError('vmin, vcenter, and vmax must be in '
1430 'ascending order')
1432 @property
1433 def vcenter(self):
1434 return self._vcenter
1436 @vcenter.setter
1437 def vcenter(self, value):
1438 if value != self._vcenter:
1439 self._vcenter = value
1440 self._changed()
1442 def autoscale_None(self, A):
1443 """
1444 Get vmin and vmax, and then clip at vcenter
1445 """
1446 super().autoscale_None(A)
1447 if self.vmin > self.vcenter:
1448 self.vmin = self.vcenter
1449 if self.vmax < self.vcenter:
1450 self.vmax = self.vcenter
1452 def __call__(self, value, clip=None):
1453 """
1454 Map value to the interval [0, 1]. The clip argument is unused.
1455 """
1456 result, is_scalar = self.process_value(value)
1457 self.autoscale_None(result) # sets self.vmin, self.vmax if None
1459 if not self.vmin <= self.vcenter <= self.vmax:
1460 raise ValueError("vmin, vcenter, vmax must increase monotonically")
1461 # note that we must extrapolate for tick locators:
1462 result = np.ma.masked_array(
1463 np.interp(result, [self.vmin, self.vcenter, self.vmax],
1464 [0, 0.5, 1], left=-np.inf, right=np.inf),
1465 mask=np.ma.getmask(result))
1466 if is_scalar:
1467 result = np.atleast_1d(result)[0]
1468 return result
1470 def inverse(self, value):
1471 if not self.scaled():
1472 raise ValueError("Not invertible until both vmin and vmax are set")
1473 (vmin,), _ = self.process_value(self.vmin)
1474 (vmax,), _ = self.process_value(self.vmax)
1475 (vcenter,), _ = self.process_value(self.vcenter)
1476 result = np.interp(value, [0, 0.5, 1], [vmin, vcenter, vmax],
1477 left=-np.inf, right=np.inf)
1478 return result
1481class CenteredNorm(Normalize):
1482 def __init__(self, vcenter=0, halfrange=None, clip=False):
1483 """
1484 Normalize symmetrical data around a center (0 by default).
1486 Unlike `TwoSlopeNorm`, `CenteredNorm` applies an equal rate of change
1487 around the center.
1489 Useful when mapping symmetrical data around a conceptual center
1490 e.g., data that range from -2 to 4, with 0 as the midpoint, and
1491 with equal rates of change around that midpoint.
1493 Parameters
1494 ----------
1495 vcenter : float, default: 0
1496 The data value that defines ``0.5`` in the normalization.
1497 halfrange : float, optional
1498 The range of data values that defines a range of ``0.5`` in the
1499 normalization, so that *vcenter* - *halfrange* is ``0.0`` and
1500 *vcenter* + *halfrange* is ``1.0`` in the normalization.
1501 Defaults to the largest absolute difference to *vcenter* for
1502 the values in the dataset.
1504 Examples
1505 --------
1506 This maps data values -2 to 0.25, 0 to 0.5, and 4 to 1.0
1507 (assuming equal rates of change above and below 0.0):
1509 >>> import matplotlib.colors as mcolors
1510 >>> norm = mcolors.CenteredNorm(halfrange=4.0)
1511 >>> data = [-2., 0., 4.]
1512 >>> norm(data)
1513 array([0.25, 0.5 , 1. ])
1514 """
1515 super().__init__(vmin=None, vmax=None, clip=clip)
1516 self._vcenter = vcenter
1517 # calling the halfrange setter to set vmin and vmax
1518 self.halfrange = halfrange
1520 def _set_vmin_vmax(self):
1521 """
1522 Set *vmin* and *vmax* based on *vcenter* and *halfrange*.
1523 """
1524 self.vmax = self._vcenter + self._halfrange
1525 self.vmin = self._vcenter - self._halfrange
1527 def autoscale(self, A):
1528 """
1529 Set *halfrange* to ``max(abs(A-vcenter))``, then set *vmin* and *vmax*.
1530 """
1531 A = np.asanyarray(A)
1532 self._halfrange = max(self._vcenter-A.min(),
1533 A.max()-self._vcenter)
1534 self._set_vmin_vmax()
1536 def autoscale_None(self, A):
1537 """Set *vmin* and *vmax*."""
1538 A = np.asanyarray(A)
1539 if self._halfrange is None and A.size:
1540 self.autoscale(A)
1542 @property
1543 def vcenter(self):
1544 return self._vcenter
1546 @vcenter.setter
1547 def vcenter(self, vcenter):
1548 if vcenter != self._vcenter:
1549 self._vcenter = vcenter
1550 self._changed()
1551 if self.vmax is not None:
1552 # recompute halfrange assuming vmin and vmax represent
1553 # min and max of data
1554 self._halfrange = max(self._vcenter-self.vmin,
1555 self.vmax-self._vcenter)
1556 self._set_vmin_vmax()
1558 @property
1559 def halfrange(self):
1560 return self._halfrange
1562 @halfrange.setter
1563 def halfrange(self, halfrange):
1564 if halfrange is None:
1565 self._halfrange = None
1566 self.vmin = None
1567 self.vmax = None
1568 else:
1569 self._halfrange = abs(halfrange)
1571 def __call__(self, value, clip=None):
1572 if self._halfrange is not None:
1573 # enforce symmetry, reset vmin and vmax
1574 self._set_vmin_vmax()
1575 return super().__call__(value, clip=clip)
1578def make_norm_from_scale(scale_cls, base_norm_cls=None, *, init=None):
1579 """
1580 Decorator for building a `.Normalize` subclass from a `~.scale.ScaleBase`
1581 subclass.
1583 After ::
1585 @make_norm_from_scale(scale_cls)
1586 class norm_cls(Normalize):
1587 ...
1589 *norm_cls* is filled with methods so that normalization computations are
1590 forwarded to *scale_cls* (i.e., *scale_cls* is the scale that would be used
1591 for the colorbar of a mappable normalized with *norm_cls*).
1593 If *init* is not passed, then the constructor signature of *norm_cls*
1594 will be ``norm_cls(vmin=None, vmax=None, clip=False)``; these three
1595 parameters will be forwarded to the base class (``Normalize.__init__``),
1596 and a *scale_cls* object will be initialized with no arguments (other than
1597 a dummy axis).
1599 If the *scale_cls* constructor takes additional parameters, then *init*
1600 should be passed to `make_norm_from_scale`. It is a callable which is
1601 *only* used for its signature. First, this signature will become the
1602 signature of *norm_cls*. Second, the *norm_cls* constructor will bind the
1603 parameters passed to it using this signature, extract the bound *vmin*,
1604 *vmax*, and *clip* values, pass those to ``Normalize.__init__``, and
1605 forward the remaining bound values (including any defaults defined by the
1606 signature) to the *scale_cls* constructor.
1607 """
1609 if base_norm_cls is None:
1610 return functools.partial(make_norm_from_scale, scale_cls, init=init)
1612 if isinstance(scale_cls, functools.partial):
1613 scale_args = scale_cls.args
1614 scale_kwargs_items = tuple(scale_cls.keywords.items())
1615 scale_cls = scale_cls.func
1616 else:
1617 scale_args = scale_kwargs_items = ()
1619 if init is None:
1620 def init(vmin=None, vmax=None, clip=False): pass
1622 return _make_norm_from_scale(
1623 scale_cls, scale_args, scale_kwargs_items,
1624 base_norm_cls, inspect.signature(init))
1627@functools.lru_cache(None)
1628def _make_norm_from_scale(
1629 scale_cls, scale_args, scale_kwargs_items,
1630 base_norm_cls, bound_init_signature,
1631):
1632 """
1633 Helper for `make_norm_from_scale`.
1635 This function is split out to enable caching (in particular so that
1636 different unpickles reuse the same class). In order to do so,
1638 - ``functools.partial`` *scale_cls* is expanded into ``func, args, kwargs``
1639 to allow memoizing returned norms (partial instances always compare
1640 unequal, but we can check identity based on ``func, args, kwargs``;
1641 - *init* is replaced by *init_signature*, as signatures are picklable,
1642 unlike to arbitrary lambdas.
1643 """
1645 class Norm(base_norm_cls):
1646 def __reduce__(self):
1647 cls = type(self)
1648 # If the class is toplevel-accessible, it is possible to directly
1649 # pickle it "by name". This is required to support norm classes
1650 # defined at a module's toplevel, as the inner base_norm_cls is
1651 # otherwise unpicklable (as it gets shadowed by the generated norm
1652 # class). If either import or attribute access fails, fall back to
1653 # the general path.
1654 try:
1655 if cls is getattr(importlib.import_module(cls.__module__),
1656 cls.__qualname__):
1657 return (_create_empty_object_of_class, (cls,), vars(self))
1658 except (ImportError, AttributeError):
1659 pass
1660 return (_picklable_norm_constructor,
1661 (scale_cls, scale_args, scale_kwargs_items,
1662 base_norm_cls, bound_init_signature),
1663 vars(self))
1665 def __init__(self, *args, **kwargs):
1666 ba = bound_init_signature.bind(*args, **kwargs)
1667 ba.apply_defaults()
1668 super().__init__(
1669 **{k: ba.arguments.pop(k) for k in ["vmin", "vmax", "clip"]})
1670 self._scale = functools.partial(
1671 scale_cls, *scale_args, **dict(scale_kwargs_items))(
1672 axis=None, **ba.arguments)
1673 self._trf = self._scale.get_transform()
1675 __init__.__signature__ = bound_init_signature.replace(parameters=[
1676 inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD),
1677 *bound_init_signature.parameters.values()])
1679 def __call__(self, value, clip=None):
1680 value, is_scalar = self.process_value(value)
1681 if self.vmin is None or self.vmax is None:
1682 self.autoscale_None(value)
1683 if self.vmin > self.vmax:
1684 raise ValueError("vmin must be less or equal to vmax")
1685 if self.vmin == self.vmax:
1686 return np.full_like(value, 0)
1687 if clip is None:
1688 clip = self.clip
1689 if clip:
1690 value = np.clip(value, self.vmin, self.vmax)
1691 t_value = self._trf.transform(value).reshape(np.shape(value))
1692 t_vmin, t_vmax = self._trf.transform([self.vmin, self.vmax])
1693 if not np.isfinite([t_vmin, t_vmax]).all():
1694 raise ValueError("Invalid vmin or vmax")
1695 t_value -= t_vmin
1696 t_value /= (t_vmax - t_vmin)
1697 t_value = np.ma.masked_invalid(t_value, copy=False)
1698 return t_value[0] if is_scalar else t_value
1700 def inverse(self, value):
1701 if not self.scaled():
1702 raise ValueError("Not invertible until scaled")
1703 if self.vmin > self.vmax:
1704 raise ValueError("vmin must be less or equal to vmax")
1705 t_vmin, t_vmax = self._trf.transform([self.vmin, self.vmax])
1706 if not np.isfinite([t_vmin, t_vmax]).all():
1707 raise ValueError("Invalid vmin or vmax")
1708 value, is_scalar = self.process_value(value)
1709 rescaled = value * (t_vmax - t_vmin)
1710 rescaled += t_vmin
1711 value = (self._trf
1712 .inverted()
1713 .transform(rescaled)
1714 .reshape(np.shape(value)))
1715 return value[0] if is_scalar else value
1717 def autoscale_None(self, A):
1718 # i.e. A[np.isfinite(...)], but also for non-array A's
1719 in_trf_domain = np.extract(np.isfinite(self._trf.transform(A)), A)
1720 if in_trf_domain.size == 0:
1721 in_trf_domain = np.ma.masked
1722 return super().autoscale_None(in_trf_domain)
1724 if base_norm_cls is Normalize:
1725 Norm.__name__ = f"{scale_cls.__name__}Norm"
1726 Norm.__qualname__ = f"{scale_cls.__qualname__}Norm"
1727 else:
1728 Norm.__name__ = base_norm_cls.__name__
1729 Norm.__qualname__ = base_norm_cls.__qualname__
1730 Norm.__module__ = base_norm_cls.__module__
1731 Norm.__doc__ = base_norm_cls.__doc__
1733 return Norm
1736def _create_empty_object_of_class(cls):
1737 return cls.__new__(cls)
1740def _picklable_norm_constructor(*args):
1741 return _create_empty_object_of_class(_make_norm_from_scale(*args))
1744@make_norm_from_scale(
1745 scale.FuncScale,
1746 init=lambda functions, vmin=None, vmax=None, clip=False: None)
1747class FuncNorm(Normalize):
1748 """
1749 Arbitrary normalization using functions for the forward and inverse.
1751 Parameters
1752 ----------
1753 functions : (callable, callable)
1754 two-tuple of the forward and inverse functions for the normalization.
1755 The forward function must be monotonic.
1757 Both functions must have the signature ::
1759 def forward(values: array-like) -> array-like
1761 vmin, vmax : float or None
1762 If *vmin* and/or *vmax* is not given, they are initialized from the
1763 minimum and maximum value, respectively, of the first input
1764 processed; i.e., ``__call__(A)`` calls ``autoscale_None(A)``.
1766 clip : bool, default: False
1767 If ``True`` values falling outside the range ``[vmin, vmax]``,
1768 are mapped to 0 or 1, whichever is closer, and masked values are
1769 set to 1. If ``False`` masked values remain masked.
1771 Clipping silently defeats the purpose of setting the over, under,
1772 and masked colors in a colormap, so it is likely to lead to
1773 surprises; therefore the default is ``clip=False``.
1774 """
1777LogNorm = make_norm_from_scale(
1778 functools.partial(scale.LogScale, nonpositive="mask"))(Normalize)
1779LogNorm.__name__ = LogNorm.__qualname__ = "LogNorm"
1780LogNorm.__doc__ = "Normalize a given value to the 0-1 range on a log scale."
1783@make_norm_from_scale(
1784 scale.SymmetricalLogScale,
1785 init=lambda linthresh, linscale=1., vmin=None, vmax=None, clip=False, *,
1786 base=10: None)
1787class SymLogNorm(Normalize):
1788 """
1789 The symmetrical logarithmic scale is logarithmic in both the
1790 positive and negative directions from the origin.
1792 Since the values close to zero tend toward infinity, there is a
1793 need to have a range around zero that is linear. The parameter
1794 *linthresh* allows the user to specify the size of this range
1795 (-*linthresh*, *linthresh*).
1797 Parameters
1798 ----------
1799 linthresh : float
1800 The range within which the plot is linear (to avoid having the plot
1801 go to infinity around zero).
1802 linscale : float, default: 1
1803 This allows the linear range (-*linthresh* to *linthresh*) to be
1804 stretched relative to the logarithmic range. Its value is the
1805 number of decades to use for each half of the linear range. For
1806 example, when *linscale* == 1.0 (the default), the space used for
1807 the positive and negative halves of the linear range will be equal
1808 to one decade in the logarithmic range.
1809 base : float, default: 10
1810 """
1812 @property
1813 def linthresh(self):
1814 return self._scale.linthresh
1816 @linthresh.setter
1817 def linthresh(self, value):
1818 self._scale.linthresh = value
1821@make_norm_from_scale(
1822 scale.AsinhScale,
1823 init=lambda linear_width=1, vmin=None, vmax=None, clip=False: None)
1824class AsinhNorm(Normalize):
1825 """
1826 The inverse hyperbolic sine scale is approximately linear near
1827 the origin, but becomes logarithmic for larger positive
1828 or negative values. Unlike the `SymLogNorm`, the transition between
1829 these linear and logarithmic regions is smooth, which may reduce
1830 the risk of visual artifacts.
1832 .. note::
1834 This API is provisional and may be revised in the future
1835 based on early user feedback.
1837 Parameters
1838 ----------
1839 linear_width : float, default: 1
1840 The effective width of the linear region, beyond which
1841 the transformation becomes asymptotically logarithmic
1842 """
1844 @property
1845 def linear_width(self):
1846 return self._scale.linear_width
1848 @linear_width.setter
1849 def linear_width(self, value):
1850 self._scale.linear_width = value
1853class PowerNorm(Normalize):
1854 """
1855 Linearly map a given value to the 0-1 range and then apply
1856 a power-law normalization over that range.
1857 """
1858 def __init__(self, gamma, vmin=None, vmax=None, clip=False):
1859 super().__init__(vmin, vmax, clip)
1860 self.gamma = gamma
1862 def __call__(self, value, clip=None):
1863 if clip is None:
1864 clip = self.clip
1866 result, is_scalar = self.process_value(value)
1868 self.autoscale_None(result)
1869 gamma = self.gamma
1870 vmin, vmax = self.vmin, self.vmax
1871 if vmin > vmax:
1872 raise ValueError("minvalue must be less than or equal to maxvalue")
1873 elif vmin == vmax:
1874 result.fill(0)
1875 else:
1876 if clip:
1877 mask = np.ma.getmask(result)
1878 result = np.ma.array(np.clip(result.filled(vmax), vmin, vmax),
1879 mask=mask)
1880 resdat = result.data
1881 resdat -= vmin
1882 resdat[resdat < 0] = 0
1883 np.power(resdat, gamma, resdat)
1884 resdat /= (vmax - vmin) ** gamma
1886 result = np.ma.array(resdat, mask=result.mask, copy=False)
1887 if is_scalar:
1888 result = result[0]
1889 return result
1891 def inverse(self, value):
1892 if not self.scaled():
1893 raise ValueError("Not invertible until scaled")
1894 gamma = self.gamma
1895 vmin, vmax = self.vmin, self.vmax
1897 if np.iterable(value):
1898 val = np.ma.asarray(value)
1899 return np.ma.power(val, 1. / gamma) * (vmax - vmin) + vmin
1900 else:
1901 return pow(value, 1. / gamma) * (vmax - vmin) + vmin
1904class BoundaryNorm(Normalize):
1905 """
1906 Generate a colormap index based on discrete intervals.
1908 Unlike `Normalize` or `LogNorm`, `BoundaryNorm` maps values to integers
1909 instead of to the interval 0-1.
1910 """
1912 # Mapping to the 0-1 interval could have been done via piece-wise linear
1913 # interpolation, but using integers seems simpler, and reduces the number
1914 # of conversions back and forth between int and float.
1916 def __init__(self, boundaries, ncolors, clip=False, *, extend='neither'):
1917 """
1918 Parameters
1919 ----------
1920 boundaries : array-like
1921 Monotonically increasing sequence of at least 2 bin edges: data
1922 falling in the n-th bin will be mapped to the n-th color.
1924 ncolors : int
1925 Number of colors in the colormap to be used.
1927 clip : bool, optional
1928 If clip is ``True``, out of range values are mapped to 0 if they
1929 are below ``boundaries[0]`` or mapped to ``ncolors - 1`` if they
1930 are above ``boundaries[-1]``.
1932 If clip is ``False``, out of range values are mapped to -1 if
1933 they are below ``boundaries[0]`` or mapped to *ncolors* if they are
1934 above ``boundaries[-1]``. These are then converted to valid indices
1935 by `Colormap.__call__`.
1937 extend : {'neither', 'both', 'min', 'max'}, default: 'neither'
1938 Extend the number of bins to include one or both of the
1939 regions beyond the boundaries. For example, if ``extend``
1940 is 'min', then the color to which the region between the first
1941 pair of boundaries is mapped will be distinct from the first
1942 color in the colormap, and by default a
1943 `~matplotlib.colorbar.Colorbar` will be drawn with
1944 the triangle extension on the left or lower end.
1946 Notes
1947 -----
1948 If there are fewer bins (including extensions) than colors, then the
1949 color index is chosen by linearly interpolating the ``[0, nbins - 1]``
1950 range onto the ``[0, ncolors - 1]`` range, effectively skipping some
1951 colors in the middle of the colormap.
1952 """
1953 if clip and extend != 'neither':
1954 raise ValueError("'clip=True' is not compatible with 'extend'")
1955 super().__init__(vmin=boundaries[0], vmax=boundaries[-1], clip=clip)
1956 self.boundaries = np.asarray(boundaries)
1957 self.N = len(self.boundaries)
1958 if self.N < 2:
1959 raise ValueError("You must provide at least 2 boundaries "
1960 f"(1 region) but you passed in {boundaries!r}")
1961 self.Ncmap = ncolors
1962 self.extend = extend
1964 self._scale = None # don't use the default scale.
1966 self._n_regions = self.N - 1 # number of colors needed
1967 self._offset = 0
1968 if extend in ('min', 'both'):
1969 self._n_regions += 1
1970 self._offset = 1
1971 if extend in ('max', 'both'):
1972 self._n_regions += 1
1973 if self._n_regions > self.Ncmap:
1974 raise ValueError(f"There are {self._n_regions} color bins "
1975 "including extensions, but ncolors = "
1976 f"{ncolors}; ncolors must equal or exceed the "
1977 "number of bins")
1979 def __call__(self, value, clip=None):
1980 """
1981 This method behaves similarly to `.Normalize.__call__`, except that it
1982 returns integers or arrays of int16.
1983 """
1984 if clip is None:
1985 clip = self.clip
1987 xx, is_scalar = self.process_value(value)
1988 mask = np.ma.getmaskarray(xx)
1989 # Fill masked values a value above the upper boundary
1990 xx = np.atleast_1d(xx.filled(self.vmax + 1))
1991 if clip:
1992 np.clip(xx, self.vmin, self.vmax, out=xx)
1993 max_col = self.Ncmap - 1
1994 else:
1995 max_col = self.Ncmap
1996 # this gives us the bins in the lookup table in the range
1997 # [0, _n_regions - 1] (the offset is set in the init)
1998 iret = np.digitize(xx, self.boundaries) - 1 + self._offset
1999 # if we have more colors than regions, stretch the region
2000 # index computed above to full range of the color bins. This
2001 # will make use of the full range (but skip some of the colors
2002 # in the middle) such that the first region is mapped to the
2003 # first color and the last region is mapped to the last color.
2004 if self.Ncmap > self._n_regions:
2005 if self._n_regions == 1:
2006 # special case the 1 region case, pick the middle color
2007 iret[iret == 0] = (self.Ncmap - 1) // 2
2008 else:
2009 # otherwise linearly remap the values from the region index
2010 # to the color index spaces
2011 iret = (self.Ncmap - 1) / (self._n_regions - 1) * iret
2012 # cast to 16bit integers in all cases
2013 iret = iret.astype(np.int16)
2014 iret[xx < self.vmin] = -1
2015 iret[xx >= self.vmax] = max_col
2016 ret = np.ma.array(iret, mask=mask)
2017 if is_scalar:
2018 ret = int(ret[0]) # assume python scalar
2019 return ret
2021 def inverse(self, value):
2022 """
2023 Raises
2024 ------
2025 ValueError
2026 BoundaryNorm is not invertible, so calling this method will always
2027 raise an error
2028 """
2029 raise ValueError("BoundaryNorm is not invertible")
2032class NoNorm(Normalize):
2033 """
2034 Dummy replacement for `Normalize`, for the case where we want to use
2035 indices directly in a `~matplotlib.cm.ScalarMappable`.
2036 """
2037 def __call__(self, value, clip=None):
2038 return value
2040 def inverse(self, value):
2041 return value
2044def rgb_to_hsv(arr):
2045 """
2046 Convert float RGB values (in the range [0, 1]), in a numpy array to HSV
2047 values.
2049 Parameters
2050 ----------
2051 arr : (..., 3) array-like
2052 All values must be in the range [0, 1]
2054 Returns
2055 -------
2056 (..., 3) ndarray
2057 Colors converted to HSV values in range [0, 1]
2058 """
2059 arr = np.asarray(arr)
2061 # check length of the last dimension, should be _some_ sort of rgb
2062 if arr.shape[-1] != 3:
2063 raise ValueError("Last dimension of input array must be 3; "
2064 "shape {} was found.".format(arr.shape))
2066 in_shape = arr.shape
2067 arr = np.array(
2068 arr, copy=False,
2069 dtype=np.promote_types(arr.dtype, np.float32), # Don't work on ints.
2070 ndmin=2, # In case input was 1D.
2071 )
2072 out = np.zeros_like(arr)
2073 arr_max = arr.max(-1)
2074 ipos = arr_max > 0
2075 delta = arr.ptp(-1)
2076 s = np.zeros_like(delta)
2077 s[ipos] = delta[ipos] / arr_max[ipos]
2078 ipos = delta > 0
2079 # red is max
2080 idx = (arr[..., 0] == arr_max) & ipos
2081 out[idx, 0] = (arr[idx, 1] - arr[idx, 2]) / delta[idx]
2082 # green is max
2083 idx = (arr[..., 1] == arr_max) & ipos
2084 out[idx, 0] = 2. + (arr[idx, 2] - arr[idx, 0]) / delta[idx]
2085 # blue is max
2086 idx = (arr[..., 2] == arr_max) & ipos
2087 out[idx, 0] = 4. + (arr[idx, 0] - arr[idx, 1]) / delta[idx]
2089 out[..., 0] = (out[..., 0] / 6.0) % 1.0
2090 out[..., 1] = s
2091 out[..., 2] = arr_max
2093 return out.reshape(in_shape)
2096def hsv_to_rgb(hsv):
2097 """
2098 Convert HSV values to RGB.
2100 Parameters
2101 ----------
2102 hsv : (..., 3) array-like
2103 All values assumed to be in range [0, 1]
2105 Returns
2106 -------
2107 (..., 3) ndarray
2108 Colors converted to RGB values in range [0, 1]
2109 """
2110 hsv = np.asarray(hsv)
2112 # check length of the last dimension, should be _some_ sort of rgb
2113 if hsv.shape[-1] != 3:
2114 raise ValueError("Last dimension of input array must be 3; "
2115 "shape {shp} was found.".format(shp=hsv.shape))
2117 in_shape = hsv.shape
2118 hsv = np.array(
2119 hsv, copy=False,
2120 dtype=np.promote_types(hsv.dtype, np.float32), # Don't work on ints.
2121 ndmin=2, # In case input was 1D.
2122 )
2124 h = hsv[..., 0]
2125 s = hsv[..., 1]
2126 v = hsv[..., 2]
2128 r = np.empty_like(h)
2129 g = np.empty_like(h)
2130 b = np.empty_like(h)
2132 i = (h * 6.0).astype(int)
2133 f = (h * 6.0) - i
2134 p = v * (1.0 - s)
2135 q = v * (1.0 - s * f)
2136 t = v * (1.0 - s * (1.0 - f))
2138 idx = i % 6 == 0
2139 r[idx] = v[idx]
2140 g[idx] = t[idx]
2141 b[idx] = p[idx]
2143 idx = i == 1
2144 r[idx] = q[idx]
2145 g[idx] = v[idx]
2146 b[idx] = p[idx]
2148 idx = i == 2
2149 r[idx] = p[idx]
2150 g[idx] = v[idx]
2151 b[idx] = t[idx]
2153 idx = i == 3
2154 r[idx] = p[idx]
2155 g[idx] = q[idx]
2156 b[idx] = v[idx]
2158 idx = i == 4
2159 r[idx] = t[idx]
2160 g[idx] = p[idx]
2161 b[idx] = v[idx]
2163 idx = i == 5
2164 r[idx] = v[idx]
2165 g[idx] = p[idx]
2166 b[idx] = q[idx]
2168 idx = s == 0
2169 r[idx] = v[idx]
2170 g[idx] = v[idx]
2171 b[idx] = v[idx]
2173 rgb = np.stack([r, g, b], axis=-1)
2175 return rgb.reshape(in_shape)
2178def _vector_magnitude(arr):
2179 # things that don't work here:
2180 # * np.linalg.norm: drops mask from ma.array
2181 # * np.sum: drops mask from ma.array unless entire vector is masked
2182 sum_sq = 0
2183 for i in range(arr.shape[-1]):
2184 sum_sq += arr[..., i, np.newaxis] ** 2
2185 return np.sqrt(sum_sq)
2188class LightSource:
2189 """
2190 Create a light source coming from the specified azimuth and elevation.
2191 Angles are in degrees, with the azimuth measured
2192 clockwise from north and elevation up from the zero plane of the surface.
2194 `shade` is used to produce "shaded" RGB values for a data array.
2195 `shade_rgb` can be used to combine an RGB image with an elevation map.
2196 `hillshade` produces an illumination map of a surface.
2197 """
2199 def __init__(self, azdeg=315, altdeg=45, hsv_min_val=0, hsv_max_val=1,
2200 hsv_min_sat=1, hsv_max_sat=0):
2201 """
2202 Specify the azimuth (measured clockwise from south) and altitude
2203 (measured up from the plane of the surface) of the light source
2204 in degrees.
2206 Parameters
2207 ----------
2208 azdeg : float, default: 315 degrees (from the northwest)
2209 The azimuth (0-360, degrees clockwise from North) of the light
2210 source.
2211 altdeg : float, default: 45 degrees
2212 The altitude (0-90, degrees up from horizontal) of the light
2213 source.
2215 Notes
2216 -----
2217 For backwards compatibility, the parameters *hsv_min_val*,
2218 *hsv_max_val*, *hsv_min_sat*, and *hsv_max_sat* may be supplied at
2219 initialization as well. However, these parameters will only be used if
2220 "blend_mode='hsv'" is passed into `shade` or `shade_rgb`.
2221 See the documentation for `blend_hsv` for more details.
2222 """
2223 self.azdeg = azdeg
2224 self.altdeg = altdeg
2225 self.hsv_min_val = hsv_min_val
2226 self.hsv_max_val = hsv_max_val
2227 self.hsv_min_sat = hsv_min_sat
2228 self.hsv_max_sat = hsv_max_sat
2230 @property
2231 def direction(self):
2232 """The unit vector direction towards the light source."""
2233 # Azimuth is in degrees clockwise from North. Convert to radians
2234 # counterclockwise from East (mathematical notation).
2235 az = np.radians(90 - self.azdeg)
2236 alt = np.radians(self.altdeg)
2237 return np.array([
2238 np.cos(az) * np.cos(alt),
2239 np.sin(az) * np.cos(alt),
2240 np.sin(alt)
2241 ])
2243 def hillshade(self, elevation, vert_exag=1, dx=1, dy=1, fraction=1.):
2244 """
2245 Calculate the illumination intensity for a surface using the defined
2246 azimuth and elevation for the light source.
2248 This computes the normal vectors for the surface, and then passes them
2249 on to `shade_normals`
2251 Parameters
2252 ----------
2253 elevation : 2D array-like
2254 The height values used to generate an illumination map
2255 vert_exag : number, optional
2256 The amount to exaggerate the elevation values by when calculating
2257 illumination. This can be used either to correct for differences in
2258 units between the x-y coordinate system and the elevation
2259 coordinate system (e.g. decimal degrees vs. meters) or to
2260 exaggerate or de-emphasize topographic effects.
2261 dx : number, optional
2262 The x-spacing (columns) of the input *elevation* grid.
2263 dy : number, optional
2264 The y-spacing (rows) of the input *elevation* grid.
2265 fraction : number, optional
2266 Increases or decreases the contrast of the hillshade. Values
2267 greater than one will cause intermediate values to move closer to
2268 full illumination or shadow (and clipping any values that move
2269 beyond 0 or 1). Note that this is not visually or mathematically
2270 the same as vertical exaggeration.
2272 Returns
2273 -------
2274 ndarray
2275 A 2D array of illumination values between 0-1, where 0 is
2276 completely in shadow and 1 is completely illuminated.
2277 """
2279 # Because most image and raster GIS data has the first row in the array
2280 # as the "top" of the image, dy is implicitly negative. This is
2281 # consistent to what `imshow` assumes, as well.
2282 dy = -dy
2284 # compute the normal vectors from the partial derivatives
2285 e_dy, e_dx = np.gradient(vert_exag * elevation, dy, dx)
2287 # .view is to keep subclasses
2288 normal = np.empty(elevation.shape + (3,)).view(type(elevation))
2289 normal[..., 0] = -e_dx
2290 normal[..., 1] = -e_dy
2291 normal[..., 2] = 1
2292 normal /= _vector_magnitude(normal)
2294 return self.shade_normals(normal, fraction)
2296 def shade_normals(self, normals, fraction=1.):
2297 """
2298 Calculate the illumination intensity for the normal vectors of a
2299 surface using the defined azimuth and elevation for the light source.
2301 Imagine an artificial sun placed at infinity in some azimuth and
2302 elevation position illuminating our surface. The parts of the surface
2303 that slope toward the sun should brighten while those sides facing away
2304 should become darker.
2306 Parameters
2307 ----------
2308 fraction : number, optional
2309 Increases or decreases the contrast of the hillshade. Values
2310 greater than one will cause intermediate values to move closer to
2311 full illumination or shadow (and clipping any values that move
2312 beyond 0 or 1). Note that this is not visually or mathematically
2313 the same as vertical exaggeration.
2315 Returns
2316 -------
2317 ndarray
2318 A 2D array of illumination values between 0-1, where 0 is
2319 completely in shadow and 1 is completely illuminated.
2320 """
2322 intensity = normals.dot(self.direction)
2324 # Apply contrast stretch
2325 imin, imax = intensity.min(), intensity.max()
2326 intensity *= fraction
2328 # Rescale to 0-1, keeping range before contrast stretch
2329 # If constant slope, keep relative scaling (i.e. flat should be 0.5,
2330 # fully occluded 0, etc.)
2331 if (imax - imin) > 1e-6:
2332 # Strictly speaking, this is incorrect. Negative values should be
2333 # clipped to 0 because they're fully occluded. However, rescaling
2334 # in this manner is consistent with the previous implementation and
2335 # visually appears better than a "hard" clip.
2336 intensity -= imin
2337 intensity /= (imax - imin)
2338 intensity = np.clip(intensity, 0, 1)
2340 return intensity
2342 def shade(self, data, cmap, norm=None, blend_mode='overlay', vmin=None,
2343 vmax=None, vert_exag=1, dx=1, dy=1, fraction=1, **kwargs):
2344 """
2345 Combine colormapped data values with an illumination intensity map
2346 (a.k.a. "hillshade") of the values.
2348 Parameters
2349 ----------
2350 data : 2D array-like
2351 The height values used to generate a shaded map.
2352 cmap : `~matplotlib.colors.Colormap`
2353 The colormap used to color the *data* array. Note that this must be
2354 a `~matplotlib.colors.Colormap` instance. For example, rather than
2355 passing in ``cmap='gist_earth'``, use
2356 ``cmap=plt.get_cmap('gist_earth')`` instead.
2357 norm : `~matplotlib.colors.Normalize` instance, optional
2358 The normalization used to scale values before colormapping. If
2359 None, the input will be linearly scaled between its min and max.
2360 blend_mode : {'hsv', 'overlay', 'soft'} or callable, optional
2361 The type of blending used to combine the colormapped data
2362 values with the illumination intensity. Default is
2363 "overlay". Note that for most topographic surfaces,
2364 "overlay" or "soft" appear more visually realistic. If a
2365 user-defined function is supplied, it is expected to
2366 combine an MxNx3 RGB array of floats (ranging 0 to 1) with
2367 an MxNx1 hillshade array (also 0 to 1). (Call signature
2368 ``func(rgb, illum, **kwargs)``) Additional kwargs supplied
2369 to this function will be passed on to the *blend_mode*
2370 function.
2371 vmin : float or None, optional
2372 The minimum value used in colormapping *data*. If *None* the
2373 minimum value in *data* is used. If *norm* is specified, then this
2374 argument will be ignored.
2375 vmax : float or None, optional
2376 The maximum value used in colormapping *data*. If *None* the
2377 maximum value in *data* is used. If *norm* is specified, then this
2378 argument will be ignored.
2379 vert_exag : number, optional
2380 The amount to exaggerate the elevation values by when calculating
2381 illumination. This can be used either to correct for differences in
2382 units between the x-y coordinate system and the elevation
2383 coordinate system (e.g. decimal degrees vs. meters) or to
2384 exaggerate or de-emphasize topography.
2385 dx : number, optional
2386 The x-spacing (columns) of the input *elevation* grid.
2387 dy : number, optional
2388 The y-spacing (rows) of the input *elevation* grid.
2389 fraction : number, optional
2390 Increases or decreases the contrast of the hillshade. Values
2391 greater than one will cause intermediate values to move closer to
2392 full illumination or shadow (and clipping any values that move
2393 beyond 0 or 1). Note that this is not visually or mathematically
2394 the same as vertical exaggeration.
2395 Additional kwargs are passed on to the *blend_mode* function.
2397 Returns
2398 -------
2399 ndarray
2400 An MxNx4 array of floats ranging between 0-1.
2401 """
2402 if vmin is None:
2403 vmin = data.min()
2404 if vmax is None:
2405 vmax = data.max()
2406 if norm is None:
2407 norm = Normalize(vmin=vmin, vmax=vmax)
2409 rgb0 = cmap(norm(data))
2410 rgb1 = self.shade_rgb(rgb0, elevation=data, blend_mode=blend_mode,
2411 vert_exag=vert_exag, dx=dx, dy=dy,
2412 fraction=fraction, **kwargs)
2413 # Don't overwrite the alpha channel, if present.
2414 rgb0[..., :3] = rgb1[..., :3]
2415 return rgb0
2417 def shade_rgb(self, rgb, elevation, fraction=1., blend_mode='hsv',
2418 vert_exag=1, dx=1, dy=1, **kwargs):
2419 """
2420 Use this light source to adjust the colors of the *rgb* input array to
2421 give the impression of a shaded relief map with the given *elevation*.
2423 Parameters
2424 ----------
2425 rgb : array-like
2426 An (M, N, 3) RGB array, assumed to be in the range of 0 to 1.
2427 elevation : array-like
2428 An (M, N) array of the height values used to generate a shaded map.
2429 fraction : number
2430 Increases or decreases the contrast of the hillshade. Values
2431 greater than one will cause intermediate values to move closer to
2432 full illumination or shadow (and clipping any values that move
2433 beyond 0 or 1). Note that this is not visually or mathematically
2434 the same as vertical exaggeration.
2435 blend_mode : {'hsv', 'overlay', 'soft'} or callable, optional
2436 The type of blending used to combine the colormapped data values
2437 with the illumination intensity. For backwards compatibility, this
2438 defaults to "hsv". Note that for most topographic surfaces,
2439 "overlay" or "soft" appear more visually realistic. If a
2440 user-defined function is supplied, it is expected to combine an
2441 MxNx3 RGB array of floats (ranging 0 to 1) with an MxNx1 hillshade
2442 array (also 0 to 1). (Call signature
2443 ``func(rgb, illum, **kwargs)``)
2444 Additional kwargs supplied to this function will be passed on to
2445 the *blend_mode* function.
2446 vert_exag : number, optional
2447 The amount to exaggerate the elevation values by when calculating
2448 illumination. This can be used either to correct for differences in
2449 units between the x-y coordinate system and the elevation
2450 coordinate system (e.g. decimal degrees vs. meters) or to
2451 exaggerate or de-emphasize topography.
2452 dx : number, optional
2453 The x-spacing (columns) of the input *elevation* grid.
2454 dy : number, optional
2455 The y-spacing (rows) of the input *elevation* grid.
2456 Additional kwargs are passed on to the *blend_mode* function.
2458 Returns
2459 -------
2460 ndarray
2461 An (m, n, 3) array of floats ranging between 0-1.
2462 """
2463 # Calculate the "hillshade" intensity.
2464 intensity = self.hillshade(elevation, vert_exag, dx, dy, fraction)
2465 intensity = intensity[..., np.newaxis]
2467 # Blend the hillshade and rgb data using the specified mode
2468 lookup = {
2469 'hsv': self.blend_hsv,
2470 'soft': self.blend_soft_light,
2471 'overlay': self.blend_overlay,
2472 }
2473 if blend_mode in lookup:
2474 blend = lookup[blend_mode](rgb, intensity, **kwargs)
2475 else:
2476 try:
2477 blend = blend_mode(rgb, intensity, **kwargs)
2478 except TypeError as err:
2479 raise ValueError('"blend_mode" must be callable or one of {}'
2480 .format(lookup.keys)) from err
2482 # Only apply result where hillshade intensity isn't masked
2483 if np.ma.is_masked(intensity):
2484 mask = intensity.mask[..., 0]
2485 for i in range(3):
2486 blend[..., i][mask] = rgb[..., i][mask]
2488 return blend
2490 def blend_hsv(self, rgb, intensity, hsv_max_sat=None, hsv_max_val=None,
2491 hsv_min_val=None, hsv_min_sat=None):
2492 """
2493 Take the input data array, convert to HSV values in the given colormap,
2494 then adjust those color values to give the impression of a shaded
2495 relief map with a specified light source. RGBA values are returned,
2496 which can then be used to plot the shaded image with imshow.
2498 The color of the resulting image will be darkened by moving the (s, v)
2499 values (in HSV colorspace) toward (hsv_min_sat, hsv_min_val) in the
2500 shaded regions, or lightened by sliding (s, v) toward (hsv_max_sat,
2501 hsv_max_val) in regions that are illuminated. The default extremes are
2502 chose so that completely shaded points are nearly black (s = 1, v = 0)
2503 and completely illuminated points are nearly white (s = 0, v = 1).
2505 Parameters
2506 ----------
2507 rgb : ndarray
2508 An MxNx3 RGB array of floats ranging from 0 to 1 (color image).
2509 intensity : ndarray
2510 An MxNx1 array of floats ranging from 0 to 1 (grayscale image).
2511 hsv_max_sat : number, default: 1
2512 The maximum saturation value that the *intensity* map can shift the
2513 output image to.
2514 hsv_min_sat : number, optional
2515 The minimum saturation value that the *intensity* map can shift the
2516 output image to. Defaults to 0.
2517 hsv_max_val : number, optional
2518 The maximum value ("v" in "hsv") that the *intensity* map can shift
2519 the output image to. Defaults to 1.
2520 hsv_min_val : number, optional
2521 The minimum value ("v" in "hsv") that the *intensity* map can shift
2522 the output image to. Defaults to 0.
2524 Returns
2525 -------
2526 ndarray
2527 An MxNx3 RGB array representing the combined images.
2528 """
2529 # Backward compatibility...
2530 if hsv_max_sat is None:
2531 hsv_max_sat = self.hsv_max_sat
2532 if hsv_max_val is None:
2533 hsv_max_val = self.hsv_max_val
2534 if hsv_min_sat is None:
2535 hsv_min_sat = self.hsv_min_sat
2536 if hsv_min_val is None:
2537 hsv_min_val = self.hsv_min_val
2539 # Expects a 2D intensity array scaled between -1 to 1...
2540 intensity = intensity[..., 0]
2541 intensity = 2 * intensity - 1
2543 # Convert to rgb, then rgb to hsv
2544 hsv = rgb_to_hsv(rgb[:, :, 0:3])
2545 hue, sat, val = np.moveaxis(hsv, -1, 0)
2547 # Modify hsv values (in place) to simulate illumination.
2548 # putmask(A, mask, B) <=> A[mask] = B[mask]
2549 np.putmask(sat, (np.abs(sat) > 1.e-10) & (intensity > 0),
2550 (1 - intensity) * sat + intensity * hsv_max_sat)
2551 np.putmask(sat, (np.abs(sat) > 1.e-10) & (intensity < 0),
2552 (1 + intensity) * sat - intensity * hsv_min_sat)
2553 np.putmask(val, intensity > 0,
2554 (1 - intensity) * val + intensity * hsv_max_val)
2555 np.putmask(val, intensity < 0,
2556 (1 + intensity) * val - intensity * hsv_min_val)
2557 np.clip(hsv[:, :, 1:], 0, 1, out=hsv[:, :, 1:])
2559 # Convert modified hsv back to rgb.
2560 return hsv_to_rgb(hsv)
2562 def blend_soft_light(self, rgb, intensity):
2563 """
2564 Combine an RGB image with an intensity map using "soft light" blending,
2565 using the "pegtop" formula.
2567 Parameters
2568 ----------
2569 rgb : ndarray
2570 An MxNx3 RGB array of floats ranging from 0 to 1 (color image).
2571 intensity : ndarray
2572 An MxNx1 array of floats ranging from 0 to 1 (grayscale image).
2574 Returns
2575 -------
2576 ndarray
2577 An MxNx3 RGB array representing the combined images.
2578 """
2579 return 2 * intensity * rgb + (1 - 2 * intensity) * rgb**2
2581 def blend_overlay(self, rgb, intensity):
2582 """
2583 Combine an RGB image with an intensity map using "overlay" blending.
2585 Parameters
2586 ----------
2587 rgb : ndarray
2588 An MxNx3 RGB array of floats ranging from 0 to 1 (color image).
2589 intensity : ndarray
2590 An MxNx1 array of floats ranging from 0 to 1 (grayscale image).
2592 Returns
2593 -------
2594 ndarray
2595 An MxNx3 RGB array representing the combined images.
2596 """
2597 low = 2 * intensity * rgb
2598 high = 1 - 2 * (1 - intensity) * (1 - rgb)
2599 return np.where(rgb <= 0.5, low, high)
2602def from_levels_and_colors(levels, colors, extend='neither'):
2603 """
2604 A helper routine to generate a cmap and a norm instance which
2605 behave similar to contourf's levels and colors arguments.
2607 Parameters
2608 ----------
2609 levels : sequence of numbers
2610 The quantization levels used to construct the `BoundaryNorm`.
2611 Value ``v`` is quantized to level ``i`` if ``lev[i] <= v < lev[i+1]``.
2612 colors : sequence of colors
2613 The fill color to use for each level. If *extend* is "neither" there
2614 must be ``n_level - 1`` colors. For an *extend* of "min" or "max" add
2615 one extra color, and for an *extend* of "both" add two colors.
2616 extend : {'neither', 'min', 'max', 'both'}, optional
2617 The behaviour when a value falls out of range of the given levels.
2618 See `~.Axes.contourf` for details.
2620 Returns
2621 -------
2622 cmap : `~matplotlib.colors.Normalize`
2623 norm : `~matplotlib.colors.Colormap`
2624 """
2625 slice_map = {
2626 'both': slice(1, -1),
2627 'min': slice(1, None),
2628 'max': slice(0, -1),
2629 'neither': slice(0, None),
2630 }
2631 _api.check_in_list(slice_map, extend=extend)
2632 color_slice = slice_map[extend]
2634 n_data_colors = len(levels) - 1
2635 n_expected = n_data_colors + color_slice.start - (color_slice.stop or 0)
2636 if len(colors) != n_expected:
2637 raise ValueError(
2638 f'With extend == {extend!r} and {len(levels)} levels, '
2639 f'expected {n_expected} colors, but got {len(colors)}')
2641 cmap = ListedColormap(colors[color_slice], N=n_data_colors)
2643 if extend in ['min', 'both']:
2644 cmap.set_under(colors[0])
2645 else:
2646 cmap.set_under('none')
2648 if extend in ['max', 'both']:
2649 cmap.set_over(colors[-1])
2650 else:
2651 cmap.set_over('none')
2653 cmap.colorbar_extend = extend
2655 norm = BoundaryNorm(levels, ncolors=n_data_colors)
2656 return cmap, norm