From b46591186d9d0ca74401999196a7a4d8b00b480c Mon Sep 17 00:00:00 2001 From: andrzejnovak Date: Tue, 29 Sep 2020 20:06:39 +0200 Subject: [PATCH 1/2] feat: StepPatch to take array as baseline --- .../lines_bars_and_markers/stairs_demo.py | 9 +++ lib/matplotlib/axes/_axes.py | 12 ++-- lib/matplotlib/patches.py | 52 +++++++++++------- .../test_axes/test_stairs_options.png | Bin 13278 -> 15641 bytes lib/matplotlib/tests/test_axes.py | 8 ++- 5 files changed, 53 insertions(+), 28 deletions(-) diff --git a/examples/lines_bars_and_markers/stairs_demo.py b/examples/lines_bars_and_markers/stairs_demo.py index 8667ddefa42b..ad874b70efb9 100644 --- a/examples/lines_bars_and_markers/stairs_demo.py +++ b/examples/lines_bars_and_markers/stairs_demo.py @@ -43,6 +43,15 @@ ax.legend() plt.show() +############################################################################# +# *baseline* can take an array to allow for stacked histogram plots + +A = np.tile([1, 2, 3], (3, 1)) * np.arange(1, 4).reshape(-1, 1) +A = np.vstack([np.zeros(3), A]) + +for i in range(3): + plt.stairs(A[i+1], baseline=A[i], fill=True) + ############################################################################# # Comparison of `.pyplot.step` and `.pyplot.stairs` # ------------------------------------------------- diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index b458edd89c75..32f0e368981d 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6744,8 +6744,8 @@ def stairs(self, values, edges=None, *, The direction of the steps. Vertical means that *values* are along the y-axis, and edges are along the x-axis. - baseline : float or None, default: 0 - Determines starting value of the bounding edges or when + baseline : float, array-like or None, default: 0 + Determines bottom value of the bounding edges or when ``fill=True``, position of lower edge. fill : bool, default: False @@ -6775,8 +6775,8 @@ def stairs(self, values, edges=None, *, if edges is None: edges = np.arange(len(values) + 1) - edges, values = self._process_unit_info( - [("x", edges), ("y", values)], kwargs) + edges, values, baseline = self._process_unit_info( + [("x", edges), ("y", values), ("y", baseline)], kwargs) patch = mpatches.StepPatch(values, edges, @@ -6788,9 +6788,9 @@ def stairs(self, values, edges=None, *, if baseline is None: baseline = 0 if orientation == 'vertical': - patch.sticky_edges.y.append(baseline) + patch.sticky_edges.y.append(np.min(baseline)) else: - patch.sticky_edges.x.append(baseline) + patch.sticky_edges.x.append(np.min(baseline)) self._request_autoscale_view() return patch diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 63be433c542b..51828b71447e 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -942,18 +942,18 @@ def __init__(self, values, edges, *, The direction of the steps. Vertical means that *values* are along the y-axis, and edges are along the x-axis. - baseline : float or None, default: 0 - Determines starting value of the bounding edges or when + baseline : float, 1D array-like or None, default: 0 + Determines bottom value of the bounding edges or when ``fill=True``, position of lower edge. Other valid keyword arguments are: %(Patch)s """ - self.baseline = baseline self.orientation = orientation self._edges = np.asarray(edges) self._values = np.asarray(values) + self._baseline = np.asarray(baseline) if baseline is not None else None self._update_path() super().__init__(self._path, **kwargs) @@ -966,13 +966,24 @@ def _update_path(self): f"`len(values) = {self._values.size}` and " f"`len(edges) = {self._edges.size}`.") verts, codes = [], [] - for idx0, idx1 in cbook.contiguous_regions(~np.isnan(self._values)): + + _nan_mask = np.isnan(self._values) + if self._baseline is not None: + _nan_mask |= np.isnan(self._baseline) + for idx0, idx1 in cbook.contiguous_regions(~_nan_mask): x = np.repeat(self._edges[idx0:idx1+1], 2) y = np.repeat(self._values[idx0:idx1], 2) - if self.baseline is not None: - y = np.hstack((self.baseline, y, self.baseline)) - else: + if self._baseline is None: y = np.hstack((y[0], y, y[-1])) + elif self._baseline.ndim == 0: # single baseline value + y = np.hstack((self._baseline, y, self._baseline)) + elif self._baseline.ndim == 1: # baseline array + base = np.repeat(self._baseline[idx0:idx1], 2)[::-1] + x = np.concatenate([x, x[::-1]]) + y = np.concatenate([np.hstack((base[-1], y, base[0], + base[0], base, base[-1]))]) + else: # no baseline + raise ValueError('Invalid `baseline` specified') if self.orientation == 'vertical': xy = np.column_stack([x, y]) else: @@ -982,23 +993,26 @@ def _update_path(self): self._path = Path(np.vstack(verts), np.hstack(codes)) def get_data(self): - """Get `.StepPatch` values and edges.""" - return self._values, self._edges + """Get `.StepPatch` values, edges and baseline.""" + return self._values, self._edges, self._baseline - def set_data(self, values, edges=None): + def set_data(self, values, edges=None, baseline=None): """ - Set `.StepPatch` values and optionally edges. + Set `.StepPatch` values and optionally edges and baseline. Parameters ---------- values : 1D array-like or None Will not update values, if passing None edges : 1D array-like, optional + baseline : float, 1D array-like or None """ if values is not None: self._values = np.asarray(values) if edges is not None: self._edges = np.asarray(edges) + if baseline is not None: + self._baseline = np.asarray(baseline) self._update_path() self.stale = True @@ -1010,7 +1024,7 @@ def set_values(self, values): ---------- values : 1D array-like """ - self.set_data(values, edges=None) + self.set_data(values, edges=None, baseline=None) def set_edges(self, edges): """ @@ -1020,23 +1034,21 @@ def set_edges(self, edges): ---------- edges : 1D array-like """ - self.set_data(None, edges=edges) + self.set_data(None, edges=edges, baseline=None) def get_baseline(self): - """Get `.StepPatch` baseline value.""" - return self.baseline + """Get `.StepPatch` baseline.""" + return self._baseline def set_baseline(self, baseline): """ - Set `.StepPatch` baseline value. + Set `.StepPatch` baseline. Parameters ---------- - baseline : float or None + baseline : float, array-like or None, default: 0 """ - self.baseline = baseline - self._update_path() - self.stale = True + self.set_data(None, edges=None, baseline=baseline) class Polygon(Patch): diff --git a/lib/matplotlib/tests/baseline_images/test_axes/test_stairs_options.png b/lib/matplotlib/tests/baseline_images/test_axes/test_stairs_options.png index 0f750bc421d94a454ccb0f83e7e9e233003ce59a..3367067f36055e6ae54650ec28d4cd2d19576d42 100644 GIT binary patch literal 15641 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 literal 13278 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 Date: Fri, 2 Oct 2020 02:10:42 +0200 Subject: [PATCH 2/2] limit StepPatch to get/set_data() --- .../lines_bars_and_markers/stairs_demo.py | 9 +-- lib/matplotlib/axes/_axes.py | 9 ++- lib/matplotlib/patches.py | 61 ++++++------------- lib/matplotlib/tests/test_axes.py | 12 ++-- 4 files changed, 34 insertions(+), 57 deletions(-) diff --git a/examples/lines_bars_and_markers/stairs_demo.py b/examples/lines_bars_and_markers/stairs_demo.py index ad874b70efb9..d1f02d37ff34 100644 --- a/examples/lines_bars_and_markers/stairs_demo.py +++ b/examples/lines_bars_and_markers/stairs_demo.py @@ -45,11 +45,12 @@ ############################################################################# # *baseline* can take an array to allow for stacked histogram plots +A = [[0, 0, 0], + [1, 2, 3], + [2, 4, 6], + [3, 6, 9]] -A = np.tile([1, 2, 3], (3, 1)) * np.arange(1, 4).reshape(-1, 1) -A = np.vstack([np.zeros(3), A]) - -for i in range(3): +for i in range(len(A) - 1): plt.stairs(A[i+1], baseline=A[i], fill=True) ############################################################################# diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 32f0e368981d..4f84d5e99ff7 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6729,7 +6729,8 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, def stairs(self, values, edges=None, *, orientation='vertical', baseline=0, fill=False, **kwargs): """ - A stepwise constant line or filled plot. + A stepwise constant function as a line with bounding edges + or a filled plot. Parameters ---------- @@ -6745,8 +6746,10 @@ def stairs(self, values, edges=None, *, the y-axis, and edges are along the x-axis. baseline : float, array-like or None, default: 0 - Determines bottom value of the bounding edges or when - ``fill=True``, position of lower edge. + The bottom value of the bounding edges or when + ``fill=True``, position of lower edge. If *fill* is + True or an array is passed to *baseline*, a closed + path is drawn. fill : bool, default: False Whether the area under the step curve should be filled. diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 51828b71447e..4a689ecc00c2 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -4,6 +4,7 @@ import math from numbers import Number import textwrap +from collections import namedtuple import numpy as np @@ -920,7 +921,8 @@ class StepPatch(PathPatch): """ A path patch describing a stepwise constant function. - The path is unclosed. It starts and stops at baseline. + By default the path is not closed and starts and stops at + baseline value. """ _edge_default = False @@ -939,12 +941,14 @@ def __init__(self, values, edges, *, between which the curve takes on vals values. orientation : {'vertical', 'horizontal'}, default: 'vertical' - The direction of the steps. Vertical means that *values* are along - the y-axis, and edges are along the x-axis. + The direction of the steps. Vertical means that *values* are + along the y-axis, and edges are along the x-axis. - baseline : float, 1D array-like or None, default: 0 - Determines bottom value of the bounding edges or when - ``fill=True``, position of lower edge. + baseline : float, array-like or None, default: 0 + The bottom value of the bounding edges or when + ``fill=True``, position of lower edge. If *fill* is + True or an array is passed to *baseline*, a closed + path is drawn. Other valid keyword arguments are: @@ -993,12 +997,13 @@ def _update_path(self): self._path = Path(np.vstack(verts), np.hstack(codes)) def get_data(self): - """Get `.StepPatch` values, edges and baseline.""" - return self._values, self._edges, self._baseline + """Get `.StepPatch` values, edges and baseline as namedtuple.""" + StairData = namedtuple('StairData', 'values edges baseline') + return StairData(self._values, self._edges, self._baseline) - def set_data(self, values, edges=None, baseline=None): + def set_data(self, values=None, edges=None, baseline=None): """ - Set `.StepPatch` values and optionally edges and baseline. + Set `.StepPatch` values, edges and baseline. Parameters ---------- @@ -1007,6 +1012,8 @@ def set_data(self, values, edges=None, baseline=None): edges : 1D array-like, optional baseline : float, 1D array-like or None """ + if values is None and edges is None and baseline is None: + raise ValueError("Must set *values*, *edges* or *baseline*.") if values is not None: self._values = np.asarray(values) if edges is not None: @@ -1016,40 +1023,6 @@ def set_data(self, values, edges=None, baseline=None): self._update_path() self.stale = True - def set_values(self, values): - """ - Set `.StepPatch` values. - - Parameters - ---------- - values : 1D array-like - """ - self.set_data(values, edges=None, baseline=None) - - def set_edges(self, edges): - """ - Set `.StepPatch` edges. - - Parameters - ---------- - edges : 1D array-like - """ - self.set_data(None, edges=edges, baseline=None) - - def get_baseline(self): - """Get `.StepPatch` baseline.""" - return self._baseline - - def set_baseline(self, baseline): - """ - Set `.StepPatch` baseline. - - Parameters - ---------- - baseline : float, array-like or None, default: 0 - """ - self.set_data(None, edges=None, baseline=baseline) - class Polygon(Patch): """A general polygon patch.""" diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index a8e9c43fdf4a..b48b041e80bd 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1884,15 +1884,15 @@ def test_stairs_update(fig_test, fig_ref): test_ax = fig_test.add_subplot() h = test_ax.stairs([1, 2, 3]) test_ax.set_ylim(ylim) - h.set_values([3, 2, 1]) - h.set_edges(np.arange(4)+2) + h.set_data([3, 2, 1]) + h.set_data(edges=np.arange(4)+2) h.set_data([1, 2, 1], np.arange(4)/2) h.set_data([1, 2, 3]) h.set_data(None, np.arange(4)) assert np.allclose(h.get_data()[0], np.arange(1, 4)) assert np.allclose(h.get_data()[1], np.arange(4)) - h.set_baseline(-2) - assert h.get_baseline() == -2 + h.set_data(baseline=-2) + assert h.get_data().baseline == -2 # Ref ref_ax = fig_ref.add_subplot() @@ -1913,13 +1913,13 @@ def test_stairs_invalid_mismatch(): def test_stairs_invalid_update(): h = plt.stairs([1, 2], [0, 1, 2]) with pytest.raises(ValueError, match='Nan values in "edges"'): - h.set_edges([1, np.nan, 2]) + h.set_data(edges=[1, np.nan, 2]) def test_stairs_invalid_update2(): h = plt.stairs([1, 2], [0, 1, 2]) with pytest.raises(ValueError, match='Size mismatch'): - h.set_edges(np.arange(5)) + h.set_data(edges=np.arange(5)) @image_comparison(['test_stairs_options.png'], remove_text=True)