Skip to content

[MNT]: #28701 separate the generation of polygon vertices in fill_between to enable resampling #28702

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
5f23388
feat: #28701 separate the generation of polygon vertices
cmp0xff Aug 10, 2024
045aed0
chore(pre-commit): #28701 fix
cmp0xff Aug 10, 2024
6692213
chore(pytest): #28701 fix
cmp0xff Aug 10, 2024
5204fee
feat: #28701 attempt to fix pytest
cmp0xff Aug 10, 2024
2d6f430
feat(comment): #28701 attempt to make a subclass instead of exposing …
cmp0xff Aug 11, 2024
3a80bd1
chore(typing): #28701
cmp0xff Aug 11, 2024
55c7efb
fix(duplication): #28701 remove
cmp0xff Aug 11, 2024
6dc5b5d
fix(pytest): #28701 attempt to fix pytest by normalising kwargs
cmp0xff Aug 11, 2024
70e9901
chore(typing): #28701 attempt to fix ci
cmp0xff Aug 11, 2024
9796a7e
fix(comment): #28701 naming @timhoffm https://github.com/matplotlib/m…
cmp0xff Aug 13, 2024
557a882
fix(comment): #28701 interface @cmp0xff https://github.com/matplotlib…
cmp0xff Aug 13, 2024
1dd26dc
feat(example): #27801 @jklymak https://github.com/matplotlib/matplotl…
cmp0xff Aug 13, 2024
3485cfe
feat(test): #27801 unit test for set_data @jklymak https://github.com…
cmp0xff Aug 13, 2024
fde3583
chore(pytest): #27801 `python tools/boilerplate.py`
cmp0xff Aug 13, 2024
24f0bb3
chore(import): #27801 fix
cmp0xff Aug 13, 2024
5aefddd
feat: #27801 simplify the interface
cmp0xff Aug 13, 2024
603704b
feat(test): #27801 increase test coverage
cmp0xff Aug 13, 2024
856bc18
chore(mypy): #27801
cmp0xff Aug 13, 2024
a140c07
chore: #27801 reduce duplicated memory usage
cmp0xff Aug 14, 2024
130a631
fix(comment): #27801 @cmp0xff https://github.com/matplotlib/matplotli…
cmp0xff Aug 14, 2024
c29aef2
Merge branch 'main' into feature/cmp0xff/28701-separate-generating-ve…
cmp0xff Aug 14, 2024
d88315f
Merge branch 'main' into feature/cmp0xff/28701-separate-generating-ve…
cmp0xff Sep 4, 2024
e112d5a
fix(comment): #28702 the attribute `self.axes` should always exist ht…
cmp0xff Sep 4, 2024
ccd52a1
fix(comment): #28701 @timhoffm https://github.com/matplotlib/matplotl…
cmp0xff Sep 4, 2024
c72ad39
fix(comment): #28701 py39 compatibility @timhoffm https://github.com/…
cmp0xff Sep 4, 2024
53fd059
fix(comment): #28701 naming @timhoffm https://github.com/matplotlib/m…
cmp0xff Sep 4, 2024
fae8e90
chore(flake8): #28701 fix E501
cmp0xff Sep 4, 2024
38cd7b9
fix(comment): #28701 remove fixtures @timhoffm https://github.com/mat…
cmp0xff Sep 4, 2024
b716ffc
fix(stub): #28701 propagate 53fd059074e976f534934afb3ca9796dea0a9d3c
cmp0xff Sep 4, 2024
db3af64
fix(comment): #28701 @timhoffm https://github.com/matplotlib/matplotl…
cmp0xff Sep 4, 2024
d4e05ce
fix(comment): #28701 use decorator @timhoffm https://github.com/matpl…
cmp0xff Sep 4, 2024
a663a3d
chore(typo): #28701 fixes
cmp0xff Sep 4, 2024
bea3e83
doc(notes): #28701 directive and release notes
cmp0xff Sep 4, 2024
320877e
fix(naming): #28701 propagate 53fd059074e976f534934afb3ca9796dea0a9d3c
cmp0xff Sep 4, 2024
cb942c1
doc(axes): #28701 fix
cmp0xff Sep 4, 2024
a24c874
fix(sphinx): #28701 py:class reference target not found
cmp0xff Sep 4, 2024
50552b5
fix(comment): #28701 @timhoffm simplify the logic https://github.com/…
cmp0xff Sep 5, 2024
573b54d
fix(comment): #28701 remove default value @timhoffm https://github.co…
cmp0xff Sep 5, 2024
13cad5d
fix(comment): #28701 further simplify the test @timhoffm https://gith…
cmp0xff Sep 5, 2024
187dd6e
fix(comment): #28701 default value, kw-only, typing @timhoffm https:/…
cmp0xff Sep 6, 2024
cf44eb7
fix(comment): #28701 make FillBetweenPolyCollection._update_axes_data…
cmp0xff Sep 6, 2024
f2abc04
fix(comment): #28701 @timhoffm https://github.com/matplotlib/matplotl…
cmp0xff Sep 6, 2024
55347c3
fix(comment): #28701 make the example intuitive @timhoffm https://git…
cmp0xff Sep 6, 2024
7fd8b52
fix(comment): #28701 "what's new" is sufficient @timhoffm https://git…
cmp0xff Sep 6, 2024
fd9072c
chore(flake8): #28701 fix E302 expected 2 blank lines, found 1
cmp0xff Sep 6, 2024
1b11a17
fix(comment): #28701 simplify test @timhoffm https://github.com/matpl…
cmp0xff Sep 6, 2024
63723c2
refactor: #28701 further simplify the code
cmp0xff Sep 6, 2024
8ba8478
chore(naming): #28701 propagate 320877e034a33859d9eb5a1db068139d192e8f3a
cmp0xff Sep 6, 2024
5c0bd7e
doc: #28701
cmp0xff Sep 6, 2024
bc145e5
chore(naming): #28701
cmp0xff Sep 6, 2024
e394cfa
Merge branch 'main' into feature/cmp0xff/28701-separate-generating-ve…
cmp0xff Sep 6, 2024
e34e610
fix: #28701 attempt to fix pytest
cmp0xff Sep 6, 2024
ef31e8d
fix(comment): #28701 @anntzer spelling https://github.com/matplotlib/…
cmp0xff Sep 11, 2024
84c5e55
feat: #28701 remove the argument axes @timhoffm
cmp0xff Sep 12, 2024
ce0fdbb
refactor: #28701 attempt to simplify make_verts_per_region
cmp0xff Sep 12, 2024
1234ac0
chore: #28701 spacing
cmp0xff Sep 12, 2024
9f2dfe4
Merge branch 'main' into feature/cmp0xff/28701-separate-generating-ve…
cmp0xff Sep 12, 2024
ed2636c
fix: #28701 attempt to fix pytest
cmp0xff Sep 12, 2024
964be6c
feat: #28701 recover the original low memory trace
cmp0xff Sep 13, 2024
4dd9b8f
doc: #28701 attempt to fix
cmp0xff Sep 13, 2024
0835298
fix: #28701 attempt to fix pytest
cmp0xff Sep 13, 2024
7806e46
refactor: #28701 make semantic function from code blocks
cmp0xff Sep 13, 2024
8c056a9
feat: #28701 @timhoffm drop interpolate, step and kwargs in set_data …
cmp0xff Sep 13, 2024
ade691d
doc: #28701 @timhoffm make the docstring more informative to the end …
cmp0xff Sep 13, 2024
79c4d49
fix(comment): #28701 @timhoffm https://github.com/matplotlib/matplotl…
cmp0xff Sep 13, 2024
428ef2f
doc: #28701 @timhoffm https://github.com/matplotlib/matplotlib/pull/2…
cmp0xff Sep 13, 2024
ea15827
fix(comment): #28701 use bbox and its minpos to set data limits @timh…
cmp0xff Sep 18, 2024
8990b5f
feat(comment): #28701 get_datalim for FillBetweenPolyCollection @timh…
cmp0xff Sep 19, 2024
2e9463f
fix: #28701 remove internal property from typing
cmp0xff Sep 19, 2024
614b301
doc: #28701 formating @timhoffm https://github.com/matplotlib/matplot…
cmp0xff Sep 22, 2024
1f03a14
Merge branch 'matplotlib:main' into feature/cmp0xff/28701-separate-ge…
cmp0xff Sep 22, 2024
cba918c
fix(comment): #28701 @cmp0xff https://github.com/matplotlib/matplotli…
cmp0xff Sep 22, 2024
6920c0c
doc: #28701 get_data_mask @timhoffm https://github.com/matplotlib/mat…
cmp0xff Sep 22, 2024
a697987
fix(commment): #28701 naming @timhoffm https://github.com/matplotlib/…
cmp0xff Sep 22, 2024
3447f78
fix(comment): #28701 @cmp0xff https://github.com/matplotlib/matplotli…
cmp0xff Sep 22, 2024
1bd670a
fix(comment): #28701 @timhoffm https://github.com/matplotlib/matplotl…
cmp0xff Sep 22, 2024
a75e02b
doc(example): #28701 @timhoffm https://github.com/matplotlib/matplotl…
cmp0xff Sep 25, 2024
8db3849
Merge branch 'matplotlib:main' into feature/cmp0xff/28701-separate-ge…
cmp0xff Oct 1, 2024
b0bc7f7
doc: #28701 attempt to fix the doc build
cmp0xff Oct 1, 2024
e34a98e
doc: #28701 attempt to fix the doc build
cmp0xff Oct 1, 2024
c547a8a
doc: #28701 attempt to fix the doc build
cmp0xff Oct 1, 2024
505811f
chore: #28701 restore b0bc7f72c6bba226b99aa64155d994cb1cd8cc1a
cmp0xff Oct 1, 2024
34ead11
Merge branch 'matplotlib:main' into feature/cmp0xff/28701-separate-ge…
cmp0xff Oct 7, 2024
23aaca3
Merge branch 'matplotlib:main' into feature/cmp0xff/28701-separate-ge…
cmp0xff Oct 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions doc/missing-references.json
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,8 @@
"matplotlib.collections._CollectionWithSizes.set_sizes": [
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.barbs:179",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.broken_barh:84",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_between:120",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_betweenx:120",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_between:121",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_betweenx:121",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.hexbin:213",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolor:182",
"lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.quiver:215",
Expand All @@ -316,10 +316,11 @@
"lib/matplotlib/collections.py:docstring of matplotlib.artist.PolyQuadMesh.set:44",
"lib/matplotlib/collections.py:docstring of matplotlib.artist.RegularPolyCollection.set:44",
"lib/matplotlib/collections.py:docstring of matplotlib.artist.StarPolygonCollection.set:44",
"lib/matplotlib/collections.py:docstring of matplotlib.artist.FillBetweenPolyCollection.set:45",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.barbs:179",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.broken_barh:84",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_between:120",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_betweenx:120",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_between:121",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_betweenx:121",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.hexbin:213",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolor:182",
"lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.quiver:215",
Expand Down
22 changes: 22 additions & 0 deletions doc/users/next_whats_new/fill_between_poly_collection.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
``FillBetweenPolyCollection``
-----------------------------

The new class :class:`matplotlib.collections.FillBetweenPolyCollection` provides
the ``set_data`` method, enabling e.g. resampling
(:file:`galleries/event_handling/resample.html`).
:func:`matplotlib.axes.Axes.fill_between` and
:func:`matplotlib.axes.Axes.fill_betweenx` now return this new class.

.. code-block:: python

import numpy as np
from matplotlib import pyplot as plt

t = np.linspace(0, 1)

fig, ax = plt.subplots()
coll = ax.fill_between(t, -t**2, t**2)
fig.savefig("before.png")

coll.set_data(t, -t**4, t**4)
fig.savefig("after.png")
35 changes: 23 additions & 12 deletions galleries/examples/event_handling/resample.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,19 @@

# A class that will downsample the data and recompute when zoomed.
class DataDisplayDownsampler:
def __init__(self, xdata, ydata):
self.origYData = ydata
def __init__(self, xdata, y1data, y2data):
self.origY1Data = y1data
self.origY2Data = y2data
self.origXData = xdata
self.max_points = 50
self.delta = xdata[-1] - xdata[0]

def downsample(self, xstart, xend):
def plot(self, ax):
x, y1, y2 = self._downsample(self.origXData.min(), self.origXData.max())
(self.line,) = ax.plot(x, y1, 'o-')
self.poly_collection = ax.fill_between(x, y1, y2, step="pre", color="r")

def _downsample(self, xstart, xend):
# get the points in the view range
mask = (self.origXData > xstart) & (self.origXData < xend)
# dilate the mask by one to catch the points just outside
Expand All @@ -39,36 +45,41 @@ def downsample(self, xstart, xend):

# mask data
xdata = self.origXData[mask]
ydata = self.origYData[mask]
y1data = self.origY1Data[mask]
y2data = self.origY2Data[mask]

# downsample data
xdata = xdata[::ratio]
ydata = ydata[::ratio]
y1data = y1data[::ratio]
y2data = y2data[::ratio]

print(f"using {len(ydata)} of {np.sum(mask)} visible points")
print(f"using {len(y1data)} of {np.sum(mask)} visible points")

return xdata, ydata
return xdata, y1data, y2data

def update(self, ax):
# Update the line
# Update the artists
lims = ax.viewLim
if abs(lims.width - self.delta) > 1e-8:
self.delta = lims.width
xstart, xend = lims.intervalx
self.line.set_data(*self.downsample(xstart, xend))
x, y1, y2 = self._downsample(xstart, xend)
self.line.set_data(x, y1)
self.poly_collection.set_data(x, y1, y2, step="pre")
ax.figure.canvas.draw_idle()


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

d = DataDisplayDownsampler(xdata, ydata)
d = DataDisplayDownsampler(xdata, y1data, y2data)

fig, ax = plt.subplots()

# Hook up the line
d.line, = ax.plot(xdata, ydata, 'o-')
d.plot(ax)
ax.set_autoscale_on(False) # Otherwise, infinite loop

# Connect for changing the view limits
Expand Down
134 changes: 24 additions & 110 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import re
import numpy as np
from numpy import ma

import matplotlib as mpl
import matplotlib.category # Register category unit converter as side effect.
Expand Down Expand Up @@ -5551,143 +5550,58 @@ def _fill_between_x_or_y(
i.e. constant in between *{ind}*. The value determines where the
step will occur:

- 'pre': The y value is continued constantly to the left from
every *x* position, i.e. the interval ``(x[i-1], x[i]]`` has the
value ``y[i]``.
- 'pre': The {dep} value is continued constantly to the left from
every *{ind}* position, i.e. the interval ``({ind}[i-1], {ind}[i]]``
has the value ``{dep}[i]``.
- 'post': The y value is continued constantly to the right from
every *x* position, i.e. the interval ``[x[i], x[i+1])`` has the
value ``y[i]``.
- 'mid': Steps occur half-way between the *x* positions.
every *{ind}* position, i.e. the interval ``[{ind}[i], {ind}[i+1])``
has the value ``{dep}[i]``.
- 'mid': Steps occur half-way between the *{ind}* positions.

Returns
-------
`.PolyCollection`
A `.PolyCollection` containing the plotted polygons.
`.FillBetweenPolyCollection`
A `.FillBetweenPolyCollection` containing the plotted polygons.

Other Parameters
----------------
data : indexable object, optional
DATA_PARAMETER_PLACEHOLDER

**kwargs
All other keyword arguments are passed on to `.PolyCollection`.
They control the `.Polygon` properties:
All other keyword arguments are passed on to
`.FillBetweenPolyCollection`. They control the `.Polygon` properties:

%(PolyCollection:kwdoc)s
%(FillBetweenPolyCollection:kwdoc)s

See Also
--------
fill_between : Fill between two sets of y-values.
fill_betweenx : Fill between two sets of x-values.
"""

dep_dir = {"x": "y", "y": "x"}[ind_dir]
dep_dir = mcoll.FillBetweenPolyCollection._f_dir_from_t(ind_dir)

if not mpl.rcParams["_internal.classic_mode"]:
kwargs = cbook.normalize_kwargs(kwargs, mcoll.Collection)
if not any(c in kwargs for c in ("color", "facecolor")):
kwargs["facecolor"] = \
self._get_patches_for_fill.get_next_color()

# Handle united data, such as dates
ind, dep1, dep2 = map(
ma.masked_invalid, self._process_unit_info(
[(ind_dir, ind), (dep_dir, dep1), (dep_dir, dep2)], kwargs))

for name, array in [
(ind_dir, ind), (f"{dep_dir}1", dep1), (f"{dep_dir}2", dep2)]:
if array.ndim > 1:
raise ValueError(f"{name!r} is not 1-dimensional")
kwargs["facecolor"] = self._get_patches_for_fill.get_next_color()

if where is None:
where = True
else:
where = np.asarray(where, dtype=bool)
if where.size != ind.size:
raise ValueError(f"where size ({where.size}) does not match "
f"{ind_dir} size ({ind.size})")
where = where & ~functools.reduce(
np.logical_or, map(np.ma.getmaskarray, [ind, dep1, dep2]))

ind, dep1, dep2 = np.broadcast_arrays(
np.atleast_1d(ind), dep1, dep2, subok=True)

polys = []
for idx0, idx1 in cbook.contiguous_regions(where):
indslice = ind[idx0:idx1]
dep1slice = dep1[idx0:idx1]
dep2slice = dep2[idx0:idx1]
if step is not None:
step_func = cbook.STEP_LOOKUP_MAP["steps-" + step]
indslice, dep1slice, dep2slice = \
step_func(indslice, dep1slice, dep2slice)

if not len(indslice):
continue
ind, dep1, dep2 = self._fill_between_process_units(
ind_dir, dep_dir, ind, dep1, dep2, **kwargs)

N = len(indslice)
pts = np.zeros((2 * N + 2, 2))

if interpolate:
def get_interp_point(idx):
im1 = max(idx - 1, 0)
ind_values = ind[im1:idx+1]
diff_values = dep1[im1:idx+1] - dep2[im1:idx+1]
dep1_values = dep1[im1:idx+1]

if len(diff_values) == 2:
if np.ma.is_masked(diff_values[1]):
return ind[im1], dep1[im1]
elif np.ma.is_masked(diff_values[0]):
return ind[idx], dep1[idx]

diff_order = diff_values.argsort()
diff_root_ind = np.interp(
0, diff_values[diff_order], ind_values[diff_order])
ind_order = ind_values.argsort()
diff_root_dep = np.interp(
diff_root_ind,
ind_values[ind_order], dep1_values[ind_order])
return diff_root_ind, diff_root_dep

start = get_interp_point(idx0)
end = get_interp_point(idx1)
else:
# Handle scalar dep2 (e.g. 0): the fill should go all
# the way down to 0 even if none of the dep1 sample points do.
start = indslice[0], dep2slice[0]
end = indslice[-1], dep2slice[-1]

pts[0] = start
pts[N + 1] = end

pts[1:N+1, 0] = indslice
pts[1:N+1, 1] = dep1slice
pts[N+2:, 0] = indslice[::-1]
pts[N+2:, 1] = dep2slice[::-1]

if ind_dir == "y":
pts = pts[:, ::-1]

polys.append(pts)

collection = mcoll.PolyCollection(polys, **kwargs)

# now update the datalim and autoscale
pts = np.vstack([np.hstack([ind[where, None], dep1[where, None]]),
np.hstack([ind[where, None], dep2[where, None]])])
if ind_dir == "y":
pts = pts[:, ::-1]

up_x = up_y = True
if "transform" in kwargs:
up_x, up_y = kwargs["transform"].contains_branch_seperately(self.transData)
self.update_datalim(pts, updatex=up_x, updatey=up_y)
collection = mcoll.FillBetweenPolyCollection(
ind_dir, ind, dep1, dep2,
where=where, interpolate=interpolate, step=step, **kwargs)

self.add_collection(collection, autolim=False)
self.add_collection(collection)
self._request_autoscale_view()
return collection

def _fill_between_process_units(self, ind_dir, dep_dir, ind, dep1, dep2, **kwargs):
"""Handle united data, such as dates."""
return map(np.ma.masked_invalid, self._process_unit_info(
[(ind_dir, ind), (dep_dir, dep1), (dep_dir, dep2)], kwargs))

def fill_between(self, x, y1, y2=0, where=None, interpolate=False,
step=None, **kwargs):
return self._fill_between_x_or_y(
Expand Down
5 changes: 3 additions & 2 deletions lib/matplotlib/axes/_axes.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ from matplotlib.artist import Artist
from matplotlib.backend_bases import RendererBase
from matplotlib.collections import (
Collection,
FillBetweenPolyCollection,
LineCollection,
PathCollection,
PolyCollection,
Expand Down Expand Up @@ -459,7 +460,7 @@ class Axes(_AxesBase):
*,
data=...,
**kwargs
) -> PolyCollection: ...
) -> FillBetweenPolyCollection: ...
def fill_betweenx(
self,
y: ArrayLike,
Expand All @@ -471,7 +472,7 @@ class Axes(_AxesBase):
*,
data=...,
**kwargs
) -> PolyCollection: ...
) -> FillBetweenPolyCollection: ...
def imshow(
self,
X: ArrayLike | PIL.Image.Image,
Expand Down
Loading
Loading