Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Add Axes.fill_disjoint to make invert fill_between easier #24742

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

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
198 changes: 194 additions & 4 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5219,9 +5219,8 @@
self._request_autoscale_view()
return patches

def _fill_between_x_or_y(
self, ind_dir, ind, dep1, dep2=0, *,
where=None, interpolate=False, step=None, **kwargs):
def _fill_between_x_or_y(self, ind_dir, ind, dep1, dep2=0, *, where=None,
interpolate=False, step=None, **kwargs):
# Common implementation between fill_between (*ind_dir*="x") and
# fill_betweenx (*ind_dir*="y"). *ind* is the independent variable,
# *dep* the dependent variable. The docstring below is interpolated
Expand Down Expand Up @@ -5307,7 +5306,6 @@
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]

if not mpl.rcParams["_internal.classic_mode"]:
Expand Down Expand Up @@ -5440,6 +5438,198 @@

#### plotting z(x, y): imshow, pcolor and relatives, contour

def _fill_above_or_below(self, ind, dep1, dep2=0, *, where=None,
interpolate=False, step=None, **kwargs):
ind_dir = "y"
dep_dir = "x"

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()

ind, dep1, dep2 = map(
ma.masked_invalid, self._process_unit_info(
[(ind_dir, ind), (dep_dir, dep1), (dep_dir, dep2)], kwargs))

Check warning on line 5455 in lib/matplotlib/axes/_axes.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axes/_axes.py#L5455

Added line #L5455 was not covered by tests
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")

if where is None:
where = True
else:
where = np.asarray(where, dtype=bool)
if where.size != ind.size:

Check warning on line 5465 in lib/matplotlib/axes/_axes.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axes/_axes.py#L5465

Added line #L5465 was not covered by tests
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)

Check warning on line 5472 in lib/matplotlib/axes/_axes.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axes/_axes.py#L5472

Added line #L5472 was not covered by tests

if interpolate:
N = len(ind)
xslices = np.stack([ind, dep1, dep2], axis=-1)
sorted_idx = np.argsort(ind, kind="stable")
xslices = xslices[sorted_idx]
ind_interps = []
dep_interps = []
where_interps = []
for i in range(N-1):
# sliding window
# [[dep1[i], dep2[i+1]],
# [dep2[i], dep2[i+1]],
# [ind[i], ind[i+1]]]
cur_slice = xslices[i]
next_slice = xslices[i+1]
d1 = cur_slice[1] - cur_slice[2]
d2 = next_slice[1] - next_slice[2]
dx = next_slice[0] - cur_slice[0]
if dx == 0:
# multiple ind with same value
pass
if (d1 > 0) == (d2 > 0):
continue
if (d1 == 0) or (d2 == 0):
continue
# calculate intersect pt
r = -d2/d1
ind_interps.append((next_slice[0] + r*cur_slice[0])/(1+r))
dep_interps.append((next_slice[1] + r*cur_slice[1])/(1+r))
where_interps.append(where[i] or where[i+1])

Check warning on line 5504 in lib/matplotlib/axes/_axes.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axes/_axes.py#L5504

Added line #L5504 was not covered by tests
ind = np.concatenate([ind, ind_interps])
dep1 = np.concatenate([dep1, dep_interps])
dep2 = np.concatenate([dep2, dep_interps])
where = np.concatenate([where, where_interps])

sorted_idx = np.argsort(ind, kind="stable")
ind = ind[sorted_idx]
dep1 = dep1[sorted_idx]
dep2 = dep2[sorted_idx]
where = where[sorted_idx]

ret = []
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

N = len(indslice)
temp = np.stack([dep1slice, dep2slice])

vertices_above = np.zeros((N, 2))
vertices_below = np.zeros((N, 2))

Check warning on line 5534 in lib/matplotlib/axes/_axes.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/axes/_axes.py#L5534

Added line #L5534 was not covered by tests

vertices_above[0:N, 0] = indslice
vertices_below[0:N, 0] = indslice

vertices_above[0:N, 1] = np.amax(temp, axis=0)
vertices_below[0:N, 1] = np.amin(temp, axis=0)

plane_above = mpatches.BoundedSemiplane(vertices_above, 'bottom',
**kwargs)
plane_below = mpatches.BoundedSemiplane(vertices_below, 'top',
**kwargs)

self.add_artist(plane_above)
self.add_artist(plane_below)

ret.append(plane_above)
ret.append(plane_below)

# now update the datalim and autoscale
pts = np.row_stack([np.column_stack([ind[where], dep1[where]]),
np.column_stack([ind[where], dep2[where]])])
self.update_datalim(pts, updatex=True, updatey=True)
self._request_autoscale_view()

return ret

