Skip to content

FIX: fix radial gridlines and outer spine collapsing on polar plots (#20388, #26972)#31701

Draft
SharadhNaidu wants to merge 2 commits into
matplotlib:mainfrom
SharadhNaidu:fix-20388-polar-arc-unwrap
Draft

FIX: fix radial gridlines and outer spine collapsing on polar plots (#20388, #26972)#31701
SharadhNaidu wants to merge 2 commits into
matplotlib:mainfrom
SharadhNaidu:fix-20388-polar-arc-unwrap

Conversation

@SharadhNaidu
Copy link
Copy Markdown

@SharadhNaidu SharadhNaidu commented May 18, 2026

This is a fix for two related polar plot bugs that have been hanging around for a while: #20388 (radial grid disappears with certain set_theta_direction(-1) + set_theta_zero_location combinations) and #26972 (outer spine outline disappears when set_theta_offset is used). They look like separate problems but they're caused by the same thing.

What's actually going on

Path.arc(theta1, theta2) unwraps the requested angular span into [0, 360]:

eta2 = theta2 - 360 * np.floor((theta2 - theta1) / 360)
if theta2 != theta1 and eta2 <= eta1:
    eta2 += 360

That's the right thing for most callers. The problem is that the polar code paths (PolarTransform.transform_path_non_affine and Spine._adjust_location for the polar spine) build up low/high from direction * limit + offset. When the resulting span is supposed to be exactly 360°, floating-point arithmetic often makes it 360 + tiny_epsilon instead. Path.arc then unwraps that to a near-zero span and the full circle quietly collapses into a near-empty arc - so the grid or spine just vanishes.

The chunking loop in PolarTransform.transform_path_non_affine (while td - last_td > 360: ...) was there to work around this, but the chunking threshold and Path.arc's unwrap threshold use slightly different floating-point comparisons. When the difference falls in that narrow gap, you end up with one Path.arc call that draws basically nothing.

anntzer did the bisect on #20388 and identified this exact mechanism. timhoffm replied on the same thread suggesting the fix:

Simplest solution seems to be introducing a flag Path.arc(..., unwrap_angles=True).

That's what this PR does.

Changes

  • lib/matplotlib/path.py - add an unwrap_angles=True keyword-only parameter to Path.arc. Default is True so existing callers see no behaviour change at all. When set to False, theta1/theta2 are used directly, which lets callers produce multi-turn arcs and avoid the floating-point edge case.
  • lib/matplotlib/projections/polar.py - the constant-radius arc branch now calls Path.arc(..., unwrap_angles=False) directly instead of using the chunking loop. I verified the output is byte-identical to the old code for every common case (full circles, partial arcs, reversed arcs, near-360 gridlines).
  • lib/matplotlib/spines.py - the polar outer spine now also passes unwrap_angles=False. This is the second place the bug manifests and is what makes [Bug]: set_theta_offset removes grid outline #26972 go away too.
  • Tests in test_path.py (covers the new parameter and the floating-point edge case) and test_polar.py (regression tests for both Radial grid missing in polar plots with ax.set_theta_direction(-1) and ax.set_theta_zero_location #20388 and [Bug]: set_theta_offset removes grid outline #26972, parametrised across multiple angles/offsets).
  • A next_whats_new entry for the new parameter.
  • path.pyi updated to match the new signature so stubtest stays happy.

A few things worth flagging

I went through the polar baseline images and none of the scenarios they cover happen to hit a buggy parameter combo, so this PR doesn't need any baseline regenerations. I checked polar_theta_position in particular since ('NW', 30, clockwise) looked suspicious - it's fine, spine spread is ~373 px (not collapsed).

I'm aware of #26991 which targets #26972 with a different approach (tolerance-based comparison inside Path.arc itself). That approach would also work but it changes behaviour for every Path.arc caller; the explicit-flag approach here keeps the default exactly the same and only opts in the two polar code paths. Happy to discuss if reviewers prefer the other direction.

Tests

I'd recommend running lib/matplotlib/tests/test_polar.py::test_polar_inverted_theta_outer_spine and test_polar_theta_offset_outer_spine to see the regression tests for both issues. The new test_arc_unwrap_angles in test_path.py covers the API addition directly.

Closes #20388
Closes #26972


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.

@SharadhNaidu SharadhNaidu force-pushed the fix-20388-polar-arc-unwrap branch 2 times, most recently from 9828bb6 to 6f8613d Compare May 18, 2026 12:27
Path.arc previously unwrapped the angular span to fit within 360
degrees, which silently collapsed near-but-not-quite-full-circle
spans caused by floating-point noise (delta = 360 + epsilon) into
near-empty arcs. This dropped the outer spine in several polar
configurations and skipped radial gridlines drawn via the chunking
loop in PolarTransform.transform_path_non_affine.

Add an unwrap_angles keyword-only parameter to Path.arc (default
True, preserving existing behaviour). PolarTransform's chunking
loop is preserved unchanged; only the final partial-chunk Path.arc
call uses unwrap_angles=False, which is exactly where the FP edge
case triggers. Spine._adjust_location's polar branch also passes
unwrap_angles=False so the outer spine is no longer collapsed when
set_theta_offset is set.

Closes matplotlib#20388
Closes matplotlib#26972
@SharadhNaidu SharadhNaidu force-pushed the fix-20388-polar-arc-unwrap branch from 6f8613d to 3e7188b Compare May 18, 2026 12:32
@SharadhNaidu
Copy link
Copy Markdown
Author

SharadhNaidu commented May 19, 2026

A couple of follow-up notes on this PR:

Why Path.wedge doesn't get unwrap_angles

Path.wedge and Path.arc look similar but have different semantics. wedge always closes back to the centre, so a request for a 360° wedge is the full pie — the angle unwrap step would only matter if you asked for a wedge wider than 360°, which doesn't have a well-defined geometric meaning (you'd be drawing the same filled disc twice). The polar bug fixed here is specifically about open arcs along radial paths and the spine outline, neither of which goes through wedge. Extending the parameter to wedge would add a configuration option with no real use case, so I've kept the change tight to where it's actually needed.

Adding the PR to draft , Open for help.

@SharadhNaidu SharadhNaidu changed the title Fix radial gridlines and outer spine collapsing on polar plots (#20388, #26972) FIX: fix radial gridlines and outer spine collapsing on polar plots (#20388, #26972) May 19, 2026
@SharadhNaidu SharadhNaidu marked this pull request as draft May 19, 2026 01:30
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

1 participant