Skip to content

Color cycle handling #6291

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 8 commits into from
Apr 28, 2016
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
9 changes: 6 additions & 3 deletions examples/pylab_examples/color_demo.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python
"""
matplotlib gives you 4 ways to specify colors,
matplotlib gives you 5 ways to specify colors,

1) as a single letter string, ala MATLAB

Expand All @@ -11,6 +11,9 @@
4) as a string representing a floating point number
from 0 to 1, corresponding to shades of gray.

5) as a special color "Cn", where n is a number 0-9 specifying the
Copy link
Member

Choose a reason for hiding this comment

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

The sentence above should be updated to say 5 ways.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for noticing. Done.

nth color in the currently active color cycle.

See help(colors) for more info.
"""
import matplotlib.pyplot as plt
Expand All @@ -20,8 +23,8 @@
#subplot(111, facecolor='#ababab')
t = np.arange(0.0, 2.0, 0.01)
s = np.sin(2*np.pi*t)
plt.plot(t, s, 'y')
plt.xlabel('time (s)', color='r')
plt.plot(t, s, 'C1')
plt.xlabel('time (s)', color='C1')
plt.ylabel('voltage (mV)', color='0.5') # grayscale color
plt.title('About as silly as it gets, folks', color='#afeeee')
plt.show()
51 changes: 35 additions & 16 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from matplotlib.externals import six
from matplotlib.externals.six.moves import reduce, xrange, zip, zip_longest

import itertools
import math
import warnings

