-
-
Notifications
You must be signed in to change notification settings - Fork 7.8k
Handle MOVETO's, CLOSEPOLY's and empty paths in Path.interpolated #29919
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
Conversation
lib/matplotlib/path.py
Outdated
@@ -667,13 +667,27 @@ def intersects_bbox(self, bbox, filled=True): | |||
|
|||
def interpolated(self, steps): | |||
""" | |||
Return a new path resampled to length N x *steps*. | |||
Return a new path with each `LINETO` resampled to *steps* `LINETO`'s. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Return a new path with each `LINETO` resampled to *steps* `LINETO`'s. | |
Return a new path with each segment is divided into *steps* parts. |
Note that "segment" is the term for one "step" (c.f. https://matplotlib.org/devdocs/api/path_api.html#matplotlib.path.Path.iter_segments)
Edit: I've just read one maybe "each LINETO step ..."
lib/matplotlib/path.py
Outdated
|
||
Codes other than `LINETO` are not handled correctly. | ||
Codes other than `LINETO` and `MOVETO` are not handled correctly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not checked, but what about CLOSEPOLY? That should also be sub sampled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can go wrong. Adapting the example from https://matplotlib.org/stable/gallery/shapes_and_collections/compound_path.html
import matplotlib.pyplot as plt
from matplotlib.patches import PathPatch
from matplotlib.path import Path
codes = [Path.MOVETO] + [Path.LINETO]*2 + [Path.CLOSEPOLY]
vertices = [(4, 4), (5, 5), (5, 4), (0, 0)]
path = Path(vertices, codes)
pathpatch = PathPatch(path, facecolor='none', edgecolor='blue', linewidth=3,
label='original')
pathpatch2 = PathPatch(path.interpolated(2), facecolor='none', edgecolor='red',
linewidth=3, linestyle='dashed', label='interpolated')
fig, ax = plt.subplots()
ax.add_patch(pathpatch)
ax.add_patch(pathpatch2)
ax.autoscale_view()
ax.legend()
plt.show()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The vertex corresponding to CLOSEPOLY
is not correctly ignored here; it should be interpolated from the previous point to the first point in the path (i.e., the most recent MOVETO
or the first point in the whole thing.)
lib/matplotlib/path.py
Outdated
if self.codes[-1] == self.CLOSEPOLY and not np.all(self.vertices[-1] == | ||
self.vertices[0]): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am assuming that after splitting by MOVETO
, any CLOSEPOLY
will be at the end of the path. Is that correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like _iter_connected_components
only splits on MOVETO
, so it wouldn't notice CLOSEPOLY
. I'm not sure what happens to a Path
with CLOSEPOLY
stuck in the middle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A CLOSEPOLY the middle can only happen if it was not followed by a MOVETO in the original path. But that means you closed a shape, and continue to draw from that position. People would likely just use LINETOs in such a case, But AFAIK there's nothing in the rules prohibiting LINETO, CLOSEPOLY, LINETO
, and there could be rare cases where it makes sense/could happen.
That said, I haven't checked what we actually do here and whether that pattern renders reasonably.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmmm
import matplotlib.pyplot as plt
from matplotlib.patches import PathPatch
from matplotlib.path import Path
codes = [Path.MOVETO] + ([Path.LINETO]*2 + [Path.CLOSEPOLY]) * 2
vertices = [(4, 4), (5, 5), (5, 4), (0, 0), (3, 3), (4, 3), (0, 0)]
path = Path(vertices, codes)
pathpatch = PathPatch(path, facecolor='none', edgecolor='blue', linewidth=3,
label='original')
pathpatch2 = PathPatch(path.interpolated(2), facecolor='none', edgecolor='red',
linewidth=3, linestyle='dashed', label='interpolated')
fig, ax = plt.subplots()
ax.add_patch(pathpatch)
ax.add_patch(pathpatch2)
ax.autoscale_view()
ax.legend()
plt.show()
It seems that the rendering at least handles internal CLOSEPOLY
's. So probably we should handle them here too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Edit: given the axes limits here go down beyond (0,0), I guess that whatever tells
autoscale
the extent of the path also does not know to ignoreCLOSEPOLY
.
I'd say this is a separate topic. Without checking: I suspect that autoscale just takes all the points, no matter what the associated code is (likely also the control points for curve).
At least for CLOSEPOLY
people can work around this by using existing coordinates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say this is a separate topic.
Sure, that was very much an aside - not proposing to investigate it here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like it could be considered a bug-fix and backported?
|
||
|
||
def test_interpolated_closepoly(): | ||
codes = [Path.MOVETO] + [Path.LINETO]*2 + [Path.CLOSEPOLY] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test a compound path with multiple closepolys by doubling this again similar to your above test to make sure the implementation handles multiple MOVETOs and CLOSEPOLYS together?
I could go either way. I figured it's a New Feature since we are making a public method do something it didn't before (and the docstring stated it didn't). OTOH this clearly fixes a bug for the geo projections, and I'll be astonished if anyone is relying on these codes not being supported. |
I just realized that this does not in fact close #29917. I had not checked unfilled contours, but they are even more broken than filled contours. |
OK the problem with the unfilled contours is just that it has an empty path, so ended up at
This is an easy fix, though I am slightly worried that different input data could turn up different bugs. |
if self.codes is not None and self.CLOSEPOLY in self.codes and not np.all( | ||
self.vertices[self.codes == self.CLOSEPOLY] == self.vertices[0]): | ||
vertices = self.vertices.copy() | ||
vertices[self.codes == self.CLOSEPOLY] = vertices[0] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this patch is not sufficient if you have a path with mixtures of CLOSEPOLY AND MOVETO. The MOVETO creates a new separate separate component with a starting point that is generally different from vertices[0]
.
Maybe a variation of _iter_connected_components
is the way to go?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But if we got to here, we have no internal MOVETO
's because we already split on them and called the function again on the subpath. I think this case is covered by the new test_interpolated_moveto_closepoly
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you are right.
I don't have a strong opinion on 3.10.2 vs 3.11. Either is ok, and their releases will likely not be far apart. |
OK, I think there are two votes for v3.10.2 and none against, so I removed the whatsnew. |
…paths in Path.interpolated
…919-on-v3.10.x Backport PR #29919 on branch v3.10.x (Handle MOVETO's, CLOSEPOLY's and empty paths in Path.interpolated)
PR summary
Fixes #29917. The geo projection's path transform relies on
Path.interpolated
matplotlib/lib/matplotlib/projections/geo.py
Lines 249 to 252 in b7e7663
At v3.8
ContourSet
changed from having one path per contour to one path per level, and so its paths can now contain internalMOVETO
's. We can make theinterpolated
method handle those by just splitting the path up, applying itself to each sub-path, and re-joining the result. The example code provided in #29917 now givesFor
contour
the given example also had one or more empty paths which caused an exception in the interpolation. We can just pass those straight out again.I welcome better wording for the docstring, though I do not think the original was correct since a unclosed path with N vertices would have become a path with
(N-1) * steps + 1
vertices.PR checklist