Skip to content
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

[Bug]: Path.cleaned() always returns a closed path #28176

Open
mfreeborn opened this issue May 6, 2024 · 8 comments
Open

[Bug]: Path.cleaned() always returns a closed path #28176

mfreeborn opened this issue May 6, 2024 · 8 comments

Comments

@mfreeborn
Copy link

Bug summary

I have a path - a simple line - which is created from a large number of points. I call the cleaned(simplify=True) method on it and it returns a closed path

Code for reproduction

path = Path([(0, 0.5), (1, 0.5), (2, 0.5), (3, 0.5), (4, 0.5)])
cleaned_path = path.cleaned(simplify=True)
# cleaned path coords now [(0, 0.5), (4, 0.5), (4, 0.5), (0, 0.5)]
# expected [(0, 0.5), (4, 0.5)]

Actual outcome

The path is simplified as I hoped, but it is now closed.

Expected outcome

For the path not to be closed.

Additional information

No response

Operating system

Ubuntu

Matplotlib Version

3.8.4

Matplotlib Backend

No response

Python version

3.12

Jupyter version

No response

Installation

pip

@AnsonTran
Copy link
Contributor

I don't think this creates a closed path. Looking at the path codes, it's [MOVETO, LINETO, LINETO, STOP], which ignores the last vertex.

Path(array([[0. , 0.5],
       [4. , 0.5],
       [4. , 0.5],
       [0. , 0. ]]), array([1, 2, 2, 0], dtype=uint8))

@mfreeborn
Copy link
Author

Ah, I do see what you mean regarding the STOP code. I've got a full example here:

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.path import Path

fig, axes = plt.subplots()
x = np.arange(0, 11, 1)
y = np.array([0, 1, 2, 3, 4, 5, 4, 3, 2, 1, 0])

path = Path(np.column_stack((x, y)))
print(f"{path.vertices=}")
axes.plot(
    path.vertices[:, 0],
    path.vertices[:, 1],
    marker="o",
    markersize=10,
    label="Original",
)

path = path.cleaned(simplify=True)
print(f"{path.vertices=}")

axes.plot(
    path.vertices[:, 0] + 15,
    path.vertices[:, 1],
    marker="o",
    markersize=10,
    label="Cleaned",
)

axes.legend()

plt.show()

And the output is:

Figure_1

I am trying to explicitly plot a simplified Line2D by using the Path class directly.

@AnsonTran
Copy link
Contributor

Ohhh I see, I'm not sure if it's recommended to plot Path objects this way, but removing the vertex corresponding to the stop code would fix it.

@mfreeborn
Copy link
Author

Just reading a bit more about the Path methods, and looking through the source code, Path.iter_segments() seems to be the proper way to get the actual points of the line i.e. it stops yielding vertices when it sees the STOP code.

So, to get the simplified vertices, one can write:

vertices = np.array([p for p, _ in path.iter_segments(simplify=True)])
print(vertices)

Output:

[[ 0.  0.]
 [ 5.  5.]
 [10.  0.]
 [10.  0.]]

It's not quite as elegant as a single attribute access i.e. Path.vertices, but anyway...

I'm still not sure why the simplified path duplicates the last coordinate though. Is that a bug? The actual implementation of the simplification algorithm is a bit dense for me at the moment.

@AnsonTran
Copy link
Contributor

I'm inclined to say no, since it doesn't impact the behaviour of path drawing for Artists, and the vertices aren't meant to be used as data points. However, others might have a different opinion than mine?

@ksunden
Copy link
Member

ksunden commented May 7, 2024

I do believe @AnsonTran is correct on all points. The relationship between Path.codes and Path.vertices is very tightly coupled, and based in how low level tools (mostly AGG, but to some extent SVG/PDF/etc, they are mostly pretty similar) use Paths, and essentially every entry needs both a code value and a vertex value (i.e. the two arrays are the same length, which helps organize them/build complex things out of them), but some code values ignore the contents of the vertex (so there is a spacer value in there, that I think we usually put the starting vertex of the polygon, which for closed paths essentially allows things like you are doing here, where you don't actually ignore the value, even though the "correct" thing is to ignore it).

#28179 was opened to discuss whether STOP in particular is needed, which would potentially resolve this particular use case, though iter_segments is still likely the better/higher level method to use.

@tacaswell
Copy link
Member

I think dropping STOP would elide this particular issue, but in general Paths can be more complicated than just a sequence of [MOVETO, LINETO, ...] . For example:

In [27]: Path(np.column_stack((np.hstack([x, x+15]), np.hstack([y, y]))), [Path.MOVETO] + [Path.LINETO]*10 + [Path.MOVETO] + [Path.LINETO]*10).cleaned(simplify
    ...: =True)
Out[27]:
Path(array([[ 0.,  0.],
       [ 5.,  5.],
       [10.,  0.],
       [15.,  0.],
       [20.,  5.],
       [25.,  0.],
       [25.,  0.],
       [ 0.,  0.]]), array([1, 2, 2, 1, 2, 2, 2, 0], dtype=uint8))

or more dramatically https://matplotlib.org/stable/users/explain/artists/paths.html#compound-paths and the bezier examples.

@timhoffm
Copy link
Member

timhoffm commented May 8, 2024

For sure, plotting vertices is not a correct visualization of the path in general. OTOH, I see the argument that cleaned() should remove unnecessary STOP nodes at the end.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants