diff --git a/.travis.yml b/.travis.yml index d41c61bdfaf5..0a008a48255f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -67,7 +67,7 @@ install: pip install $PRE python-dateutil $NUMPY pyparsing pillow sphinx!=1.3.0; fi # Always install from pypi - - pip install $PRE nose pep8 + - pip install $PRE nose pep8 cycler # Install mock on python 2. Python 2.6 requires mock 1.0.1 # Since later versions have dropped support diff --git a/doc/users/whats_new/2015-05_filledstep.rst b/doc/users/whats_new/2015-05_filledstep.rst new file mode 100644 index 000000000000..d9d0a3073b3a --- /dev/null +++ b/doc/users/whats_new/2015-05_filledstep.rst @@ -0,0 +1,12 @@ +Add step kwargs to fill_between +------------------------------- + +Added ``step`` kwarg to `Axes.fill_between` to allow to fill between +lines drawn using the 'step' draw style. The values of ``step`` match +those of the ``where`` kwarg of `Axes.step`. The asymmetry of of the +kwargs names is not ideal, but `Axes.fill_between` already has a +``where`` kwarg. + +This is particularly useful for plotting pre-binned histograms. + +.. plot:: mpl_examples/api/filled_step.py diff --git a/examples/api/filled_step.py b/examples/api/filled_step.py new file mode 100644 index 000000000000..1c31dea72b88 --- /dev/null +++ b/examples/api/filled_step.py @@ -0,0 +1,206 @@ +import itertools +from functools import partial + +import numpy as np +import matplotlib.pyplot as plt +from cycler import cycler +from six.moves import zip + + +def filled_hist(ax, edges, values, bottoms=None, orientation='v', + **kwargs): + """ + Draw a histogram as a stepped patch. + + Extra kwargs are passed through to `fill_between` + + Parameters + ---------- + ax : Axes + The axes to plot to + + edges : array + A length n+1 array giving the left edges of each bin and the + right edge of the last bin. + + values : array + A length n array of bin counts or values + + bottoms : scalar or array, optional + A length n array of the bottom of the bars. If None, zero is used. + + orientation : {'v', 'h'} + Orientation of the histogram. 'v' (default) has + the bars increasing in the positive y-direction. + + Returns + ------- + ret : PolyCollection + Artist added to the Axes + """ + print(orientation) + if orientation not in set('hv'): + raise ValueError("orientation must be in {'h', 'v'} " + "not {o}".format(o=orientation)) + + kwargs.setdefault('step', 'post') + edges = np.asarray(edges) + values = np.asarray(values) + if len(edges) - 1 != len(values): + raise ValueError('Must provide one more bin edge than value not: ' + 'len(edges): {lb} len(values): {lv}'.format( + lb=len(edges), lv=len(values))) + + if bottoms is None: + bottoms = np.zeros_like(values) + if np.isscalar(bottoms): + bottoms = np.ones_like(values) * bottoms + + values = np.r_[values, values[-1]] + bottoms = np.r_[bottoms, bottoms[-1]] + if orientation == 'h': + return ax.fill_betweenx(edges, values, bottoms, **kwargs) + elif orientation == 'v': + return ax.fill_between(edges, values, bottoms, **kwargs) + else: + raise AssertionError("you should never be here") + + +def stack_hist(ax, stacked_data, sty_cycle, bottoms=None, + hist_func=None, labels=None, + plot_func=None, plot_kwargs=None): + """ + ax : axes.Axes + The axes to add artists too + + stacked_data : array or Mapping + A (N, M) shaped array. The first dimension will be iterated over to + compute histograms row-wise + + sty_cycle : Cycler or operable of dict + Style to apply to each set + + bottoms : array, optional + The initial positions of the bottoms, defaults to 0 + + hist_func : callable, optional + Must have signature `bin_vals, bin_edges = f(data)`. + `bin_edges` expected to be one longer than `bin_vals` + + labels : list of str, optional + The label for each set. + + If not given and stacked data is an array defaults to 'default set {n}' + + If stacked_data is a mapping, and labels is None, default to the keys + (which may come out in a random order). + + If stacked_data is a mapping and labels is given then only + the columns listed by be plotted. + + plot_func : callable, optional + Function to call to draw the histogram must have signature: + + ret = plot_func(ax, edges, top, bottoms=bottoms, + label=label, **kwargs) + + plot_kwargs : dict, optional + Any extra kwargs to pass through to the plotting function. This + will be the same for all calls to the plotting function and will + over-ride the values in cycle. + + Returns + ------- + arts : dict + Dictionary of artists keyed on their labels + """ + # deal with default binning function + if hist_func is None: + hist_func = np.histogram + + # deal with default plotting function + if plot_func is None: + plot_func = filled_hist + + # deal with default + if plot_kwargs is None: + plot_kwargs = {} + print(plot_kwargs) + try: + l_keys = stacked_data.keys() + label_data = True + if labels is None: + labels = l_keys + + except AttributeError: + label_data = False + if labels is None: + labels = itertools.repeat(None) + + if label_data: + loop_iter = enumerate((stacked_data[lab], lab, s) for lab, s in + zip(labels, sty_cycle)) + else: + loop_iter = enumerate(zip(stacked_data, labels, sty_cycle)) + + arts = {} + for j, (data, label, sty) in loop_iter: + if label is None: + label = 'default set {n}'.format(n=j) + label = sty.pop('label', label) + vals, edges = hist_func(data) + if bottoms is None: + bottoms = np.zeros_like(vals) + top = bottoms + vals + print(sty) + sty.update(plot_kwargs) + print(sty) + ret = plot_func(ax, edges, top, bottoms=bottoms, + label=label, **sty) + bottoms = top + arts[label] = ret + ax.legend() + return arts + + +# set up histogram function to fixed bins +edges = np.linspace(-3, 3, 20, endpoint=True) +hist_func = partial(np.histogram, bins=edges) + +# set up style cycles +color_cycle = cycler('facecolor', 'rgbm') +label_cycle = cycler('label', ['set {n}'.format(n=n) for n in range(4)]) +hatch_cycle = cycler('hatch', ['/', '*', '+', '|']) + +# make some synthetic data +stack_data = np.random.randn(4, 12250) +dict_data = {lab: d for lab, d in zip(list(c['label'] for c in label_cycle), + stack_data)} + +# work with plain arrays +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6), tight_layout=True) +arts = stack_hist(ax1, stack_data, color_cycle + label_cycle + hatch_cycle, + hist_func=hist_func) + +arts = stack_hist(ax2, stack_data, color_cycle, + hist_func=hist_func, + plot_kwargs=dict(edgecolor='w', orientation='h')) +ax1.set_ylabel('counts') +ax1.set_xlabel('x') +ax2.set_xlabel('counts') +ax2.set_ylabel('x') + +# work with labeled data + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6), + tight_layout=True, sharey=True) + +arts = stack_hist(ax1, dict_data, color_cycle + hatch_cycle, + hist_func=hist_func) + +arts = stack_hist(ax2, dict_data, color_cycle + hatch_cycle, + hist_func=hist_func, labels=['set 0', 'set 3']) + +ax1.set_ylabel('counts') +ax1.set_xlabel('x') +ax2.set_xlabel('x') diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index eb941666832c..89cf5986946f 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -13,7 +13,7 @@ import matplotlib import matplotlib.cbook as cbook -from matplotlib.cbook import _string_to_bool, mplDeprecation +from matplotlib.cbook import mplDeprecation, STEP_LOOKUP_MAP import matplotlib.collections as mcoll import matplotlib.colors as mcolors import matplotlib.contour as mcontour @@ -4428,49 +4428,59 @@ def fill(self, *args, **kwargs): @docstring.dedent_interpd def fill_between(self, x, y1, y2=0, where=None, interpolate=False, + step=None, **kwargs): """ Make filled polygons between two curves. - Call signature:: - - fill_between(x, y1, y2=0, where=None, **kwargs) Create a :class:`~matplotlib.collections.PolyCollection` filling the regions between *y1* and *y2* where ``where==True`` - *x* : + Parameters + ---------- + x : array An N-length array of the x data - *y1* : + y1 : array An N-length array (or scalar) of the y data - *y2* : + y2 : array An N-length array (or scalar) of the y data - *where* : - If *None*, default to fill between everywhere. If not *None*, + where : array, optional + If `None`, default to fill between everywhere. If not `None`, it is an N-length numpy boolean array and the fill will only happen over the regions where ``where==True``. - *interpolate* : - If *True*, interpolate between the two lines to find the + interpolate : bool, optional + If `True`, interpolate between the two lines to find the precise point of intersection. Otherwise, the start and end points of the filled region will only occur on explicit values in the *x* array. - *kwargs* : - Keyword args passed on to the - :class:`~matplotlib.collections.PolyCollection`. + step : {'pre', 'post', 'mid'}, optional + If not None, fill with step logic. + + + Notes + ----- + + Additional Keyword args passed on to the + :class:`~matplotlib.collections.PolyCollection`. kwargs control the :class:`~matplotlib.patches.Polygon` properties: %(PolyCollection)s + Examples + -------- + .. plot:: mpl_examples/pylab_examples/fill_between_demo.py - .. seealso:: + See Also + -------- :meth:`fill_betweenx` for filling between two sets of x-values @@ -4507,6 +4517,9 @@ def fill_between(self, x, y1, y2=0, where=None, interpolate=False, xslice = x[ind0:ind1] y1slice = y1[ind0:ind1] y2slice = y2[ind0:ind1] + if step is not None: + step_func = STEP_LOOKUP_MAP[step] + xslice, y1slice, y2slice = step_func(xslice, y1slice, y2slice) if not len(xslice): continue @@ -4567,7 +4580,8 @@ def get_interp_point(ind): return collection @docstring.dedent_interpd - def fill_betweenx(self, y, x1, x2=0, where=None, **kwargs): + def fill_betweenx(self, y, x1, x2=0, where=None, + step=None, **kwargs): """ Make filled polygons between two horizontal curves. @@ -4579,31 +4593,42 @@ def fill_betweenx(self, y, x1, x2=0, where=None, **kwargs): filling the regions between *x1* and *x2* where ``where==True`` - *y* : + Parameters + ---------- + y : array An N-length array of the y data - *x1* : + x1 : array An N-length array (or scalar) of the x data - *x2* : + x2 : array, optional An N-length array (or scalar) of the x data - *where* : - If *None*, default to fill between everywhere. If not *None*, - it is a N length numpy boolean array and the fill will - only happen over the regions where ``where==True`` + where : array, optional + If *None*, default to fill between everywhere. If not *None*, + it is a N length numpy boolean array and the fill will + only happen over the regions where ``where==True`` - *kwargs* : - keyword args passed on to the + step : {'pre', 'post', 'mid'}, optional + If not None, fill with step logic. + + Notes + ----- + + keyword args passed on to the :class:`~matplotlib.collections.PolyCollection` kwargs control the :class:`~matplotlib.patches.Polygon` properties: %(PolyCollection)s + Examples + -------- + .. plot:: mpl_examples/pylab_examples/fill_betweenx_demo.py - .. seealso:: + See Also + -------- :meth:`fill_between` for filling between two sets of y-values @@ -4640,6 +4665,9 @@ def fill_betweenx(self, y, x1, x2=0, where=None, **kwargs): yslice = y[ind0:ind1] x1slice = x1[ind0:ind1] x2slice = x2[ind0:ind1] + if step is not None: + step_func = STEP_LOOKUP_MAP[step] + yslice, x1slice, x2slice = step_func(yslice, x1slice, x2slice) if not len(yslice): continue diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 47ba95ad994c..bec4002b1ec7 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2406,6 +2406,158 @@ def get_instancemethod(self): return getattr(self.parent_obj, self.instancemethod_name) +def _step_validation(x, *args): + """ + Helper function of `pts_to_*step` functions + + This function does all of the normalization required to the + input and generate the template for output + + + """ + args = tuple(np.asanyarray(y) for y in args) + x = np.asanyarray(x) + if x.ndim != 1: + raise ValueError("x must be 1 dimenional") + if len(args) == 0: + raise ValueError("At least one Y value must be passed") + + return np.vstack((x, ) + args) + + +def pts_to_prestep(x, *args): + """ + Covert continuous line to pre-steps + + Given a set of N points convert to 2 N -1 points + which when connected linearly give a step function + which changes values at the begining the intervals. + + Parameters + ---------- + x : array + The x location of the steps + + y1, y2, ... : array + Any number of y arrays to be turned into steps. + All must be the same length as ``x`` + + Returns + ------- + x, y1, y2, .. : array + The x and y values converted to steps in the same order + as the input. If the input is length ``N``, each of these arrays + will be length ``2N + 1`` + + + Example + ------- + + >> x_s, y1_s, y2_s = pts_to_prestep(x, y1, y2) + """ + # do normalization + vertices = _step_validation(x, *args) + # create the output array + steps = np.zeros((vertices.shape[0], 2 * len(x) - 1), np.float) + # do the to step conversion logic + steps[0, 0::2], steps[0, 1::2] = vertices[0, :], vertices[0, :-1] + steps[1:, 0::2], steps[1:, 1:-1:2] = vertices[1:, :], vertices[1:, 1:] + # convert 2D array back to tuple + return tuple(steps) + + +def pts_to_poststep(x, *args): + """ + Covert continuous line to pre-steps + + Given a set of N points convert to 2 N -1 points + which when connected linearly give a step function + which changes values at the begining the intervals. + + Parameters + ---------- + x : array + The x location of the steps + + y1, y2, ... : array + Any number of y arrays to be turned into steps. + All must be the same length as ``x`` + + Returns + ------- + x, y1, y2, .. : array + The x and y values converted to steps in the same order + as the input. If the input is length ``N``, each of these arrays + will be length ``2N + 1`` + + + Example + ------- + + >> x_s, y1_s, y2_s = pts_to_prestep(x, y1, y2) + """ + # do normalization + vertices = _step_validation(x, *args) + # create the output array + steps = ma.zeros((vertices.shape[0], 2 * len(x) - 1), np.float) + # do the to step conversion logic + steps[0, ::2], steps[0, 1:-1:2] = vertices[0, :], vertices[0, 1:] + steps[1:, 0::2], steps[1:, 1::2] = vertices[1:, :], vertices[1:, :-1] + + # convert 2D array back to tuple + return tuple(steps) + + +def pts_to_midstep(x, *args): + """ + Covert continuous line to pre-steps + + Given a set of N points convert to 2 N -1 points + which when connected linearly give a step function + which changes values at the begining the intervals. + + Parameters + ---------- + x : array + The x location of the steps + + y1, y2, ... : array + Any number of y arrays to be turned into steps. + All must be the same length as ``x`` + + Returns + ------- + x, y1, y2, .. : array + The x and y values converted to steps in the same order + as the input. If the input is length ``N``, each of these arrays + will be length ``2N + 1`` + + + Example + ------- + + >> x_s, y1_s, y2_s = pts_to_prestep(x, y1, y2) + """ + # do normalization + vertices = _step_validation(x, *args) + # create the output array + steps = ma.zeros((vertices.shape[0], 2 * len(x)), np.float) + steps[0, 1:-1:2] = 0.5 * (vertices[0, :-1] + vertices[0, 1:]) + steps[0, 2::2] = 0.5 * (vertices[0, :-1] + vertices[0, 1:]) + steps[0, 0] = vertices[0, 0] + steps[0, -1] = vertices[0, -1] + steps[1:, 0::2], steps[1:, 1::2] = vertices[1:, :], vertices[1:, :] + + # convert 2D array back to tuple + return tuple(steps) + +STEP_LOOKUP_MAP = {'pre': pts_to_prestep, + 'post': pts_to_poststep, + 'mid': pts_to_midstep, + 'step-pre': pts_to_prestep, + 'step-post': pts_to_poststep, + 'step-mid': pts_to_midstep} + # Numpy > 1.6.x deprecates putmask in favor of the new copyto. # So long as we support versions 1.6.x and less, we need the # following local version of putmask. We choose to make a diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 82beb95428ef..52e21b8a4b9b 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -16,7 +16,9 @@ from matplotlib import verbose from . import artist from .artist import Artist -from .cbook import iterable, is_string_like, is_numlike, ls_mapper_r +from .cbook import (iterable, is_string_like, is_numlike, ls_mapper_r, + pts_to_prestep, pts_to_poststep, pts_to_midstep) + from .colors import colorConverter from .path import Path from .transforms import Bbox, TransformedPath, IdentityTransform @@ -1154,36 +1156,21 @@ def _draw_lines(self, renderer, gc, path, trans): self._lineFunc(renderer, gc, path, trans) def _draw_steps_pre(self, renderer, gc, path, trans): - vertices = self._xy - steps = ma.zeros((2 * len(vertices) - 1, 2), np.float_) - - steps[0::2, 0], steps[1::2, 0] = vertices[:, 0], vertices[:-1, 0] - steps[0::2, 1], steps[1:-1:2, 1] = vertices[:, 1], vertices[1:, 1] + steps = np.vstack(pts_to_prestep(*self._xy.T)).T path = Path(steps) path = path.transformed(self.get_transform()) self._lineFunc(renderer, gc, path, IdentityTransform()) def _draw_steps_post(self, renderer, gc, path, trans): - vertices = self._xy - steps = ma.zeros((2 * len(vertices) - 1, 2), np.float_) - - steps[::2, 0], steps[1:-1:2, 0] = vertices[:, 0], vertices[1:, 0] - steps[0::2, 1], steps[1::2, 1] = vertices[:, 1], vertices[:-1, 1] + steps = np.vstack(pts_to_poststep(*self._xy.T)).T path = Path(steps) path = path.transformed(self.get_transform()) self._lineFunc(renderer, gc, path, IdentityTransform()) def _draw_steps_mid(self, renderer, gc, path, trans): - vertices = self._xy - steps = ma.zeros((2 * len(vertices), 2), np.float_) - - steps[1:-1:2, 0] = 0.5 * (vertices[:-1, 0] + vertices[1:, 0]) - steps[2::2, 0] = 0.5 * (vertices[:-1, 0] + vertices[1:, 0]) - steps[0, 0] = vertices[0, 0] - steps[-1, 0] = vertices[-1, 0] - steps[0::2, 1], steps[1::2, 1] = vertices[:, 1], vertices[:, 1] + steps = np.vstack(pts_to_midstep(*self._xy.T)).T path = Path(steps) path = path.transformed(self.get_transform()) diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index cbe24e54cb1b..2b916b08566f 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -8,7 +8,8 @@ import numpy as np from numpy.testing.utils import (assert_array_equal, assert_approx_equal, assert_array_almost_equal) -from nose.tools import assert_equal, assert_not_equal, raises, assert_true +from nose.tools import (assert_equal, assert_not_equal, raises, assert_true, + assert_raises) import matplotlib.cbook as cbook import matplotlib.colors as mcolors @@ -304,3 +305,74 @@ def test_callback_complete(self): def dummy(self): pass + + +def test_to_prestep(): + x = np.arange(4) + y1 = np.arange(4) + y2 = np.arange(4)[::-1] + + xs, y1s, y2s = cbook.pts_to_prestep(x, y1, y2) + + x_target = np.asarray([0, 0, 1, 1, 2, 2, 3], dtype='float') + y1_target = np.asarray([0, 1, 1, 2, 2, 3, 3], dtype='float') + y2_target = np.asarray([3, 2, 2, 1, 1, 0, 0], dtype='float') + + assert_array_equal(x_target, xs) + assert_array_equal(y1_target, y1s) + assert_array_equal(y2_target, y2s) + + xs, y1s = cbook.pts_to_prestep(x, y1) + assert_array_equal(x_target, xs) + assert_array_equal(y1_target, y1s) + + +def test_to_poststep(): + x = np.arange(4) + y1 = np.arange(4) + y2 = np.arange(4)[::-1] + + xs, y1s, y2s = cbook.pts_to_poststep(x, y1, y2) + + x_target = np.asarray([0, 1, 1, 2, 2, 3, 3], dtype='float') + y1_target = np.asarray([0, 0, 1, 1, 2, 2, 3], dtype='float') + y2_target = np.asarray([3, 3, 2, 2, 1, 1, 0], dtype='float') + + assert_array_equal(x_target, xs) + assert_array_equal(y1_target, y1s) + assert_array_equal(y2_target, y2s) + + xs, y1s = cbook.pts_to_poststep(x, y1) + assert_array_equal(x_target, xs) + assert_array_equal(y1_target, y1s) + + +def test_to_midstep(): + x = np.arange(4) + y1 = np.arange(4) + y2 = np.arange(4)[::-1] + + xs, y1s, y2s = cbook.pts_to_midstep(x, y1, y2) + + x_target = np.asarray([0, .5, .5, 1.5, 1.5, 2.5, 2.5, 3], dtype='float') + y1_target = np.asarray([0, 0, 1, 1, 2, 2, 3, 3], dtype='float') + y2_target = np.asarray([3, 3, 2, 2, 1, 1, 0, 0], dtype='float') + + assert_array_equal(x_target, xs) + assert_array_equal(y1_target, y1s) + assert_array_equal(y2_target, y2s) + + xs, y1s = cbook.pts_to_midstep(x, y1) + assert_array_equal(x_target, xs) + assert_array_equal(y1_target, y1s) + + +def test_step_fails(): + assert_raises(ValueError, cbook._step_validation, + np.arange(12).reshape(3, 4), 'a') + assert_raises(ValueError, cbook._step_validation, + np.arange(12), 'a') + assert_raises(ValueError, cbook._step_validation, + np.arange(12)) + assert_raises(ValueError, cbook._step_validation, + np.arange(12), np.arange(3))