Skip to content

Commit 954dbbc

Browse files
iangowclaude
andcommitted
Extend coord_radial: partial-arc fixes, thetalim/rlim, theta axis labels
- 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 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d51d211 commit 954dbbc

1 file changed

Lines changed: 84 additions & 0 deletions

File tree

plotnine/coords/coord_radial.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from dataclasses import replace
34
from typing import TYPE_CHECKING
45

56
import numpy as np
@@ -10,6 +11,7 @@
1011
import pandas as pd
1112
from matplotlib.axes import Axes
1213
from plotnine.iapi import panel_view
14+
from plotnine.scales.scale import scale
1315

1416

1517
class coord_radial(coord_polar):
@@ -53,6 +55,14 @@ class coord_radial(coord_polar):
5355
If ``True``, automatically add the local theta angle (in degrees) to
5456
the ``angle`` aesthetic so that text or other rotated marks align with
5557
the spoke direction. Default ``False``.
58+
thetalim :
59+
Data-space limits for the theta axis as ``(lo, hi)``. Only data
60+
within this range is mapped to the arc; equivalent to zooming on the
61+
angular axis. ``None`` (default) uses the full data range.
62+
rlim :
63+
Data-space limits for the r axis as ``(lo, hi)``. Only data within
64+
this range is shown; equivalent to zooming on the radial axis.
65+
``None`` (default) uses the full data range.
5666
"""
5767

5868
def __init__(
@@ -65,6 +75,8 @@ def __init__(
6575
inner_radius: float = 0,
6676
r_axis_inside: bool | float | None = None,
6777
rotate_angle: bool = False,
78+
thetalim: tuple[float, float] | None = None,
79+
rlim: tuple[float, float] | None = None,
6880
) -> None:
6981
super().__init__(
7082
theta=theta,
@@ -76,6 +88,78 @@ def __init__(
7688
self.inner_radius = inner_radius
7789
self.r_axis_inside = r_axis_inside
7890
self.rotate_angle = rotate_angle
91+
self.thetalim = thetalim
92+
self.rlim = rlim
93+
94+
# ------------------------------------------------------------------
95+
# Panel params
96+
# ------------------------------------------------------------------
97+
98+
def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view:
99+
from .coord_cartesian import coord_cartesian
100+
101+
# Capture data-space theta breaks before super() clears them.
102+
pv_data = coord_cartesian(expand=False).setup_panel_params(scale_x, scale_y)
103+
if self.theta == "x":
104+
theta_breaks = list(pv_data.x.breaks)
105+
theta_labels = list(pv_data.x.labels)
106+
else:
107+
theta_breaks = list(pv_data.y.breaks)
108+
theta_labels = list(pv_data.y.labels)
109+
110+
pv = super().setup_panel_params(scale_x, scale_y)
111+
112+
# thetalim: zoom the theta data range — only this slice maps to the arc.
113+
if self.thetalim is not None:
114+
self.params["theta_range"] = tuple(self.thetalim)
115+
116+
# rlim: zoom the r data range — update params, panel view y axis, and
117+
# filter breaks/labels to within rlim so set_yticks doesn't force the
118+
# PolarAxes r-axis to expand beyond the requested limits.
119+
if self.rlim is not None:
120+
self.params["r_range"] = tuple(self.rlim)
121+
rlo, rhi = self.rlim
122+
breaks, labels = pv.y.breaks, pv.y.labels
123+
mask = [rlo <= b <= rhi for b in breaks]
124+
new_y = replace(
125+
pv.y,
126+
limits=tuple(self.rlim),
127+
range=tuple(self.rlim),
128+
breaks=[b for b, m in zip(breaks, mask) if m],
129+
labels=[l for l, m in zip(labels, mask) if m],
130+
)
131+
pv = replace(pv, y=new_y)
132+
133+
# Compute arc bounds for partial-arc plots (None means full circle).
134+
arc_lo = arc_hi = None
135+
if self.end is not None:
136+
arc = self._arc
137+
arc_lo = min(self.start, self.start + arc)
138+
arc_hi = max(self.start, self.start + arc)
139+
140+
# For partial arcs only: convert data-space theta breaks to radian
141+
# positions and restore them as theta axis tick labels on the outer edge.
142+
# Full-circle charts (pac-man, coxcomb) keep breaks=[] as set by super().
143+
x_updates: dict = {}
144+
if theta_breaks and arc_lo is not None:
145+
radian_pos = list(self._to_radians(np.asarray(theta_breaks, dtype=float)))
146+
keep = [arc_lo <= r <= arc_hi for r in radian_pos]
147+
radian_pos = [r for r, k in zip(radian_pos, keep) if k]
148+
theta_labels = [l for l, k in zip(theta_labels, keep) if k]
149+
x_updates["breaks"] = radian_pos
150+
x_updates["labels"] = theta_labels
151+
152+
# Partial arc: x panel range must match [arc_lo, arc_hi] so that
153+
# set_limits_breaks_and_labels calls ax.set_xlim(arc_lo, arc_hi) rather
154+
# than ax.set_xlim(0, 2π), which would override set_thetalim.
155+
if arc_lo is not None:
156+
x_updates["limits"] = (arc_lo, arc_hi)
157+
x_updates["range"] = (arc_lo, arc_hi)
158+
159+
if x_updates:
160+
pv = replace(pv, x=replace(pv.x, **x_updates))
161+
162+
return pv
79163

80164
# ------------------------------------------------------------------
81165
# Helpers

0 commit comments

Comments
 (0)