Skip to content

Update find_nearest_contour and revert contour deprecations #27088

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
merged 2 commits into from
Oct 26, 2023
Merged
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
5 changes: 5 additions & 0 deletions doc/api/next_api_changes/deprecations/27088-JK.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Deprecations removed in ``contour``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``contour.allsegs``, ``contour.allkinds``, and ``contour.find_nearest_contour`` are no
longer marked for deprecation.
79 changes: 27 additions & 52 deletions lib/matplotlib/contour.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Classes to support contour plotting and labelling for the Axes class.
"""

from contextlib import ExitStack
import functools
import math
from numbers import Integral
Expand Down Expand Up @@ -929,12 +930,12 @@ def __init__(self, ax, *args,
", ".join(map(repr, kwargs))
)

allsegs = _api.deprecated("3.8", pending=True)(property(lambda self: [
allsegs = property(lambda self: [
[subp.vertices for subp in p._iter_connected_components()]
for p in self.get_paths()]))
allkinds = _api.deprecated("3.8", pending=True)(property(lambda self: [
for p in self.get_paths()])
allkinds = property(lambda self: [
[subp.codes for subp in p._iter_connected_components()]
for p in self.get_paths()]))
for p in self.get_paths()])
tcolors = _api.deprecated("3.8")(property(lambda self: [
(tuple(rgba),) for rgba in self.to_rgba(self.cvalues, self.alpha)]))
tlinewidths = _api.deprecated("3.8")(property(lambda self: [
Expand Down Expand Up @@ -1388,7 +1389,6 @@ def _find_nearest_contour(self, xy, indices=None):

return idx_level_min, idx_vtx_min, proj_min

@_api.deprecated("3.8")
def find_nearest_contour(self, x, y, indices=None, pixel=True):
"""
Find the point in the contour plot that is closest to ``(x, y)``.
Expand All @@ -1409,64 +1409,39 @@ def find_nearest_contour(self, x, y, indices=None, pixel=True):

