Added two new coordinate systems: coord_polar and coord_radial.#1059
Added two new coordinate systems: coord_polar and coord_radial.#1059iangow wants to merge 17 commits intohas2k1:mainfrom
coord_polar and coord_radial.#1059Conversation
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 Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
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? |
|
@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. |
|
I realised that one other related thing from In the meantime, I may just work on cleaning up the gallery of examples, as these may assist you [@has2k1] in your review. |
This picks up on a thread started with #10. I implemented both the superseded (in
ggplot2)coord_polar()and the newercoord_radial(). Almost all arguments of the R equivalents have been implemented (I omittedclipbecause it is not supported incoord_cartesian()here either).I created a small gallery of examples here, including examples from the
ggplot2documentation 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
PolarAxesas 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.