8000 Add a registry for color sequences · matplotlib/matplotlib@92dd83c · GitHub
[go: up one dir, main page]

Skip to content

Commit 92dd83c

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 2aa71eb commit 92dd83c

File tree

7 files changed

+185
-13
lines changed

7 files changed

+185
-13
lines changed

doc/api/colors_api.rst

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,27 +14,44 @@
1414
:no-members:
1515
:no-inherited-members:
1616

17-
Classes
18-
-------
17+
Color norms
18+
-----------
1919

2020
.. autosummary::
2121
:toctree: _as_gen/
2222
:template: autosummary.rst
2323

24+
NoNorm
25+
Normalize
2426
AsinhNorm
2527
BoundaryNorm
26-
Colormap
2728
CenteredNorm
28-
LightSource
29-
LinearSegmentedColormap
30-
ListedColormap
29+
FuncNorm
3130
LogNorm
32-
NoNorm
33-
Normalize
3431
PowerNorm
3532
SymLogNorm
3633
TwoSlopeNorm
37-
FuncNorm
34+
35+
Colormaps
36+
---------
37+
38+
.. autosummary::
39+
:toctree: _as_gen/
40+
:template: autosummary.rst
41+
42+
Colormap
43+
LinearSegmentedColormap
44+
ListedColormap
45+
46+
Other classes
47+
-------------
48+
49+
.. autosummary::
50+
:toctree: _as_gen/
51+
:template: autosummary.rst
52+
53+
ColorSequenceRegistry
54+
LightSource
3855

3956
Functions
4057
---------

doc/api/matplotlib_configuration_api.rst

Lines changed: 5 additions & 2 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,3 +1455,4 @@ def inner(ax, *args, data=None, **kwargs):
14551455
# workaround: we must defer colormaps import to after loading rcParams, because
14561456
# colormap creation depends on rcParams
14571457
from matplotlib.cm import _colormaps as colormaps
1458+
from matplotlib.colors import _color_sequences as color_sequences

lib/matplotlib/colors.py

Lines changed: 109 additions & 2 deletions
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 importlib
@@ -54,7 +54,7 @@
5454

5555
import matplotlib as mpl
5656
import numpy as np
57-
from matplotlib import _api, cbook, scale
57+
from matplotlib import _api, _cm, cbook, scale
5858
from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS
5959

6060

@@ -94,6 +94,113 @@ def get_named_colors_mapping():
9494
return _colors_full_map
9595

9696

97+
class ColorSequenceRegistry(Mapping):
98+
r"""
99+
Container for sequences of colors that are known to Matplotlib by name.
100+
101+
The universal registry instance is `matplotlib.color_sequences`. There
102+
should be no need for users to instantiate `.ColorSequenceRegistry`
103+
themselves.
104+
105+
Read access uses a dict-like interface mapping names to lists of colors::
106+
107+
import matplotlib as mpl
108+
cmap = mpl.color_sequences['tab10']
109+
110+
The returned lists are copies, so that their modification does not change
111+
the global definition of the color sequence.
112+
113+
Additional color sequences can be added via
114+
`.ColorSequenceRegistry.register`::
115+
116+
mpl.color_sequences.register('rgb', ['r', 'g' ,'b'])
117+
"""
118+
119+
_BUILTIN_COLOR_SEQUENCES = {
120+
'tab10': _cm._tab10_data,
121+
'tab20': _cm._tab20_data,
122+
'tab20b': _cm._tab20b_data,
123+
'tab20c': _cm._tab20c_data,
124+
'Pastel1': _cm._Pastel1_data,
125+
'Pastel2': _cm._Pastel2_data,
126+
'Paired': _cm._Paired_data,
127+
'Accent': _cm._Accent_data,
128+
'Dark2': _cm._Dark2_data,
129+
'Set1': _cm._Set1_data,
130+
'Set2': _cm._Set1_data,
131+
'Set3': _cm._Set1_data,
132+
}
133+
134+
def __init__(self):
135+
self._color_sequences = {**self._BUILTIN_COLOR_SEQUENCES}
136+
137+
def __getitem__(self, item):
138+
try:
139+
return list(self._color_sequences[item])
140+
except KeyError:
141+
raise KeyError(f"{item!r} is not a known color sequence name")
142+
143+
def __iter__(self):
144+
return iter(self._color_sequences)
145+
146+
def __len__(self):
147+
return len(self._color_sequences)
148+
149+
def __str__(self):
150+
return ('ColorSequenceRegistry; available colormaps:\n' +
151+
', '.join(f"'{name}'" for name in self))
152+
153+
def register(self, name, color_list):
154+
"""
155+
Register a new color sequence.
156+
157+
The color sequence registry stores a copy of the given *color_list*, so
158+
that future changes to the original list do not affect the registered
159+
color sequence. Think of this as the registry taking a snapshot
160+
of *color_list* at registration.
161+
162+
Parameters
163+
----------
164+
name : str
165+
The name for the color sequence.
166+
167+
color_list : list of colors
168+
An iterable returning valid Matplotlib colors when iterating over.
169+
Note however that the returned color sequence will always be a
170+
list regardless of the input type.
171+
172+
"""
173+
if name in self._BUILTIN_COLOR_SEQUENCES:
174+
raise ValueError(f"{name!r} is a reserved name for a builtin "
175+
"color sequence")
176+
177+
color_list = list(color_list) # force copy and coerce type to list
178+
for color in color_list:
179+
try:
180+
to_rgba(color)
181+
except ValueError:
182+
raise ValueError(
183+
f"{color!r} is not a valid color specification")
184+
185+
self._color_sequences[name] = color_list
186+
187+
def unregister(self, name):
188+
"""
189+
Remove a sequence from the registry.
190+
191+
You cannot remove built-in color sequences.
192+
193+
If the name is not registered, returns with no error.
194+
"""
195+
if name in self._BUILTIN_COLOR_SEQUENCES:
196+
raise ValueError(
197+
f"Cannot unregister builtin color sequence {name!r}")
198+
self._color_sequences.pop(name, None)
199+
200+
201+
_color_sequences = ColorSequenceRegistry()
202+
203+
97204
def _sanitize_extrema(ex):
98205
if ex is None:
99206
return ex