Returns
-------
contour : `.Collection`
The contour that is closest to ``(x, y)``.
segment : int
The index of the `.Path` in *contour* that is closest to
``(x, y)``.
path : int
The index of the path that is closest to ``(x, y)``. Each path corresponds
to one contour level.
subpath : int
The index within that closest path of the subpath that is closest to
``(x, y)``. Each subpath corresponds to one unbroken contour line.
index : int
The index of the path segment in *segment* that is closest to
The index of the vertices within that subpath that are closest to
``(x, y)``.
xmin, ymin : float
The point in the contour plot that is closest to ``(x, y)``.
d2 : float
The squared distance from ``(xmin, ymin)`` to ``(x, y)``.
"""
segment = index = d2 = None

# This function uses a method that is probably quite
# inefficient based on converting each contour segment to
# pixel coordinates and then comparing the given point to
# those coordinates for each contour. This will probably be
# quite slow for complex contours, but for normal use it works
# sufficiently well that the time is not noticeable.
# Nonetheless, improvements could probably be made.
with ExitStack() as stack:
if not pixel:
# _find_nearest_contour works in pixel space. We want axes space, so
# effectively disable the transformation here by setting to identity.
stack.enter_context(self._cm_set(
transform=mtransforms.IdentityTransform()))

if self.filled:
raise ValueError("Method does not support filled contours.")
i_level, i_vtx, (xmin, ymin) = self._find_nearest_contour((x, y), indices)

if indices is None:
indices = range(len(self.collections))
if i_level is not None:
cc_cumlens = np.cumsum(
[*map(len, self._paths[i_level]._iter_connected_components())])
segment = cc_cumlens.searchsorted(i_vtx, "right")
index = i_vtx if segment == 0 else i_vtx - cc_cumlens[segment - 1]
d2 = (xmin-x)**2 + (ymin-y)**2

d2min = np.inf
conmin = None
segmin = None
imin = None
xmin = None
ymin = None

point = np.array([x, y])

for icon in indices:
con = self.collections[icon]
trans = con.get_transform()
paths = con.get_paths()

for segNum, linepath in enumerate(paths):
lc = linepath.vertices
# transfer all data points to screen coordinates if desired
if pixel:
lc = trans.transform(lc)

d2, xc, leg = _find_closest_point_on_path(lc, point)
if d2 < d2min:
d2min = d2
conmin = icon
segmin = segNum
imin = leg[1]
xmin = xc[0]
ymin = xc[1]

return (conmin, segmin, imin, xmin, ymin, d2min)
return (i_level, segment, index, xmin, ymin, d2)

def draw(self, renderer):
paths = self._paths
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/contour.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,6 @@ class ContourSet(ContourLabeler, Collection):
) -> tuple[list[Artist], list[str]]: ...
def find_nearest_contour(
self, x: float, y: float, indices: Iterable[int] | None = ..., pixel: bool = ...
) -> tuple[Collection, int, int, float, float, float]: ...
) -> tuple[int, int, int, float, float, float]: ...

class QuadContourSet(ContourSet): ...
32 changes: 12 additions & 20 deletions lib/matplotlib/tests/test_contour.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,23 +530,19 @@ def test_find_nearest_contour():
img = np.exp(-np.pi * (np.sum((xy - 5)**2, 0)/5.**2))
cs = plt.contour(img, 10)

with pytest.warns(mpl._api.MatplotlibDeprecationWarning):
nearest_contour = cs.find_nearest_contour(1, 1, pixel=False)
nearest_contour = cs.find_nearest_contour(1, 1, pixel=False)
expected_nearest = (1, 0, 33, 1.965966, 1.965966, 1.866183)
assert_array_almost_equal(nearest_contour, expected_nearest)

with pytest.warns(mpl._api.MatplotlibDeprecationWarning):
nearest_contour = cs.find_nearest_contour(8, 1, pixel=False)
nearest_contour = cs.find_nearest_contour(8, 1, pixel=False)
expected_nearest = (1, 0, 5, 7.550173, 1.587542, 0.547550)
assert_array_almost_equal(nearest_contour, expected_nearest)

with pytest.warns(mpl._api.MatplotlibDeprecationWarning):
nearest_contour = cs.find_nearest_contour(2, 5, pixel=False)
nearest_contour = cs.find_nearest_contour(2, 5, pixel=False)
expected_nearest = (3, 0, 21, 1.884384, 5.023335, 0.013911)
assert_array_almost_equal(nearest_contour, expected_nearest)

with pytest.warns(mpl._api.MatplotlibDeprecationWarning):
nearest_contour = cs.find_nearest_contour(2, 5, indices=(5, 7), pixel=False)
nearest_contour = cs.find_nearest_contour(2, 5, indices=(5, 7), pixel=False)
expected_nearest = (5, 0, 16, 2.628202, 5.0, 0.394638)
assert_array_almost_equal(nearest_contour, expected_nearest)

Expand All @@ -556,16 +552,13 @@ def test_find_nearest_contour_no_filled():
img = np.exp(-np.pi * (np.sum((xy - 5)**2, 0)/5.**2))
cs = plt.contourf(img, 10)

with pytest.warns(mpl._api.MatplotlibDeprecationWarning), \
pytest.raises(ValueError, match="Method does not support filled contours."):
with pytest.raises(ValueError, match="Method does not support filled contours"):
cs.find_nearest_contour(1, 1, pixel=False)

with pytest.warns(mpl._api.MatplotlibDeprecationWarning), \
pytest.raises(ValueError, match="Method does not support filled contours."):
with pytest.raises(ValueError, match="Method does not support filled contours"):
cs.find_nearest_contour(1, 10, indices=(5, 7), pixel=False)

with pytest.warns(mpl._api.MatplotlibDeprecationWarning), \
pytest.raises(ValueError, match="Method does not support filled contours."):
with pytest.raises(ValueError, match="Method does not support filled contours"):
cs.find_nearest_contour(2, 5, indices=(2, 7), pixel=True)


Expand Down Expand Up @@ -825,12 +818,11 @@ def test_allsegs_allkinds():

cs = plt.contour(x, y, z, levels=[0, 0.5])

# Expect two levels, first with 5 segments and the second with 4.
with pytest.warns(PendingDeprecationWarning, match="all"):
for result in [cs.allsegs, cs.allkinds]:
assert len(result) == 2
assert len(result[0]) == 5
assert len(result[1]) == 4
# Expect two levels, the first with 5 segments and the second with 4.
for result in [cs.allsegs, cs.allkinds]:
assert len(result) == 2
assert len(result[0]) == 5
assert len(result[1]) == 4


def test_deprecated_apis():
Expand Down