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)) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy