Skip to content

Added two new coordinate systems: coord_polar and coord_radial.#1059

Open
iangow wants to merge 17 commits intohas2k1:mainfrom
iangow:coord-polar
Open

Added two new coordinate systems: coord_polar and coord_radial.#1059
iangow wants to merge 17 commits intohas2k1:mainfrom
iangow:coord-polar

Conversation

@iangow
Copy link
Copy Markdown

@iangow iangow commented May 4, 2026

This picks up on a thread started with #10. I implemented both the superseded (in ggplot2) coord_polar() and the newer coord_radial(). Almost all arguments of the R equivalents have been implemented (I omitted clip because it is not supported in coord_cartesian() here either).

I created a small gallery of examples here, including examples from the ggplot2 documentation and some interesting plots that seem to provide some rationale for using these coordinate systems.

I made some documentation mirroring the style of other plotnine documentation (and the original from ggplot2).

I got a lot of help from Claude Code (and Codex when I hit limits on Claude) on this, but I was careful to nudge it use Matplotlib's native PolarAxes as much as possible to keep the implementation lean. Looking at the code, it seems pretty concise.

Let me know if there's anything you like me to do to refine this or explain things better.

iangow added 14 commits May 4, 2026 15:04
Implements polar coordinates by transforming x/y data to angle/radius
at the Cartesian level, so all standard geoms work without modification.
Adds a draw() hook to the coord base class for post-layer decorations;
coord_polar uses it to draw concentric-circle and radial-spoke grid lines.
Replace the manual Cartesian-projection approach with subplot_kw={"projection": "polar"},
so geom_bar naturally becomes pie/bullseye wedges via munching. transform() now outputs
(theta_rad, r) pairs; draw() configures zero-location, direction, and r limits. Guard
axis_line and axis_text_x theme elements against PolarAxes spine/tick-param differences.
- Override setup_panel_params to fix partial arcs (start/end): set x panel
  range to [arc_lo, arc_hi] so set_limits_breaks_and_labels does not
  overwrite set_thetalim with the default (0, 2π)
- Add thetalim and rlim parameters for data-space zoom on each axis,
  matching ggplot2's coord_radial() interface; filter r-axis breaks to
  within rlim to prevent PolarAxes autoscale expansion
- Restore theta axis tick labels on the outer edge for partial-arc plots
  by converting data-space breaks to radian positions; suppressed for
  full-circle charts (pac-man, coxcomb) to preserve existing behaviour
… labels

Partial-arc plots already show theta tick labels on the outer edge.
Full-circle charts (pac-man, coxcomb) suppress them by default.
theta_labels=True opts a full-circle plot into the same behaviour,
passing scale breaks through to Matplotlib's PolarAxes which places
and rotates them outside the circle automatically.
Without padding, theta labels sit right on the outer boundary.
8 points of pad applies whenever theta labels are shown — both
for full-circle plots (theta_labels=True) and partial arcs.
Replaces the hard-coded pad=8 with a user-facing theta_label_pad
parameter (default 8) so callers can tune the gap between the outer
circle spine and theta tick labels without post-processing the figure.
facet.set_limits_breaks_and_labels() called ax.tick_params(axis='x', pad=pad_x)
after coord.draw(), silently overwriting any custom theta_label_pad set by
coord_radial. Add a post_setup_ax() hook to the coord base class, called by
set_limits_breaks_and_labels after the margin pad, so coord_radial can apply
theta_label_pad at the correct point in the rendering pipeline.
ax.set_xticks() with negative radian values silently extends xlim below 0,
converting a full circle into a partial arc.  When start is chosen so that
the first few months map to negative radians (e.g. start = -π/2), the theta
labels passed to set_xticks triggered this matplotlib behaviour.