lib/matplotlib/pyplot.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171

7272
from matplotlib import cm
7373
from matplotlib.cm import _colormaps as colormaps, get_cmap, register_cmap
74+
from matplotlib.colors import _color_sequences as color_sequences
7475

7576
import numpy as np
7677

lib/matplotlib/tests/test_colors.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1510,3 +1510,43 @@ def test_make_norm_from_scale_name():
15101510
logitnorm = mcolors.make_norm_from_scale(
15111511
mscale.LogitScale, mcolors.Normalize)
15121512
assert logitnorm.__name__ == logitnorm.__qualname__ == "LogitScaleNorm"
1513+
1514+
1515+
def test_color_sequences():
1516+
# basic access
1517+
assert plt.color_sequences is matplotlib.color_sequences # same registry
1518+
assert list(plt.color_sequences) == [
1519+
'tab10', 'tab20', 'tab20b', 'tab20c', 'Pastel1', 'Pastel2', 'Paired',
1520+
'Accent', 'Dark2', 'Set1', 'Set2', 'Set3']
1521+
assert len(plt.color_sequences['tab10']) == 10
1522+
assert len(plt.color_sequences['tab20']) == 20
1523+
1524+
tab_colors = [
1525+
'tab:blue', 'tab:orange', 'tab:green', 'tab:red', 'tab:purple',
1526+
'tab:brown', 'tab:pink', 'tab:gray', 'tab:olive', 'tab:cyan']
1527+
for seq_color, tab_color in zip(plt.color_sequences['tab10'], tab_colors):
1528+
assert mcolors.to_rgb(seq_color) == mcolors.to_rgb(tab_color)
1529+
1530+
# registering
1531+
with pytest.raises(ValueError, match="reserved name"):
1532+
plt.color_sequences.register('tab10', ['r', 'g', 'b'])
1533+
with pytest.raises(ValueError, match="not a valid color specification"):
1534+
plt.color_sequences.register('invalid', ['not a color'])
1535+
1536+
rgb_colors = ['r', 'g', 'b']
1537+
plt.color_sequences.register('rgb', rgb_colors)
1538+
assert plt.color_sequences['rgb'] == ['r', 'g', 'b']
1539+
# should not affect the registered sequence because input is copied
1540+
rgb_colors.append('c')
1541+
assert plt.color_sequences['rgb'] == ['r', 'g', 'b']
1542+
# should not affect the registered sequence because returned list is a copy
1543+
plt.color_sequences['rgb'].append('c')
1544+
assert plt.color_sequences['rgb'] == ['r', 'g', 'b']
1545+
1546+
# unregister
1547+
plt.color_sequences.unregister('rgb')
1548+
with pytest.raises(KeyError):
1549+
plt.color_sequences['rgb'] # rgb is gone
1550+
plt.color_sequences.unregister('rgb') # multiple unregisters are ok
1551+
with pytest.raises(ValueError, match="Cannot unregister builtin"):
1552+
plt.color_sequences.unregister('tab10')

0 commit comments

Comments
 (0)
0