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

Skip to content

Path.arc is broken for clockwise arcs #13717

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

Open
HPLegion opened this issue Mar 19, 2019 · 6 comments
Open

Path.arc is broken for clockwise arcs #13717

HPLegion opened this issue Mar 19, 2019 · 6 comments
Labels
Documentation keep Items to be ignored by the “Stale” Github Action New feature topic: path handling

Comments

@HPLegion
Copy link

Bug report

Bug summary

When calling Path.arc in a clockwise fashion (theta1 > theta2) , the angle wrapping results in drawing the arc over the opposing angle in a counterclockwise direction.

Code for reproduction

from matplotlib.path import Path
import matplotlib as mpl
import numpy as np

### Version
print("MPL Version", mpl.__version__)

### Demo the problem
theta1 = -1
theta2 = 1
path = Path.arc(theta1, theta2)
vrt = path.vertices
print(f"Counterclockwise: {len(vrt)} steps.")
theta1 = 1
theta2 = -1
path = Path.arc(theta1, theta2)
vrt = path.vertices
print(f"Clockwise: {len(vrt)} steps.")

### The following code demonstrates what is happening inside Path.arc
theta1 = -1.0
theta2 = 1.0
## Snippet from matplotlib.path.Path.arc
eta1 = theta1
eta2 = theta2 - 360 * np.floor((theta2 - theta1) / 360)
# Ensure 2pi range is not flattened to 0 due to floating-point errors,
# but don't try to expand existing 0 range.
if theta2 != theta1 and eta2 <= eta1:
    eta2 += 360

print(f"Counterclockwise: Input({theta1}, {theta2}), Output({eta1}, {eta2})")

theta1 = 1.0
theta2 = -1.0

eta1 = theta1
eta2 = theta2 - 360 * np.floor((theta2 - theta1) / 360)
# Ensure 2pi range is not flattened to 0 due to floating-point errors,
# but don't try to expand existing 0 range.
if theta2 != theta1 and eta2 <= eta1:
    eta2 += 360

print(f"Clockwise: Input({theta1}, {theta2}), Output({eta1}, {eta2})")

Actual outcome

MPL Version 3.0.3
Counterclockwise: 7 steps.
Clockwise: 49 steps.
Counterclockwise: Input(-1.0, 1.0), Output(-1.0, 1.0)
Clockwise: Input(1.0, -1.0), Output(1.0, 359.0)

Expected outcome

The direction of the arc should not matter, either direction should draw an arc that covers 2 degrees, and the vertices generated by the function should go in the direction chosen by the user.

Proposed fix

For my purposes I fixed the problem by adding another condition into the angle wrapping logic.

    eta1 = theta1
    eta2 = theta2 - 360 * np.floor((theta2 - theta1) / 360)
    # Ensure 2pi range is not flattened to 0 due to floating-point errors,
    # but don't try to expand existing 0 range.
    if theta2 != theta1 and eta2 <= eta1:
        eta2 += 360

   # This is the new logic
    if theta1 > theta2:
        eta1 += 360

    eta1, eta2 = np.deg2rad([eta1, eta2])

This assures that directionality is kept and the correct angle is covered.

Matplotlib version

Probably irrelevant but:

  • Operating system: Windows
  • Matplotlib version: 3.0.3 (conda)
  • Python version: 3.7.0 (conda)
@ImportanceOfBeingErnest
Copy link
Member

It is by pure convention that angles are usually measured counter-clockwise, but there are some good reasons for that; https://en.wikipedia.org/wiki/Right-hand_rule#Coordinates, https://en.wikipedia.org/wiki/Orientation_(vector_space), https://en.wikipedia.org/wiki/Complex_number#Exponentiation Of course this is again based on the convention of the x axis being the horizontal axis and denoting ordinates and in the end one could of course define everything just the other way around. But I suppose it makes sense for matplotlib to follow those conventions and hence define the direction of the arc in counter-clockwise direction.

I do agree that this is not very obvious from the documentation. While matplotlib.patches.Arc at least tells that its angle is "anti-clockwise" such that theta1 and theta2 need to follow this implicitely, matplotlib.path.Path.arc does not mention orientation at all.

I will hence label this with "documentation". Though, if you think that this issue is not solved by clearly stating in the docs that arcs are defined counter-clockwise, please feel free to clarify.

@HPLegion
Copy link
Author

HPLegion commented Mar 20, 2019

Hej,

first of all I want to point out my gratitude for the entire matplotlib-team's valuable and good work, that slipped my attention in the initial posting :)

Now concerning the issue:

I am aware of the convention, but from the documentation it wasn't clear that it would be enforced instead of sticking to the input logic, so I think adjusting the documentation would be very helpful. Further, if one does not think with a certain direction in mind, I think that the expression "produce the shortest arc within 360 degrees" could actually suggest that the arc is never longer than 180 degrees (Yay to the pain that is angle wrapping).

Now as for the functionality:

The following is very subjective and quite possibly influenced by my very limited knowledge of the path module and bezier curves.
I understand the arc method as a convenience function to produce arc segments without wrestling with Beziercurves manually. I have used it to produce arc segments that are intended to be parts of a longer closed path. In such a path I have no control over the sense of rotation that an arc has to follow, therefore having the possibility to create clockwise arcs would definitely be useful.
I do, however, understand that changing the behaviour of the arc method could have dire consequences for any derived code, so it would probably have to come with a default flag as an input parameter.

If you prefer to leave the functionality as it currently is, maybe the following snippet could be added to documentation, I have finally figured out how to traverse the arc in the direction I want without changing the code of path.arc itself.

if t0 <= t1:
    arc = Path.arc(t0, t1)
if t0 > t1:
    arc = Path.arc(t1, t0)
    codes = list(arc.codes[:])
    verts = list(arc.vertices[:])
    verts = verts[::-1]
    codes = codes[::-1]
    codes[0], codes[-1] = codes[-1], codes[0]
    arc = Path(verts, codes)

EDIT: Improved snippet readability

@ImportanceOfBeingErnest
Copy link
Member

Thanks for the detailing this; to summarize, there are two action items here:

  • Document the orientation of Arcs.
  • Add an option to create clockwise arc paths.

@ImportanceOfBeingErnest ImportanceOfBeingErnest added this to the v3.2.0 milestone Mar 20, 2019
@HPLegion
Copy link
Author

Nice :) If I find some time in the upcoming week I will try to test how bulletproof the current and modified angle wrapping logic are and potentially make a pull request :)

@anntzer
Copy link
Contributor

anntzer commented Mar 21, 2019

I wonder if it would make more sense to have a generic Path().reverse() method, rather than having to add this functionality to each path generator separately (in fact, it's basically already there (privately) as Sankey._revert (modulo what to do with the first point, and likely mishandling of CLOSEPOLY)).

@github-actions
Copy link

github-actions bot commented Jun 9, 2023

This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!

@github-actions github-actions bot added the status: inactive Marked by the “Stale” Github Action label Jun 9, 2023
@anntzer anntzer added the keep Items to be ignored by the “Stale” Github Action label Jun 9, 2023
@rcomer rcomer removed the status: inactive Marked by the “Stale” Github Action label Jun 9, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Documentation keep Items to be ignored by the “Stale” Github Action New feature topic: path handling
Projects
None yet
Development

No branches or pull requests

6 participants