diff --git a/examples/pylab_examples/color_demo.py b/examples/pylab_examples/color_demo.py index 1d471b0086a9..adfdf273a683 100755 --- a/examples/pylab_examples/color_demo.py +++ b/examples/pylab_examples/color_demo.py @@ -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 @@ -11,6 +11,10 @@ 4) as a string representing a floating point number from 0 to 1, corresponding to shades of gray. + 5) as a string representing the Nth color in the + currently active color cycle, e.g. "[0]" + represents the first color in the color cycle. + See help(colors) for more info. """ import matplotlib.pyplot as plt @@ -20,8 +24,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, '[0]') +plt.xlabel('time (s)', color='[0]') plt.ylabel('voltage (mV)', color='0.5') # grayscale color plt.title('About as silly as it gets, folks', color='#afeeee') plt.show() diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 938cbefa5bc5..71fdc2c2c28f 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -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 @@ -2452,7 +2453,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, @@ -2471,8 +2472,9 @@ def pie(self, x, explode=None, labels=None, colors=None, fraction of the radius with which to offset each wedge. *colors*: [ *None* | color sequence ] - A sequence of matplotlib color args through which the pie chart - will cycle. + A sequence of matplotlib color args through which the pie + chart will cycle. If `None`, will use the colors in the + default property cycle. *labels*: [ *None* | len(x) sequence of strings ] A sequence of strings providing the labels for each wedge @@ -2561,7 +2563,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 @@ -2597,7 +2604,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) @@ -3017,8 +3024,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 = six.next(self._get_lines.prop_cycler)['color'] + if l0 is None: + ecolor = self._get_lines.get_next_color() else: ecolor = l0.get_color() @@ -5966,9 +5973,8 @@ def hist(self, x, bins=None, range=None, normed=False, weights=None, nx = len(x) # number of datasets - if color is None and 'color' in self._get_lines._prop_keys: - color = [six.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: diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index fa0a42f17c99..1f7955b2e389 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -10,6 +10,7 @@ import warnings import math from operator import itemgetter +import re import numpy as np from numpy import ma @@ -94,6 +95,17 @@ def _process_plot_format(fmt): linestyle = 'None' fmt = fmt.replace(' ', '') + # handle the N-th color in the color cycle + matches = re.findall('\[[0-9]+\]', fmt) + if len(matches) == 1: + color = mcolors.colorConverter._get_nth_color( + int(matches[0][1:-1])) + fmt = fmt.replace(matches[0], '') + elif len(matches) != 0: + if color is not None: + raise ValueError( + 'Illegal format string "%s"; two color symbols' % fmt) + chars = [c for c in fmt] for c in chars: @@ -160,6 +172,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 @@ -185,6 +201,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) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index df4a2ce2d216..f37ee7f108f7 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -31,6 +31,14 @@ - k: black - w: white +To use the colors that are part of the active color cycle in the +current style, use a string with a positive index in square brackets. +If the index is larger than the color cycle, it is wrapped around. +For example: + + - `[0]`: The first color in the cycle + - `[1]`: 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' @@ -213,6 +221,18 @@ def is_color_like(c): 'Return *True* if *c* can be converted to *RGB*' + # Special-case the N-th color cycle syntax, because it's 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'^\[[0-9]\]$', c) + if match is not None: + return True + elif isinstance(c, six.text_type): + match = re.match('^\[[0-9]\]$', c) + if match is not None: + return True + try: colorConverter.to_rgb(c) return True @@ -260,8 +280,35 @@ class ColorConverter(object): 'k': (0.0, 0.0, 0.0), 'w': (1.0, 1.0, 1.0), } + _prop_cycler = None + cache = {} + @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('^\[[0-9]\]$', val) + if match is not None: + return cls._get_nth_color(int(val[1:-1])) + + raise ValueError("Not a color cycle color") + def to_rgb(self, arg): """ Returns an *RGB* tuple of three floats from 0-1. @@ -299,6 +346,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(argl) + except ValueError: + pass str1 = cnames.get(argl, argl) if str1.startswith('#'): color = hex2color(str1) diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 883db3b0f7da..8cdd60998570 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -827,7 +827,7 @@ def validate_hist_bins(s): # line props 'lines.linewidth': [1.0, validate_float], # line width in points 'lines.linestyle': ['-', six.text_type], # solid line - 'lines.color': ['b', validate_color], # blue + 'lines.color': ['[0]', validate_color], # blue 'lines.marker': ['None', six.text_type], # black 'lines.markeredgewidth': [1.0, validate_float], 'lines.markersize': [6, validate_float], # markersize, in points @@ -843,7 +843,7 @@ def validate_hist_bins(s): ## patch props 'patch.linewidth': [None, validate_float_or_None], # line width in points 'patch.edgecolor': ['k', validate_color], # black - 'patch.facecolor': ['b', validate_color], # blue + 'patch.facecolor': ['[0]', validate_color], # blue 'patch.antialiased': [True, validate_bool], # antialiased (no jaggies) ## Histogram properties @@ -992,14 +992,15 @@ def validate_hist_bins(s): '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'], - deprecate_axes_colorcycle], # cycle of plot - # line colors # This entry can be either a cycler object or a # string repr of a cycler-object, which gets eval()'ed # to create the object. - 'axes.prop_cycle': [ccycler('color', 'bgrcmyk'), - validate_cycler], + 'axes.prop_cycle': [ + ccycler('color', + ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', + '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', + '#bcbd22', '#17becf']), + validate_cycler], # If 'data', axes limits are set close to the data. # If 'round_numbers' axes limits are set to the nearest round numbers. 'axes.autolimit_mode': [ diff --git a/lib/matplotlib/sankey.py b/lib/matplotlib/sankey.py index 423884723461..aa03bfb79560 100755 --- a/lib/matplotlib/sankey.py +++ b/lib/matplotlib/sankey.py @@ -7,6 +7,7 @@ from matplotlib.externals import six from matplotlib.externals.six.moves import zip +from matplotlib import rcParams # Original version by Yannick Copin (ycopin@ipnl.in2p3.fr) 10/2/2010, available # at: @@ -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. diff --git a/lib/matplotlib/stackplot.py b/lib/matplotlib/stackplot.py index 8a34f97c389a..e44885953373 100644 --- a/lib/matplotlib/stackplot.py +++ b/lib/matplotlib/stackplot.py @@ -108,10 +108,7 @@ def stackplot(axes, x, *args, **kwargs): raise ValueError(errstr) # Color between x = 0 and the first array. - if 'color' in axes._get_lines._prop_keys: - color = six.next(axes._get_lines.prop_cycler)['color'] - else: - color = None + color = axes._get_lines.get_next_color() r.append(axes.fill_between(x, first_line, stack[0, :], facecolor=color, label= six.next(labels, None), @@ -120,10 +117,7 @@ def stackplot(axes, x, *args, **kwargs): # Color between array i-1 and array i for i in xrange(len(y) - 1): - if 'color' in axes._get_lines._prop_keys: - color = six.next(axes._get_lines.prop_cycler)['color'] - else: - color = None + color = axes._get_lines.get_next_color() r.append(axes.fill_between(x, stack[i, :], stack[i + 1, :], facecolor=color, label= six.next(labels, None), diff --git a/lib/matplotlib/streamplot.py b/lib/matplotlib/streamplot.py index 7d564c76eb77..470065d9c523 100644 --- a/lib/matplotlib/streamplot.py +++ b/lib/matplotlib/streamplot.py @@ -82,8 +82,8 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, if transform is None: transform = axes.transData - if color is None and 'color' in axes._get_lines._prop_keys: - color = six.next(axes._get_lines.prop_cycler)['color'] + if color is None: + color = axes._get_lines.get_next_color() if linewidth is None: linewidth = matplotlib.rcParams['lines.linewidth'] diff --git a/lib/matplotlib/tests/baseline_images/test_artist/default_edges.png b/lib/matplotlib/tests/baseline_images/test_artist/default_edges.png index 612eb3dd3c48..10bdce3cb132 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_artist/default_edges.png and b/lib/matplotlib/tests/baseline_images/test_artist/default_edges.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_cycles/lineprop_cycle_basic.png b/lib/matplotlib/tests/baseline_images/test_cycles/lineprop_cycle_basic.png index e3706c3216f2..2d20423df44d 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_cycles/lineprop_cycle_basic.png and b/lib/matplotlib/tests/baseline_images/test_cycles/lineprop_cycle_basic.png differ diff --git a/matplotlibrc.template b/matplotlibrc.template index 45568b04ef33..bc720c645072 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -314,7 +314,10 @@ backend : %(backend)s #axes.unicode_minus : True # use unicode for the minus symbol # rather than hyphen. See # http://en.wikipedia.org/wiki/Plus_and_minus_signs#Character_codes -#axes.prop_cycle : cycler('color', 'bgrcmyk') +#axes.prop_cycle : cycler('color', +# ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', +# '#9467bd', '#8c564b', '#e377c2', '#7f7f7f', +# '#bcbd22', '#17becf']) # color cycle for plot lines # as list of string colorspecs: # single letter, long name, or