Normalise all break positions into [0, 2π] for full-circle plots before they
are stored in panel_params.x.breaks so that set_xticks never receives a
negative value.  Partial-arc plots are unaffected (their breaks are always
within [arc_lo, arc_hi] which is already in [0, 2π]).
coord_polar hardcoded x limits to [0, 2π], but _to_radians maps data
to [start, start+2π]. With start=3π/2 the bars from ~Oct–Mar land at
theta > 2π and get clipped by the xlim. Fix by setting limits to
[start, start+2π] so the data range always falls within the visible
window. Remove the now-redundant break-wrapping mod-2π in coord_radial
since breaks in [start, start+2π] are naturally within the new limits.
For PolarAxes the outer circle is ax.spines['polar'], which is separate
from the rectangular panel_border patch used for Cartesian axes.
When panel_border is blank, explicitly hide the polar spine so the
theme element works consistently for polar and Cartesian plots.
post_setup_ax iterates ax.texts after the facet creates them and disables
clipping, allowing spoke labels placed just beyond the outermost bar tip
to render past the axes bounding box.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 5, 2026

Codecov Report

❌ Patch coverage is 94.60784% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.00%. Comparing base (7d85258) to head (9764016).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
plotnine/coords/coord_radial.py 93.75% 0 Missing and 6 partials ⚠️
plotnine/themes/themeable.py 55.55% 2 Missing and 2 partials ⚠️
plotnine/coords/coord_polar.py 98.85% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1059      +/-   ##
==========================================
+ Coverage   86.87%   87.00%   +0.12%     
==========================================
  Files         203      205       +2     
  Lines       13757    13959     +202     
  Branches     1688     1725      +37     
==========================================
+ Hits        11952    12145     +193     
- Misses       1256     1257       +1     
- Partials      549      557       +8     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@has2k1
Copy link
Copy Markdown
Owner

has2k1 commented May 5, 2026

I got a lot of help from Claude Code (and Codex when I hit limits on Claude) on this, but I was careful to nudge it use Matplotlib's native PolarAxes as much as possible to keep the implementation lean. Looking at the code, it seems pretty concise.

Using AI is fine as is any other tool, but for common development work I would rather leave out the attribution in the commit message. Mainly because we (the humans) are still responsible for reviewing, understanding and maintaining the code. The commit history should reflect intent, decisions and context.

We can include tools in the history when their output is deterministic, or when there is purpose in signalling some level of detachment from the result.

@iangow
Copy link
Copy Markdown
Author

iangow commented May 5, 2026

I got a lot of help from Claude Code (and Codex when I hit limits on Claude) on this, but I was careful to nudge it use Matplotlib's native PolarAxes as much as possible to keep the implementation lean. Looking at the code, it seems pretty concise.

Using AI is fine as is any other tool, but for common development work I would rather leave out the attribution in the commit message. Mainly because we (the humans) are still responsible for reviewing, understanding and maintaining the code. The commit history should reflect intent, decisions and context.

We can include tools in the history when their output is deterministic, or when there is purpose in signalling some level of detachment from the result.

Sure. Would you like me to edit and resubmit?

@iangow
Copy link
Copy Markdown
Author

iangow commented May 5, 2026

@has2k1 I added some tests and redid the commit messages to remove attribution to AI. Let me know if you want additional tests (e.g., output image comparisons).

@has2k1
Copy link
Copy Markdown
Owner

has2k1 commented May 5, 2026

@has2k1 I added some tests and redid the commit messages to remove attribution to AI. Let me know if you want additional tests (e.g., output image comparisons).

I will have a better idea when I start reviewing it, hopefully next week.

@iangow
Copy link
Copy Markdown
Author

iangow commented May 5, 2026

I realised that one other related thing from ggplot2 that I did not implement is guide_axis_theta() (one thing this does is rotation of axis labels). It seems this wouldn't be too difficult, but I figure this can wait for a review of what is there now.

In the meantime, I may just work on cleaning up the gallery of examples, as these may assist you [@has2k1] in your review.

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

Successfully merging this pull request may close these issues.

2 participants