11from __future__ import annotations
22
3+ from dataclasses import replace
34from typing import TYPE_CHECKING
45
56import numpy as np
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
1517class 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