diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 31242ccaecd2..8a4d665bcaf7 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1397,6 +1397,7 @@ def tk_window_focus(): 'matplotlib.tests.test_colors', 'matplotlib.tests.test_compare_images', 'matplotlib.tests.test_contour', + 'matplotlib.tests.test_cycle', 'matplotlib.tests.test_dates', 'matplotlib.tests.test_delaunay', 'matplotlib.tests.test_figure', diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 3461cf349653..b1153301c9ca 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2876,7 +2876,7 @@ def xywhere(xs, ys, mask): if ecolor is None: if l0 is None: - ecolor = six.next(self._get_lines.color_cycle) + ecolor = self._get_lines.cycle.get_next_color() else: ecolor = l0.get_color() @@ -5678,7 +5678,7 @@ 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) + color = [self._get_lines.cycle.get_next_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 1e4f52d377cd..8fd6c28197f4 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -29,6 +29,8 @@ import matplotlib.font_manager as font_manager import matplotlib.text as mtext import matplotlib.image as mimage +import matplotlib.cycle as mcycle + from matplotlib.artist import allow_rasterization from matplotlib.cbook import iterable @@ -137,20 +139,14 @@ class _process_plot_var_args(object): def __init__(self, axes, command='plot'): self.axes = axes self.command = command - self.set_color_cycle() + self.cycle = mcycle.Cycle() def __getstate__(self): - # note: it is not possible to pickle a itertools.cycle instance - return {'axes': self.axes, 'command': self.command} + return {'axes': self.axes, 'command': self.command, + 'cycle': self.cycle} def __setstate__(self, state): self.__dict__ = state.copy() - self.set_color_cycle() - - def set_color_cycle(self, clist=None): - if clist is None: - clist = rcParams['axes.color_cycle'] - self.color_cycle = itertools.cycle(clist) def __call__(self, *args, **kwargs): @@ -229,12 +225,8 @@ def _xy_from_xy(self, x, y): return x, y def _makeline(self, x, y, kw, kwargs): - kw = kw.copy() # Don't modify the original kw. + kw = self.cycle.next(kw.copy()) 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 ) @@ -245,7 +237,7 @@ def _makefill(self, x, y, kw, kwargs): try: facecolor = kw['color'] except KeyError: - facecolor = six.next(self.color_cycle) + facecolor = self.cycle.get_next_color() seg = mpatches.Polygon(np.hstack((x[:, np.newaxis], y[:, np.newaxis])), facecolor=facecolor, @@ -971,14 +963,47 @@ def clear(self): """clear the axes""" self.cla() + def set_cycle(self, style, slist): + """ + Set the cycle for a line attribute for any future plot commands + on this Axes + + *style* is a key to a dictionary for cycles in the cycle class + *slist* is a list of mpl style specifiers + """ + self._get_lines.cycle.set_cycle(style, slist) + + def clear_all_cycle(self): + """ + Clear all the current line attribute cycles + """ + self._get_lines.cycle.clear_all_cycle() + + def clear_cycle(self, style): + """ + Clear a cycle for a line attribute specified by style + + *style* is a key to a dictionary for cycles in the cycle class + """ + self._get_lines.cycle.clear_cycle(style) + 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. """ - self._get_lines.set_color_cycle(clist) - self._get_patches_for_fill.set_color_cycle(clist) + self._get_lines.cycle.set_color_cycle(clist) + self._get_patches_for_fill.cycle.set_color_cycle(clist) + + def set_line_cycle(self, llist): + """ + Set the line style cycle for any future plot commands on this + Axes. + + *llist* is a list of mpl line style specifiers. + """ + self._get_lines.cycle.set_line_cycle(llist) def ishold(self): """return the HOLD status of the axes""" diff --git a/lib/matplotlib/cycle.py b/lib/matplotlib/cycle.py new file mode 100644 index 000000000000..a13024a40a7b --- /dev/null +++ b/lib/matplotlib/cycle.py @@ -0,0 +1,143 @@ +import itertools +import six +from matplotlib import rcParams + + +class Cycle(object): + + def __init__(self): + """ + Set the initial cycle of styles to be used by the lines of the graph + """ + self._styles = {'color': None, + 'linestyle': None, + 'linewidth': None, + 'marker': None, + 'markersize': None, + 'markeredgewidth': None, + 'markeredgecolor': None, + 'markerfacecolor': None, + 'antialiased': None, + 'dash_capstyle': None, + 'solid_capstyle': None, + 'dash_joinstyle': None, + 'solid_joinstyle': None, + } + self._styles_list = {} + self.set_color_cycle() + self.set_line_cycle() + + def __getstate__(self): + return {'_styles_list': self._styles_list} + + def __setstate__(self, state): + self.__init__() + self.__dict__.update(state) + for style in self._styles_list.keys(): + self.set_cycle(style, self._styles_list[style]) + + def next(self, args=None): + """ + Returns the next set of line attributes for a line on the graph to use + *args* is an optional dictionary of style arguments + Styles that already exist in *args* will not be cycled through + """ + if args is None: + args = {} + else: + args = args.copy() + for style in self._styles.keys(): + if self._styles[style] is not None and style not in args: + args[style] = six.next(self._styles[style]) + return args + + def set_cycle(self, style, slist): + """ + Set a cycle for a line attribute specified by style, the cycle to be + used to is specified by slist + + *style* is a key to the _style dictionary + *slist* is a list of mpl style specifiers + """ + if self._validate(style, slist): + self._styles_list[style] = slist + self._styles[style] = itertools.cycle(slist) + + def _validate(self, style, slist): + """ + Ensures that the style given is a valid attribute to by cycled over + If the style is a valid cycle, ensure that the list of specifiers + given are valid specifiers for that style + + *style* is a key to the _style dictionary + *plist* is a list of mpl style specifiers + """ + if type(slist) not in (list, tuple) or slist == []: + msg = "'slist' must be of type [list | tuple ] and non empty" + raise ValueError(msg) + if style not in self._styles.keys(): + msg = "set cycle value error, %s is not a valid style" % style + raise ValueError(msg) + param = 'lines.' + style + for val in slist: + try: + rcParams.validate[param](val) + except ValueError: + msg = "Set cycle value error, Style %s: %s" % (style, str(val)) + raise ValueError(msg) + + return True + + def clear_cycle(self, style): + """ + Clears(resets) a cycle for a line attribute specified by style + + *style* is a key to the _style dictionary + """ + if style not in self._styles.keys(): + msg = "clear cycle value error, %s is not a valid style" % style + raise ValueError(msg) + self._styles[style] = None + + def clear_all_cycle(self): + """ + Clears (resets) all cycles for the lines on a plot + """ + for style in self._styles.keys(): + self._styles[style] = None + + def set_line_cycle(self, llist=None): + """ + Sets a line style cycle to be used for the lines on the graph, + if none are specified the default line style cycle will be used + """ + if llist is None: + llist = rcParams['axes.line_cycle'] + self.set_cycle('linestyle', llist) + + def set_color_cycle(self, clist=None): + """ + Sets a color cycle to be used for the lines on the graph, if none are + specified the default color cycle will be used + """ + if clist is None: + clist = rcParams['axes.color_cycle'] + self.set_cycle('color', clist) + + def get_next_color(self): + """ + Return the next color or defaults to rcParams if none + """ + try: + return six.next(self._styles['color']) + except TypeError: + return rcParams['lines.color'] + + def get_next_linestyle(self): + """ + Return the next linestyle or defaults to rcParams if none + """ + try: + return six.next(self._styles['linestyle']) + except TypeError: + return rcParams['lines.linestyle'] diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 1fdf121b32d8..73a71baacd3f 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -235,6 +235,23 @@ def __call__(self, s): raise ValueError('Could not convert all entries to ints') +def validate_line(s): + 'return a valid arg' + if s in (None, 'none', 'None', ' ', ''): + return 'None' + if s in ('-', '--', '-.', ':'): + return s + raise ValueError('%s is not a valid line arg' % s) + + +def validate_markercolor(s): + 'return a valid marker color arg' + if s == 'auto': + return s + else: + return validate_color(s) + + def validate_color(s): 'return a valid color arg' try: @@ -278,6 +295,18 @@ def validate_colorlist(s): msg = "'s' must be of type [ string | list | tuple ]" raise ValueError(msg) + +def validate_linelist(s): + 'return a list of colorspecs' + if isinstance(s, six.string_types): + return [validate_line(l.strip()) for l in s.split(',')] + elif type(s) in (list, tuple) and s != []: + return [validate_line(l) for l in s] + else: + msg = "'s' must be of type [ string | list | tuple ] and non empty" + raise ValueError(msg) + + def validate_stringlist(s): 'return a list' if isinstance(s, six.string_types): @@ -524,11 +553,13 @@ def __call__(self, s): # line props 'lines.linewidth': [1.0, validate_float], # line width in points - 'lines.linestyle': ['-', six.text_type], # solid line + 'lines.linestyle': ['-', validate_line], # solid line 'lines.color': ['b', validate_color], # blue 'lines.marker': ['None', six.text_type], # black 'lines.markeredgewidth': [0.5, validate_float], 'lines.markersize': [6, validate_float], # markersize, in points + 'lines.markeredgecolor': ['auto', validate_markercolor], + 'lines.markerfacecolor': ['auto', validate_markercolor], 'lines.antialiased': [True, validate_bool], # antialised (no jaggies) 'lines.dash_joinstyle': ['round', validate_joinstyle], 'lines.solid_joinstyle': ['round', validate_joinstyle], @@ -636,7 +667,7 @@ def __call__(self, s): 'axes.unicode_minus': [True, validate_bool], 'axes.color_cycle': [['b', 'g', 'r', 'c', 'm', 'y', 'k'], validate_colorlist], # cycle of plot - # line colors + 'axes.line_cycle': [['-'], validate_linelist], '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 b42ca98702b4..b04765fb62bb 100644 --- a/lib/matplotlib/stackplot.py +++ b/lib/matplotlib/stackplot.py @@ -103,13 +103,13 @@ def stackplot(axes, x, *args, **kwargs): # Color between x = 0 and the first array. r.append(axes.fill_between(x, first_line, stack[0, :], - facecolor=six.next(axes._get_lines.color_cycle), + facecolor=axes._get_lines.cycle.get_next_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) + color = axes._get_lines.cycle.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 3708afa647db..e2f5f441005d 100644 --- a/lib/matplotlib/streamplot.py +++ b/lib/matplotlib/streamplot.py @@ -80,7 +80,7 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, transform = axes.transData if color is None: - color = six.next(axes._get_lines.color_cycle) + color = axes._get_lines.cycle.get_next_color() if linewidth is None: linewidth = matplotlib.rcParams['lines.linewidth'] diff --git a/lib/matplotlib/tests/baseline_images/test_cycle/clear_all_cycle.pdf b/lib/matplotlib/tests/baseline_images/test_cycle/clear_all_cycle.pdf new file mode 100644 index 000000000000..e46c94a24909 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_cycle/clear_all_cycle.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_cycle/clear_all_cycle.png b/lib/matplotlib/tests/baseline_images/test_cycle/clear_all_cycle.png new file mode 100644 index 000000000000..8adf22818098 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_cycle/clear_all_cycle.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_cycle/clear_all_cycle.svg b/lib/matplotlib/tests/baseline_images/test_cycle/clear_all_cycle.svg new file mode 100644 index 000000000000..aa00ce020e46 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_cycle/clear_all_cycle.svg @@ -0,0 +1,660 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_cycle/clear_one_cycle.pdf b/lib/matplotlib/tests/baseline_images/test_cycle/clear_one_cycle.pdf new file mode 100644 index 000000000000..caaf171ee6d5 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_cycle/clear_one_cycle.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_cycle/clear_one_cycle.png b/lib/matplotlib/tests/baseline_images/test_cycle/clear_one_cycle.png new file mode 100644 index 000000000000..db2f0dc465a5 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_cycle/clear_one_cycle.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_cycle/clear_one_cycle.svg b/lib/matplotlib/tests/baseline_images/test_cycle/clear_one_cycle.svg new file mode 100644 index 000000000000..5c13c9ca3035 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_cycle/clear_one_cycle.svg @@ -0,0 +1,660 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_cycle/set_cycles.pdf b/lib/matplotlib/tests/baseline_images/test_cycle/set_cycles.pdf new file mode 100644 index 000000000000..5bc5d696b325 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_cycle/set_cycles.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_cycle/set_cycles.png b/lib/matplotlib/tests/baseline_images/test_cycle/set_cycles.png new file mode 100644 index 000000000000..a1c797e2c209 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_cycle/set_cycles.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_cycle/set_cycles.svg b/lib/matplotlib/tests/baseline_images/test_cycle/set_cycles.svg new file mode 100644 index 000000000000..b7acae5b27f8 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_cycle/set_cycles.svg @@ -0,0 +1,902 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/test_cycle.py b/lib/matplotlib/tests/test_cycle.py new file mode 100644 index 000000000000..1ab76831e935 --- /dev/null +++ b/lib/matplotlib/tests/test_cycle.py @@ -0,0 +1,52 @@ +import numpy as np +import matplotlib +from matplotlib.testing.decorators import image_comparison +import matplotlib.pyplot as plt + + +@image_comparison(baseline_images=['set_cycles']) +def test_set_cycles(): + x = np.linspace(0, 2 * np.pi) + offsets = np.linspace(0, 2 * np.pi, 4, endpoint=False) + yy = np.transpose([np.sin(x + phi) for phi in offsets]) + fig, ax1 = plt.subplots(nrows=1) + ax1.set_color_cycle(['c', 'm', 'y', 'k']) + ax1.set_cycle('linestyle', ['--', '-.', ':']) + ax1.set_cycle('linewidth', [3, 1]) + ax1.set_cycle('marker', ['>', '<', 'o']) + ax1.set_cycle('markersize', [5, 10]) + ax1.set_cycle('markerfacecolor', ['black']) + ax1.set_cycle('markeredgecolor', ['blue']) + ax1.set_cycle('markeredgewidth', [1]) + ax1.set_cycle('antialiased', [True, False]) + ax1.set_cycle('dash_joinstyle', ['miter', 'round', 'bevel']) + ax1.set_cycle('solid_joinstyle', ['miter', 'round', 'bevel']) + ax1.set_cycle('dash_capstyle', ['butt', 'round', 'projecting']) + ax1.set_cycle('solid_capstyle', ['butt', 'round', 'projecting']) + ax1.plot(yy) + + +@image_comparison(baseline_images=['clear_one_cycle']) +def test_clear_one_cycle(): + x = np.linspace(0, 2 * np.pi) + offsets = np.linspace(0, 2 * np.pi, 4, endpoint=False) + yy = np.transpose([np.sin(x + phi) for phi in offsets]) + fig, ax1 = plt.subplots(nrows=1) + ax1.set_color_cycle(['c', 'm', 'y', 'k']) + ax1.set_cycle('linestyle', ['--', '-.', ':']) + ax1.set_cycle('linewidth', [3, 1]) + ax1.clear_cycle('linestyle') + ax1.plot(yy) + + +@image_comparison(baseline_images=['clear_all_cycle']) +def test_clear_all_cycle(): + x = np.linspace(0, 2 * np.pi) + offsets = np.linspace(0, 2 * np.pi, 4, endpoint=False) + yy = np.transpose([np.sin(x + phi) for phi in offsets]) + fig, ax1 = plt.subplots(nrows=1) + ax1.set_color_cycle(['c', 'm', 'y', 'k']) + ax1.set_cycle('linestyle', ['--', '-.', ':']) + ax1.set_cycle('linewidth', [3, 1]) + ax1.clear_all_cycle() + ax1.plot(yy)