1010if TYPE_CHECKING :
1111 import pandas as pd
1212 from matplotlib .axes import Axes
13+
1314 from plotnine .iapi import panel_view
1415 from plotnine .scales .scale import scale
1516
1617
1718class coord_radial (coord_polar ):
1819 """
19- Radial coordinate system.
20-
21- A modernised polar coordinate system that adds support for partial arcs,
22- inner radius (donut/gauge charts), configurable radial-axis placement, and
23- automatic rotation of the ``angle`` aesthetic to align with theta.
20+ Radial coordinate system
2421
25- Inherits from :class:`coord_polar`; all standard geoms work without
26- modification.
22+ `coord_radial` maps one position aesthetic to the angle and the other
23+ to the radius. Compared with ``coord_polar``, it adds support for
24+ partial arcs, inner radius holes, theta/radius limits, radial-axis
25+ placement, and rotation of the ``angle`` aesthetic.
2726
2827 Parameters
2928 ----------
@@ -39,7 +38,8 @@ class coord_radial(coord_polar):
3938 ``1`` = clockwise (default), ``-1`` = counter-clockwise.
4039 Only used when *end* is ``None``.
4140 expand :
42- Add a small buffer around the data on the radius axis. Default ``True``.
41+ Add a small buffer around the data on the radius axis.
42+ Default ``True``.
4343 inner_radius :
4444 Size of the inner hole as a fraction of the outer radius, in
4545 ``[0, 1)``. ``0`` (default) means no hole; ``0.3`` creates a 30 %
@@ -50,7 +50,11 @@ class coord_radial(coord_polar):
5050 * ``None`` (default) — let Matplotlib decide (usually outside).
5151 * ``True`` — force inside, aligned just past the *start* angle.
5252 * ``False`` — force outside (Matplotlib default).
53- * *float* — place at this theta angle in radians (clockwise from North).
53+ * *float* — place at this theta angle in radians (clockwise from
54+ North).
55+
56+ Unlike ggplot2's ``r.axis.inside``, a length-2 value for separate
57+ primary and secondary axis placement is not supported.
5458 rotate_angle :
5559 If ``True``, automatically add the local theta angle (in degrees) to
5660 the ``angle`` aesthetic so that text or other rotated marks align with
@@ -71,6 +75,52 @@ class coord_radial(coord_polar):
7175 theta_label_pad :
7276 Distance in points between the outer circle spine and the theta tick
7377 labels. Default ``8``. Only applied when theta labels are shown.
78+
79+ Notes
80+ -----
81+ The Python API uses snake_case names for arguments that are dotted in
82+ ggplot2: ``inner_radius``, ``r_axis_inside``, and ``rotate_angle``.
83+
84+ Unlike ggplot2, plotnine coordinate systems do not currently expose a
85+ ``clip`` argument. The ggplot2 ``reverse`` argument is not currently
86+ implemented.
87+
88+ Examples
89+ --------
90+ A donut chart is a stacked bar chart with an inner radius.
91+
92+ ```python
93+ import pandas as pd
94+ from plotnine import aes, coord_radial, geom_col, ggplot
95+
96+ df = pd.DataFrame({
97+ "x": [1, 1, 1],
98+ "y": [2, 3, 5],
99+ "group": ["a", "b", "c"],
100+ })
101+
102+ (
103+ ggplot(df, aes("x", "y", fill="group"))
104+ + geom_col()
105+ + coord_radial(theta="y", inner_radius=0.4)
106+ )
107+ ```
108+
109+ Partial arcs can be used for gauge-like displays.
110+
111+ ```python
112+ import numpy as np
113+ import pandas as pd
114+ from plotnine import aes, coord_radial, geom_point, ggplot
115+
116+ df = pd.DataFrame({"x": [1, 2, 3], "y": [2, 4, 3]})
117+
118+ ggplot(df, aes("x", "y")) + geom_point() + coord_radial(
119+ start=-0.4 * np.pi,
120+ end=0.4 * np.pi,
121+ inner_radius=0.3,
122+ )
123+ ```
74124 """
75125
76126 def __init__ (
@@ -111,7 +161,9 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view:
111161 from .coord_cartesian import coord_cartesian
112162
113163 # Capture data-space theta breaks before super() clears them.
114- pv_data = coord_cartesian (expand = False ).setup_panel_params (scale_x , scale_y )
164+ pv_data = coord_cartesian (expand = False ).setup_panel_params (
165+ scale_x , scale_y
166+ )
115167 if self .theta == "x" :
116168 theta_breaks = list (pv_data .x .breaks )
117169 theta_labels = list (pv_data .x .labels )
@@ -121,7 +173,8 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view:
121173
122174 pv = super ().setup_panel_params (scale_x , scale_y )
123175
124- # thetalim: zoom the theta data range — only this slice maps to the arc.
176+ # thetalim: zoom the theta data range — only this slice maps to the
177+ # arc.
125178 if self .thetalim is not None :
126179 self .params ["theta_range" ] = tuple (self .thetalim )
127180
@@ -155,7 +208,9 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view:
155208 # pac-man / coxcomb charts keep breaks=[] as set by super()).
156209 x_updates : dict = {}
157210 if theta_breaks and (arc_lo is not None or self .theta_labels ):
158- radian_pos = list (self ._to_radians (np .asarray (theta_breaks , dtype = float )))
211+ radian_pos = list (
212+ self ._to_radians (np .asarray (theta_breaks , dtype = float ))
213+ )
159214 if arc_lo is not None :
160215 keep = [arc_lo <= r <= arc_hi for r in radian_pos ]
161216 radian_pos = [r for r , k in zip (radian_pos , keep ) if k ]
@@ -181,7 +236,11 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view:
181236
182237 @property
183238 def _arc (self ) -> float :
184- """Total arc in radians (signed: positive when going clockwise for direction=1)."""
239+ """
240+ Total arc in radians.
241+
242+ A positive value represents clockwise movement when ``direction=1``.
243+ """
185244 if self .end is not None :
186245 return self .end - self .start
187246 return self .direction * 2.0 * np .pi
@@ -207,7 +266,11 @@ def transform(
207266 ) -> pd .DataFrame :
208267 data = super ().transform (data , panel_params , munch = munch )
209268 # After super().transform(), data["x"] is always theta in radians.
210- if self .rotate_angle and "angle" in data .columns and "x" in data .columns :
269+ if (
270+ self .rotate_angle
271+ and "angle" in data .columns
272+ and "x" in data .columns
273+ ):
211274 data = data .copy ()
212275 data ["angle" ] = data ["angle" ] + np .degrees (data ["x" ])
213276 return data
@@ -252,10 +315,14 @@ def draw(self, axs: list[Axes]) -> None:
252315 # Just inside the start angle keeps it out of the data.
253316 ax .set_rlabel_position (np .degrees (self .start ) + 10 )
254317 else :
255- ax .set_rlabel_position (np .degrees (float (self .r_axis_inside )))
318+ ax .set_rlabel_position (
319+ np .degrees (float (self .r_axis_inside ))
320+ )
256321
257322 def post_setup_ax (self , ax : Axes ) -> None :
258- """Apply theta label pad after facet has set tick positions and padding."""
323+ """
324+ Apply theta label pad after facet has set tick positions and padding.
325+ """
259326 if self .theta_labels or self .end is not None :
260327 ax .tick_params (axis = "x" , pad = self .theta_label_pad )
261328 # Allow geom_text labels to extend past the polar axes bounding box
0 commit comments