Skip to content

Add a registry for color sequences #22387

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions doc/api/colors_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
---------
Expand Down
7 changes: 5 additions & 2 deletions doc/api/matplotlib_configuration_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,15 @@ Logging

.. autofunction:: set_loglevel

Colormaps
=========
Colormaps and color sequences
=============================

.. autodata:: colormaps
:no-value:

.. autodata:: color_sequences
:no-value:

Miscellaneous
=============

Expand Down
3 changes: 3 additions & 0 deletions doc/api/pyplot_summary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ For a more in-depth look at colormaps, see the

.. autodata:: colormaps
:no-value:

.. autodata:: color_sequences
:no-value:
1 change: 1 addition & 0 deletions lib/matplotlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
111 changes: 109 additions & 2 deletions lib/matplotlib/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


Expand Down Expand Up @@ -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")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we include the list of known keys here?


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
Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
40 changes: 40 additions & 0 deletions lib/matplotlib/tests/test_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')