def fill_disjoint(self, x, y1, y2, where=None, interpolate=False,
step=None, **kwargs):
"""
Fill everything not in between the lines *y1* and *y2*

Parameters
----------
x : array (length N)
The x coordinates of the nodes defining the curves.

y1 : array (length N) or scalar
The y coordinates of the nodes defining the first curve.

y2 : array (length N) or scalar: default 0
The y coordinates of the nodes defining the second curve.

where : array of bool (length N), optional
Define *where* to exclude some regions from being filled.
The filled regions are defined by the coordinates ``x[where]``.
More precisely, fill between ``x[i]`` and ``x[i+1]`` if
``where[i] and where[i+1]``. Note that this definition implies
that an isolated *True* value between two *False* values in *where*
will not result in filling. Both sides of the *True* position
remain unfilled due to the adjacent *False* values.

interpolate : bool, default: False
This option is only relevant if *where* is used and the two curves
are crossing each other.

Semantically, *where* is often used for *y1* > *y2* or
similar. By default, the nodes of the polygon defining the in
between region will only be placed at the positions in the *x*
array. Such a polygon cannot describe the above semantics close to
the intersection. The x-sections containing the intersection are
simply clipped.

Setting *interpolate* to *True* will calculate the actual
intersection point and extend the filled region up to this point.

step : {{'pre', 'post', 'mid'}}, optional
Define *step* if the filling should be a step function,
i.e. constant in between *x*. 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]``.
- '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.

Other Parameters
----------------
**kwargs
All other keyword arguments are passed on to `.BoundedSemiplane`.
%(BoundedSemiplane:kwdoc)s

Returns
-------
list of `.BoundedSemiplane`
A list `.BoundedSemiplane` containing the plotted Lines.

See Also
--------
fill_between : Fill between two sets of y-values.
fill_betweenx : Fill between two sets of x-values.
"""
return self._fill_above_or_below(x, y1, y2, where=where,
interpolate=interpolate, step=step,
**kwargs)

@_preprocess_data()
@_docstring.interpd
def imshow(self, X, cmap=None, norm=None, *, aspect=None,
Expand Down
120 changes: 120 additions & 0 deletions lib/matplotlib/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,126 @@
self.stale = True


class BoundedSemiplane(Patch):
"""A semiplane bounded by a polyline on one side."""
def __str__(self):
if len(self._path.vertices):
s = "Line%d((%g, %g) ...)"
return s % (len(self._path.vertices), *self._path.vertices[0])
else:
return "Line0()"

@_docstring.dedent_interpd
def __init__(self, xy, direction, **kwargs):
"""
*xy* is a numpy array with shape Nx2.

*direction* determines bounding direction of the semiplane
"""
super().__init__(**kwargs)
self.set_direction(direction)
self.set_polyline(xy)
self._update_bounded_path()

def set_direction(self, direction):
"""
Set the direction of the patch's bound.

Parameters
----------
direction : {{'top', 'bottom'}}
- 'top': Semiplane is upper bounded by the specified polyline
- 'bottom': Semiplane is lower bounded by the specified polyline
"""
if direction not in ['top', 'bottom']:
raise ValueError(f"{direction!r} is not 'top' or 'bottom'")
self._direction = direction

Check warning on line 1102 in lib/matplotlib/patches.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/patches.py#L1102

Added line #L1102 was not covered by tests
self.stale = True

def get_direction(self):
"""
Get the direction of the patch's bound.

Returns
-------
{{'top', 'bottom'}}
"""
return self._direction

def set_polyline(self, xy):
"""
Set the vertices of the polyline.

Parameters
----------
xy : (N, 2) array-like
The vertices of polyline.

Notes
-----
*xy* is stably sorted by *x* first before setting the polyline. This is
to ensure the fill does not cover any vertices.
"""
xy = np.asarray(xy)
sorted_idxs = np.argsort(xy[:, 0], kind='stable')
xy = xy[sorted_idxs]
self._polyline = xy
self.stale = True

def get_polyline(self):
"""
Get the bounding polyline.

Returns
-------
(N, 2) numpy array
The vertices of polyline.
"""
return self._polyline

def get_path(self):
"""
Get the `.Path` of the patch.

Returns
-------
`.Path`

Notes
-----
Returns the `.Path` defined by the bounding polyline if there is no
containing `.Axes`. Otherwise, the path is defined by the polyline, and
two extra endpoints from projecting the polyline's endpoints to the
bound of the axes container.
"""
self._update_bounded_path()
return self._path

def _update_bounded_path(self):
"""
Updates the bounded path according to the container limits
"""
container = self.axes
if container is None:
self._path = Path(self._polyline)
return

bottom, top = container.get_ylim()
bound_dict = {'top': bottom, 'bottom': top}
bound = bound_dict[self._direction]

N, _ = self._polyline.shape
left_x = self._polyline[0, 0]
right_x = self._polyline[N-1, 0]
verts = np.empty((N+2, 2))

verts[1:N+1] = self._polyline
verts[0] = [left_x, bound]
verts[N+1] = [right_x, bound]

self._path = Path(verts)


class Polygon(Patch):
"""A general polygon patch."""

Expand Down
10 changes: 10 additions & 0 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -2565,6 +2565,16 @@
**({"data": data} if data is not None else {}), **kwargs)


# Autogenerated by boilerplate.py. Do not edit as changes will be lost.

Check warning on line 2568 in lib/matplotlib/pyplot.py

View check run for this annotation

Codecov / codecov/patch

lib/matplotlib/pyplot.py#L2568

Added line #L2568 was not covered by tests
@_copy_docstring_and_deprecators(Axes.fill_disjoint)
def fill_disjoint(
x, y1, y2, where=None, interpolate=False, step=None,
**kwargs):
return gca().fill_disjoint(
x, y1, y2, where=where, interpolate=interpolate, step=step,
**kwargs)


# Autogenerated by boilerplate.py. Do not edit as changes will be lost.
@_copy_docstring_and_deprecators(Axes.grid)
def grid(visible=None, which='major', axis='both', **kwargs):
Expand Down
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading