Skip to content

Commit 5503774

Browse files
cmp0xfftimhoffm
andauthored
[MNT]: #28701 separate the generation of polygon vertices in fill_between to enable resampling (#28702)
Move generation of polygon vertices from plotting method to Artist sub-class. This allows the data on `ax.fill_between` to be updated after the initial plotting. Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
1 parent 0439b37 commit 5503774

File tree

9 files changed

+376
-130
lines changed

9 files changed

+376
-130
lines changed

doc/missing-references.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -304,8 +304,8 @@
304304
"matplotlib.collections._CollectionWithSizes.set_sizes": [
305305
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.barbs:179",
306306
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.broken_barh:84",
307-
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_between:120",
308-
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_betweenx:120",
307+
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_between:121",
308+
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_betweenx:121",
309309
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.hexbin:213",
310310
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolor:182",
311311
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.quiver:215",
@@ -316,10 +316,11 @@
316316
"lib/matplotlib/collections.py:docstring of matplotlib.artist.PolyQuadMesh.set:44",
317317
"lib/matplotlib/collections.py:docstring of matplotlib.artist.RegularPolyCollection.set:44",
318318
"lib/matplotlib/collections.py:docstring of matplotlib.artist.StarPolygonCollection.set:44",
319+
"lib/matplotlib/collections.py:docstring of matplotlib.artist.FillBetweenPolyCollection.set:45",
319320
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.barbs:179",
320321
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.broken_barh:84",
321-
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_between:120",
322-
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_betweenx:120",
322+
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_between:121",
323+
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_betweenx:121",
323324
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.hexbin:213",
324325
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolor:182",
325326
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.quiver:215",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
``FillBetweenPolyCollection``
2+
-----------------------------
3+
4+
The new class :class:`matplotlib.collections.FillBetweenPolyCollection` provides
5+
the ``set_data`` method, enabling e.g. resampling
6+
(:file:`galleries/event_handling/resample.html`).
7+
:func:`matplotlib.axes.Axes.fill_between` and
8+
:func:`matplotlib.axes.Axes.fill_betweenx` now return this new class.
9+
10+
.. code-block:: python
11+
12+
import numpy as np
13+
from matplotlib import pyplot as plt
14+
15+
t = np.linspace(0, 1)
16+
17+
fig, ax = plt.subplots()
18+
coll = ax.fill_between(t, -t**2, t**2)
19+
fig.savefig("before.png")
20+
21+
coll.set_data(t, -t**4, t**4)
22+
fig.savefig("after.png")

galleries/examples/event_handling/resample.py

+23-12
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,19 @@
2222

2323
# A class that will downsample the data and recompute when zoomed.
2424
class DataDisplayDownsampler:
25-
def __init__(self, xdata, ydata):
26-
self.origYData = ydata
25+
def __init__(self, xdata, y1data, y2data):
26+
self.origY1Data = y1data
27+
self.origY2Data = y2data
2728
self.origXData = xdata
2829
self.max_points = 50
2930
self.delta = xdata[-1] - xdata[0]
3031

31-
def downsample(self, xstart, xend):
32+
def plot(self, ax):
33+
x, y1, y2 = self._downsample(self.origXData.min(), self.origXData.max())
34+
(self.line,) = ax.plot(x, y1, 'o-')
35+
self.poly_collection = ax.fill_between(x, y1, y2, step="pre", color="r")
36+
37+
def _downsample(self, xstart, xend):
3238
# get the points in the view range
3339
mask = (self.origXData > xstart) & (self.origXData < xend)
3440
# dilate the mask by one to catch the points just outside
@@ -39,36 +45,41 @@ def downsample(self, xstart, xend):
3945

4046
# mask data
4147
xdata = self.origXData[mask]
42-
ydata = self.origYData[mask]
48+
y1data = self.origY1Data[mask]
49+
y2data = self.origY2Data[mask]
4350

4451
# downsample data
4552
xdata = xdata[::ratio]
46-
ydata = ydata[::ratio]
53+
y1data = y1data[::ratio]
54+
y2data = y2data[::ratio]
4755

48-
print(f"using {len(ydata)} of {np.sum(mask)} visible points")
56+
print(f"using {len(y1data)} of {np.sum(mask)} visible points")
4957

50-
return xdata, ydata
58+
return xdata, y1data, y2data
5159

5260
def update(self, ax):
53-
# Update the line
61+
# Update the artists
5462
lims = ax.viewLim
5563
if abs(lims.width - self.delta) > 1e-8:
5664
self.delta = lims.width
5765
xstart, xend = lims.intervalx
58-
self.line.set_data(*self.downsample(xstart, xend))
66+
x, y1, y2 = self._downsample(xstart, xend)
67+
self.line.set_data(x, y1)
68+
self.poly_collection.set_data(x, y1, y2, step="pre")
5969
ax.figure.canvas.draw_idle()
6070

6171

6272
# Create a signal
6373
xdata = np.linspace(16, 365, (365-16)*4)
64-
ydata = np.sin(2*np.pi*xdata/153) + np.cos(2*np.pi*xdata/127)
74+
y1data = np.sin(2*np.pi*xdata/153) + np.cos(2*np.pi*xdata/127)
75+
y2data = y1data + .2
6576

66-
d = DataDisplayDownsampler(xdata, ydata)
77+
d = DataDisplayDownsampler(xdata, y1data, y2data)
6778

6879
fig, ax = plt.subplots()
6980

7081
# Hook up the line
71-
d.line, = ax.plot(xdata, ydata, 'o-')
82+
d.plot(ax)
7283
ax.set_autoscale_on(False) # Otherwise, infinite loop
7384

7485
# Connect for changing the view limits

lib/matplotlib/axes/_axes.py

+24-110
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import re
88
import numpy as np
9-
from numpy import ma
109

1110
import matplotlib as mpl
1211
import matplotlib.category # Register category unit converter as side effect.
@@ -5551,143 +5550,58 @@ def _fill_between_x_or_y(
55515550
i.e. constant in between *{ind}*. The value determines where the
55525551
step will occur:
55535552
5554-
- 'pre': The y value is continued constantly to the left from
5555-
every *x* position, i.e. the interval ``(x[i-1], x[i]]`` has the
5556-
value ``y[i]``.
5553+
- 'pre': The {dep} value is continued constantly to the left from
5554+
every *{ind}* position, i.e. the interval ``({ind}[i-1], {ind}[i]]``
5555+
has the value ``{dep}[i]``.
55575556
- 'post': The y value is continued constantly to the right from
5558-
every *x* position, i.e. the interval ``[x[i], x[i+1])`` has the
5559-
value ``y[i]``.
5560-
- 'mid': Steps occur half-way between the *x* positions.
5557+
every *{ind}* position, i.e. the interval ``[{ind}[i], {ind}[i+1])``
5558+
has the value ``{dep}[i]``.
5559+
- 'mid': Steps occur half-way between the *{ind}* positions.
55615560
55625561
Returns
55635562
-------
5564-
`.PolyCollection`
5565-
A `.PolyCollection` containing the plotted polygons.
5563+
`.FillBetweenPolyCollection`
5564+
A `.FillBetweenPolyCollection` containing the plotted polygons.
55665565
55675566
Other Parameters
55685567
----------------
55695568
data : indexable object, optional
55705569
DATA_PARAMETER_PLACEHOLDER
55715570
55725571
**kwargs
5573-
All other keyword arguments are passed on to `.PolyCollection`.
5574-
They control the `.Polygon` properties:
5572+
All other keyword arguments are passed on to
5573+
`.FillBetweenPolyCollection`. They control the `.Polygon` properties:
55755574
5576-
%(PolyCollection:kwdoc)s
5575+
%(FillBetweenPolyCollection:kwdoc)s
55775576
55785577
See Also
55795578
--------
55805579
fill_between : Fill between two sets of y-values.
55815580
fill_betweenx : Fill between two sets of x-values.
55825581
"""
5583-
5584-
dep_dir = {"x": "y", "y": "x"}[ind_dir]
5582+
dep_dir = mcoll.FillBetweenPolyCollection._f_dir_from_t(ind_dir)
55855583

55865584
if not mpl.rcParams["_internal.classic_mode"]:
55875585
kwargs = cbook.normalize_kwargs(kwargs, mcoll.Collection)
55885586
if not any(c in kwargs for c in ("color", "facecolor")):
5589-
kwargs["facecolor"] = \
5590-
self._get_patches_for_fill.get_next_color()
5591-
5592-
# Handle united data, such as dates
5593-
ind, dep1, dep2 = map(
5594-
ma.masked_invalid, self._process_unit_info(
5595-
[(ind_dir, ind), (dep_dir, dep1), (dep_dir, dep2)], kwargs))
5596-
5597-
for name, array in [
5598-
(ind_dir, ind), (f"{dep_dir}1", dep1), (f"{dep_dir}2", dep2)]:
5599-
if array.ndim > 1:
5600-
raise ValueError(f"{name!r} is not 1-dimensional")
5587+
kwargs["facecolor"] = self._get_patches_for_fill.get_next_color()
56015588

5602-
if where is None:
5603-
where = True
5604-
else:
5605-
where = np.asarray(where, dtype=bool)
5606-
if where.size != ind.size:
5607-
raise ValueError(f"where size ({where.size}) does not match "
5608-
f"{ind_dir} size ({ind.size})")
5609-
where = where & ~functools.reduce(
5610-
np.logical_or, map(np.ma.getmaskarray, [ind, dep1, dep2]))
5611-
5612-
ind, dep1, dep2 = np.broadcast_arrays(
5613-
np.atleast_1d(ind), dep1, dep2, subok=True)
5614-
5615-
polys = []
5616-
for idx0, idx1 in cbook.contiguous_regions(where):
5617-
indslice = ind[idx0:idx1]
5618-
dep1slice = dep1[idx0:idx1]
5619-
dep2slice = dep2[idx0:idx1]
5620-
if step is not None:
5621-
step_func = cbook.STEP_LOOKUP_MAP["steps-" + step]
5622-
indslice, dep1slice, dep2slice = \
5623-
step_func(indslice, dep1slice, dep2slice)
5624-
5625-
if not len(indslice):
5626-
continue
5589+
ind, dep1, dep2 = self._fill_between_process_units(
5590+
ind_dir, dep_dir, ind, dep1, dep2, **kwargs)
56275591

5628-
N = len(indslice)
5629-
pts = np.zeros((2 * N + 2, 2))
5630-
5631-
if interpolate:
5632-
def get_interp_point(idx):
5633-
im1 = max(idx - 1, 0)
5634-
ind_values = ind[im1:idx+1]
5635-
diff_values = dep1[im1:idx+1] - dep2[im1:idx+1]
5636-
dep1_values = dep1[im1:idx+1]
5637-
5638-
if len(diff_values) == 2:
5639-
if np.ma.is_masked(diff_values[1]):
5640-
return ind[im1], dep1[im1]
5641-
elif np.ma.is_masked(diff_values[0]):
5642-
return ind[idx], dep1[idx]
5643-
5644-
diff_order = diff_values.argsort()
5645-
diff_root_ind = np.interp(
5646-
0, diff_values[diff_order], ind_values[diff_order])
5647-
ind_order = ind_values.argsort()
5648-
diff_root_dep = np.interp(
5649-
diff_root_ind,
5650-
ind_values[ind_order], dep1_values[ind_order])
5651-
return diff_root_ind, diff_root_dep
5652-
5653-
start = get_interp_point(idx0)
5654-
end = get_interp_point(idx1)
5655-
else:
5656-
# Handle scalar dep2 (e.g. 0): the fill should go all
5657-
# the way down to 0 even if none of the dep1 sample points do.
5658-
start = indslice[0], dep2slice[0]
5659-
end = indslice[-1], dep2slice[-1]
5660-
5661-
pts[0] = start
5662-
pts[N + 1] = end
5663-
5664-
pts[1:N+1, 0] = indslice
5665-
pts[1:N+1, 1] = dep1slice
5666-
pts[N+2:, 0] = indslice[::-1]
5667-
pts[N+2:, 1] = dep2slice[::-1]
5668-
5669-
if ind_dir == "y":
5670-
pts = pts[:, ::-1]
5671-
5672-
polys.append(pts)
5673-
5674-
collection = mcoll.PolyCollection(polys, **kwargs)
5675-
5676-
# now update the datalim and autoscale
5677-
pts = np.vstack([np.hstack([ind[where, None], dep1[where, None]]),
5678-
np.hstack([ind[where, None], dep2[where, None]])])
5679-
if ind_dir == "y":
5680-
pts = pts[:, ::-1]
5681-
5682-
up_x = up_y = True
5683-
if "transform" in kwargs:
5684-
up_x, up_y = kwargs["transform"].contains_branch_seperately(self.transData)
5685-
self.update_datalim(pts, updatex=up_x, updatey=up_y)
5592+
collection = mcoll.FillBetweenPolyCollection(
5593+
ind_dir, ind, dep1, dep2,
5594+
where=where, interpolate=interpolate, step=step, **kwargs)
56865595

5687-
self.add_collection(collection, autolim=False)
5596+
self.add_collection(collection)
56885597
self._request_autoscale_view()
56895598
return collection
56905599

5600+
def _fill_between_process_units(self, ind_dir, dep_dir, ind, dep1, dep2, **kwargs):
5601+
"""Handle united data, such as dates."""
5602+
return map(np.ma.masked_invalid, self._process_unit_info(
5603+
[(ind_dir, ind), (dep_dir, dep1), (dep_dir, dep2)], kwargs))
5604+
56915605
def fill_between(self, x, y1, y2=0, where=None, interpolate=False,
56925606
step=None, **kwargs):
56935607
return self._fill_between_x_or_y(

lib/matplotlib/axes/_axes.pyi

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ from matplotlib.artist import Artist
55
from matplotlib.backend_bases import RendererBase
66
from matplotlib.collections import (
77
Collection,
8+
FillBetweenPolyCollection,
89
LineCollection,
910
PathCollection,
1011
PolyCollection,
@@ -459,7 +460,7 @@ class Axes(_AxesBase):
459460
*,
460461
data=...,
461462
**kwargs
462-
) -> PolyCollection: ...
463+
) -> FillBetweenPolyCollection: ...
463464
def fill_betweenx(
464465
self,
465466
y: ArrayLike,
@@ -471,7 +472,7 @@ class Axes(_AxesBase):
471472
*,
472473
data=...,
473474
**kwargs
474-
) -> PolyCollection: ...
475+
) -> FillBetweenPolyCollection: ...
475476
def imshow(
476477
self,
477478
X: ArrayLike | PIL.Image.Image,

0 commit comments

Comments
 (0)