Skip to content

Commit fef03f9

Browse files
committed
Add a registry for color sequences
Color sequences are simply lists of colors, that we store by name in a registry. The registry is modelled similar to the ColormapRegistry to 1) support immutable builtin color sequences and 2) to return copies so that one cannot mess with the global definition of the color sequence through an obtained instance. For now, I've made the sequences used for `ListedColormap`s available as builtin sequences, but that's open for discussion. More usage documentation should be added in the color examples and/or tutorials, but I'll wait with that till after the general approval of the structure and API. One common use case will be ``` plt.rc_params['axes.prop_cycle'] = plt.cycler(color=plt.color_sequences['Pastel1') ```
1 parent 447160e commit fef03f9

File tree

6 files changed

+159
-4
lines changed

6 files changed

+159
-4
lines changed

doc/api/matplotlib_configuration_api.rst

+5-2
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,15 @@ Logging
5252

5353
.. autofunction:: set_loglevel
5454

55-
Colormaps
56-
=========
55+
Colormaps and color sequences
56+
=============================
5757

5858
.. autodata:: colormaps
5959
:no-value:
6060

61+
.. autodata:: color_sequences
62+
:no-value:
63+
6164
Miscellaneous
6265
=============
6366

doc/api/pyplot_summary.rst

+3
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ For a more in-depth look at colormaps, see the
3131

3232
.. autodata:: colormaps
3333
:no-value:
34+
35+
.. autodata:: color_sequences
36+
:no-value:

lib/matplotlib/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -1449,3 +1449,4 @@ def inner(ax, *args, data=None, **kwargs):
14491449
# workaround: we must defer colormaps import to after loading rcParams, because
14501450
# colormap creation depends on rcParams
14511451
from matplotlib.cm import _colormaps as colormaps
1452+
from matplotlib.colors import _color_sequences as color_sequences

lib/matplotlib/colors.py

+109-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"""
4141

4242
import base64
43-
from collections.abc import Sized, Sequence
43+
from collections.abc import Sized, Sequence, Mapping
4444
import copy
4545
import functools
4646
import inspect
@@ -53,7 +53,7 @@
5353

5454
import matplotlib as mpl
5555
import numpy as np
56-
from matplotlib import _api, cbook, scale
56+
from matplotlib import _api, _cm, cbook, scale
5757
from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS
5858

5959

@@ -93,6 +93,113 @@ def get_named_colors_mapping():
9393
return _colors_full_map
9494

9595

96+
class ColorSequenceRegistry(Mapping):
97+
r"""
98+
Container for sequences of colors that are known to Matplotlib by name.
99+
100+
The universal registry instance is `matplotlib.color_sequences`. There
101+
should be no need for users to instantiate `.ColorSequenceRegistry`
102+
themselves.
103+
104+
Read access uses a dict-like interface mapping names to lists of colors::
105+
106+
import matplotlib as mpl
107+
cmap = mpl.color_sequences['tab10']
108+
109+
The returned lists are copies, so that their modification does not change
110+
the global definition of the color sequence.
111+
112+
Additional color sequences can be added via
113+
`.ColorSequenceRegistry.register`::
114+
115+
mpl.color_sequences.register('rgb', ['r', 'g' ,'b'])
116+
"""
117+
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+
}
132+
133+
def __init__(self):
134+
self._color_sequences = {**self._BUILTIN_COLOR_SEQUENCES}
135+
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")
141+
142+
def __iter__(self):
143+
return iter(self._color_sequences)
144+
145+
def __len__(self):
146+
return len(self._color_sequences)
147+
148+
def __str__(self):
149+
return ('ColorSequenceRegistry; available colormaps:\n' +
150+
', '.join(f"'{name}'" for name in self))
151+
152+
def register(self, name, color_list):
153+
"""
154+
Register a new color sequence.
155+
156+
The colormap registry stores a copy of the given *color_list*, so that
157+
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.
160+
161+
Parameters
162+
----------
163+
name : str
164+
The name for the color sequence.
165+
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.
170+
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")
175+
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")
183+
184+
self._color_sequences[name] = color_list
185+
186+
def unregister(self, name):
187+
"""
188+
Remove a sequence from the registry.
189+
190+
You cannot remove built-in color sequences.
191+
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)
198+
199+
200+
_color_sequences = ColorSequenceRegistry()
201+
202+
96203
def _sanitize_extrema(ex):
97204
if ex is None:
98205
return ex

lib/matplotlib/pyplot.py

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666

6767
from matplotlib import cm
6868
from matplotlib.cm import _colormaps as colormaps, get_cmap, register_cmap
69+
from matplotlib.colors import _color_sequences as color_sequences
6970

7071
import numpy as np
7172

lib/matplotlib/tests/test_colors.py

+40
Original file line numberDiff line numberDiff line change
@@ -1481,3 +1481,43 @@ def test_make_norm_from_scale_name():
14811481
logitnorm = mcolors.make_norm_from_scale(
14821482
mscale.LogitScale, mcolors.Normalize)
14831483
assert logitnorm.__name__ == logitnorm.__qualname__ == "LogitScaleNorm"
1484+
1485+
1486+
def test_color_sequences():
1487+
# basic access
1488+
assert plt.color_sequences is matplotlib.color_sequences # same registry
1489+
assert list(plt.color_sequences) == [
1490+
'tab10', 'tab20', 'tab20b', 'tab20c', 'Pastel1', 'Pastel2', 'Paired',
1491+
'Accent', 'Dark2', 'Set1', 'Set2', 'Set3']
1492+
assert len(plt.color_sequences['tab10']) == 10
1493+
assert len(plt.color_sequences['tab20']) == 20
1494+
1495+
tab_colors = [
1496+
'tab:blue', 'tab:orange', 'tab:green', 'tab:red', 'tab:purple',
1497+
'tab:brown', 'tab:pink', 'tab:gray', 'tab:olive', 'tab:cyan']
1498+
for seq_color, tab_color in zip(plt.color_sequences['tab10'], tab_colors):
1499+
assert mcolors.to_rgb(seq_color) == mcolors.to_rgb(tab_color)
1500+
1501+
# registering
1502+
with pytest.raises(ValueError, match="reserved name"):
1503+
plt.color_sequences.register('tab10', ['r', 'g', 'b'])
1504+
with pytest.raises(ValueError, match="not a valid color specification"):
1505+
plt.color_sequences.register('invalid', ['not a color'])
1506+
1507+
rgb_colors = ['r', 'g', 'b']
1508+
plt.color_sequences.register('rgb', rgb_colors)
1509+
assert plt.color_sequences['rgb'] == ['r', 'g', 'b']
1510+
# should not affect the registered sequence because input is copied
1511+
rgb_colors.append('c')
1512+
assert plt.color_sequences['rgb'] == ['r', 'g', 'b']
1513+
# should not affect the registered sequence because returned list is a copy
1514+
plt.color_sequences['rgb'].append('c')
1515+
assert plt.color_sequences['rgb'] == ['r', 'g', 'b']
1516+
1517+
# unregister
1518+
plt.color_sequences.unregister('rgb')
1519+
with pytest.raises(KeyError):
1520+
plt.color_sequences['rgb'] # rgb is gone
1521+
plt.color_sequences.unregister('rgb') # multiple unregisters are ok
1522+
with pytest.raises(ValueError, match="Cannot unregister builtin"):
1523+
plt.color_sequences.unregister('tab10')

0 commit comments

Comments
 (0)