Expand Down Expand Up @@ -2458,7 +2459,7 @@ def pie(self, x, explode=None, labels=None, colors=None,
Call signature::

pie(x, explode=None, labels=None,
colors=('b', 'g', 'r', 'c', 'm', 'y', 'k', 'w'),
colors=None,
autopct=None, pctdistance=0.6, shadow=False,
labeldistance=1.1, startangle=None, radius=None,
counterclock=True, wedgeprops=None, textprops=None,
Expand All @@ -2478,7 +2479,8 @@ def pie(self, x, explode=None, labels=None, colors=None,

*colors*: [ *None* | color sequence ]
A sequence of matplotlib color args through which the pie chart
will cycle.
will cycle. If `None`, will use the colors in the currently
active cycle.

*labels*: [ *None* | len(x) sequence of strings ]
A sequence of strings providing the labels for each wedge
Expand Down Expand Up @@ -2567,7 +2569,12 @@ def pie(self, x, explode=None, labels=None, colors=None,
if len(x) != len(explode):
raise ValueError("'explode' must be of length 'x'")
if colors is None:
colors = ('b', 'g', 'r', 'c', 'm', 'y', 'k', 'w')
get_next_color = self._get_patches_for_fill.get_next_color
else:
color_cycle = itertools.cycle(colors)

def get_next_color():
return six.next(color_cycle)

if radius is None:
radius = 1
Expand Down Expand Up @@ -2603,7 +2610,7 @@ def pie(self, x, explode=None, labels=None, colors=None,

w = mpatches.Wedge((x, y), radius, 360. * min(theta1, theta2),
360. * max(theta1, theta2),
facecolor=colors[i % len(colors)],
facecolor=get_next_color(),
**wedgeprops)
slices.append(w)
self.add_patch(w)
Expand Down Expand Up @@ -3023,8 +3030,8 @@ def xywhere(xs, ys, mask):
l0, = self.plot(x, y, fmt, label='_nolegend_', **kwargs)

if ecolor is None:
if l0 is None and 'color' in self._get_lines._prop_keys:
ecolor = next(self._get_lines.prop_cycler)['color']
if l0 is None:
ecolor = self._get_lines.get_next_color()
else:
ecolor = l0.get_color()

Expand Down Expand Up @@ -3846,7 +3853,10 @@ def scatter(self, x, y, s=None, c=None, marker='o', cmap=None, norm=None,
if facecolors is not None:
c = facecolors
else:
c = 'b' # The original default
if rcParams['_internal.classic_mode']:
c = 'b' # The original default
else:
c = self._get_patches_for_fill.get_next_color()
Copy link
Member

Choose a reason for hiding this comment

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

I am +.75 on this.

Copy link
Member Author

Choose a reason for hiding this comment

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

vs. c = 'C0'?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, that is the only other plausibly defensible position.

My only reservation is that we have pushed back on this suggestion for so long and that in most cases if you are not mapping the color you probably want to be using plot(x, y, 'o') instead.

That said, I have no real protest with this.


if edgecolors is None and not rcParams['_internal.classic_mode']:
edgecolors = 'face'
Expand Down Expand Up @@ -6019,9 +6029,8 @@ def _normalize_input(inp, ename='input'):
raise ValueError(
'weights should have the same shape as x')

if color is None and 'color' in self._get_lines._prop_keys:
color = [next(self._get_lines.prop_cycler)['color']
for i in xrange(nx)]
if color is None:
color = [self._get_lines.get_next_color() for i in xrange(nx)]
else:
color = mcolors.colorConverter.to_rgba_array(color)
if len(color) != nx:
Expand Down Expand Up @@ -7507,6 +7516,12 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5,
perp_lines = self.vlines
par_lines = self.hlines

if rcParams['_internal.classic_mode']:
fillcolor = 'y'
edgecolor = 'r'
else:
fillcolor = edgecolor = self._get_lines.get_next_color()

# Render violins
bodies = []
for stats, pos, width in zip(vpstats, positions, widths):
Expand All @@ -7517,7 +7532,7 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5,
bodies += [fill(stats['coords'],
-vals + pos,
vals + pos,
facecolor='y',
facecolor=fillcolor,
alpha=0.3)]
means.append(stats['mean'])
mins.append(stats['min'])
Expand All @@ -7527,20 +7542,24 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5,

# Render means
if showmeans:
artists['cmeans'] = perp_lines(means, pmins, pmaxes, colors='r')
artists['cmeans'] = perp_lines(means, pmins, pmaxes,
colors=edgecolor)

# Render extrema
if showextrema:
artists['cmaxes'] = perp_lines(maxes, pmins, pmaxes, colors='r')
artists['cmins'] = perp_lines(mins, pmins, pmaxes, colors='r')
artists['cbars'] = par_lines(positions, mins, maxes, colors='r')
artists['cmaxes'] = perp_lines(maxes, pmins, pmaxes,
colors=edgecolor)
artists['cmins'] = perp_lines(mins, pmins, pmaxes,
colors=edgecolor)
artists['cbars'] = par_lines(positions, mins, maxes,
colors=edgecolor)

# Render medians
if showmedians:
artists['cmedians'] = perp_lines(medians,
pmins,
pmaxes,
colors='r')
colors=edgecolor)

return artists

Expand Down
20 changes: 19 additions & 1 deletion lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def _process_plot_format(fmt):
* 'ko': black circles
* '.b': blue dots
* 'r--': red dashed lines
* 'C2--': the third color in the color cycle, dashed lines

.. seealso::

Expand Down Expand Up @@ -97,7 +98,9 @@ def _process_plot_format(fmt):

chars = [c for c in fmt]

for c in chars:
i = 0
while i < len(chars):
c = chars[i]
if c in mlines.lineStyles:
if linestyle is not None:
raise ValueError(
Expand All @@ -113,9 +116,14 @@ def _process_plot_format(fmt):
raise ValueError(
'Illegal format string "%s"; two color symbols' % fmt)
color = c
elif c == 'C' and i < len(chars) - 1:
color_cycle_number = int(chars[i + 1])
Copy link
Member

Choose a reason for hiding this comment

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

Limited to 10 I guess, but what if you wanted to use something like Vega20 (not that we have that as a cycle at the moment)?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, we should probably document that limitation. The problem with opening it up to more than 10 is that it makes backward compatibility much harder. Should C11 be the 12th color or the 2nd color with a triangle up marker? If we did allow more than one digit, we'd force people to have to write 1C1.

Since more than 10 colors is sort of an edge case, and ill-advised even then, I think it's fine to have this limitation.

color = mcolors.colorConverter._get_nth_color(color_cycle_number)
i += 1
else:
raise ValueError(
'Unrecognized character %c in format string' % c)
i += 1

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

# Make sure the cycler always has at least one color
if 'color' not in prop_cycler.keys:
prop_cycler = prop_cycler * cycler('color', ['k'])

self.prop_cycler = itertools.cycle(prop_cycler)
# This should make a copy
self._prop_keys = prop_cycler.keys
Expand All @@ -186,6 +198,12 @@ def __call__(self, *args, **kwargs):
ret = self._grab_next_args(*args, **kwargs)
return ret

def get_next_color(self):
"""
Return the next color in the cycle.
"""
return six.next(self.prop_cycler)['color']

def set_lineprops(self, line, **kwargs):
assert self.command == 'plot', 'set_lineprops only works with "plot"'
line.set(**kwargs)
Expand Down
50 changes: 50 additions & 0 deletions lib/matplotlib/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@
- k: black
- w: white

To use the colors that are part of the active color cycle in the current style,
use `C` followed by a digit. For example:

- `C0`: The first color in the cycle
- `C1`: The second color in the cycle

Gray shades can be given as a string encoding a float in the 0-1 range, e.g.::

color = '0.75'
Expand Down Expand Up @@ -67,6 +73,19 @@

def is_color_like(c):
'Return *True* if *c* can be converted to *RGB*'

# Special-case the N-th color cycle syntax, because its parsing
# needs to be deferred. We may be reading a value from rcParams
# here before the color_cycle rcParam has been parsed.
if isinstance(c, bytes):
match = re.match(b'^C[0-9]$', c)
if match is not None:
return True
elif isinstance(c, six.text_type):
match = re.match('^C[0-9]$', c)
if match is not None:
return True

try:
colorConverter.to_rgb(c)
return True
Expand Down Expand Up @@ -114,9 +133,36 @@ class ColorConverter(object):
'k': (0, 0, 0),
'w': (1, 1, 1)}

_prop_cycler = None

cache = {}
CN_LOOKUPS = [COLOR_NAMES[k] for k in ['css4', 'xkcd']]

@classmethod
def _get_nth_color(cls, val):
"""
Get the Nth color in the current color cycle. If N is greater
than the number of colors in the cycle, it is wrapped around.
"""
from matplotlib.rcsetup import cycler
from matplotlib import rcParams

prop_cycler = rcParams['axes.prop_cycle']
if prop_cycler is None and 'axes.color_cycle' in rcParams:
clist = rcParams['axes.color_cycle']
prop_cycler = cycler('color', clist)

colors = prop_cycler._transpose()['color']
return colors[val % len(colors)]

@classmethod
def _parse_nth_color(cls, val):
match = re.match('^C[0-9]$', val)
if match is not None:
return cls._get_nth_color(int(val[1]))

raise ValueError("Not a color cycle color")

def to_rgb(self, arg):
"""
Returns an *RGB* tuple of three floats from 0-1.
Expand Down Expand Up @@ -154,6 +200,10 @@ def to_rgb(self, arg):
argl = arg.lower()
color = self.colors.get(argl, None)
if color is None:
try:
argl = self._parse_nth_color(arg)
except ValueError:
pass
for cmapping in self.CN_LOOKUPS:
str1 = cmapping.get(argl, argl)
if str1 != argl:
Expand Down
17 changes: 10 additions & 7 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -848,7 +848,7 @@ def validate_animation_writer_path(p):
# line props
'lines.linewidth': [2.5, validate_float], # line width in points
'lines.linestyle': ['-', six.text_type], # solid line
'lines.color': ['b', validate_color], # blue
'lines.color': ['C0', validate_color], # first color in color cycle
'lines.marker': ['None', six.text_type], # black
'lines.markeredgewidth': [1.0, validate_float],
'lines.markersize': [6, validate_float], # markersize, in points
Expand All @@ -867,7 +867,7 @@ def validate_animation_writer_path(p):
## patch props
'patch.linewidth': [None, validate_float_or_None], # line width in points
'patch.edgecolor': ['k', validate_color], # black
'patch.facecolor': ['#1f77b4', validate_color], # blue (first color in color cycle)
'patch.facecolor': ['C0', validate_color], # first color in color cycle
'patch.antialiased': [True, validate_bool], # antialiased (no jaggies)

## Histogram properties
Expand All @@ -885,27 +885,27 @@ def validate_animation_writer_path(p):
'boxplot.showfliers': [True, validate_bool],
'boxplot.meanline': [False, validate_bool],

'boxplot.flierprops.color': ['b', validate_color],
'boxplot.flierprops.color': ['C0', validate_color],
'boxplot.flierprops.marker': ['+', six.text_type],
'boxplot.flierprops.markerfacecolor': ['auto', validate_color_or_auto],
'boxplot.flierprops.markeredgecolor': ['k', validate_color],
'boxplot.flierprops.markersize': [6, validate_float],
'boxplot.flierprops.linestyle': ['none', six.text_type],
'boxplot.flierprops.linewidth': [1.0, validate_float],

'boxplot.boxprops.color': ['b', validate_color],
'boxplot.boxprops.color': ['C0', validate_color],
'boxplot.boxprops.linewidth': [1.0, validate_float],
'boxplot.boxprops.linestyle': ['-', six.text_type],

'boxplot.whiskerprops.color': ['b', validate_color],
'boxplot.whiskerprops.color': ['C0', validate_color],
'boxplot.whiskerprops.linewidth': [1.0, validate_float],
'boxplot.whiskerprops.linestyle': ['--', six.text_type],

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

'boxplot.medianprops.color': ['r', validate_color],
'boxplot.medianprops.color': ['C1', validate_color],
'boxplot.medianprops.linewidth': [1.0, validate_float],
'boxplot.medianprops.linestyle': ['-', six.text_type],

Expand Down Expand Up @@ -1016,7 +1016,10 @@ def validate_animation_writer_path(p):
'axes.formatter.use_mathtext': [False, validate_bool],
'axes.formatter.useoffset': [True, validate_bool],
'axes.unicode_minus': [True, validate_bool],
'axes.color_cycle': [['b', 'g', 'r', 'c', 'm', 'y', 'k'],
'axes.color_cycle': [
['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728',
'#9467bd', '#8c564b', '#e377c2', '#7f7f7f',
'#bcbd22', '#17becf'],
deprecate_axes_colorcycle], # cycle of plot
# line colors
# This entry can be either a cycler object or a
Expand Down
15 changes: 10 additions & 5 deletions lib/matplotlib/sankey.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from matplotlib.transforms import Affine2D
from matplotlib import verbose
from matplotlib import docstring
from matplotlib import rcParams

__author__ = "Kevin L. Davies"
__credits__ = ["Yannick Copin"]
Expand Down Expand Up @@ -770,11 +771,15 @@ def _get_angle(a, r):
print("lrpath\n", self._revert(lrpath))
xs, ys = list(zip(*vertices))
self.ax.plot(xs, ys, 'go-')
patch = PathPatch(Path(vertices, codes),
fc=kwargs.pop('fc', kwargs.pop('facecolor',
'#bfd1d4')), # Custom defaults
lw=kwargs.pop('lw', kwargs.pop('linewidth', 0.5)),
**kwargs)
if rcParams['_internal.classic_mode']:
fc = kwargs.pop('fc', kwargs.pop('facecolor', '#bfd1d4'))
lw = kwargs.pop('lw', kwargs.pop('linewidth', 0.5))
else:
fc = kwargs.pop('fc', kwargs.pop('facecolor', None))
lw = kwargs.pop('lw', kwargs.pop('linewidth', None))
if fc is None:
fc = six.next(self.ax._get_patches_for_fill.prop_cycler)['color']
patch = PathPatch(Path(vertices, codes), fc=fc, lw=lw, **kwargs)
self.ax.add_patch(patch)

# Add the path labels.
Expand Down
Loading