Skip to content

Commit e524354

Browse files
committed
API/ENH: Color cycle handling
API/ENH: Color cycle handling
1 parent 9f83d53 commit e524354

File tree

11 files changed

+152
-52
lines changed

11 files changed

+152
-52
lines changed

examples/pylab_examples/color_demo.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python
22
"""
3-
matplotlib gives you 4 ways to specify colors,
3+
matplotlib gives you 5 ways to specify colors,
44
55
1) as a single letter string, ala MATLAB
66
@@ -11,6 +11,9 @@
1111
4) as a string representing a floating point number
1212
from 0 to 1, corresponding to shades of gray.
1313
14+
5) as a special color "Cn", where n is a number 0-9 specifying the
15+
nth color in the currently active color cycle.
16+
1417
See help(colors) for more info.
1518
"""
1619
import matplotlib.pyplot as plt
@@ -20,8 +23,8 @@
2023
#subplot(111, facecolor='#ababab')
2124
t = np.arange(0.0, 2.0, 0.01)
2225
s = np.sin(2*np.pi*t)
23-
plt.plot(t, s, 'y')
24-
plt.xlabel('time (s)', color='r')
26+
plt.plot(t, s, 'C1')
27+
plt.xlabel('time (s)', color='C1')
2528
plt.ylabel('voltage (mV)', color='0.5') # grayscale color
2629
plt.title('About as silly as it gets, folks', color='#afeeee')
2730
plt.show()

lib/matplotlib/axes/_axes.py

+35-16
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from matplotlib.externals import six
55
from matplotlib.externals.six.moves import reduce, xrange, zip, zip_longest
66

7+
import itertools
78
import math
89
import warnings
910

