diff --git a/doc/api/api_changes/2015-08-17-remove_color_cycle.rst b/doc/api/api_changes/2015-08-17-remove_color_cycle.rst new file mode 100644 index 000000000000..1d92d74ab1cf --- /dev/null +++ b/doc/api/api_changes/2015-08-17-remove_color_cycle.rst @@ -0,0 +1,17 @@ +`color_cycle` deprecated +```````````````````````` + +In light of the new property cycling feature, +the Axes method *set_color_cycle* is now deprecated. +Calling this method will replace the current property cycle with +one that cycles just the given colors. + +Similarly, the rc parameter *axes.color_cycle* is also deprecated in +lieu of the new *axes.prop_cycle* parameter. Having both parameters in +the same rc file is not recommended as the result cannot be +predicted. For compatibility, setting *axes.color_cycle* will +replace the cycler in *axes.prop_cycle* with a color cycle. +Accessing *axes.color_cycle* will return just the color portion +of the property cycle, if it exists. + +Timeline for removal has not been set. diff --git a/doc/users/whats_new/rcparams.rst b/doc/users/whats_new/rcparams.rst index 3e0db2815628..68b37dc29ef7 100644 --- a/doc/users/whats_new/rcparams.rst +++ b/doc/users/whats_new/rcparams.rst @@ -1,3 +1,19 @@ +Added ``axes.prop_cycle`` key to rcParams +````````````````````````````````````````` +This is a more generic form of the now-deprecated ``axes.color_cycle`` param. +Now, we can cycle more than just colors, but also linestyles, hatches, +and just about any other artist property. Cycler notation is used for +defining proprty cycles. Adding cyclers together will be like you are +`zip()`-ing together two or more property cycles together:: + + axes.prop_cycle: cycler('color', 'rgb') + cycler('lw', [1, 2, 3]) + +You can even multiply cyclers, which is like using `itertools.product()` +on two or more property cycles. Remember to use parentheses if writing +a multi-line `prop_cycle` parameter. + +..plot:: mpl_examples/color/color_cycle_demo.py + Added ``errorbar.capsize`` key to rcParams `````````````````````````````````````````` Controls the length of end caps on error bars. If set to zero, errorbars diff --git a/examples/color/color_cycle_demo.py b/examples/color/color_cycle_demo.py index 5fdd01cd287e..647549e77bc2 100644 --- a/examples/color/color_cycle_demo.py +++ b/examples/color/color_cycle_demo.py @@ -1,13 +1,15 @@ """ -Demo of custom color-cycle settings to control colors for multi-line plots. +Demo of custom property-cycle settings to control colors and such +for multi-line plots. This example demonstrates two different APIs: - 1. Setting the default rc-parameter specifying the color cycle. - This affects all subsequent plots. - 2. Setting the color cycle for a specific axes. This only affects a single - axes. + 1. Setting the default rc-parameter specifying the property cycle. + This affects all subsequent axes (but not axes already created). + 2. Setting the property cycle for a specific axes. This only + affects a single axes. """ +from cycler import cycler import numpy as np import matplotlib.pyplot as plt @@ -17,13 +19,14 @@ yy = np.transpose([np.sin(x + phi) for phi in offsets]) plt.rc('lines', linewidth=4) +plt.rc('axes', prop_cycle=(cycler('color', ['r', 'g', 'b', 'y']) + + cycler('linestyle', ['-', '--', ':', '-.']))) fig, (ax0, ax1) = plt.subplots(nrows=2) - -plt.rc('axes', color_cycle=['r', 'g', 'b', 'y']) ax0.plot(yy) ax0.set_title('Set default color cycle to rgby') -ax1.set_color_cycle(['c', 'm', 'y', 'k']) +ax1.set_prop_cycle(cycler('color', ['c', 'm', 'y', 'k']) + + cycler('lw', [1, 2, 3, 4])) ax1.plot(yy) ax1.set_title('Set axes color cycle to cmyk') diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index c85984b87981..fee4bd4d3f7d 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -121,7 +121,8 @@ from matplotlib.cbook import is_string_like, mplDeprecation from matplotlib.compat import subprocess from matplotlib.rcsetup import (defaultParams, - validate_backend) + validate_backend, + cycler) import numpy from matplotlib.externals.six.moves.urllib.request import urlopen @@ -826,6 +827,8 @@ def matplotlib_fname(): 'svg.embed_char_paths': ('svg.fonttype', lambda x: "path" if x else "none", None), 'savefig.extension': ('savefig.format', lambda x: x, None), + 'axes.color_cycle': ('axes.prop_cycle', lambda x: cycler('color', x), + lambda x: [c.get('color', None) for c in x]), } _deprecated_ignore_map = { @@ -1452,6 +1455,7 @@ def tk_window_focus(): 'matplotlib.tests.test_transforms', 'matplotlib.tests.test_triangulation', 'matplotlib.tests.test_widgets', + 'matplotlib.tests.test_cycles', 'matplotlib.sphinxext.tests.test_tinypages', 'mpl_toolkits.tests.test_mplot3d', 'mpl_toolkits.tests.test_axes_grid1', diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 010bcfc1daf3..e57f1a4b1290 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -900,13 +900,20 @@ def properties(self): def set(self, **kwargs): """ - A tkstyle set command, pass *kwargs* to set properties + A property batch setter. Pass *kwargs* to set properties. + Will handle property name collisions (e.g., if both + 'color' and 'facecolor' are specified, the property + with higher priority gets set last). + """ ret = [] - for k, v in six.iteritems(kwargs): + for k, v in sorted(kwargs.items(), reverse=True): k = k.lower() funcName = "set_%s" % k - func = getattr(self, funcName) + func = getattr(self, funcName, None) + if func is None: + raise TypeError('There is no %s property "%s"' % + (self.__class__.__name__, k)) ret.extend([func(v)]) return ret @@ -1406,14 +1413,17 @@ def setp(obj, *args, **kwargs): funcvals = [] for i in range(0, len(args) - 1, 2): funcvals.append((args[i], args[i + 1])) - funcvals.extend(kwargs.items()) + funcvals.extend(sorted(kwargs.items(), reverse=True)) ret = [] for o in objs: for s, val in funcvals: s = s.lower() funcName = "set_%s" % s - func = getattr(o, funcName) + func = getattr(o, funcName, None) + if func is None: + raise TypeError('There is no %s property "%s"' % + (o.__class__.__name__, s)) ret.extend([func(val)]) return [x for x in cbook.flatten(ret)] diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 91c752983c08..225720985f67 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -1265,9 +1265,9 @@ def plot(self, *args, **kwargs): Return value is a list of lines that were added. - By default, each line is assigned a different color specified by a - 'color cycle'. To change this behavior, you can edit the - axes.color_cycle rcParam. + By default, each line is assigned a different style specified by a + 'style cycle'. To change this behavior, you can edit the + axes.prop_cycle rcParam. The following format string characters are accepted to control the line style or marker: @@ -2935,8 +2935,8 @@ def xywhere(xs, ys, mask): l0, = self.plot(x, y, fmt, **kwargs) if ecolor is None: - if l0 is None: - ecolor = six.next(self._get_lines.color_cycle) + if l0 is None and 'color' in self._get_lines._prop_keys: + ecolor = six.next(self._get_lines.prop_cycler)['color'] else: ecolor = l0.get_color() @@ -5829,8 +5829,8 @@ def hist(self, x, bins=10, range=None, normed=False, weights=None, nx = len(x) # number of datasets - if color is None: - color = [six.next(self._get_lines.color_cycle) + 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)] else: color = mcolors.colorConverter.to_rgba_array(color) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 50afb7bbfcc3..48562309ac0d 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -32,6 +32,7 @@ from matplotlib.offsetbox import OffsetBox from matplotlib.artist import allow_rasterization from matplotlib.cbook import iterable +from matplotlib.rcsetup import cycler rcParams = matplotlib.rcParams @@ -138,7 +139,7 @@ class _process_plot_var_args(object): def __init__(self, axes, command='plot'): self.axes = axes self.command = command - self.set_color_cycle() + self.set_prop_cycle() def __getstate__(self): # note: it is not possible to pickle a itertools.cycle instance @@ -146,15 +147,22 @@ def __getstate__(self): def __setstate__(self, state): self.__dict__ = state.copy() - self.set_color_cycle() + self.set_prop_cycle() + + def set_prop_cycle(self, *args, **kwargs): + if not (args or kwargs) or (len(args) == 1 and args[0] is None): + 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) + else: + prop_cycler = cycler(*args, **kwargs) - def set_color_cycle(self, clist=None): - if clist is None: - clist = rcParams['axes.color_cycle'] - self.color_cycle = itertools.cycle(clist) + self.prop_cycler = itertools.cycle(prop_cycler) + # This should make a copy + self._prop_keys = prop_cycler.keys def __call__(self, *args, **kwargs): - if self.axes.xaxis is not None and self.axes.yaxis is not None: xunits = kwargs.pop('xunits', self.axes.xaxis.units) @@ -177,21 +185,11 @@ def __call__(self, *args, **kwargs): def set_lineprops(self, line, **kwargs): assert self.command == 'plot', 'set_lineprops only works with "plot"' - for key, val in six.iteritems(kwargs): - funcName = "set_%s" % key - if not hasattr(line, funcName): - raise TypeError('There is no line property "%s"' % key) - func = getattr(line, funcName) - func(val) + line.set(**kwargs) def set_patchprops(self, fill_poly, **kwargs): assert self.command == 'fill', 'set_patchprops only works with "fill"' - for key, val in six.iteritems(kwargs): - funcName = "set_%s" % key - if not hasattr(fill_poly, funcName): - raise TypeError('There is no patch property "%s"' % key) - func = getattr(fill_poly, funcName) - func(val) + fill_poly.set(**kwargs) def _xy_from_xy(self, x, y): if self.axes.xaxis is not None and self.axes.yaxis is not None: @@ -229,24 +227,98 @@ def _xy_from_xy(self, x, y): y = y[:, np.newaxis] return x, y + def _getdefaults(self, ignore, *kwargs): + """ + Only advance the cycler if the cycler has information that + is not specified in any of the supplied tuple of dicts. + Ignore any keys specified in the `ignore` set. + + Returns a copy of defaults dictionary if there are any + keys that are not found in any of the supplied dictionaries. + If the supplied dictionaries have non-None values for + everything the property cycler has, then just return + an empty dictionary. Ignored keys are excluded from the + returned dictionary. + + """ + prop_keys = self._prop_keys + if ignore is None: + ignore = set([]) + prop_keys = prop_keys - ignore + + if any(all(kw.get(k, None) is None for kw in kwargs) + for k in prop_keys): + # Need to copy this dictionary or else the next time around + # in the cycle, the dictionary could be missing entries. + default_dict = six.next(self.prop_cycler).copy() + for p in ignore: + default_dict.pop(p, None) + else: + default_dict = {} + return default_dict + + def _setdefaults(self, defaults, *kwargs): + """ + Given a defaults dictionary, and any other dictionaries, + update those other dictionaries with information in defaults if + none of the other dictionaries contains that information. + + """ + for k in defaults: + if all(kw.get(k, None) is None for kw in kwargs): + for kw in kwargs: + kw[k] = defaults[k] + def _makeline(self, x, y, kw, kwargs): kw = kw.copy() # Don't modify the original kw. kwargs = kwargs.copy() - if kw.get('color', None) is None and kwargs.get('color', None) is None: - kwargs['color'] = kw['color'] = six.next(self.color_cycle) - # (can't use setdefault because it always evaluates - # its second argument) - seg = mlines.Line2D(x, y, - **kw - ) + default_dict = self._getdefaults(None, kw, kwargs) + self._setdefaults(default_dict, kw, kwargs) + seg = mlines.Line2D(x, y, **kw) self.set_lineprops(seg, **kwargs) return seg def _makefill(self, x, y, kw, kwargs): - try: - facecolor = kw['color'] - except KeyError: - facecolor = six.next(self.color_cycle) + kw = kw.copy() # Don't modify the original kw. + kwargs = kwargs.copy() + + # Ignore 'marker'-related properties as they aren't Polygon + # properties, but they are Line2D properties, and so they are + # likely to appear in the default cycler construction. + # This is done here to the defaults dictionary as opposed to the + # other two dictionaries because we do want to capture when a + # *user* explicitly specifies a marker which should be an error. + # We also want to prevent advancing the cycler if there are no + # defaults needed after ignoring the given properties. + ignores = set(['marker', 'markersize', 'markeredgecolor', + 'markerfacecolor', 'markeredgewidth']) + # Also ignore anything provided by *kwargs*. + for k, v in six.iteritems(kwargs): + if v is not None: + ignores.add(k) + + # Only using the first dictionary to use as basis + # for getting defaults for back-compat reasons. + # Doing it with both seems to mess things up in + # various places (probably due to logic bugs elsewhere). + default_dict = self._getdefaults(ignores, kw) + self._setdefaults(default_dict, kw) + + # Looks like we don't want "color" to be interpreted to + # mean both facecolor and edgecolor for some reason. + # So the "kw" dictionary is thrown out, and only its + # 'color' value is kept and translated as a 'facecolor'. + # This design should probably be revisited as it increases + # complexity. + facecolor = kw.get('color', None) + + # Throw out 'color' as it is now handled as a facecolor + default_dict.pop('color', None) + + # To get other properties set from the cycler + # modify the kwargs dictionary. + self._setdefaults(default_dict, kwargs) + seg = mpatches.Polygon(np.hstack((x[:, np.newaxis], y[:, np.newaxis])), facecolor=facecolor, @@ -985,14 +1057,65 @@ def clear(self): """clear the axes""" self.cla() + def set_prop_cycle(self, *args, **kwargs): + """ + Set the property cycle for any future plot commands on this Axes. + + set_prop_cycle(arg) + set_prop_cycle(label, itr) + set_prop_cycle(label1=itr1[, label2=itr2[, ...]]) + + Form 1 simply sets given `Cycler` object. + + Form 2 creates and sets a `Cycler` from a label and an iterable. + + Form 3 composes and sets a `Cycler` as an inner product of the + pairs of keyword arguments. In other words, all of the + iterables are cycled simultaneously, as if through zip(). + + Parameters + ---------- + arg : Cycler + Set the given Cycler. + Can also be `None` to reset to the cycle defined by the + current style. + + label : name + The property key. Must be a valid `Artist` property. + For example, 'color' or 'linestyle'. Aliases are allowed, + such as 'c' for 'color' and 'lw' for 'linewidth'. + + itr : iterable + Finite-length iterable of the property values. These values + are validated and will raise a ValueError if invalid. + + See Also + -------- + :func:`cycler` Convenience function for creating your + own cyclers. + + """ + if args and kwargs: + raise TypeError("Cannot supply both positional and keyword " + "arguments to this method.") + if len(args) == 1 and args[0] is None: + prop_cycle = None + else: + prop_cycle = cycler(*args, **kwargs) + self._get_lines.set_prop_cycle(prop_cycle) + self._get_patches_for_fill.set_prop_cycle(prop_cycle) + def set_color_cycle(self, clist): """ Set the color cycle for any future plot commands on this Axes. *clist* is a list of mpl color specifiers. + + .. deprecated:: 1.5 """ - self._get_lines.set_color_cycle(clist) - self._get_patches_for_fill.set_color_cycle(clist) + cbook.warn_deprecated( + '1.5', name='set_color_cycle', alternative='set_prop_cycle') + self.set_prop_cycle('color', clist) def ishold(self): """return the HOLD status of the axes""" diff --git a/lib/matplotlib/mpl-data/stylelib/bmh.mplstyle b/lib/matplotlib/mpl-data/stylelib/bmh.mplstyle index 56f2b0ed55a0..bc3831df7864 100644 --- a/lib/matplotlib/mpl-data/stylelib/bmh.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/bmh.mplstyle @@ -17,7 +17,7 @@ axes.edgecolor: bcbcbc axes.grid : True axes.titlesize: x-large axes.labelsize: large -axes.color_cycle: 348ABD, A60628, 7A68A6, 467821, D55E00, CC79A7, 56B4E9, 009E73, F0E442, 0072B2 +axes.prop_cycle: cycler('color', ['348ABD', 'A60628', '7A68A6', '467821', 'D55E00', 'CC79A7', '56B4E9', '009E73', 'F0E442', '0072B2']) legend.fancybox: True diff --git a/lib/matplotlib/mpl-data/stylelib/classic.mplstyle b/lib/matplotlib/mpl-data/stylelib/classic.mplstyle index 829b750c2cfb..d556d7a2c175 100644 --- a/lib/matplotlib/mpl-data/stylelib/classic.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/classic.mplstyle @@ -6,7 +6,7 @@ # information on line properties. lines.linewidth : 1.0 # line width in points lines.linestyle : - # solid line -lines.color : b # has no affect on plot(); see axes.color_cycle +lines.color : b # has no affect on plot(); see axes.prop_cycle lines.marker : None # the default marker lines.markeredgewidth : 0.5 # the line width around the marker symbol lines.markersize : 6 # markersize, in points @@ -198,7 +198,8 @@ axes.formatter.useoffset : True # If True, the tick label formatter 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.color_cycle : b, g, r, c, m, y, k # color cycle for plot lines +axes.prop_cycle : cycler('color', 'bgrcmyk') + # color cycle for plot lines # as list of string colorspecs: # single letter, long name, or # web-style hex diff --git a/lib/matplotlib/mpl-data/stylelib/dark_background.mplstyle b/lib/matplotlib/mpl-data/stylelib/dark_background.mplstyle index c3557c1f12b4..a8b754005bfd 100644 --- a/lib/matplotlib/mpl-data/stylelib/dark_background.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/dark_background.mplstyle @@ -8,7 +8,7 @@ text.color: white axes.facecolor: black axes.edgecolor: white axes.labelcolor: white -axes.color_cycle: 8dd3c7, feffb3, bfbbd9, fa8174, 81b1d2, fdb462, b3de69, bc82bd, ccebc4, ffed6f +axes.prop_cycle: cycler('color', ['8dd3c7', 'feffb3', 'bfbbd9', 'fa8174', '81b1d2', 'fdb462', 'b3de69', 'bc82bd', 'ccebc4', 'ffed6f']) xtick.color: white ytick.color: white diff --git a/lib/matplotlib/mpl-data/stylelib/fivethirtyeight.mplstyle b/lib/matplotlib/mpl-data/stylelib/fivethirtyeight.mplstyle index efd91ebb321b..f94046800118 100644 --- a/lib/matplotlib/mpl-data/stylelib/fivethirtyeight.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/fivethirtyeight.mplstyle @@ -6,7 +6,7 @@ lines.solid_capstyle: butt legend.fancybox: true -axes.color_cycle: 30a2da, fc4f30, e5ae38, 6d904f, 8b8b8b +axes.prop_cycle: cycler('color', ['30a2da', 'fc4f30', 'e5ae38', '6d904f', '8b8b8b']) axes.facecolor: f0f0f0 axes.labelsize: large axes.axisbelow: true diff --git a/lib/matplotlib/mpl-data/stylelib/ggplot.mplstyle b/lib/matplotlib/mpl-data/stylelib/ggplot.mplstyle index 5f7e8b3b20b6..67afd83cc42d 100644 --- a/lib/matplotlib/mpl-data/stylelib/ggplot.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/ggplot.mplstyle @@ -16,7 +16,7 @@ axes.labelsize: large axes.labelcolor: 555555 axes.axisbelow: True # grid/ticks are below elements (e.g., lines, text) -axes.color_cycle: E24A33, 348ABD, 988ED5, 777777, FBC15E, 8EBA42, FFB5B8 +axes.prop_cycle: cycler('color', ['E24A33', '348ABD', '988ED5', '777777', 'FBC15E', '8EBA42', 'FFB5B8']) # E24A33 : red # 348ABD : blue # 988ED5 : purple diff --git a/lib/matplotlib/mpl-data/stylelib/grayscale.mplstyle b/lib/matplotlib/mpl-data/stylelib/grayscale.mplstyle index 2012b1b95d36..6a1114e40698 100644 --- a/lib/matplotlib/mpl-data/stylelib/grayscale.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/grayscale.mplstyle @@ -12,7 +12,7 @@ axes.facecolor: white axes.edgecolor: black axes.labelcolor: black # black to light gray -axes.color_cycle: 0.00, 0.40, 0.60, 0.70 +axes.prop_cycle: cycler('color', ['0.00', '0.40', '0.60', '0.70']) xtick.color: black ytick.color: black diff --git a/lib/matplotlib/mpl-data/stylelib/seaborn-bright.mplstyle b/lib/matplotlib/mpl-data/stylelib/seaborn-bright.mplstyle index e4cc354e272b..cbc80dc76b16 100644 --- a/lib/matplotlib/mpl-data/stylelib/seaborn-bright.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/seaborn-bright.mplstyle @@ -1,2 +1,2 @@ # Seaborn bright palette -axes.color_cycle: 003FFF, 03ED3A, E8000B, 8A2BE2, FFC400, 00D7FF +axes.prop_cycle: cycler('color', ['003FFF', '03ED3A', 'E8000B', '8A2BE2', 'FFC400', '00D7FF']) diff --git a/lib/matplotlib/mpl-data/stylelib/seaborn-colorblind.mplstyle b/lib/matplotlib/mpl-data/stylelib/seaborn-colorblind.mplstyle index ff7f474e7fc1..adb5f34d8934 100644 --- a/lib/matplotlib/mpl-data/stylelib/seaborn-colorblind.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/seaborn-colorblind.mplstyle @@ -1,2 +1,2 @@ # Seaborn colorblind palette -axes.color_cycle: 0072B2, 009E73, D55E00, CC79A7, F0E442, 56B4E9 +axes.prop_cycle: cycler('color', ['0072B2', '009E73', 'D55E00', 'CC79A7', 'F0E442', '56B4E9']) diff --git a/lib/matplotlib/mpl-data/stylelib/seaborn-dark-palette.mplstyle b/lib/matplotlib/mpl-data/stylelib/seaborn-dark-palette.mplstyle index 70b6c82a04cd..6f437fdf7392 100644 --- a/lib/matplotlib/mpl-data/stylelib/seaborn-dark-palette.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/seaborn-dark-palette.mplstyle @@ -1,2 +1,2 @@ # Seaborn dark palette -axes.color_cycle: 001C7F, 017517, 8C0900, 7600A1, B8860B, 006374 +axes.prop_cycle: cycler('color', ['001C7F', '017517', '8C0900', '7600A1', 'B8860B', '006374']) diff --git a/lib/matplotlib/mpl-data/stylelib/seaborn-deep.mplstyle b/lib/matplotlib/mpl-data/stylelib/seaborn-deep.mplstyle index f06df83e5c9a..f07e06a13e80 100644 --- a/lib/matplotlib/mpl-data/stylelib/seaborn-deep.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/seaborn-deep.mplstyle @@ -1,2 +1,2 @@ # Seaborn deep palette -axes.color_cycle: 4C72B0, 55A868, C44E52, 8172B2, CCB974, 64B5CD +axes.prop_cycle: cycler('color', ['4C72B0', '55A868', 'C44E52', '8172B2', 'CCB974', '64B5CD']) diff --git a/lib/matplotlib/mpl-data/stylelib/seaborn-muted.mplstyle b/lib/matplotlib/mpl-data/stylelib/seaborn-muted.mplstyle index d8196d13c59d..4d7a0ae7c27a 100644 --- a/lib/matplotlib/mpl-data/stylelib/seaborn-muted.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/seaborn-muted.mplstyle @@ -1,2 +1,2 @@ # Seaborn muted palette -axes.color_cycle: 4878CF, 6ACC65, D65F5F, B47CC7, C4AD66, 77BEDB +axes.prop_cycle: cycler('color', ['4878CF', '6ACC65', 'D65F5F', 'B47CC7', 'C4AD66', '77BEDB']) diff --git a/lib/matplotlib/mpl-data/stylelib/seaborn-pastel.mplstyle b/lib/matplotlib/mpl-data/stylelib/seaborn-pastel.mplstyle index f361a4777ef2..e5787abba29d 100644 --- a/lib/matplotlib/mpl-data/stylelib/seaborn-pastel.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/seaborn-pastel.mplstyle @@ -1,2 +1,2 @@ # Seaborn pastel palette -axes.color_cycle: 92C6FF, 97F0AA, FF9F9A, D0BBFF, FFFEA3, B0E0E6 +axes.prop_cycle: cycler('color', ['92C6FF', '97F0AA', 'FF9F9A', 'D0BBFF', 'FFFEA3', 'B0E0E6']) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 07875554cf75..39d4c3858c61 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -24,6 +24,7 @@ import warnings import types +from cycler import cycler import matplotlib import matplotlib.colorbar from matplotlib import style @@ -394,7 +395,7 @@ def xkcd(scale=1, length=100, randomness=2): rcParams['grid.linewidth'] = 0.0 rcParams['axes.grid'] = False rcParams['axes.unicode_minus'] = False - rcParams['axes.color_cycle'] = ['b', 'r', 'c', 'm'] + rcParams['axes.prop_cycle'] = cycler('color', ['b', 'r', 'c', 'm']) rcParams['axes.edgecolor'] = 'black' rcParams['xtick.major.size'] = 8 rcParams['xtick.major.width'] = 3 diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index e3a64ae85efe..2699311323ec 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -18,11 +18,16 @@ from matplotlib.externals import six +from functools import reduce +import operator import os import warnings from matplotlib.fontconfig_pattern import parse_fontconfig_pattern from matplotlib.colors import is_color_like +# Don't let the original cycler collide with our validating cycler +from cycler import Cycler, cycler as ccycler + #interactive_bk = ['gtk', 'gtkagg', 'gtkcairo', 'qt4agg', # 'tkagg', 'wx', 'wxagg', 'cocoaagg', 'webagg'] # The capitalized forms are needed for ipython at present; this may @@ -60,8 +65,31 @@ def __call__(self, s): % (self.key, s, list(six.itervalues(self.valid)))) +def _listify_validator(scalar_validator, allow_stringlist=False): + def f(s): + if isinstance(s, six.string_types): + try: + return [scalar_validator(v.strip()) for v in s.split(',') + if v.strip()] + except Exception: + if allow_stringlist: + # Sometimes, a list of colors might be a single string + # of single-letter colornames. So give that a shot. + return [scalar_validator(v.strip()) for v in s if v.strip()] + else: + raise + elif type(s) in (list, tuple): + return [scalar_validator(v) for v in s if v] + else: + msg = "'s' must be of type [ string | list | tuple ]" + raise ValueError(msg) + f.__doc__ = scalar_validator.__doc__ + return f + + def validate_any(s): return s +validate_anylist = _listify_validator(validate_any) def validate_path_exists(s): @@ -106,7 +134,7 @@ def validate_float(s): return float(s) except ValueError: raise ValueError('Could not convert "%s" to float' % s) - +validate_floatlist = _listify_validator(validate_float) def validate_float_or_None(s): """convert s to float, None or raise""" @@ -300,25 +328,17 @@ def validate_color(s): raise ValueError('%s does not look like a color arg%s' % (s, msg)) -def validate_colorlist(s): - 'return a list of colorspecs' - if isinstance(s, six.string_types): - return [validate_color(c.strip()) for c in s.split(',')] - elif type(s) in (list, tuple): - return [validate_color(c) for c in s] - else: - msg = "'s' must be of type [ string | list | tuple ]" - raise ValueError(msg) +def deprecate_axes_colorcycle(value): + warnings.warn("axes.color_cycle is deprecated. Use axes.prop_cycle " + "instead. Will be removed in 2.1.0") + return validate_colorlist(value) -def validate_stringlist(s): - 'return a list' - if isinstance(s, six.string_types): - return [six.text_type(v.strip()) for v in s.split(',') if v.strip()] - elif type(s) in (list, tuple): - return [six.text_type(v) for v in s if v] - else: - msg = "'s' must be of type [ string | list | tuple ]" - raise ValueError(msg) + +validate_colorlist = _listify_validator(validate_color, allow_stringlist=True) +validate_colorlist.__doc__ = 'return a list of colorspecs' + +validate_stringlist = _listify_validator(six.text_type) +validate_stringlist.__doc__ = 'return a list' validate_orientation = ValidateInStrings( 'orientation', ['landscape', 'portrait']) @@ -343,6 +363,7 @@ def validate_fontsize(s): return float(s) except ValueError: raise ValueError('not a valid font size') +validate_fontsizelist = _listify_validator(validate_fontsize) def validate_font_properties(s): @@ -418,14 +439,17 @@ def validate_ps_distiller(s): validate_joinstyle = ValidateInStrings('joinstyle', ['miter', 'round', 'bevel'], ignorecase=True) +validate_joinstylelist = _listify_validator(validate_joinstyle) validate_capstyle = ValidateInStrings('capstyle', ['butt', 'round', 'projecting'], ignorecase=True) +validate_capstylelist = _listify_validator(validate_capstyle) validate_fillstyle = ValidateInStrings('markers.fillstyle', ['full', 'left', 'right', 'bottom', 'top', 'none']) +validate_fillstylelist = _listify_validator(validate_fillstyle) validate_negative_linestyle = ValidateInStrings('negative_linestyle', ['solid', 'dashed'], @@ -561,6 +585,168 @@ def __call__(self, s): validate_grid_axis = ValidateInStrings('axes.grid.axis', ['x', 'y', 'both']) + +def validate_hatch(s): + """ + Validate a hatch pattern. + A hatch pattern string can have any sequence of the following + characters: ``\\ / | - + * . x o O``. + + """ + if not isinstance(s, six.text_type): + raise ValueError("Hatch pattern must be a string") + unique_chars = set(s) + unknown = (unique_chars - + set(['\\', '/', '|', '-', '+', '*', '.', 'x', 'o', 'O'])) + if unknown: + raise ValueError("Unknown hatch symbol(s): %s" % list(unknown)) + return s +validate_hatchlist = _listify_validator(validate_hatch) + + +_prop_validators = { + 'color': validate_colorlist, + 'linewidth': validate_floatlist, + 'linestyle': validate_stringlist, + 'facecolor': validate_colorlist, + 'edgecolor': validate_colorlist, + 'joinstyle': validate_joinstylelist, + 'capstyle': validate_capstylelist, + 'fillstyle': validate_fillstylelist, + 'markerfacecolor': validate_colorlist, + 'markersize': validate_floatlist, + 'markeredgewidth': validate_floatlist, + 'markeredgecolor': validate_colorlist, + 'alpha': validate_floatlist, + 'marker': validate_stringlist, + 'hatch': validate_hatchlist, + } +_prop_aliases = { + 'c': 'color', + 'lw': 'linewidth', + 'ls': 'linestyle', + 'fc': 'facecolor', + 'ec': 'edgecolor', + 'mfc': 'markerfacecolor', + 'mec': 'markeredgecolor', + 'mew': 'markeredgewidth', + 'ms': 'markersize', + } + + +def cycler(*args, **kwargs): + """ + Creates a :class:`cycler.Cycler` object much like :func:`cycler.cycler`, + but includes input validation. + + cyl(arg) + cyl(label, itr) + cyl(label1=itr1[, label2=itr2[, ...]]) + + Form 1 simply copies a given `Cycler` object. + + Form 2 creates a `Cycler` from a label and an iterable. + + Form 3 composes a `Cycler` as an inner product of the + pairs of keyword arguments. In other words, all of the + iterables are cycled simultaneously, as if through zip(). + + Parameters + ---------- + arg : Cycler + Copy constructor for Cycler. + + label : name + The property key. Must be a valid `Artist` property. + For example, 'color' or 'linestyle'. Aliases are allowed, + such as 'c' for 'color' and 'lw' for 'linewidth'. + + itr : iterable + Finite-length iterable of the property values. These values + are validated and will raise a ValueError if invalid. + + Returns + ------- + cycler : Cycler + New :class:`cycler.Cycler` for the given properties + + """ + if args and kwargs: + raise TypeError("cycler() can only accept positional OR keyword " + "arguments -- not both.") + elif not args and not kwargs: + raise TypeError("cycler() must have positional OR keyword arguments") + + if len(args) == 1: + if not isinstance(args[0], Cycler): + raise TypeError("If only one positional argument given, it must " + " be a Cycler instance.") + + c = args[0] + unknowns = c.keys - (set(_prop_validators.keys()) | + set(_prop_aliases.keys())) + if unknowns: + # This is about as much validation I can do + raise TypeError("Unknown artist properties: %s" % unknowns) + else: + return Cycler(c) + elif len(args) == 2: + pairs = [(args[0], args[1])] + elif len(args) > 2: + raise TypeError("No more than 2 positional arguments allowed") + else: + pairs = six.iteritems(kwargs) + + validated = [] + for prop, vals in pairs: + norm_prop = _prop_aliases.get(prop, prop) + validator = _prop_validators.get(norm_prop, None) + if validator is None: + raise TypeError("Unknown artist property: %s" % prop) + vals = validator(vals) + # We will normalize the property names as well to reduce + # the amount of alias handling code elsewhere. + validated.append((norm_prop, vals)) + + return reduce(operator.add, (ccycler(k, v) for k, v in validated)) + + +def validate_cycler(s): + 'return a Cycler object from a string repr or the object itself' + if isinstance(s, six.string_types): + try: + # TODO: We might want to rethink this... + # While I think I have it quite locked down, + # it is execution of arbitrary code without + # sanitation. + # Combine this with the possibility that rcparams + # might come from the internet (future plans), this + # could be downright dangerous. + # I locked it down by only having the 'cycler()' function + # available. Imports and defs should not + # be possible. However, it is entirely possible that + # a security hole could open up via attributes to the + # function (this is why I decided against allowing the + # Cycler class object just to reduce the number of + # degrees of freedom (but maybe it is safer to use?). + # One possible hole I can think of (in theory) is if + # someone managed to hack the cycler module. But, if + # someone does that, this wouldn't make anything + # worse because we have to import the module anyway. + s = eval(s, {'cycler': cycler}) + except BaseException as e: + raise ValueError("'%s' is not a valid cycler construction: %s" % + (s, e)) + # Should make sure what comes from the above eval() + # is a Cycler object. + if isinstance(s, Cycler): + cycler_inst = s + else: + raise ValueError("object was not a string or Cycler instance: %s" % s) + + return cycler_inst + + # a map from key -> value, converter defaultParams = { 'backend': ['Agg', validate_backend], # agg is certainly @@ -748,8 +934,13 @@ def __call__(self, s): 'axes.formatter.useoffset': [True, validate_bool], 'axes.unicode_minus': [True, validate_bool], 'axes.color_cycle': [['b', 'g', 'r', 'c', 'm', 'y', 'k'], - validate_colorlist], # cycle of plot - # line colors + 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.xmargin': [0, ValidateInterval(0, 1, closedmin=True, closedmax=True)], # margin added to xaxis diff --git a/lib/matplotlib/stackplot.py b/lib/matplotlib/stackplot.py index 30f3156f111f..0a8c10d36a40 100644 --- a/lib/matplotlib/stackplot.py +++ b/lib/matplotlib/stackplot.py @@ -102,16 +102,23 @@ 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 r.append(axes.fill_between(x, first_line, stack[0, :], - facecolor=six.next(axes._get_lines.color_cycle), + facecolor=color, label= six.next(labels, None), **kwargs)) # Color between array i-1 and array i for i in xrange(len(y) - 1): - color = six.next(axes._get_lines.color_cycle) + if 'color' in axes._get_lines._prop_keys: + color = six.next(axes._get_lines.prop_cycler)['color'] + else: + color = None r.append(axes.fill_between(x, stack[i, :], stack[i + 1, :], - facecolor= color, + facecolor=color, label= six.next(labels, None), **kwargs)) return r diff --git a/lib/matplotlib/streamplot.py b/lib/matplotlib/streamplot.py index 480f02d9552f..ea33cde6b61a 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: - color = six.next(axes._get_lines.color_cycle) + if color is None and 'color' in axes._get_lines._prop_keys: + color = six.next(axes._get_lines.prop_cycler)['color'] if linewidth is None: linewidth = matplotlib.rcParams['lines.linewidth'] diff --git a/lib/matplotlib/tests/baseline_images/test_cycles/color_cycle_basic.png b/lib/matplotlib/tests/baseline_images/test_cycles/color_cycle_basic.png new file mode 100644 index 000000000000..38c9b11afd36 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_cycles/color_cycle_basic.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_cycles/fill_cycle_basic.png b/lib/matplotlib/tests/baseline_images/test_cycles/fill_cycle_basic.png new file mode 100644 index 000000000000..bc3263c15cb7 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_cycles/fill_cycle_basic.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_cycles/fill_cycle_ignore.png b/lib/matplotlib/tests/baseline_images/test_cycles/fill_cycle_ignore.png new file mode 100644 index 000000000000..65aa664ebda7 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_cycles/fill_cycle_ignore.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 new file mode 100644 index 000000000000..10fef6387a21 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_cycles/lineprop_cycle_basic.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_cycles/marker_cycle.png b/lib/matplotlib/tests/baseline_images/test_cycles/marker_cycle.png new file mode 100644 index 000000000000..f807aaed3e84 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_cycles/marker_cycle.png differ diff --git a/lib/matplotlib/tests/test_cycles.py b/lib/matplotlib/tests/test_cycles.py new file mode 100644 index 000000000000..606af7f9bc4a --- /dev/null +++ b/lib/matplotlib/tests/test_cycles.py @@ -0,0 +1,109 @@ +from matplotlib.testing.decorators import image_comparison +import matplotlib.pyplot as plt +import numpy as np + +from cycler import cycler + + +@image_comparison(baseline_images=['color_cycle_basic'], remove_text=True, + extensions=['png']) +def test_colorcycle_basic(): + fig = plt.figure() + ax = fig.add_subplot(111) + ax.set_prop_cycle(cycler('color', ['r', 'g', 'y'])) + xs = np.arange(10) + ys = 0.25 * xs + 2 + ax.plot(xs, ys, label='red', lw=4) + ys = 0.45 * xs + 3 + ax.plot(xs, ys, label='green', lw=4) + ys = 0.65 * xs + 4 + ax.plot(xs, ys, label='yellow', lw=4) + ys = 0.85 * xs + 5 + ax.plot(xs, ys, label='red2', lw=4) + ax.legend(loc='upper left') + + +@image_comparison(baseline_images=['marker_cycle'], remove_text=True, + extensions=['png']) +def test_marker_cycle(): + fig = plt.figure() + ax = fig.add_subplot(111) + ax.set_prop_cycle(cycler('color', ['r', 'g', 'y']) + + cycler('marker', ['.', '*', 'x'])) + xs = np.arange(10) + ys = 0.25 * xs + 2 + ax.plot(xs, ys, label='red dot', lw=4, ms=16) + ys = 0.45 * xs + 3 + ax.plot(xs, ys, label='green star', lw=4, ms=16) + ys = 0.65 * xs + 4 + ax.plot(xs, ys, label='yellow x', lw=4, ms=16) + ys = 0.85 * xs + 5 + ax.plot(xs, ys, label='red2 dot', lw=4, ms=16) + ax.legend(loc='upper left') + + +@image_comparison(baseline_images=['lineprop_cycle_basic'], remove_text=True, + extensions=['png']) +def test_linestylecycle_basic(): + fig = plt.figure() + ax = fig.add_subplot(111) + ax.set_prop_cycle(cycler('linestyle', ['-', '--', ':'])) + xs = np.arange(10) + ys = 0.25 * xs + 2 + ax.plot(xs, ys, label='solid', lw=4) + ys = 0.45 * xs + 3 + ax.plot(xs, ys, label='dashed', lw=4) + ys = 0.65 * xs + 4 + ax.plot(xs, ys, label='dotted', lw=4) + ys = 0.85 * xs + 5 + ax.plot(xs, ys, label='solid2', lw=4) + ax.legend(loc='upper left') + + +@image_comparison(baseline_images=['fill_cycle_basic'], remove_text=True, + extensions=['png']) +def test_fillcycle_basic(): + fig = plt.figure() + ax = fig.add_subplot(111) + ax.set_prop_cycle(cycler('color', ['r', 'g', 'y']) + + cycler('hatch', ['xx', 'O', '|-']) + + cycler('linestyle', ['-', '--', ':'])) + xs = np.arange(10) + ys = 0.25 * xs**.5 + 2 + ax.fill(xs, ys, label='red, xx', linewidth=3) + ys = 0.45 * xs**.5 + 3 + ax.fill(xs, ys, label='green, circle', linewidth=3) + ys = 0.65 * xs**.5 + 4 + ax.fill(xs, ys, label='yellow, cross', linewidth=3) + ys = 0.85 * xs**.5 + 5 + ax.fill(xs, ys, label='red2, xx', linewidth=3) + ax.legend(loc='upper left') + + +@image_comparison(baseline_images=['fill_cycle_ignore'], remove_text=True, + extensions=['png']) +def test_fillcycle_ignore(): + fig = plt.figure() + ax = fig.add_subplot(111) + ax.set_prop_cycle(cycler('color', ['r', 'g', 'y']) + + cycler('hatch', ['xx', 'O', '|-']) + + cycler('marker', ['.', '*', 'D'])) + xs = np.arange(10) + ys = 0.25 * xs**.5 + 2 + # Should not advance the cycler, even though there is an + # unspecified property in the cycler "marker". + # "marker" is not a Polygon property, and should be ignored. + ax.fill(xs, ys, 'r', hatch='xx', label='red, xx') + ys = 0.45 * xs**.5 + 3 + # Allow the cycler to advance, but specify some properties + ax.fill(xs, ys, hatch='O', label='red, circle') + ys = 0.65 * xs**.5 + 4 + ax.fill(xs, ys, label='green, circle') + ys = 0.85 * xs**.5 + 5 + ax.fill(xs, ys, label='yellow, cross') + ax.legend(loc='upper left') + + +if __name__ == '__main__': + import nose + nose.runmodule(argv=['-s', '--with-doctest'], exit=False) diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index c371f22bf015..e88e13b79914 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -8,6 +8,8 @@ import sys import warnings +from cycler import cycler, Cycler + import matplotlib as mpl import matplotlib.pyplot as plt from matplotlib.tests import assert_str_equal @@ -22,7 +24,9 @@ validate_stringlist, validate_bool, validate_nseq_int, - validate_nseq_float) + validate_nseq_float, + validate_cycler, + validate_hatch) mpl.rc('text', usetex=False) @@ -238,13 +242,14 @@ def test_Issue_1713(): del os.environ['LANG'] assert rc.get('timezone') == 'UTC' -if __name__ == '__main__': - nose.runmodule(argv=['-s', '--with-doctest'], exit=False) - def _validation_test_helper(validator, arg, target): res = validator(arg) - assert_equal(res, target) + if not isinstance(target, Cycler): + assert_equal(res, target) + else: + # Cyclers can't simply be asserted equal. They don't implement __eq__ + assert_equal(list(res), list(target)) def _validation_fail_helper(validator, arg, exception_type): @@ -293,8 +298,51 @@ def test_validators(): for _ in ('aardvark', ('a', 1), (1, 2, 3) )) - } - + }, + {'validator': validate_cycler, + 'success': (('cycler("color", "rgb")', + cycler("color", 'rgb')), + (cycler('linestyle', ['-', '--']), + cycler('linestyle', ['-', '--'])), + ("""(cycler("color", ["r", "g", "b"]) + + cycler("mew", [2, 3, 5]))""", + (cycler("color", 'rgb') + + cycler("markeredgewidth", [2, 3, 5]))), + ("cycler(c='rgb', lw=[1, 2, 3])", + cycler('color', 'rgb') + cycler('linewidth', [1, 2, 3])), + ("cycler('c', 'rgb') * cycler('linestyle', ['-', '--'])", + (cycler('color', 'rgb') * + cycler('linestyle', ['-', '--']))), + ), + # This is *so* incredibly important: validate_cycler() eval's + # an arbitrary string! I think I have it locked down enough, + # and that is what this is testing. + # TODO: Note that these tests are actually insufficient, as it may + # be that they raised errors, but still did an action prior to + # raising the exception. We should devise some additional tests + # for that... + 'fail': ((4, ValueError), # Gotta be a string or Cycler object + ('cycler("bleh, [])', ValueError), # syntax error + ('Cycler("linewidth", [1, 2, 3])', + ValueError), # only 'cycler()' function is allowed + ('1 + 2', ValueError), # doesn't produce a Cycler object + ('os.system("echo Gotcha")', ValueError), # os not available + ('import os', ValueError), # should not be able to import + ('def badjuju(a): return a; badjuju(cycler("color", "rgb"))', + ValueError), # Should not be able to define anything + # even if it does return a cycler + ('cycler("waka", [1, 2, 3])', ValueError), # not a property + ('cycler(c=[1, 2, 3])', ValueError), # invalid values + ("cycler(lw=['a', 'b', 'c'])", ValueError), # invalid values + ) + }, + {'validator': validate_hatch, + 'success': (('--|', '--|'), ('\\oO', '\\oO'), + ('/+*/.x', '/+*/.x'), ('', '')), + 'fail': (('--_', ValueError), + (8, ValueError), + ('X', ValueError)), + }, ) for validator_dict in validation_tests: @@ -331,3 +379,7 @@ def test_rcparams_reset_after_fail(): pass assert mpl.rcParams['text.usetex'] is False + + +if __name__ == '__main__': + nose.runmodule(argv=['-s', '--with-doctest'], exit=False) diff --git a/matplotlibrc.template b/matplotlibrc.template index 968c32802005..af25292da301 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -80,7 +80,7 @@ backend : %(backend)s # information on line properties. #lines.linewidth : 1.0 # line width in points #lines.linestyle : - # solid line -#lines.color : blue # has no affect on plot(); see axes.color_cycle +#lines.color : blue # has no affect on plot(); see axes.prop_cycle #lines.marker : None # the default marker #lines.markeredgewidth : 0.5 # the line width around the marker symbol #lines.markersize : 6 # markersize, in points @@ -267,7 +267,8 @@ 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.color_cycle : b, g, r, c, m, y, k # color cycle for plot lines +#axes.prop_cycle : cycler('color', 'bgrcmyk') + # color cycle for plot lines # as list of string colorspecs: # single letter, long name, or # web-style hex diff --git a/setup.py b/setup.py index 912d8e2d6490..26cd3438549e 100644 --- a/setup.py +++ b/setup.py @@ -67,6 +67,7 @@ setupext.Numpy(), setupext.Dateutil(), setupext.Pytz(), + setupext.Cycler(), setupext.Tornado(), setupext.Pyparsing(), setupext.LibAgg(), diff --git a/setupext.py b/setupext.py index 29e549e4d833..2f4c39780888 100755 --- a/setupext.py +++ b/setupext.py @@ -1162,6 +1162,24 @@ def get_install_requires(self): return ['pytz'] +class Cycler(SetupPackage): + name = "cycler" + + def check(self): + try: + import cycler + except ImportError: + return ( + "cycler was not found. " + "pip will attempt to install it " + "after matplotlib.") + + return "using cycler version %s" % cycler.__version__ + + def get_install_requires(self): + return ['cycler'] + + class Dateutil(SetupPackage): name = "dateutil"