diff --git a/doc/api/colors_api.rst b/doc/api/colors_api.rst index 44f8cca303fd..970986ff4438 100644 --- a/doc/api/colors_api.rst +++ b/doc/api/colors_api.rst @@ -14,27 +14,44 @@ :no-members: :no-inherited-members: -Classes -------- +Color norms +----------- .. autosummary:: :toctree: _as_gen/ :template: autosummary.rst + Normalize + NoNorm AsinhNorm BoundaryNorm - Colormap CenteredNorm - LightSource - LinearSegmentedColormap - ListedColormap + FuncNorm LogNorm - NoNorm - Normalize PowerNorm SymLogNorm TwoSlopeNorm - FuncNorm + +Colormaps +--------- + +.. autosummary:: + :toctree: _as_gen/ + :template: autosummary.rst + + Colormap + LinearSegmentedColormap + ListedColormap + +Other classes +------------- + +.. autosummary:: + :toctree: _as_gen/ + :template: autosummary.rst + + ColorSequenceRegistry + LightSource Functions --------- diff --git a/doc/api/matplotlib_configuration_api.rst b/doc/api/matplotlib_configuration_api.rst index 3636c45d0c71..c301149b8050 100644 --- a/doc/api/matplotlib_configuration_api.rst +++ b/doc/api/matplotlib_configuration_api.rst @@ -52,12 +52,15 @@ Logging .. autofunction:: set_loglevel -Colormaps -========= +Colormaps and color sequences +============================= .. autodata:: colormaps :no-value: +.. autodata:: color_sequences + :no-value: + Miscellaneous ============= diff --git a/doc/api/pyplot_summary.rst b/doc/api/pyplot_summary.rst index 8d18c8b67e3e..30454486f14a 100644 --- a/doc/api/pyplot_summary.rst +++ b/doc/api/pyplot_summary.rst @@ -31,3 +31,6 @@ For a more in-depth look at colormaps, see the .. autodata:: colormaps :no-value: + +.. autodata:: color_sequences + :no-value: diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 2a495a91eb0d..7e8f6efa9af4 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1455,3 +1455,4 @@ def inner(ax, *args, data=None, **kwargs): # workaround: we must defer colormaps import to after loading rcParams, because # colormap creation depends on rcParams from matplotlib.cm import _colormaps as colormaps +from matplotlib.colors import _color_sequences as color_sequences diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 2e519149527f..ed051e304405 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -40,7 +40,7 @@ """ import base64 -from collections.abc import Sized, Sequence +from collections.abc import Sized, Sequence, Mapping import copy import functools import importlib @@ -54,7 +54,7 @@ import matplotlib as mpl import numpy as np -from matplotlib import _api, cbook, scale +from matplotlib import _api, _cm, cbook, scale from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS @@ -94,6 +94,113 @@ def get_named_colors_mapping(): return _colors_full_map +class ColorSequenceRegistry(Mapping): + r""" + Container for sequences of colors that are known to Matplotlib by name. + + The universal registry instance is `matplotlib.color_sequences`. There + should be no need for users to instantiate `.ColorSequenceRegistry` + themselves. + + Read access uses a dict-like interface mapping names to lists of colors:: + + import matplotlib as mpl + cmap = mpl.color_sequences['tab10'] + + The returned lists are copies, so that their modification does not change + the global definition of the color sequence. + + Additional color sequences can be added via + `.ColorSequenceRegistry.register`:: + + mpl.color_sequences.register('rgb', ['r', 'g', 'b']) + """ + + _BUILTIN_COLOR_SEQUENCES = { + 'tab10': _cm._tab10_data, + 'tab20': _cm._tab20_data, + 'tab20b': _cm._tab20b_data, + 'tab20c': _cm._tab20c_data, + 'Pastel1': _cm._Pastel1_data, + 'Pastel2': _cm._Pastel2_data, + 'Paired': _cm._Paired_data, + 'Accent': _cm._Accent_data, + 'Dark2': _cm._Dark2_data, + 'Set1': _cm._Set1_data, + 'Set2': _cm._Set1_data, + 'Set3': _cm._Set1_data, + } + + def __init__(self): + self._color_sequences = {**self._BUILTIN_COLOR_SEQUENCES} + + def __getitem__(self, item): + try: + return list(self._color_sequences[item]) + except KeyError: + raise KeyError(f"{item!r} is not a known color sequence name") + + def __iter__(self): + return iter(self._color_sequences) + + def __len__(self): + return len(self._color_sequences) + + def __str__(self): + return ('ColorSequenceRegistry; available colormaps:\n' + + ', '.join(f"'{name}'" for name in self)) + + def register(self, name, color_list): + """ + Register a new color sequence. + + The color sequence registry stores a copy of the given *color_list*, so + that future changes to the original list do not affect the registered + color sequence. Think of this as the registry taking a snapshot + of *color_list* at registration. + + Parameters + ---------- + name : str + The name for the color sequence. + + color_list : list of colors + An iterable returning valid Matplotlib colors when iterating over. + Note however that the returned color sequence will always be a + list regardless of the input type. + + """ + if name in self._BUILTIN_COLOR_SEQUENCES: + raise ValueError(f"{name!r} is a reserved name for a builtin " + "color sequence") + + color_list = list(color_list) # force copy and coerce type to list + for color in color_list: + try: + to_rgba(color) + except ValueError: + raise ValueError( + f"{color!r} is not a valid color specification") + + self._color_sequences[name] = color_list + + def unregister(self, name): + """ + Remove a sequence from the registry. + + You cannot remove built-in color sequences. + + If the name is not registered, returns with no error. + """ + if name in self._BUILTIN_COLOR_SEQUENCES: + raise ValueError( + f"Cannot unregister builtin color sequence {name!r}") + self._color_sequences.pop(name, None) + + +_color_sequences = ColorSequenceRegistry() + + def _sanitize_extrema(ex): if ex is None: return ex diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 577956bedce6..a06daab90a7d 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -71,6 +71,7 @@ from matplotlib import cm from matplotlib.cm import _colormaps as colormaps, get_cmap, register_cmap +from matplotlib.colors import _color_sequences as color_sequences import numpy as np diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 80f8b663bdce..c9aed221108e 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -1510,3 +1510,43 @@ def test_make_norm_from_scale_name(): logitnorm = mcolors.make_norm_from_scale( mscale.LogitScale, mcolors.Normalize) assert logitnorm.__name__ == logitnorm.__qualname__ == "LogitScaleNorm" + + +def test_color_sequences(): + # basic access + assert plt.color_sequences is matplotlib.color_sequences # same registry + assert list(plt.color_sequences) == [ + 'tab10', 'tab20', 'tab20b', 'tab20c', 'Pastel1', 'Pastel2', 'Paired', + 'Accent', 'Dark2', 'Set1', 'Set2', 'Set3'] + assert len(plt.color_sequences['tab10']) == 10 + assert len(plt.color_sequences['tab20']) == 20 + + tab_colors = [ + 'tab:blue', 'tab:orange', 'tab:green', 'tab:red', 'tab:purple', + 'tab:brown', 'tab:pink', 'tab:gray', 'tab:olive', 'tab:cyan'] + for seq_color, tab_color in zip(plt.color_sequences['tab10'], tab_colors): + assert mcolors.same_color(seq_color, tab_color) + + # registering + with pytest.raises(ValueError, match="reserved name"): + plt.color_sequences.register('tab10', ['r', 'g', 'b']) + with pytest.raises(ValueError, match="not a valid color specification"): + plt.color_sequences.register('invalid', ['not a color']) + + rgb_colors = ['r', 'g', 'b'] + plt.color_sequences.register('rgb', rgb_colors) + assert plt.color_sequences['rgb'] == ['r', 'g', 'b'] + # should not affect the registered sequence because input is copied + rgb_colors.append('c') + assert plt.color_sequences['rgb'] == ['r', 'g', 'b'] + # should not affect the registered sequence because returned list is a copy + plt.color_sequences['rgb'].append('c') + assert plt.color_sequences['rgb'] == ['r', 'g', 'b'] + + # unregister + plt.color_sequences.unregister('rgb') + with pytest.raises(KeyError): + plt.color_sequences['rgb'] # rgb is gone + plt.color_sequences.unregister('rgb') # multiple unregisters are ok + with pytest.raises(ValueError, match="Cannot unregister builtin"): + plt.color_sequences.unregister('tab10')