@@ -2457,7 +2458,7 @@ def pie(self, x, explode=None, labels=None, colors=None,
24572458
Call signature::
24582459
24592460
pie(x, explode=None, labels=None,
2460-
colors=('b', 'g', 'r', 'c', 'm', 'y', 'k', 'w'),
2461+
colors=None,
24612462
autopct=None, pctdistance=0.6, shadow=False,
24622463
labeldistance=1.1, startangle=None, radius=None,
24632464
counterclock=True, wedgeprops=None, textprops=None,
@@ -2477,7 +2478,8 @@ def pie(self, x, explode=None, labels=None, colors=None,
24772478
24782479
*colors*: [ *None* | color sequence ]
24792480
A sequence of matplotlib color args through which the pie chart
2480-
will cycle.
2481+
will cycle. If `None`, will use the colors in the currently
2482+
active cycle.
24812483
24822484
*labels*: [ *None* | len(x) sequence of strings ]
24832485
A sequence of strings providing the labels for each wedge
@@ -2566,7 +2568,12 @@ def pie(self, x, explode=None, labels=None, colors=None,
25662568
if len(x) != len(explode):
25672569
raise ValueError("'explode' must be of length 'x'")
25682570
if colors is None:
2569-
colors = ('b', 'g', 'r', 'c', 'm', 'y', 'k', 'w')
2571+
get_next_color = self._get_patches_for_fill.get_next_color
2572+
else:
2573+
color_cycle = itertools.cycle(colors)
2574+
2575+
def get_next_color():
2576+
return six.next(color_cycle)
25702577

25712578
if radius is None:
25722579
radius = 1
@@ -2602,7 +2609,7 @@ def pie(self, x, explode=None, labels=None, colors=None,
26022609

26032610
w = mpatches.Wedge((x, y), radius, 360. * min(theta1, theta2),
26042611
360. * max(theta1, theta2),
2605-
facecolor=colors[i % len(colors)],
2612+
facecolor=get_next_color(),
26062613
**wedgeprops)
26072614
slices.append(w)
26082615
self.add_patch(w)
@@ -3022,8 +3029,8 @@ def xywhere(xs, ys, mask):
30223029
l0, = self.plot(x, y, fmt, label='_nolegend_', **kwargs)
30233030

30243031
if ecolor is None:
3025-
if l0 is None and 'color' in self._get_lines._prop_keys:
3026-
ecolor = next(self._get_lines.prop_cycler)['color']
3032+
if l0 is None:
3033+
ecolor = self._get_lines.get_next_color()
30273034
else:
30283035
ecolor = l0.get_color()
30293036

@@ -3844,7 +3851,10 @@ def scatter(self, x, y, s=None, c=None, marker='o', cmap=None, norm=None,
38443851
if facecolors is not None:
38453852
c = facecolors
38463853
else:
3847-
c = 'b' # The original default
3854+
if rcParams['_internal.classic_mode']:
3855+
c = 'b' # The original default
3856+
else:
3857+
c = self._get_patches_for_fill.get_next_color()
38483858

38493859
if edgecolors is None and not rcParams['_internal.classic_mode']:
38503860
edgecolors = 'face'
@@ -6015,9 +6025,8 @@ def _normalize_input(inp, ename='input'):
60156025
raise ValueError(
60166026
'weights should have the same shape as x')
60176027

6018-
if color is None and 'color' in self._get_lines._prop_keys:
6019-
color = [next(self._get_lines.prop_cycler)['color']
6020-
for i in xrange(nx)]
6028+
if color is None:
6029+
color = [self._get_lines.get_next_color() for i in xrange(nx)]
60216030
else:
60226031
color = mcolors.colorConverter.to_rgba_array(color)
60236032
if len(color) != nx:
@@ -7503,6 +7512,12 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5,
75037512
perp_lines = self.vlines
75047513
par_lines = self.hlines
75057514

7515+
if rcParams['_internal.classic_mode']:
7516+
fillcolor = 'y'
7517+
edgecolor = 'r'
7518+
else:
7519+
fillcolor = edgecolor = self._get_lines.get_next_color()
7520+
75067521
# Render violins
75077522
bodies = []
75087523
for stats, pos, width in zip(vpstats, positions, widths):
@@ -7513,7 +7528,7 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5,
75137528
bodies += [fill(stats['coords'],
75147529
-vals + pos,
75157530
vals + pos,
7516-
facecolor='y',
7531+
facecolor=fillcolor,
75177532
alpha=0.3)]
75187533
means.append(stats['mean'])
75197534
mins.append(stats['min'])
@@ -7523,20 +7538,24 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5,
75237538

75247539
# Render means
75257540
if showmeans:
7526-
artists['cmeans'] = perp_lines(means, pmins, pmaxes, colors='r')
7541+
artists['cmeans'] = perp_lines(means, pmins, pmaxes,
7542+
colors=edgecolor)
75277543

75287544
# Render extrema
75297545
if showextrema:
7530-
artists['cmaxes'] = perp_lines(maxes, pmins, pmaxes, colors='r')
7531-
artists['cmins'] = perp_lines(mins, pmins, pmaxes, colors='r')
7532-
artists['cbars'] = par_lines(positions, mins, maxes, colors='r')
7546+
artists['cmaxes'] = perp_lines(maxes, pmins, pmaxes,
7547+
colors=edgecolor)
7548+
artists['cmins'] = perp_lines(mins, pmins, pmaxes,
7549+
colors=edgecolor)
7550+
artists['cbars'] = par_lines(positions, mins, maxes,
7551+
colors=edgecolor)
75337552

75347553
# Render medians
75357554
if showmedians:
75367555
artists['cmedians'] = perp_lines(medians,
75377556
pmins,
75387557
pmaxes,
7539-
colors='r')
7558+
colors=edgecolor)
75407559

75417560
return artists
75427561

lib/matplotlib/axes/_base.py

+19-1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ def _process_plot_format(fmt):
5252
* 'ko': black circles
5353
* '.b': blue dots
5454
* 'r--': red dashed lines
55+
* 'C2--': the third color in the color cycle, dashed lines
5556
5657
.. seealso::
5758
@@ -97,7 +98,9 @@ def _process_plot_format(fmt):
9798

9899
chars = [c for c in fmt]
99100

100-
for c in chars:
101+
i = 0
102+
while i < len(chars):
103+
c = chars[i]
101104
if c in mlines.lineStyles:
102105
if linestyle is not None:
103106
raise ValueError(
@@ -113,9 +116,14 @@ def _process_plot_format(fmt):
113116
raise ValueError(
114117
'Illegal format string "%s"; two color symbols' % fmt)
115118
color = c
119+
elif c == 'C' and i < len(chars) - 1:
120+
color_cycle_number = int(chars[i + 1])
121+
color = mcolors.colorConverter._get_nth_color(color_cycle_number)
122+
i += 1
116123
else:
117124
raise ValueError(
118125
'Unrecognized character %c in format string' % c)
126+
i += 1
119127

120128
if linestyle is None and marker is None:
121129
linestyle = rcParams['lines.linestyle']
@@ -161,6 +169,10 @@ def set_prop_cycle(self, *args, **kwargs):
161169
else:
162170
prop_cycler = cycler(*args, **kwargs)
163171

172+
# Make sure the cycler always has at least one color
173+
if 'color' not in prop_cycler.keys:
174+
prop_cycler = prop_cycler * cycler('color', ['k'])
175+
164176
self.prop_cycler = itertools.cycle(prop_cycler)
165177
# This should make a copy
166178
self._prop_keys = prop_cycler.keys
@@ -186,6 +198,12 @@ def __call__(self, *args, **kwargs):
186198
ret = self._grab_next_args(*args, **kwargs)
187199
return ret
188200

201+
def get_next_color(self):
202+
"""
203+
Return the next color in the cycle.
204+
"""
205+
return six.next(self.prop_cycler)['color']
206+
189207
def set_lineprops(self, line, **kwargs):
190208
assert self.command == 'plot', 'set_lineprops only works with "plot"'
191209
line.set(**kwargs)

lib/matplotlib/colors.py

+50
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@
3131
- k: black
3232
- w: white
3333
34+
To use the colors that are part of the active color cycle in the current style,
35+
use `C` followed by a digit. For example:
36+
37+
- `C0`: The first color in the cycle
38+
- `C1`: The second color in the cycle
39+
3440
Gray shades can be given as a string encoding a float in the 0-1 range, e.g.::
3541
3642
color = '0.75'
@@ -67,6 +73,19 @@
6773

6874
def is_color_like(c):
6975
'Return *True* if *c* can be converted to *RGB*'
76+
77+
# Special-case the N-th color cycle syntax, because its parsing
78+
# needs to be deferred. We may be reading a value from rcParams
79+
# here before the color_cycle rcParam has been parsed.
80+
if isinstance(c, bytes):
81+
match = re.match(b'^C[0-9]$', c)
82+
if match is not None:
83+
return True
84+
elif isinstance(c, six.text_type):
85+
match = re.match('^C[0-9]$', c)
86+
if match is not None:
87+
return True
88+
7089
try:
7190
colorConverter.to_rgb(c)
7291
return True
@@ -114,9 +133,36 @@ class ColorConverter(object):
114133
'k': (0, 0, 0),
115134
'w': (1, 1, 1)}
116135

136+
_prop_cycler = None
137+
117138
cache = {}
118139
CN_LOOKUPS = [COLOR_NAMES[k] for k in ['css4', 'xkcd']]
119140

141+
@classmethod
142+
def _get_nth_color(cls, val):
143+
"""
144+
Get the Nth color in the current color cycle. If N is greater
145+
than the number of colors in the cycle, it is wrapped around.
146+
"""
147+
from matplotlib.rcsetup import cycler
148+
from matplotlib import rcParams
149+
150+
prop_cycler = rcParams['axes.prop_cycle']
151+
if prop_cycler is None and 'axes.color_cycle' in rcParams:
152+
clist = rcParams['axes.color_cycle']
153+
prop_cycler = cycler('color', clist)
154+
155+
colors = prop_cycler._transpose()['color']
156+
return colors[val % len(colors)]
157+
158+
@classmethod
159+
def _parse_nth_color(cls, val):
160+
match = re.match('^C[0-9]$', val)
161+
if match is not None:
162+
return cls._get_nth_color(int(val[1]))
163+
164+
raise ValueError("Not a color cycle color")
165+
120166
def to_rgb(self, arg):
121167
"""
122168
Returns an *RGB* tuple of three floats from 0-1.
@@ -154,6 +200,10 @@ def to_rgb(self, arg):
154200
argl = arg.lower()
155201
color = self.colors.get(argl, None)
156202
if color is None:
203+
try:
204+
argl = self._parse_nth_color(arg)
205+
except ValueError:
206+
pass
157207
for cmapping in self.CN_LOOKUPS:
158208
str1 = cmapping.get(argl, argl)
159209
if str1 != argl:

lib/matplotlib/rcsetup.py

+10-7
Original file line numberDiff line numberDiff line change
@@ -852,7 +852,7 @@ def validate_hist_bins(s):
852852
# line props
853853
'lines.linewidth': [1.5, validate_float], # line width in points
854854
'lines.linestyle': ['-', six.text_type], # solid line
855-
'lines.color': ['b', validate_color], # blue
855+
'lines.color': ['C0', validate_color], # first color in color cycle
856856
'lines.marker': ['None', six.text_type], # black
857857
'lines.markeredgewidth': [1.0, validate_float],
858858
'lines.markersize': [6, validate_float], # markersize, in points
@@ -871,7 +871,7 @@ def validate_hist_bins(s):
871871
## patch props
872872
'patch.linewidth': [None, validate_float_or_None], # line width in points
873873
'patch.edgecolor': ['k', validate_color], # black
874-
'patch.facecolor': ['#1f77b4', validate_color], # blue (first color in color cycle)
874+
'patch.facecolor': ['C0', validate_color], # first color in color cycle
875875
'patch.antialiased': [True, validate_bool], # antialiased (no jaggies)
876876

877877
## hatch props
@@ -892,27 +892,27 @@ def validate_hist_bins(s):
892892
'boxplot.showfliers': [True, validate_bool],
893893
'boxplot.meanline': [False, validate_bool],
894894

895-
'boxplot.flierprops.color': ['b', validate_color],
895+
'boxplot.flierprops.color': ['C0', validate_color],
896896
'boxplot.flierprops.marker': ['+', six.text_type],
897897
'boxplot.flierprops.markerfacecolor': ['auto', validate_color_or_auto],
898898
'boxplot.flierprops.markeredgecolor': ['k', validate_color],
899899
'boxplot.flierprops.markersize': [6, validate_float],
900900
'boxplot.flierprops.linestyle': ['none', six.text_type],
901901
'boxplot.flierprops.linewidth': [1.0, validate_float],
902902

903-
'boxplot.boxprops.color': ['b', validate_color],
903+
'boxplot.boxprops.color': ['C0', validate_color],
904904
'boxplot.boxprops.linewidth': [1.0, validate_float],
905905
'boxplot.boxprops.linestyle': ['-', six.text_type],
906906

907-
'boxplot.whiskerprops.color': ['b', validate_color],
907+
'boxplot.whiskerprops.color': ['C0', validate_color],
908908
'boxplot.whiskerprops.linewidth': [1.0, validate_float],
909909
'boxplot.whiskerprops.linestyle': ['--', six.text_type],
910910

911911
'boxplot.capprops.color': ['k', validate_color],
912912
'boxplot.capprops.linewidth': [1.0, validate_float],
913913
'boxplot.capprops.linestyle': ['-', six.text_type],
914914

915-
'boxplot.medianprops.color': ['r', validate_color],
915+
'boxplot.medianprops.color': ['C1', validate_color],
916916
'boxplot.medianprops.linewidth': [1.0, validate_float],
917917
'boxplot.medianprops.linestyle': ['-', six.text_type],
918918

@@ -1023,7 +1023,10 @@ def validate_hist_bins(s):
10231023
'axes.formatter.use_mathtext': [False, validate_bool],
10241024
'axes.formatter.useoffset': [True, validate_bool],
10251025
'axes.unicode_minus': [True, validate_bool],
1026-
'axes.color_cycle': [['b', 'g', 'r', 'c', 'm', 'y', 'k'],
1026+
'axes.color_cycle': [
1027+
['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728',
1028+
'#9467bd', '#8c564b', '#e377c2', '#7f7f7f',
1029+
'#bcbd22', '#17becf'],
10271030
deprecate_axes_colorcycle], # cycle of plot
10281031
# line colors
10291032
# This entry can be either a cycler object or a

lib/matplotlib/sankey.py

+10-5
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
from matplotlib.transforms import Affine2D
4646
from matplotlib import verbose
4747
from matplotlib import docstring
48+
from matplotlib import rcParams
4849

4950
__author__ = "Kevin L. Davies"
5051
__credits__ = ["Yannick Copin"]
@@ -770,11 +771,15 @@ def _get_angle(a, r):
770771
print("lrpath\n", self._revert(lrpath))
771772
xs, ys = list(zip(*vertices))
772773
self.ax.plot(xs, ys, 'go-')
773-
patch = PathPatch(Path(vertices, codes),
774-
fc=kwargs.pop('fc', kwargs.pop('facecolor',
775-
'#bfd1d4')), # Custom defaults
776-
lw=kwargs.pop('lw', kwargs.pop('linewidth', 0.5)),
777-
**kwargs)
774+
if rcParams['_internal.classic_mode']:
775+
fc = kwargs.pop('fc', kwargs.pop('facecolor', '#bfd1d4'))
776+
lw = kwargs.pop('lw', kwargs.pop('linewidth', 0.5))
777+
else:
778+
fc = kwargs.pop('fc', kwargs.pop('facecolor', None))
779+
lw = kwargs.pop('lw', kwargs.pop('linewidth', None))
780+
if fc is None:
781+
fc = six.next(self.ax._get_patches_for_fill.prop_cycler)['color']
782+
patch = PathPatch(Path(vertices, codes), fc=fc, lw=lw, **kwargs)
778783
self.ax.add_patch(patch)
779784

780785
# Add the path labels.

0 commit comments

Comments
 (0)