8000 Merge pull request #22387 from timhoffm/color-sequences · matplotlib/matplotlib@6997192 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6997192

Browse files
authored
Merge pull request #22387 from timhoffm/color-sequences
Add a registry for color sequences
2 parents c358ba5 + 0abe0ce commit 6997192

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+
Normalize
25+
NoNorm
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.same_color(seq_color, 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