Skip to content

FIX: snap near-integer arc windings to a full circle on polar plots (#20388, #26972)#31844

Merged
timhoffm merged 4 commits into
matplotlib:mainfrom
SharadhNaidu:fix-arc-winding-snap
Jun 9, 2026
Merged

FIX: snap near-integer arc windings to a full circle on polar plots (#20388, #26972)#31844
timhoffm merged 4 commits into
matplotlib:mainfrom
SharadhNaidu:fix-arc-winding-snap

Conversation

@SharadhNaidu

@SharadhNaidu SharadhNaidu commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

PR summary

On polar plots, radial gridlines (#20388) and the outer spine (#26972) could collapse to a near-empty arc for certain set_theta_zero_location / set_theta_offset / set_theta_direction combinations.

The root cause is in Path.arc's angle-unwrap step. A span of 360*n plus a tiny floating-point rounding error was reduced modulo 360 to a near-zero arc instead of a full circle:

eta2 = theta2 - 360 * np.floor((theta2 - theta1) / 360)   # 360 + 1e-9 -> 1e-9

Fix

Detect spans that are within floating-point tolerance of a whole number of turns and draw a complete circle; otherwise unwrap theta2 to the shortest arc within 360 degrees exactly as before:

n_turns = (theta2 - theta1) / 360
if (theta2 != theta1 and round(n_turns) >= 1
        and abs(n_turns - round(n_turns)) < 1e-9):
    eta2 = theta1 + 360            # complete circle
else:
    eta2 = theta2 - 360 * np.floor(n_turns)   # shortest arc

The change is byte-identical for all non-degenerate inputs (e.g. 90°, 359.99°, 360°, 410°→50°, 540°→180°, 720°→full circle) and only affects the 360*n + ε collapse cases, so no call sites need to change.

Closes #20388
Closes #26972

Tests

  • test_arc_full_circle_snap / test_arc_unwrap_partial_turn in test_path.py cover the snap-vs-unwrap behaviour at the unit level.
  • test_path.py and end-to-end polar gridline / outer-spine regression tests in test_polar.py.

PR checklist

  • Code is PEP8 compliant.
  • New and changed code is tested.
  • Plot-related changes verified in CI (no image baselines change; behaviour is byte-identical outside the bug case).

AI Disclosure

AI was used to help proofread the grammar of this PR description and to understand some parts of the existing codebase during development. The code changes and design decisions were developed independently.

@rcomer rcomer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @SharadhNaidu, this fix makes sense to me and I have even learned something as I did not know about np.nextafter!

I am wondering if we actually need the polar-specific tests. Obviously for the purpose of this work is it necessary to verify that the reported polar cases are fixed. However, the fix is entirely within path.py, and the underlying problem is now covered by the computationally cheaper tests in test_path.py.

Comment thread lib/matplotlib/path.py Outdated
Comment thread lib/matplotlib/tests/test_polar.py Outdated
Comment thread lib/matplotlib/tests/test_polar.py Outdated
Comment thread lib/matplotlib/tests/test_path.py Outdated
Comment thread lib/matplotlib/tests/test_path.py Outdated
Comment thread lib/matplotlib/tests/test_path.py Outdated
Comment thread lib/matplotlib/tests/test_path.py Outdated
@rcomer

rcomer commented Jun 6, 2026

Copy link
Copy Markdown
Member

I suggest to also rebase you branch against main, as it is a long way behind.

@SharadhNaidu

Copy link
Copy Markdown
Contributor Author

I suggest to also rebase you branch against main, as it is a long way behind.

done , rebased and working on the current main

matplotlib#26972)

On polar plots, radial gridlines and the outer spine could collapse to a
near-empty arc. The root cause is Path.arc's angle-unwrap step: a span of
360*n plus a tiny floating-point rounding error was reduced modulo 360 to
a near-zero arc instead of a full circle.

Detect spans that are within floating-point tolerance of a whole number
of turns and draw a complete circle; otherwise unwrap theta2 to the
shortest arc within 360 degrees as before. The change is byte-identical
for all non-degenerate inputs, so no call sites need to change.

Adds unit coverage for the snap/unwrap behaviour and end-to-end polar
gridline and spine regression tests.
- Replace the 1e-9 magic tolerance with 1e-12 turns, justified by the
  observed error from the polar transform pipeline (machine-epsilon level,
  under 1e-15 turns), via np.rint(n_turns).
- Snap on any non-zero whole number of turns (abs winding), restoring the
  legacy full-circle behaviour for exact negative multiples such as
  Path.arc(0, -360); the previous rework collapsed those to an empty arc.
- Add regression tests for negative full circles and a tolerance-tightness
  guard (a span just past a full turn must not snap).
- Drop the contrast and GitHub issue references from the Path.arc
  docstring; just state that a complete circle is drawn.
- Remove issue references and reword the test_path.py comments for a
  reader unfamiliar with this PR's history.
- Make test_arc_full_circle_snap compare all vertices against the
  reference circle, consistent with (and slightly stricter than) the
  neighbouring test.
- Remove test_polar_transform_constant_r_arc (it passes against main and
  no longer guards the fix) and merge the two outer-spine tests into a
  single test_polar_outer_spine_not_collapsed using the spine bbox.
@SharadhNaidu SharadhNaidu force-pushed the fix-arc-winding-snap branch from 560f59f to bd946aa Compare June 6, 2026 14:25
@SharadhNaidu

Copy link
Copy Markdown
Contributor Author

@rcomer , I am sorry if i am violating the rules of directly tagging you but , is there any changes that i have to make ?

@rcomer rcomer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @SharadhNaidu

I am still not sure about

I am wondering if we actually need the polar-specific tests. Obviously for the purpose of this work is it necessary to verify that the reported polar cases are fixed. However, the fix is entirely within path.py, and the underlying problem is now covered by the computationally cheaper tests in test_path.py.

But I will leave it for the second reviewer or other developer to weigh in on that. I note there are now fewer of these tests than when I wrote the comment.

For code changes, we require approvals from two core developers, so this will have to wait a bit longer before merge.

I am glad that your experience in #31674 has not put you off!

@SharadhNaidu

SharadhNaidu commented Jun 8, 2026

Copy link
Copy Markdown
Contributor Author

For code changes, we require approvals from two core developers, so this will have to wait a bit longer before merge.

no worries , I will be waiting for their review and make the recommended changes , if any.

I am glad that your experience in #31844 31674 has not put you off!

Yup , learnt a lot in that process.

Comment thread lib/matplotlib/path.py Outdated
@SharadhNaidu

Copy link
Copy Markdown
Contributor Author

applied the recommended changes , is_full_circle reads much cleaner.

will improve interms of writing comments.

@SharadhNaidu

Copy link
Copy Markdown
Contributor Author

@timhoffm I am sorry if i am violating the rules of directly tagging you but , is there any changes that i have to make ?

@timhoffm timhoffm modified the milestones: v3.12.0, v3.11.0 Jun 9, 2026
@timhoffm timhoffm merged commit 319174c into matplotlib:main Jun 9, 2026
37 of 40 checks passed
@timhoffm

timhoffm commented Jun 9, 2026

Copy link
Copy Markdown
Member

Thanks @SharadhNaidu, and congatulations on your first PR to Matplotlib! 🎉 We hope to see you back.

@SharadhNaidu

Copy link
Copy Markdown
Contributor Author

@timhoffm thanks and looking to contribute as much as i can.

timhoffm added a commit that referenced this pull request Jun 9, 2026
timhoffm added a commit that referenced this pull request Jun 9, 2026
…844-on-v3.11.x

Backport PR #31844 on branch v3.11.x (FIX: snap near-integer arc windings to a full circle on polar plots (#20388, #26972))
@rcomer rcomer mentioned this pull request Jun 9, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: set_theta_offset removes grid outline Radial grid missing in polar plots with ax.set_theta_direction(-1) and ax.set_theta_zero_location

3 participants