From 8cd543d7c0b2bb8cb80115c22d895a8a90e7f2af Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Fri, 1 May 2026 08:25:56 -0400 Subject: [PATCH 001/126] Add coord_polar coordinate system 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. --- notebooks/coord-polar.qmd | 89 +++++++++++++++ plotnine/__init__.py | 2 + plotnine/coords/__init__.py | 2 + plotnine/coords/coord.py | 8 ++ plotnine/coords/coord_polar.py | 202 +++++++++++++++++++++++++++++++++ plotnine/ggplot.py | 1 + 6 files changed, 304 insertions(+) create mode 100644 notebooks/coord-polar.qmd create mode 100644 plotnine/coords/coord_polar.py diff --git a/notebooks/coord-polar.qmd b/notebooks/coord-polar.qmd new file mode 100644 index 0000000000..6b999bbb19 --- /dev/null +++ b/notebooks/coord-polar.qmd @@ -0,0 +1,89 @@ +--- +title: "Polar Coordinates with `coord_polar()`" +execute: + warning: false +--- + +```{python} +from datetime import date + +import polars as pl +import plotnine_polars as p9 +import socviz_pl as sv +from plotnine_polars import aes +``` + +## FARS pedestrian data + +The `farsinvolved` dataset records daily counts of child pedestrians (aged 0–17) +involved in fatal motor vehicle crashes in the United States from 2009 to 2023. +We aggregate by calendar month and day, averaging across years. +Year 2000 is used as a placeholder date because 2000 was a leap year. + +```{python} +farsinvolved = sv.load_data("farsinvolved") + +month_order = [ + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December", +] +month_num = {m: i + 1 for i, m in enumerate(month_order)} + +fars_agg = ( + farsinvolved + .with_columns(pl.col("day").cast(pl.Int32)) + .group_by("month", "day") + .agg(n=pl.col("n").mean()) + .with_columns( + month_num=pl.col("month").replace(month_num).cast(pl.Int32), + ) + .with_columns( + fake_yr=pl.date(2000, pl.col("month_num"), pl.col("day")), + flag=(pl.col("month") == "October") & (pl.col("day") == 31), + ) + .sort("fake_yr") +) +``` + +Halloween (October 31) stands out as the single most dangerous day for child pedestrians. + +## Polar chart + +`coord_polar()` maps x to angle and y to radius. +Each circle below is one calendar day; the lone orange circle is Halloween. + +```{python} +#| label: fig-coord-polar-fars +#| fig-cap: "Pedestrians aged 0–17 in Fatal Motor Vehicle Crashes. Daily average, 2009–2023." +plot_data = fars_agg.with_columns(doy=pl.col("fake_yr").dt.ordinal_day()) +halloween = plot_data.filter(pl.col("flag")) + +n_label = plot_data["n"].max() + 0.5 +month_starts = pl.DataFrame({ + "doy": [date(2000, m, 15).timetuple().tm_yday for m in range(1, 13)], + "label": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + "n": [n_label] * 12, +}) + +( + plot_data + .ggplot(aes(x="doy", y="n")) + .geom_point(shape="o", size=1.5, color="#222222", fill="white", stroke=0.4) + .geom_point(data=halloween, mapping=aes(x="doy", y="n"), + size=4, color="#E06000", inherit_aes=False) + .geom_text(data=month_starts, mapping=aes(x="doy", y="n", label="label"), + size=7, color="#444444", inherit_aes=False) + .coord_polar() + .labs( + title="Pedestrians aged 0–17 in Fatal Motor Vehicle Crashes", + subtitle="Daily average, 2009–2023", + ) + .add_theme( + axis_title=p9.element_blank(), + axis_text=p9.element_blank(), + axis_ticks=p9.element_blank(), + figure_size=(6, 6), + ) +) +``` diff --git a/plotnine/__init__.py b/plotnine/__init__.py index 5b7a79c493..3da5453b56 100644 --- a/plotnine/__init__.py +++ b/plotnine/__init__.py @@ -20,6 +20,7 @@ coord_equal, coord_fixed, coord_flip, + coord_polar, coord_trans, ) from .facets import ( @@ -291,6 +292,7 @@ "coord_equal", "coord_fixed", "coord_flip", + "coord_polar", "coord_trans", "dup_axis", "element_blank", diff --git a/plotnine/coords/__init__.py b/plotnine/coords/__init__.py index d49f9f8ff2..8601bb1c1a 100644 --- a/plotnine/coords/__init__.py +++ b/plotnine/coords/__init__.py @@ -5,6 +5,7 @@ from .coord_cartesian import coord_cartesian from .coord_fixed import coord_equal, coord_fixed from .coord_flip import coord_flip +from .coord_polar import coord_polar from .coord_trans import coord_trans __all__ = ( @@ -12,5 +13,6 @@ "coord_fixed", "coord_equal", "coord_flip", + "coord_polar", "coord_trans", ) diff --git a/plotnine/coords/coord.py b/plotnine/coords/coord.py index 91b1ed3ddf..02b7bb23c9 100644 --- a/plotnine/coords/coord.py +++ b/plotnine/coords/coord.py @@ -299,6 +299,14 @@ def _setup_secondary_axis( p9ax.sides_with_an_axis.add(sec.position) ax.spines[sec.position].set_visible(True) + def draw(self, axs: list) -> None: + """ + Draw coordinate-system decorations onto each panel axes. + + Called after all layers are drawn. Subclasses override this to + add elements such as polar grid lines. + """ + def labels(self, cur_labels: labels_view) -> labels_view: """ Modify labels diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py new file mode 100644 index 0000000000..20a86882ec --- /dev/null +++ b/plotnine/coords/coord_polar.py @@ -0,0 +1,202 @@ +from __future__ import annotations + +from dataclasses import replace +from typing import TYPE_CHECKING + +import numpy as np + +from .coord import coord, dist_euclidean + +if TYPE_CHECKING: + import pandas as pd + from matplotlib.axes import Axes + from plotnine.iapi import panel_view + from plotnine.scales.scale import scale + + +class coord_polar(coord): + """ + Polar coordinate system. + + Maps one aesthetic to angle (theta) and the other to radius. + Data is transformed at the Cartesian level so every standard geom + works without modification. Concentric circle and radial-spoke + grid lines are drawn automatically from the scale breaks. + + Parameters + ---------- + theta : + Which variable maps to the angle axis, ``"x"`` (default) or ``"y"``. + start : + Starting angle in radians, measured clockwise from 12 o'clock + (i.e. from the positive-y axis). Default 0. + direction : + ``1`` = clockwise (default), ``-1`` = counter-clockwise. + expand : + Add a small buffer around the unit circle. Default ``True``. + """ + + is_linear = False + + def __init__( + self, + theta: str = "x", + start: float = 0, + direction: int = 1, + expand: bool = True, + ) -> None: + self.theta = theta + self.start = start + self.direction = direction + self.expand = expand + self.params: dict = {} + + def setup_panel_params( + self, scale_x: scale, scale_y: scale + ) -> panel_view: + from .coord_cartesian import coord_cartesian + + # Theta should always fill exactly one full revolution — no expansion. + # R uses the caller-controlled expand flag (for padding around data). + pv_theta = coord_cartesian(expand=False).setup_panel_params(scale_x, scale_y) + pv_r = coord_cartesian(expand=self.expand).setup_panel_params(scale_x, scale_y) + + # Store original ranges and breaks so transform() and + # draw() can use them. + if self.theta == "x": + self.params["theta_range"] = pv_theta.x.range + self.params["r_range"] = pv_r.y.range + self.params["r_breaks"] = list(pv_r.y.breaks) + self.params["theta_breaks"] = list(pv_theta.x.breaks) + else: + self.params["theta_range"] = pv_theta.y.range + self.params["r_range"] = pv_r.x.range + self.params["r_breaks"] = list(pv_r.x.breaks) + self.params["theta_breaks"] = list(pv_theta.y.breaks) + + pv = pv_r # use r-expanded view as the base for output ranges + + # Return a fixed unit-square output range; drop all tick marks so + # the Cartesian theming draws nothing — the polar grid is drawn + # explicitly in draw(). + pad = 0.1 if self.expand else 0.0 + out_range = (-1.0 - pad, 1.0 + pad) + empty = np.array([], dtype=float) + new_x = replace( + pv.x, limits=out_range, range=out_range, breaks=[], minor_breaks=empty, labels=[] + ) + new_y = replace( + pv.y, limits=out_range, range=out_range, breaks=[], minor_breaks=empty, labels=[] + ) + return replace(pv, x=new_x, y=new_y) + + def transform( + self, + data: pd.DataFrame, + panel_params: panel_view, + munch: bool = False, + ) -> pd.DataFrame: + if self.theta == "x": + theta_col, r_col = "x", "y" + else: + theta_col, r_col = "y", "x" + + if theta_col not in data.columns or r_col not in data.columns: + return data + + theta_vals = data[theta_col].to_numpy(dtype=float) + r_vals = data[r_col].to_numpy(dtype=float) + + t_min, t_max = ( + float(self.params["theta_range"][0]), + float(self.params["theta_range"][1]), + ) + r_min, r_max = ( + float(self.params["r_range"][0]), + float(self.params["r_range"][1]), + ) + + # Normalise theta → [0, 1] → radians. + denom_t = t_max - t_min + theta_norm = (theta_vals - t_min) / denom_t if denom_t else np.zeros_like(theta_vals) + angle = self.start + self.direction * theta_norm * 2.0 * np.pi + + # Normalise r → [0, 1]. Clamp below zero only; values slightly above + # 1.0 land between the unit circle and the panel edge (which extends + # to 1 + pad ≈ 1.1) and are used for axis-label annotations. + denom_r = r_max - r_min + r_norm = (r_vals - r_min) / denom_r if denom_r else np.zeros_like(r_vals) + r_norm = np.clip(r_norm, 0.0, None) + + # Project to Cartesian. Angle is measured from the positive-y axis + # (12 o'clock) so x = r·sin(θ), y = r·cos(θ). + data = data.copy() + data[theta_col] = r_norm * np.sin(angle) + data[r_col] = r_norm * np.cos(angle) + return data + + def _r_norm(self, r_val: float) -> float: + r_min, r_max = self.params["r_range"] + denom = float(r_max) - float(r_min) + return (float(r_val) - float(r_min)) / denom if denom else 0.0 + + def _theta_angle(self, t_val: float) -> float: + t_min, t_max = self.params["theta_range"] + denom = float(t_max) - float(t_min) + t_norm = (float(t_val) - float(t_min)) / denom if denom else 0.0 + return self.start + self.direction * t_norm * 2.0 * np.pi + + def draw(self, axs: list[Axes]) -> None: + """Draw concentric circles and radial spokes onto each panel axes.""" + import matplotlib.patches as mpatches + import matplotlib.lines as mlines + + r_breaks = [v for v in self.params.get("r_breaks", []) if v is not None] + t_breaks = [v for v in self.params.get("theta_breaks", []) if v is not None] + + if not r_breaks and not t_breaks: + return + + # Outermost circle radius (used as spoke length). + valid_r = [self._r_norm(v) for v in r_breaks if np.isfinite(v)] + r_outer = max(valid_r) if valid_r else 1.0 + + grid_kw = dict(color="gray", alpha=0.3, linewidth=0.5, zorder=0.5) + + for ax in axs: + # Concentric circles at each r break. + for r_val in r_breaks: + if not np.isfinite(r_val): + continue + r = self._r_norm(r_val) + circle = mpatches.Circle( + (0, 0), r, + fill=False, transform=ax.transData, + **grid_kw, + ) + ax.add_patch(circle) + + # Radial spokes at each theta break. + for t_val in t_breaks: + if not np.isfinite(t_val): + continue + angle = self._theta_angle(t_val) + line = mlines.Line2D( + [0, r_outer * np.sin(angle)], + [0, r_outer * np.cos(angle)], + transform=ax.transData, + **grid_kw, + ) + ax.add_line(line) + + def aspect(self, panel_params: panel_view) -> float: + return 1.0 + + def distance( + self, + x: pd.Series, + y: pd.Series, + panel_params: panel_view, + ) -> np.ndarray: + # Output space is [-1, 1]² so max diagonal ≈ 2√2. + return dist_euclidean(x, y) / (2.0 * np.sqrt(2.0)) diff --git a/plotnine/ggplot.py b/plotnine/ggplot.py index 8592014085..ce7505fe0b 100755 --- a/plotnine/ggplot.py +++ b/plotnine/ggplot.py @@ -546,6 +546,7 @@ def _draw_layers(self): """ # Draw the geoms self.layers.draw(self.layout, self.coordinates) + self.coordinates.draw(self.axs) def _draw_breaks_and_labels(self): """ From d7f694f469dc4ce61284a049e7d8f295d49fb915 Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Fri, 1 May 2026 15:38:18 -0400 Subject: [PATCH 002/126] Rewrite coord_polar to use Matplotlib's native PolarAxes 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. --- plotnine/coords/coord_polar.py | 225 +++++++++++++++++---------------- plotnine/facets/facet.py | 8 +- 2 files changed, 120 insertions(+), 113 deletions(-) diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py index 20a86882ec..a452f0a1b3 100644 --- a/plotnine/coords/coord_polar.py +++ b/plotnine/coords/coord_polar.py @@ -5,6 +5,7 @@ import numpy as np +from ..iapi import panel_ranges from .coord import coord, dist_euclidean if TYPE_CHECKING: @@ -18,10 +19,10 @@ class coord_polar(coord): """ Polar coordinate system. - Maps one aesthetic to angle (theta) and the other to radius. - Data is transformed at the Cartesian level so every standard geom - works without modification. Concentric circle and radial-spoke - grid lines are drawn automatically from the scale breaks. + Uses Matplotlib's native ``PolarAxes`` so every standard geom + (including bar → pie/bullseye) renders correctly without manual + Cartesian conversion. Concentric-circle and radial-spoke grid + lines are drawn automatically by Matplotlib. Parameters ---------- @@ -33,7 +34,7 @@ class coord_polar(coord): direction : ``1`` = clockwise (default), ``-1`` = counter-clockwise. expand : - Add a small buffer around the unit circle. Default ``True``. + Add a small buffer around the data on the radius axis. Default ``True``. """ is_linear = False @@ -51,44 +52,68 @@ def __init__( self.expand = expand self.params: dict = {} + # ------------------------------------------------------------------ + # Panel params + # ------------------------------------------------------------------ + def setup_panel_params( self, scale_x: scale, scale_y: scale ) -> panel_view: from .coord_cartesian import coord_cartesian - # Theta should always fill exactly one full revolution — no expansion. - # R uses the caller-controlled expand flag (for padding around data). - pv_theta = coord_cartesian(expand=False).setup_panel_params(scale_x, scale_y) - pv_r = coord_cartesian(expand=self.expand).setup_panel_params(scale_x, scale_y) + # Theta fills exactly one full revolution — no expansion on that axis. + # R uses the caller-controlled expand flag. + pv_no_exp = coord_cartesian(expand=False).setup_panel_params( + scale_x, scale_y + ) + pv_exp = coord_cartesian(expand=self.expand).setup_panel_params( + scale_x, scale_y + ) - # Store original ranges and breaks so transform() and - # draw() can use them. if self.theta == "x": - self.params["theta_range"] = pv_theta.x.range - self.params["r_range"] = pv_r.y.range - self.params["r_breaks"] = list(pv_r.y.breaks) - self.params["theta_breaks"] = list(pv_theta.x.breaks) + theta_range = pv_no_exp.x.range + r_sv = pv_exp.y else: - self.params["theta_range"] = pv_theta.y.range - self.params["r_range"] = pv_r.x.range - self.params["r_breaks"] = list(pv_r.x.breaks) - self.params["theta_breaks"] = list(pv_theta.y.breaks) - - pv = pv_r # use r-expanded view as the base for output ranges - - # Return a fixed unit-square output range; drop all tick marks so - # the Cartesian theming draws nothing — the polar grid is drawn - # explicitly in draw(). - pad = 0.1 if self.expand else 0.0 - out_range = (-1.0 - pad, 1.0 + pad) + theta_range = pv_no_exp.y.range + r_sv = pv_exp.x + + self.params["theta_range"] = theta_range + self.params["r_range"] = r_sv.range + empty = np.array([], dtype=float) + + # x → theta axis: PolarAxes expects radians in [0, 2π]; suppress ticks + # because our data ticks would be in original data units, not radians. new_x = replace( - pv.x, limits=out_range, range=out_range, breaks=[], minor_breaks=empty, labels=[] + pv_exp.x, + limits=(0.0, 2 * np.pi), + range=(0.0, 2 * np.pi), + breaks=[], + minor_breaks=empty, + labels=[], ) - new_y = replace( - pv.y, limits=out_range, range=out_range, breaks=[], minor_breaks=empty, labels=[] - ) - return replace(pv, x=new_x, y=new_y) + + # y → r axis: use the scale for the r dimension with its natural breaks. + new_y = replace(r_sv) + + return replace(pv_exp, x=new_x, y=new_y) + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + def _to_radians(self, vals: np.ndarray) -> np.ndarray: + """Normalise data-space theta values to [start, start + 2π].""" + t_min, t_max = self.params["theta_range"] + denom = float(t_max) - float(t_min) + if denom == 0: + return np.zeros_like(vals, dtype=float) + norm = (np.asarray(vals, dtype=float) - float(t_min)) / denom + return self.start + self.direction * norm * 2.0 * np.pi + + # ------------------------------------------------------------------ + # Data transformation + # ------------------------------------------------------------------ def transform( self, @@ -96,6 +121,11 @@ def transform( panel_params: panel_view, munch: bool = False, ) -> pd.DataFrame: + # Munch first (in original data space) so curved edges get enough + # interpolation points before we convert theta → radians. + if munch: + data = self.munch(data, panel_params) + if self.theta == "x": theta_col, r_col = "x", "y" else: @@ -104,99 +134,70 @@ def transform( if theta_col not in data.columns or r_col not in data.columns: return data - theta_vals = data[theta_col].to_numpy(dtype=float) - r_vals = data[r_col].to_numpy(dtype=float) - - t_min, t_max = ( - float(self.params["theta_range"][0]), - float(self.params["theta_range"][1]), - ) - r_min, r_max = ( - float(self.params["r_range"][0]), - float(self.params["r_range"][1]), - ) - - # Normalise theta → [0, 1] → radians. - denom_t = t_max - t_min - theta_norm = (theta_vals - t_min) / denom_t if denom_t else np.zeros_like(theta_vals) - angle = self.start + self.direction * theta_norm * 2.0 * np.pi + data = data.copy() + data[theta_col] = self._to_radians(data[theta_col].to_numpy()) - # Normalise r → [0, 1]. Clamp below zero only; values slightly above - # 1.0 land between the unit circle and the panel edge (which extends - # to 1 + pad ≈ 1.1) and are used for axis-label annotations. - denom_r = r_max - r_min - r_norm = (r_vals - r_min) / denom_r if denom_r else np.zeros_like(r_vals) - r_norm = np.clip(r_norm, 0.0, None) + # PolarAxes always expects x = theta (radians) and y = r. + # When theta = "y" we need to swap the columns. + if self.theta == "y": + data["x"], data["y"] = data["y"].copy(), data["x"].copy() - # Project to Cartesian. Angle is measured from the positive-y axis - # (12 o'clock) so x = r·sin(θ), y = r·cos(θ). - data = data.copy() - data[theta_col] = r_norm * np.sin(angle) - data[r_col] = r_norm * np.cos(angle) return data - def _r_norm(self, r_val: float) -> float: - r_min, r_max = self.params["r_range"] - denom = float(r_max) - float(r_min) - return (float(r_val) - float(r_min)) / denom if denom else 0.0 + # ------------------------------------------------------------------ + # Distance (used by munch, called before transform) + # ------------------------------------------------------------------ - def _theta_angle(self, t_val: float) -> float: + def distance( + self, + x: pd.Series, + y: pd.Series, + panel_params: panel_view, + ) -> np.ndarray: + # Normalise theta and r to [0, 1] then compute Euclidean distance. t_min, t_max = self.params["theta_range"] - denom = float(t_max) - float(t_min) - t_norm = (float(t_val) - float(t_min)) / denom if denom else 0.0 - return self.start + self.direction * t_norm * 2.0 * np.pi + r_min, r_max = self.params["r_range"] + t_denom = float(t_max - t_min) or 1.0 + r_denom = float(r_max - r_min) or 1.0 - def draw(self, axs: list[Axes]) -> None: - """Draw concentric circles and radial spokes onto each panel axes.""" - import matplotlib.patches as mpatches - import matplotlib.lines as mlines + if self.theta == "x": + theta_vals = np.asarray(x, dtype=float) + r_vals = np.asarray(y, dtype=float) + else: + theta_vals = np.asarray(y, dtype=float) + r_vals = np.asarray(x, dtype=float) - r_breaks = [v for v in self.params.get("r_breaks", []) if v is not None] - t_breaks = [v for v in self.params.get("theta_breaks", []) if v is not None] + theta_norm = (theta_vals - float(t_min)) / t_denom + r_norm = (r_vals - float(r_min)) / r_denom + return dist_euclidean(theta_norm, r_norm) - if not r_breaks and not t_breaks: - return + def backtransform_range(self, panel_params: panel_view) -> panel_ranges: + t_range = tuple(self.params["theta_range"]) + r_range = tuple(self.params["r_range"]) + if self.theta == "x": + return panel_ranges(x=t_range, y=r_range) # type: ignore[arg-type] + return panel_ranges(x=r_range, y=t_range) # type: ignore[arg-type] + + # ------------------------------------------------------------------ + # Draw decorations on PolarAxes + # ------------------------------------------------------------------ - # Outermost circle radius (used as spoke length). - valid_r = [self._r_norm(v) for v in r_breaks if np.isfinite(v)] - r_outer = max(valid_r) if valid_r else 1.0 + def draw(self, axs: list[Axes]) -> None: + """Configure each PolarAxes: zero location, direction, r limits.""" + r_min, r_max = self.params.get("r_range", (0.0, 1.0)) - grid_kw = dict(color="gray", alpha=0.3, linewidth=0.5, zorder=0.5) + # Matplotlib PolarAxes theta_direction: -1 = clockwise, 1 = counter-CW. + mpl_direction = -1 if self.direction == 1 else 1 for ax in axs: - # Concentric circles at each r break. - for r_val in r_breaks: - if not np.isfinite(r_val): - continue - r = self._r_norm(r_val) - circle = mpatches.Circle( - (0, 0), r, - fill=False, transform=ax.transData, - **grid_kw, - ) - ax.add_patch(circle) - - # Radial spokes at each theta break. - for t_val in t_breaks: - if not np.isfinite(t_val): - continue - angle = self._theta_angle(t_val) - line = mlines.Line2D( - [0, r_outer * np.sin(angle)], - [0, r_outer * np.cos(angle)], - transform=ax.transData, - **grid_kw, - ) - ax.add_line(line) + ax.set_theta_zero_location("N") # 12 o'clock = 0 + ax.set_theta_direction(mpl_direction) + if np.isfinite(r_min) and np.isfinite(r_max) and r_min != r_max: + ax.set_rlim(float(r_min), float(r_max)) + + # ------------------------------------------------------------------ + # Misc + # ------------------------------------------------------------------ def aspect(self, panel_params: panel_view) -> float: return 1.0 - - def distance( - self, - x: pd.Series, - y: pd.Series, - panel_params: panel_view, - ) -> np.ndarray: - # Output space is [-1, 1]² so max diagonal ≈ 2√2. - return dist_euclidean(x, y) / (2.0 * np.sqrt(2.0)) diff --git a/plotnine/facets/facet.py b/plotnine/facets/facet.py index fd7fe4745c..20d04e4967 100644 --- a/plotnine/facets/facet.py +++ b/plotnine/facets/facet.py @@ -342,16 +342,22 @@ def _make_axes(self) -> tuple[p9GridSpec, list[Axes]]: """ Create and return subplot axes """ + from ..coords.coord_polar import coord_polar num_panels = len(self.layout.layout) axsarr = np.empty((self.nrow, self.ncol), dtype=object) gs = self._make_gridspec() # Create axes + projection = ( + "polar" + if isinstance(self.plot.coordinates, coord_polar) + else p9Axes.name + ) it = itertools.product(range(self.nrow), range(self.ncol)) for i, (row, col) in enumerate(it): axsarr[row, col] = self.figure.add_subplot( - gs[i], projection=p9Axes.name + gs[i], projection=projection ) # Rearrange axes From bc214527d94e071632569ed6e632343bc1bfabf3 Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Sat, 2 May 2026 11:37:56 -0400 Subject: [PATCH 003/126] Add radial coordinate system --- plotnine/__init__.py | 2 + plotnine/coords/__init__.py | 2 + plotnine/coords/coord_radial.py | 157 ++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 plotnine/coords/coord_radial.py diff --git a/plotnine/__init__.py b/plotnine/__init__.py index 3da5453b56..98507e5ece 100644 --- a/plotnine/__init__.py +++ b/plotnine/__init__.py @@ -21,6 +21,7 @@ coord_fixed, coord_flip, coord_polar, + coord_radial, coord_trans, ) from .facets import ( @@ -293,6 +294,7 @@ "coord_fixed", "coord_flip", "coord_polar", + "coord_radial", "coord_trans", "dup_axis", "element_blank", diff --git a/plotnine/coords/__init__.py b/plotnine/coords/__init__.py index 8601bb1c1a..ae14967bef 100644 --- a/plotnine/coords/__init__.py +++ b/plotnine/coords/__init__.py @@ -6,6 +6,7 @@ from .coord_fixed import coord_equal, coord_fixed from .coord_flip import coord_flip from .coord_polar import coord_polar +from .coord_radial import coord_radial from .coord_trans import coord_trans __all__ = ( @@ -14,5 +15,6 @@ "coord_equal", "coord_flip", "coord_polar", + "coord_radial", "coord_trans", ) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py new file mode 100644 index 0000000000..f48d423d05 --- /dev/null +++ b/plotnine/coords/coord_radial.py @@ -0,0 +1,157 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np + +from .coord_polar import coord_polar + +if TYPE_CHECKING: + import pandas as pd + from matplotlib.axes import Axes + from plotnine.iapi import panel_view + + +class coord_radial(coord_polar): + """ + Radial coordinate system. + + A modernised polar coordinate system that adds support for partial arcs, + inner radius (donut/gauge charts), configurable radial-axis placement, and + automatic rotation of the ``angle`` aesthetic to align with theta. + + Inherits from :class:`coord_polar`; all standard geoms work without + modification. + + Parameters + ---------- + theta : + Which variable maps to the angle axis, ``"x"`` (default) or ``"y"``. + start : + Starting angle in radians, measured clockwise from 12 o'clock. + Default 0. + end : + Ending angle in radians, measured clockwise from 12 o'clock. + ``None`` (default) gives a full circle (``start + 2π * direction``). + direction : + ``1`` = clockwise (default), ``-1`` = counter-clockwise. + Only used when *end* is ``None``. + expand : + Add a small buffer around the data on the radius axis. Default ``True``. + inner_radius : + Size of the inner hole as a fraction of the outer radius, in + ``[0, 1)``. ``0`` (default) means no hole; ``0.3`` creates a 30 % + donut hole, useful for gauge and donut charts. + r_axis_inside : + Where to place the radial (r) axis tick labels. + + * ``None`` (default) — let Matplotlib decide (usually outside). + * ``True`` — force inside, aligned just past the *start* angle. + * ``False`` — force outside (Matplotlib default). + * *float* — place at this theta angle in radians (clockwise from North). + rotate_angle : + If ``True``, automatically add the local theta angle (in degrees) to + the ``angle`` aesthetic so that text or other rotated marks align with + the spoke direction. Default ``False``. + """ + + def __init__( + self, + theta: str = "x", + start: float = 0, + end: float | None = None, + direction: int = 1, + expand: bool = True, + inner_radius: float = 0, + r_axis_inside: bool | float | None = None, + rotate_angle: bool = False, + ) -> None: + super().__init__( + theta=theta, + start=start, + direction=direction, + expand=expand, + ) + self.end = end + self.inner_radius = inner_radius + self.r_axis_inside = r_axis_inside + self.rotate_angle = rotate_angle + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + @property + def _arc(self) -> float: + """Total arc in radians (signed: positive when going clockwise for direction=1).""" + if self.end is not None: + return self.end - self.start + return self.direction * 2.0 * np.pi + + def _to_radians(self, vals: np.ndarray) -> np.ndarray: + """Normalize theta values to [start, start + arc].""" + t_min, t_max = self.params["theta_range"] + denom = float(t_max) - float(t_min) + if denom == 0: + return np.zeros_like(vals, dtype=float) + norm = (np.asarray(vals, dtype=float) - float(t_min)) / denom + return self.start + norm * self._arc + + # ------------------------------------------------------------------ + # Data transformation + # ------------------------------------------------------------------ + + def transform( + self, + data: pd.DataFrame, + panel_params: panel_view, + munch: bool = False, + ) -> pd.DataFrame: + data = super().transform(data, panel_params, munch=munch) + # After super().transform(), data["x"] is always theta in radians. + if self.rotate_angle and "angle" in data.columns and "x" in data.columns: + data = data.copy() + data["angle"] = data["angle"] + np.degrees(data["x"]) + return data + + # ------------------------------------------------------------------ + # Draw decorations on PolarAxes + # ------------------------------------------------------------------ + + def draw(self, axs: list[Axes]) -> None: + """Configure PolarAxes: arc limits, inner radius, axis placement.""" + super().draw(axs) + + r_min, r_max = self.params.get("r_range", (0.0, 1.0)) + arc = self._arc + + for ax in axs: + # Restrict visible theta range for partial arcs. + if self.end is not None: + theta_lo = min(self.start, self.start + arc) + theta_hi = max(self.start, self.start + arc) + ax.set_thetalim(theta_lo, theta_hi) + + # Inner radius: push the data away from the centre by setting a + # virtual r-origin below r_min. Formula: solve + # inner_radius = (r_min - r_origin) / (r_max - r_origin) + if ( + self.inner_radius > 0 + and np.isfinite(r_min) + and np.isfinite(r_max) + and r_max > r_min + and self.inner_radius < 1.0 + ): + r_origin = (r_min - self.inner_radius * r_max) / ( + 1.0 - self.inner_radius + ) + ax.set_rorigin(r_origin) + + # Radial axis label placement. + if self.r_axis_inside is not None: + if isinstance(self.r_axis_inside, bool): + if self.r_axis_inside: + # Just inside the start angle keeps it out of the data. + ax.set_rlabel_position(np.degrees(self.start) + 10) + else: + ax.set_rlabel_position(np.degrees(float(self.r_axis_inside))) From 80667e9272e49f19ad91e9db735d5cc165c8da18 Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Sat, 2 May 2026 13:18:53 -0400 Subject: [PATCH 004/126] Extend coord_radial: partial-arc fixes, thetalim/rlim, theta axis labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- plotnine/coords/coord_radial.py | 84 +++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index f48d423d05..a97dd9532f 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import replace from typing import TYPE_CHECKING import numpy as np @@ -10,6 +11,7 @@ import pandas as pd from matplotlib.axes import Axes from plotnine.iapi import panel_view + from plotnine.scales.scale import scale class coord_radial(coord_polar): @@ -53,6 +55,14 @@ class coord_radial(coord_polar): If ``True``, automatically add the local theta angle (in degrees) to the ``angle`` aesthetic so that text or other rotated marks align with the spoke direction. Default ``False``. + thetalim : + Data-space limits for the theta axis as ``(lo, hi)``. Only data + within this range is mapped to the arc; equivalent to zooming on the + angular axis. ``None`` (default) uses the full data range. + rlim : + Data-space limits for the r axis as ``(lo, hi)``. Only data within + this range is shown; equivalent to zooming on the radial axis. + ``None`` (default) uses the full data range. """ def __init__( @@ -65,6 +75,8 @@ def __init__( inner_radius: float = 0, r_axis_inside: bool | float | None = None, rotate_angle: bool = False, + thetalim: tuple[float, float] | None = None, + rlim: tuple[float, float] | None = None, ) -> None: super().__init__( theta=theta, @@ -76,6 +88,78 @@ def __init__( self.inner_radius = inner_radius self.r_axis_inside = r_axis_inside self.rotate_angle = rotate_angle + self.thetalim = thetalim + self.rlim = rlim + + # ------------------------------------------------------------------ + # Panel params + # ------------------------------------------------------------------ + + def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: + from .coord_cartesian import coord_cartesian + + # Capture data-space theta breaks before super() clears them. + pv_data = coord_cartesian(expand=False).setup_panel_params(scale_x, scale_y) + if self.theta == "x": + theta_breaks = list(pv_data.x.breaks) + theta_labels = list(pv_data.x.labels) + else: + theta_breaks = list(pv_data.y.breaks) + theta_labels = list(pv_data.y.labels) + + pv = super().setup_panel_params(scale_x, scale_y) + + # thetalim: zoom the theta data range — only this slice maps to the arc. + if self.thetalim is not None: + self.params["theta_range"] = tuple(self.thetalim) + + # rlim: zoom the r data range — update params, panel view y axis, and + # filter breaks/labels to within rlim so set_yticks doesn't force the + # PolarAxes r-axis to expand beyond the requested limits. + if self.rlim is not None: + self.params["r_range"] = tuple(self.rlim) + rlo, rhi = self.rlim + breaks, labels = pv.y.breaks, pv.y.labels + mask = [rlo <= b <= rhi for b in breaks] + new_y = replace( + pv.y, + limits=tuple(self.rlim), + range=tuple(self.rlim), + breaks=[b for b, m in zip(breaks, mask) if m], + labels=[l for l, m in zip(labels, mask) if m], + ) + pv = replace(pv, y=new_y) + + # Compute arc bounds for partial-arc plots (None means full circle). + arc_lo = arc_hi = None + if self.end is not None: + arc = self._arc + arc_lo = min(self.start, self.start + arc) + arc_hi = max(self.start, self.start + arc) + + # For partial arcs only: convert data-space theta breaks to radian + # positions and restore them as theta axis tick labels on the outer edge. + # Full-circle charts (pac-man, coxcomb) keep breaks=[] as set by super(). + x_updates: dict = {} + if theta_breaks and arc_lo is not None: + radian_pos = list(self._to_radians(np.asarray(theta_breaks, dtype=float))) + keep = [arc_lo <= r <= arc_hi for r in radian_pos] + radian_pos = [r for r, k in zip(radian_pos, keep) if k] + theta_labels = [l for l, k in zip(theta_labels, keep) if k] + x_updates["breaks"] = radian_pos + x_updates["labels"] = theta_labels + + # Partial arc: x panel range must match [arc_lo, arc_hi] so that + # set_limits_breaks_and_labels calls ax.set_xlim(arc_lo, arc_hi) rather + # than ax.set_xlim(0, 2π), which would override set_thetalim. + if arc_lo is not None: + x_updates["limits"] = (arc_lo, arc_hi) + x_updates["range"] = (arc_lo, arc_hi) + + if x_updates: + pv = replace(pv, x=replace(pv.x, **x_updates)) + + return pv # ------------------------------------------------------------------ # Helpers From 7347950b7580aac324205a91ce19cb059f1b096c Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Sat, 2 May 2026 16:15:08 -0400 Subject: [PATCH 005/126] Add theta_labels parameter to coord_radial for full-circle theta axis 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. --- plotnine/coords/coord_radial.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index a97dd9532f..4da2148747 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -63,6 +63,11 @@ class coord_radial(coord_polar): Data-space limits for the r axis as ``(lo, hi)``. Only data within this range is shown; equivalent to zooming on the radial axis. ``None`` (default) uses the full data range. + theta_labels : + If ``True``, show theta axis tick labels on the outer edge of the + circle for full-circle plots, using the breaks and labels from the + theta scale. Default ``False``. Partial-arc plots always show + theta labels (filtered to the visible arc) regardless of this flag. """ def __init__( @@ -77,6 +82,7 @@ def __init__( rotate_angle: bool = False, thetalim: tuple[float, float] | None = None, rlim: tuple[float, float] | None = None, + theta_labels: bool = False, ) -> None: super().__init__( theta=theta, @@ -90,6 +96,7 @@ def __init__( self.rotate_angle = rotate_angle self.thetalim = thetalim self.rlim = rlim + self.theta_labels = theta_labels # ------------------------------------------------------------------ # Panel params @@ -137,15 +144,17 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: arc_lo = min(self.start, self.start + arc) arc_hi = max(self.start, self.start + arc) - # For partial arcs only: convert data-space theta breaks to radian - # positions and restore them as theta axis tick labels on the outer edge. - # Full-circle charts (pac-man, coxcomb) keep breaks=[] as set by super(). + # Convert data-space theta breaks to radian positions and restore them + # as theta axis tick labels on the outer edge. Always done for partial + # arcs; for full circles only when theta_labels=True (opt-in, so that + # pac-man / coxcomb charts keep breaks=[] as set by super()). x_updates: dict = {} - if theta_breaks and arc_lo is not None: + if theta_breaks and (arc_lo is not None or self.theta_labels): radian_pos = list(self._to_radians(np.asarray(theta_breaks, dtype=float))) - keep = [arc_lo <= r <= arc_hi for r in radian_pos] - radian_pos = [r for r, k in zip(radian_pos, keep) if k] - theta_labels = [l for l, k in zip(theta_labels, keep) if k] + if arc_lo is not None: + keep = [arc_lo <= r <= arc_hi for r in radian_pos] + radian_pos = [r for r, k in zip(radian_pos, keep) if k] + theta_labels = [l for l, k in zip(theta_labels, keep) if k] x_updates["breaks"] = radian_pos x_updates["labels"] = theta_labels From 6916129e1cc21334cadb51411dd173e9d1b5abcc Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Sat, 2 May 2026 16:28:22 -0400 Subject: [PATCH 006/126] Add pad to theta tick labels to clear the outer circle spine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- plotnine/coords/coord_radial.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 4da2148747..9a052ab8d0 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -248,3 +248,8 @@ def draw(self, axs: list[Axes]) -> None: ax.set_rlabel_position(np.degrees(self.start) + 10) else: ax.set_rlabel_position(np.degrees(float(self.r_axis_inside))) + + # Push theta tick labels away from the outer circle so they don't + # sit right on the spine. + if self.theta_labels or self.end is not None: + ax.tick_params(axis="x", pad=8) From fb42ab7f1c87a8d151e84612a8210e743c6b1e26 Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Sat, 2 May 2026 16:33:52 -0400 Subject: [PATCH 007/126] Expose theta_label_pad parameter on coord_radial 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. --- plotnine/coords/coord_radial.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 9a052ab8d0..3f501f87d4 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -68,6 +68,9 @@ class coord_radial(coord_polar): circle for full-circle plots, using the breaks and labels from the theta scale. Default ``False``. Partial-arc plots always show theta labels (filtered to the visible arc) regardless of this flag. + theta_label_pad : + Distance in points between the outer circle spine and the theta tick + labels. Default ``8``. Only applied when theta labels are shown. """ def __init__( @@ -83,6 +86,7 @@ def __init__( thetalim: tuple[float, float] | None = None, rlim: tuple[float, float] | None = None, theta_labels: bool = False, + theta_label_pad: float = 8, ) -> None: super().__init__( theta=theta, @@ -97,6 +101,7 @@ def __init__( self.thetalim = thetalim self.rlim = rlim self.theta_labels = theta_labels + self.theta_label_pad = theta_label_pad # ------------------------------------------------------------------ # Panel params @@ -252,4 +257,4 @@ def draw(self, axs: list[Axes]) -> None: # Push theta tick labels away from the outer circle so they don't # sit right on the spine. if self.theta_labels or self.end is not None: - ax.tick_params(axis="x", pad=8) + ax.tick_params(axis="x", pad=self.theta_label_pad) From a829df128e475d9b0a58f6dfa0e1902083971017 Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Sat, 2 May 2026 17:16:45 -0400 Subject: [PATCH 008/126] Fix theta_label_pad being overwritten by facet margin padding 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. --- plotnine/coords/coord.py | 8 ++++++++ plotnine/coords/coord_radial.py | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/plotnine/coords/coord.py b/plotnine/coords/coord.py index 02b7bb23c9..8e7924167d 100644 --- a/plotnine/coords/coord.py +++ b/plotnine/coords/coord.py @@ -307,6 +307,14 @@ def draw(self, axs: list) -> None: add elements such as polar grid lines. """ + def post_setup_ax(self, ax: Any) -> None: + """ + Hook called for each axes after set_limits_breaks_and_labels. + + Override in subclasses to apply per-axes settings that must + run after the facet has set tick positions and label padding. + """ + def labels(self, cur_labels: labels_view) -> labels_view: """ Modify labels diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 3f501f87d4..9197a1b68d 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -254,7 +254,7 @@ def draw(self, axs: list[Axes]) -> None: else: ax.set_rlabel_position(np.degrees(float(self.r_axis_inside))) - # Push theta tick labels away from the outer circle so they don't - # sit right on the spine. - if self.theta_labels or self.end is not None: - ax.tick_params(axis="x", pad=self.theta_label_pad) + def post_setup_ax(self, ax: Axes) -> None: + """Apply theta label pad after facet has set tick positions and padding.""" + if self.theta_labels or self.end is not None: + ax.tick_params(axis="x", pad=self.theta_label_pad) From b9a7b2058746a68f74a7c92b131815c8dc757163 Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Sat, 2 May 2026 21:56:46 -0400 Subject: [PATCH 009/126] Fix theta breaks with negative angles causing partial-arc regression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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π]). --- plotnine/coords/coord_radial.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 9197a1b68d..b67c5b40b2 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -160,6 +160,12 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: keep = [arc_lo <= r <= arc_hi for r in radian_pos] radian_pos = [r for r, k in zip(radian_pos, keep) if k] theta_labels = [l for l, k in zip(theta_labels, keep) if k] + else: + # Full circle: ax.set_xticks with negative values silently extends + # xlim below 0, turning the full circle into a partial arc. Wrap + # all break positions into [0, 2π] to avoid this. + tau = 2.0 * np.pi + radian_pos = [r % tau for r in radian_pos] x_updates["breaks"] = radian_pos x_updates["labels"] = theta_labels From 6f5ca3636b2e3acea505c94a1bda38c096794622 Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Sat, 2 May 2026 22:25:56 -0400 Subject: [PATCH 010/126] Fix start-angle rotation hiding bars for full-circle plots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- plotnine/coords/coord_polar.py | 10 ++++++---- plotnine/coords/coord_radial.py | 6 ------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py index a452f0a1b3..2b2d6b74c1 100644 --- a/plotnine/coords/coord_polar.py +++ b/plotnine/coords/coord_polar.py @@ -82,12 +82,14 @@ def setup_panel_params( empty = np.array([], dtype=float) - # x → theta axis: PolarAxes expects radians in [0, 2π]; suppress ticks - # because our data ticks would be in original data units, not radians. + # x → theta axis: data ticks are in original units (not radians), so + # suppress them. Limits span [start, start+2π] so that bars rotated + # by a non-zero start angle stay within the displayed theta range. + theta_start = float(self.start) new_x = replace( pv_exp.x, - limits=(0.0, 2 * np.pi), - range=(0.0, 2 * np.pi), + limits=(theta_start, theta_start + 2 * np.pi), + range=(theta_start, theta_start + 2 * np.pi), breaks=[], minor_breaks=empty, labels=[], diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index b67c5b40b2..9197a1b68d 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -160,12 +160,6 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: keep = [arc_lo <= r <= arc_hi for r in radian_pos] radian_pos = [r for r, k in zip(radian_pos, keep) if k] theta_labels = [l for l, k in zip(theta_labels, keep) if k] - else: - # Full circle: ax.set_xticks with negative values silently extends - # xlim below 0, turning the full circle into a partial arc. Wrap - # all break positions into [0, 2π] to avoid this. - tau = 2.0 * np.pi - radian_pos = [r % tau for r in radian_pos] x_updates["breaks"] = radian_pos x_updates["labels"] = theta_labels From b4cfa0f9bdc5c50050a6715c157621e6a9761f45 Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Sat, 2 May 2026 22:39:05 -0400 Subject: [PATCH 011/126] Hide polar spine when panel_border=element_blank() 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. --- plotnine/ggplot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plotnine/ggplot.py b/plotnine/ggplot.py index ce7505fe0b..65a516030b 100755 --- a/plotnine/ggplot.py +++ b/plotnine/ggplot.py @@ -520,6 +520,11 @@ def _draw_panel_borders(self): # grid lines below the borders. We leave ax.patch for the # background only. if self.theme.T.is_blank("panel_border"): + # For PolarAxes the default circular spine is separate from the + # panel border Rectangle; hide it explicitly when blank. + for ax in self.axs: + if "polar" in ax.spines: + ax.spines["polar"].set_visible(False) return from matplotlib.patches import Rectangle From 472dbef137858e3f2e73d5756ab930bafb44bade Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Sun, 3 May 2026 05:39:47 -0400 Subject: [PATCH 012/126] Set clip_on=False on geom_text artists in PolarAxes 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. --- plotnine/coords/coord_radial.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 9197a1b68d..9cea4c9df8 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -258,3 +258,7 @@ def post_setup_ax(self, ax: Axes) -> None: """Apply theta label pad after facet has set tick positions and padding.""" if self.theta_labels or self.end is not None: ax.tick_params(axis="x", pad=self.theta_label_pad) + # Allow geom_text labels to extend past the polar axes bounding box + # (e.g. spoke labels placed just beyond the outermost bar tip). + for text in ax.texts: + text.set_clip_on(False) From 89f29a684193fd4373f2ad17a91a9b884e4a1b51 Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Sun, 3 May 2026 07:47:00 -0400 Subject: [PATCH 013/126] Transform polar segment endpoints --- plotnine/coords/coord_polar.py | 12 ++++++++++++ tests/test_coord_polar.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 tests/test_coord_polar.py diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py index 2b2d6b74c1..75d1318b9d 100644 --- a/plotnine/coords/coord_polar.py +++ b/plotnine/coords/coord_polar.py @@ -130,19 +130,31 @@ def transform( if self.theta == "x": theta_col, r_col = "x", "y" + theta_end_col, r_end_col = "xend", "yend" else: theta_col, r_col = "y", "x" + theta_end_col, r_end_col = "yend", "xend" if theta_col not in data.columns or r_col not in data.columns: return data data = data.copy() data[theta_col] = self._to_radians(data[theta_col].to_numpy()) + has_endpoints = theta_end_col in data.columns and r_end_col in data.columns + if has_endpoints: + data[theta_end_col] = self._to_radians( + data[theta_end_col].to_numpy() + ) # PolarAxes always expects x = theta (radians) and y = r. # When theta = "y" we need to swap the columns. if self.theta == "y": data["x"], data["y"] = data["y"].copy(), data["x"].copy() + if has_endpoints: + data["xend"], data["yend"] = ( + data["yend"].copy(), + data["xend"].copy(), + ) return data diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py new file mode 100644 index 0000000000..1365d5109c --- /dev/null +++ b/tests/test_coord_polar.py @@ -0,0 +1,30 @@ +import numpy as np +import pandas as pd + +from plotnine.coords.coord_polar import coord_polar + + +def test_coord_polar_transforms_segment_endpoints_theta_x(): + coord = coord_polar(theta="x") + coord.params = {"theta_range": (0, 10), "r_range": (0, 10)} + data = pd.DataFrame({"x": [0], "y": [1], "xend": [10], "yend": [2]}) + + out = coord.transform(data, None) + + assert out.loc[0, "x"] == 0 + assert out.loc[0, "y"] == 1 + assert np.isclose(out.loc[0, "xend"], 2 * np.pi) + assert out.loc[0, "yend"] == 2 + + +def test_coord_polar_transforms_segment_endpoints_theta_y(): + coord = coord_polar(theta="y") + coord.params = {"theta_range": (0, 10), "r_range": (0, 10)} + data = pd.DataFrame({"x": [1], "y": [0], "xend": [2], "yend": [10]}) + + out = coord.transform(data, None) + + assert out.loc[0, "x"] == 0 + assert out.loc[0, "y"] == 1 + assert np.isclose(out.loc[0, "xend"], 2 * np.pi) + assert out.loc[0, "yend"] == 2 From c7f5ff234ebc9f774e3f94d5c0778a793a5142fa Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Mon, 4 May 2026 14:42:38 -0400 Subject: [PATCH 014/126] Document polar coordinates --- doc/.gitignore | 2 + doc/_quartodoc.yml | 2 + plotnine/coords/coord_polar.py | 51 +++++++++++++---- plotnine/coords/coord_radial.py | 99 +++++++++++++++++++++++++++------ 4 files changed, 126 insertions(+), 28 deletions(-) diff --git a/doc/.gitignore b/doc/.gitignore index 4e48027747..19459cf67a 100644 --- a/doc/.gitignore +++ b/doc/.gitignore @@ -13,3 +13,5 @@ objects.json objects.txt objects.inv gallery/thumbnails + +**/*.quarto_ipynb diff --git a/doc/_quartodoc.yml b/doc/_quartodoc.yml index de9be48dd4..76c8625b6c 100644 --- a/doc/_quartodoc.yml +++ b/doc/_quartodoc.yml @@ -565,6 +565,8 @@ quartodoc: - coord_equal - coord_fixed - coord_flip + - coord_polar + - coord_radial - coord_trans - title: Composing Plots diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py index 75d1318b9d..e3ca59eac0 100644 --- a/plotnine/coords/coord_polar.py +++ b/plotnine/coords/coord_polar.py @@ -11,18 +11,18 @@ if TYPE_CHECKING: import pandas as pd from matplotlib.axes import Axes + from plotnine.iapi import panel_view from plotnine.scales.scale import scale class coord_polar(coord): """ - Polar coordinate system. + Polar coordinate system - Uses Matplotlib's native ``PolarAxes`` so every standard geom - (including bar → pie/bullseye) renders correctly without manual - Cartesian conversion. Concentric-circle and radial-spoke grid - lines are drawn automatically by Matplotlib. + `coord_polar` maps one position aesthetic to the angle and the other + to the radius. It is commonly used for pie charts, which are stacked + bar charts in polar coordinates. Parameters ---------- @@ -34,7 +34,33 @@ class coord_polar(coord): direction : ``1`` = clockwise (default), ``-1`` = counter-clockwise. expand : - Add a small buffer around the data on the radius axis. Default ``True``. + Add a small buffer around the data on the radius axis. + Default ``True``. + + Notes + ----- + Unlike ggplot2, plotnine coordinate systems do not currently expose a + ``clip`` argument. + + For partial arcs, donut charts, and theta/radius limits, use + ``coord_radial``. + + Examples + -------- + A pie chart is a stacked bar chart with the y position mapped to angle. + + ```python + import pandas as pd + from plotnine import aes, coord_polar, geom_col, ggplot + + df = pd.DataFrame({ + "x": [1, 1, 1], + "y": [2, 3, 5], + "group": ["a", "b", "c"], + }) + + ggplot(df, aes("x", "y", fill="group")) + geom_col() + coord_polar("y") + ``` """ is_linear = False @@ -56,9 +82,7 @@ def __init__( # Panel params # ------------------------------------------------------------------ - def setup_panel_params( - self, scale_x: scale, scale_y: scale - ) -> panel_view: + def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: from .coord_cartesian import coord_cartesian # Theta fills exactly one full revolution — no expansion on that axis. @@ -95,7 +119,8 @@ def setup_panel_params( labels=[], ) - # y → r axis: use the scale for the r dimension with its natural breaks. + # y → r axis: use the scale for the r dimension with its natural + # breaks. new_y = replace(r_sv) return replace(pv_exp, x=new_x, y=new_y) @@ -140,7 +165,9 @@ def transform( data = data.copy() data[theta_col] = self._to_radians(data[theta_col].to_numpy()) - has_endpoints = theta_end_col in data.columns and r_end_col in data.columns + has_endpoints = ( + theta_end_col in data.columns and r_end_col in data.columns + ) if has_endpoints: data[theta_end_col] = self._to_radians( data[theta_end_col].to_numpy() @@ -204,7 +231,7 @@ def draw(self, axs: list[Axes]) -> None: mpl_direction = -1 if self.direction == 1 else 1 for ax in axs: - ax.set_theta_zero_location("N") # 12 o'clock = 0 + ax.set_theta_zero_location("N") # 12 o'clock = 0 ax.set_theta_direction(mpl_direction) if np.isfinite(r_min) and np.isfinite(r_max) and r_min != r_max: ax.set_rlim(float(r_min), float(r_max)) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 9cea4c9df8..37f56ff084 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -10,20 +10,19 @@ if TYPE_CHECKING: import pandas as pd from matplotlib.axes import Axes + from plotnine.iapi import panel_view from plotnine.scales.scale import scale class coord_radial(coord_polar): """ - Radial coordinate system. - - A modernised polar coordinate system that adds support for partial arcs, - inner radius (donut/gauge charts), configurable radial-axis placement, and - automatic rotation of the ``angle`` aesthetic to align with theta. + Radial coordinate system - Inherits from :class:`coord_polar`; all standard geoms work without - modification. + `coord_radial` maps one position aesthetic to the angle and the other + to the radius. Compared with ``coord_polar``, it adds support for + partial arcs, inner radius holes, theta/radius limits, radial-axis + placement, and rotation of the ``angle`` aesthetic. Parameters ---------- @@ -39,7 +38,8 @@ class coord_radial(coord_polar): ``1`` = clockwise (default), ``-1`` = counter-clockwise. Only used when *end* is ``None``. expand : - Add a small buffer around the data on the radius axis. Default ``True``. + Add a small buffer around the data on the radius axis. + Default ``True``. inner_radius : Size of the inner hole as a fraction of the outer radius, in ``[0, 1)``. ``0`` (default) means no hole; ``0.3`` creates a 30 % @@ -50,7 +50,11 @@ class coord_radial(coord_polar): * ``None`` (default) — let Matplotlib decide (usually outside). * ``True`` — force inside, aligned just past the *start* angle. * ``False`` — force outside (Matplotlib default). - * *float* — place at this theta angle in radians (clockwise from North). + * *float* — place at this theta angle in radians (clockwise from + North). + + Unlike ggplot2's ``r.axis.inside``, a length-2 value for separate + primary and secondary axis placement is not supported. rotate_angle : If ``True``, automatically add the local theta angle (in degrees) to the ``angle`` aesthetic so that text or other rotated marks align with @@ -71,6 +75,52 @@ class coord_radial(coord_polar): theta_label_pad : Distance in points between the outer circle spine and the theta tick labels. Default ``8``. Only applied when theta labels are shown. + + Notes + ----- + The Python API uses snake_case names for arguments that are dotted in + ggplot2: ``inner_radius``, ``r_axis_inside``, and ``rotate_angle``. + + Unlike ggplot2, plotnine coordinate systems do not currently expose a + ``clip`` argument. The ggplot2 ``reverse`` argument is not currently + implemented. + + Examples + -------- + A donut chart is a stacked bar chart with an inner radius. + + ```python + import pandas as pd + from plotnine import aes, coord_radial, geom_col, ggplot + + df = pd.DataFrame({ + "x": [1, 1, 1], + "y": [2, 3, 5], + "group": ["a", "b", "c"], + }) + + ( + ggplot(df, aes("x", "y", fill="group")) + + geom_col() + + coord_radial(theta="y", inner_radius=0.4) + ) + ``` + + Partial arcs can be used for gauge-like displays. + + ```python + import numpy as np + import pandas as pd + from plotnine import aes, coord_radial, geom_point, ggplot + + df = pd.DataFrame({"x": [1, 2, 3], "y": [2, 4, 3]}) + + ggplot(df, aes("x", "y")) + geom_point() + coord_radial( + start=-0.4 * np.pi, + end=0.4 * np.pi, + inner_radius=0.3, + ) + ``` """ def __init__( @@ -111,7 +161,9 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: from .coord_cartesian import coord_cartesian # Capture data-space theta breaks before super() clears them. - pv_data = coord_cartesian(expand=False).setup_panel_params(scale_x, scale_y) + pv_data = coord_cartesian(expand=False).setup_panel_params( + scale_x, scale_y + ) if self.theta == "x": theta_breaks = list(pv_data.x.breaks) theta_labels = list(pv_data.x.labels) @@ -121,7 +173,8 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: pv = super().setup_panel_params(scale_x, scale_y) - # thetalim: zoom the theta data range — only this slice maps to the arc. + # thetalim: zoom the theta data range — only this slice maps to the + # arc. if self.thetalim is not None: self.params["theta_range"] = tuple(self.thetalim) @@ -155,7 +208,9 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: # pac-man / coxcomb charts keep breaks=[] as set by super()). x_updates: dict = {} if theta_breaks and (arc_lo is not None or self.theta_labels): - radian_pos = list(self._to_radians(np.asarray(theta_breaks, dtype=float))) + radian_pos = list( + self._to_radians(np.asarray(theta_breaks, dtype=float)) + ) if arc_lo is not None: keep = [arc_lo <= r <= arc_hi for r in radian_pos] 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: @property def _arc(self) -> float: - """Total arc in radians (signed: positive when going clockwise for direction=1).""" + """ + Total arc in radians. + + A positive value represents clockwise movement when ``direction=1``. + """ if self.end is not None: return self.end - self.start return self.direction * 2.0 * np.pi @@ -207,7 +266,11 @@ def transform( ) -> pd.DataFrame: data = super().transform(data, panel_params, munch=munch) # After super().transform(), data["x"] is always theta in radians. - if self.rotate_angle and "angle" in data.columns and "x" in data.columns: + if ( + self.rotate_angle + and "angle" in data.columns + and "x" in data.columns + ): data = data.copy() data["angle"] = data["angle"] + np.degrees(data["x"]) return data @@ -252,10 +315,14 @@ def draw(self, axs: list[Axes]) -> None: # Just inside the start angle keeps it out of the data. ax.set_rlabel_position(np.degrees(self.start) + 10) else: - ax.set_rlabel_position(np.degrees(float(self.r_axis_inside))) + ax.set_rlabel_position( + np.degrees(float(self.r_axis_inside)) + ) def post_setup_ax(self, ax: Axes) -> None: - """Apply theta label pad after facet has set tick positions and padding.""" + """ + Apply theta label pad after facet has set tick positions and padding. + """ if self.theta_labels or self.end is not None: ax.tick_params(axis="x", pad=self.theta_label_pad) # Allow geom_text labels to extend past the polar axes bounding box From c675aaa470b330cbc7145cdf04ffa690b104a2bf Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Tue, 5 May 2026 07:31:00 -0400 Subject: [PATCH 015/126] Add coord polar and radial coverage tests --- tests/test_coord_polar.py | 257 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index 1365d5109c..fd1d169625 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -1,7 +1,72 @@ import numpy as np import pandas as pd +from matplotlib import pyplot as plt +from numpy.testing import assert_allclose +from plotnine import ( + aes, + coord_radial, + element_blank, + element_line, + geom_col, + ggplot, + theme, +) from plotnine.coords.coord_polar import coord_polar +from plotnine.scales import scale_x_continuous, scale_y_continuous + + +def trained_scales( + x=(0, 10), + y=(0, 10), + x_breaks=(0, 5, 10), + y_breaks=(0, 5, 10), + x_labels=("0", "5", "10"), + y_labels=("0", "5", "10"), +): + scale_x = scale_x_continuous(breaks=x_breaks, labels=x_labels) + scale_y = scale_y_continuous(breaks=y_breaks, labels=y_labels) + scale_x.train(x) + scale_y.train(y) + return scale_x, scale_y + + +def test_coord_polar_setup_panel_params_theta_x(): + scale_x, scale_y = trained_scales( + y_breaks=(0, 2, 5, 10), + y_labels=("0", "2", "5", "10"), + ) + coord = coord_polar(theta="x", start=np.pi / 4, expand=False) + + panel_params = coord.setup_panel_params(scale_x, scale_y) + + assert coord.params["theta_range"] == (0, 10) + assert coord.params["r_range"] == (0, 10) + assert panel_params.x.range == (np.pi / 4, np.pi / 4 + 2 * np.pi) + assert panel_params.x.breaks == [] + assert panel_params.x.labels == [] + assert panel_params.y.breaks == [0, 2, 5, 10] + + +def test_coord_polar_setup_panel_params_theta_y(): + scale_x, scale_y = trained_scales( + x_breaks=(0, 2, 5, 10), + x_labels=("0", "2", "5", "10"), + ) + coord = coord_polar(theta="y", expand=False) + + panel_params = coord.setup_panel_params(scale_x, scale_y) + + assert coord.params["theta_range"] == (0, 10) + assert coord.params["r_range"] == (0, 10) + assert panel_params.y.breaks == [0, 2, 5, 10] + + +def test_coord_polar_to_radians_zero_width_range(): + coord = coord_polar() + coord.params = {"theta_range": (1, 1)} + + assert_allclose(coord._to_radians(np.array([1, 2, 3])), [0, 0, 0]) def test_coord_polar_transforms_segment_endpoints_theta_x(): @@ -28,3 +93,195 @@ def test_coord_polar_transforms_segment_endpoints_theta_y(): assert out.loc[0, "y"] == 1 assert np.isclose(out.loc[0, "xend"], 2 * np.pi) assert out.loc[0, "yend"] == 2 + + +def test_coord_polar_transforms_theta_y_without_endpoints(): + coord = coord_polar(theta="y") + coord.params = {"theta_range": (0, 10), "r_range": (0, 10)} + data = pd.DataFrame({"x": [1], "y": [5]}) + + out = coord.transform(data, None) + + assert_allclose(out.loc[0, "x"], np.pi) + assert out.loc[0, "y"] == 1 + + +def test_coord_polar_munches_before_radian_transform(): + coord = coord_polar() + coord.params = {"theta_range": (0, 10), "r_range": (0, 10)} + data = pd.DataFrame({"x": [0, 10], "y": [1, 2], "group": [1, 1]}) + + out = coord.transform(data, None, munch=True) + + assert len(out) > len(data) + assert out["x"].between(0, 2 * np.pi).all() + + +def test_coord_polar_leaves_non_position_data_unchanged(): + coord = coord_polar() + data = pd.DataFrame({"label": ["A"]}) + + assert coord.transform(data, None) is data + + +def test_coord_polar_distance_and_backtransform_theta_x(): + coord = coord_polar() + coord.params = {"theta_range": (0, 10), "r_range": (0, 20)} + + distance = coord.distance(pd.Series([0, 10]), pd.Series([0, 10]), None) + + assert_allclose(distance, [np.sqrt(1.25)]) + assert coord.backtransform_range(None).x == (0, 10) + assert coord.backtransform_range(None).y == (0, 20) + + +def test_coord_polar_distance_and_backtransform_theta_y(): + coord = coord_polar(theta="y") + coord.params = {"theta_range": (0, 10), "r_range": (0, 20)} + + distance = coord.distance(pd.Series([0, 10]), pd.Series([0, 10]), None) + + assert_allclose(distance, [np.sqrt(1.25)]) + assert coord.backtransform_range(None).x == (0, 20) + assert coord.backtransform_range(None).y == (0, 10) + + +def test_coord_polar_draw_sets_polar_axis(): + coord = coord_polar(direction=-1) + coord.params = {"r_range": (2, 8)} + fig, ax = plt.subplots(subplot_kw={"projection": "polar"}) + + try: + coord.draw([ax]) + assert ax.get_theta_direction() == 1 + assert_allclose(ax.get_ylim(), (2, 8)) + finally: + plt.close(fig) + + +def test_coord_polar_aspect_is_square(): + assert coord_polar().aspect(None) == 1 + + +def test_coord_polar_draw_uses_polar_axes_and_hides_blank_border(): + data = pd.DataFrame({"x": ["a", "b"], "y": [1, 2]}) + p = ( + ggplot(data, aes("x", "y")) + + geom_col() + + coord_polar() + + theme(panel_border=element_blank(), axis_line=element_line()) + ) + + fig = p.draw() + + try: + ax = fig.axes[0] + assert ax.name == "polar" + assert not ax.spines["polar"].get_visible() + finally: + plt.close(fig) + + +def test_coord_radial_arc_uses_end_or_direction(): + assert coord_radial(start=1, end=4)._arc == 3 + assert coord_radial(direction=-1)._arc == -2 * np.pi + + +def test_coord_radial_setup_panel_params_for_partial_arc(): + scale_x, scale_y = trained_scales( + y_breaks=(0, 2, 4, 8, 10), + y_labels=("0", "2", "4", "8", "10"), + ) + coord = coord_radial( + start=0, + end=np.pi, + thetalim=(0, 10), + rlim=(2, 8), + expand=False, + ) + + panel_params = coord.setup_panel_params(scale_x, scale_y) + + assert coord.params["theta_range"] == (0, 10) + assert coord.params["r_range"] == (2, 8) + assert_allclose(panel_params.x.breaks, [0, np.pi / 2, np.pi]) + assert panel_params.x.labels == ["0", "5", "10"] + assert panel_params.x.range == (0, np.pi) + assert panel_params.y.range == (2, 8) + assert panel_params.y.breaks == [2, 4, 8] + assert panel_params.y.labels == ["2", "4", "8"] + + +def test_coord_radial_setup_panel_params_theta_y_with_labels(): + scale_x, scale_y = trained_scales( + y_breaks=(0, 5, 10), + y_labels=("low", "mid", "high"), + ) + coord = coord_radial(theta="y", theta_labels=True, expand=False) + + panel_params = coord.setup_panel_params(scale_x, scale_y) + + assert_allclose(panel_params.x.breaks, [0, np.pi, 2 * np.pi]) + assert panel_params.x.labels == ["low", "mid", "high"] + + +def test_coord_radial_to_radians_zero_width_range(): + coord = coord_radial() + coord.params = {"theta_range": (1, 1)} + + assert_allclose(coord._to_radians(np.array([1, 2, 3])), [0, 0, 0]) + + +def test_coord_radial_transform_rotates_angle(): + coord = coord_radial(rotate_angle=True) + coord.params = {"theta_range": (0, 10), "r_range": (0, 10)} + data = pd.DataFrame({"x": [0, 5], "y": [1, 1], "angle": [10, 20]}) + + out = coord.transform(data, None) + + assert_allclose(out["x"], [0, np.pi]) + assert_allclose(out["angle"], [10, 200]) + + +def test_coord_radial_draw_sets_arc_inner_radius_and_axis_position(): + coord = coord_radial( + start=np.pi / 4, + end=3 * np.pi / 4, + inner_radius=0.5, + r_axis_inside=True, + ) + coord.params = {"r_range": (2, 10)} + fig, ax = plt.subplots(subplot_kw={"projection": "polar"}) + + try: + coord.draw([ax]) + assert_allclose(ax.get_xlim(), (np.pi / 4, 3 * np.pi / 4)) + assert_allclose(ax.get_rorigin(), -6) + assert ax.get_rlabel_position() == 55 + finally: + plt.close(fig) + + +def test_coord_radial_draw_float_r_axis_position(): + coord = coord_radial(r_axis_inside=np.pi / 2) + coord.params = {"r_range": (0, 10)} + fig, ax = plt.subplots(subplot_kw={"projection": "polar"}) + + try: + coord.draw([ax]) + assert ax.get_rlabel_position() == 90 + finally: + plt.close(fig) + + +def test_coord_radial_post_setup_ax_sets_pad_and_unclips_text(): + coord = coord_radial(theta_label_pad=17, theta_labels=True) + fig, ax = plt.subplots(subplot_kw={"projection": "polar"}) + text = ax.text(0, 1, "label", clip_on=True) + + try: + coord.post_setup_ax(ax) + assert ax.xaxis.get_tick_params()["pad"] == 17 + assert not text.get_clip_on() + finally: + plt.close(fig) From 66ac9b809b2539bd7a643d943f3f2b5737c76584 Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Tue, 5 May 2026 08:11:39 -0400 Subject: [PATCH 016/126] Fix polar coordinate type checking --- plotnine/coords/coord_polar.py | 14 ++++++++------ plotnine/coords/coord_radial.py | 17 +++++++++++------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py index e3ca59eac0..d9519b8f6f 100644 --- a/plotnine/coords/coord_polar.py +++ b/plotnine/coords/coord_polar.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import replace -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast import numpy as np @@ -11,6 +11,7 @@ if TYPE_CHECKING: import pandas as pd from matplotlib.axes import Axes + from matplotlib.projections.polar import PolarAxes from plotnine.iapi import panel_view from plotnine.scales.scale import scale @@ -216,8 +217,8 @@ def backtransform_range(self, panel_params: panel_view) -> panel_ranges: t_range = tuple(self.params["theta_range"]) r_range = tuple(self.params["r_range"]) if self.theta == "x": - return panel_ranges(x=t_range, y=r_range) # type: ignore[arg-type] - return panel_ranges(x=r_range, y=t_range) # type: ignore[arg-type] + return panel_ranges(x=t_range, y=r_range) + return panel_ranges(x=r_range, y=t_range) # ------------------------------------------------------------------ # Draw decorations on PolarAxes @@ -231,10 +232,11 @@ def draw(self, axs: list[Axes]) -> None: mpl_direction = -1 if self.direction == 1 else 1 for ax in axs: - ax.set_theta_zero_location("N") # 12 o'clock = 0 - ax.set_theta_direction(mpl_direction) + polar_ax = cast("PolarAxes", ax) + polar_ax.set_theta_zero_location("N") # 12 o'clock = 0 + polar_ax.set_theta_direction(mpl_direction) if np.isfinite(r_min) and np.isfinite(r_max) and r_min != r_max: - ax.set_rlim(float(r_min), float(r_max)) + polar_ax.set_rlim(float(r_min), float(r_max)) # ------------------------------------------------------------------ # Misc diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 37f56ff084..302d4624c8 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import replace -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Sequence, cast import numpy as np @@ -10,6 +10,7 @@ if TYPE_CHECKING: import pandas as pd from matplotlib.axes import Axes + from matplotlib.projections.polar import PolarAxes from plotnine.iapi import panel_view from plotnine.scales.scale import scale @@ -184,7 +185,8 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: if self.rlim is not None: self.params["r_range"] = tuple(self.rlim) rlo, rhi = self.rlim - breaks, labels = pv.y.breaks, pv.y.labels + breaks = cast("Sequence[float]", pv.y.breaks) + labels = pv.y.labels mask = [rlo <= b <= rhi for b in breaks] new_y = replace( pv.y, @@ -287,11 +289,12 @@ def draw(self, axs: list[Axes]) -> None: arc = self._arc for ax in axs: + polar_ax = cast("PolarAxes", ax) # Restrict visible theta range for partial arcs. if self.end is not None: theta_lo = min(self.start, self.start + arc) theta_hi = max(self.start, self.start + arc) - ax.set_thetalim(theta_lo, theta_hi) + polar_ax.set_thetalim(theta_lo, theta_hi) # Inner radius: push the data away from the centre by setting a # virtual r-origin below r_min. Formula: solve @@ -306,16 +309,18 @@ def draw(self, axs: list[Axes]) -> None: r_origin = (r_min - self.inner_radius * r_max) / ( 1.0 - self.inner_radius ) - ax.set_rorigin(r_origin) + polar_ax.set_rorigin(r_origin) # Radial axis label placement. if self.r_axis_inside is not None: if isinstance(self.r_axis_inside, bool): if self.r_axis_inside: # Just inside the start angle keeps it out of the data. - ax.set_rlabel_position(np.degrees(self.start) + 10) + polar_ax.set_rlabel_position( + np.degrees(self.start) + 10 + ) else: - ax.set_rlabel_position( + polar_ax.set_rlabel_position( np.degrees(float(self.r_axis_inside)) ) From d006abea3d60918da404c9e31585008386b4e2f1 Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Tue, 5 May 2026 09:08:48 -0400 Subject: [PATCH 017/126] Remove coord polar notebook --- notebooks/coord-polar.qmd | 89 --------------------------------------- 1 file changed, 89 deletions(-) delete mode 100644 notebooks/coord-polar.qmd diff --git a/notebooks/coord-polar.qmd b/notebooks/coord-polar.qmd deleted file mode 100644 index 6b999bbb19..0000000000 --- a/notebooks/coord-polar.qmd +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: "Polar Coordinates with `coord_polar()`" -execute: - warning: false ---- - -```{python} -from datetime import date - -import polars as pl -import plotnine_polars as p9 -import socviz_pl as sv -from plotnine_polars import aes -``` - -## FARS pedestrian data - -The `farsinvolved` dataset records daily counts of child pedestrians (aged 0–17) -involved in fatal motor vehicle crashes in the United States from 2009 to 2023. -We aggregate by calendar month and day, averaging across years. -Year 2000 is used as a placeholder date because 2000 was a leap year. - -```{python} -farsinvolved = sv.load_data("farsinvolved") - -month_order = [ - "January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December", -] -month_num = {m: i + 1 for i, m in enumerate(month_order)} - -fars_agg = ( - farsinvolved - .with_columns(pl.col("day").cast(pl.Int32)) - .group_by("month", "day") - .agg(n=pl.col("n").mean()) - .with_columns( - month_num=pl.col("month").replace(month_num).cast(pl.Int32), - ) - .with_columns( - fake_yr=pl.date(2000, pl.col("month_num"), pl.col("day")), - flag=(pl.col("month") == "October") & (pl.col("day") == 31), - ) - .sort("fake_yr") -) -``` - -Halloween (October 31) stands out as the single most dangerous day for child pedestrians. - -## Polar chart - -`coord_polar()` maps x to angle and y to radius. -Each circle below is one calendar day; the lone orange circle is Halloween. - -```{python} -#| label: fig-coord-polar-fars -#| fig-cap: "Pedestrians aged 0–17 in Fatal Motor Vehicle Crashes. Daily average, 2009–2023." -plot_data = fars_agg.with_columns(doy=pl.col("fake_yr").dt.ordinal_day()) -halloween = plot_data.filter(pl.col("flag")) - -n_label = plot_data["n"].max() + 0.5 -month_starts = pl.DataFrame({ - "doy": [date(2000, m, 15).timetuple().tm_yday for m in range(1, 13)], - "label": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], - "n": [n_label] * 12, -}) - -( - plot_data - .ggplot(aes(x="doy", y="n")) - .geom_point(shape="o", size=1.5, color="#222222", fill="white", stroke=0.4) - .geom_point(data=halloween, mapping=aes(x="doy", y="n"), - size=4, color="#E06000", inherit_aes=False) - .geom_text(data=month_starts, mapping=aes(x="doy", y="n", label="label"), - size=7, color="#444444", inherit_aes=False) - .coord_polar() - .labs( - title="Pedestrians aged 0–17 in Fatal Motor Vehicle Crashes", - subtitle="Daily average, 2009–2023", - ) - .add_theme( - axis_title=p9.element_blank(), - axis_text=p9.element_blank(), - axis_ticks=p9.element_blank(), - figure_size=(6, 6), - ) -) -``` From 0ad26b5b606270e10ba09fd374368b5ab8e86d01 Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Wed, 27 May 2026 13:39:47 -0400 Subject: [PATCH 018/126] Move projection selection to coord classes --- plotnine/coords/coord.py | 3 +++ plotnine/coords/coord_polar.py | 1 + plotnine/facets/facet.py | 8 +++----- tests/test_coord_polar.py | 17 +++++++++++++++++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/plotnine/coords/coord.py b/plotnine/coords/coord.py index 8e7924167d..ff7360b80d 100644 --- a/plotnine/coords/coord.py +++ b/plotnine/coords/coord.py @@ -119,6 +119,9 @@ class coord: Base class for all coordinate systems """ + # Matplotlib projection name to use when creating panel axes. + _projection: str | None = None + # If the coordinate system is linear is_linear = False diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py index d9519b8f6f..489a8da57e 100644 --- a/plotnine/coords/coord_polar.py +++ b/plotnine/coords/coord_polar.py @@ -65,6 +65,7 @@ class coord_polar(coord): """ is_linear = False + _projection = "polar" def __init__( self, diff --git a/plotnine/facets/facet.py b/plotnine/facets/facet.py index 20d04e4967..842681bd3c 100644 --- a/plotnine/facets/facet.py +++ b/plotnine/facets/facet.py @@ -342,17 +342,15 @@ def _make_axes(self) -> tuple[p9GridSpec, list[Axes]]: """ Create and return subplot axes """ - from ..coords.coord_polar import coord_polar - num_panels = len(self.layout.layout) axsarr = np.empty((self.nrow, self.ncol), dtype=object) gs = self._make_gridspec() # Create axes projection = ( - "polar" - if isinstance(self.plot.coordinates, coord_polar) - else p9Axes.name + p9Axes.name + if self.plot.coordinates._projection is None + else self.plot.coordinates._projection ) it = itertools.product(range(self.nrow), range(self.ncol)) for i, (row, col) in enumerate(it): diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index fd1d169625..0183a6def3 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -9,9 +9,11 @@ element_blank, element_line, geom_col, + geom_point, ggplot, theme, ) +from plotnine.coords.coord_cartesian import coord_cartesian from plotnine.coords.coord_polar import coord_polar from plotnine.scales import scale_x_continuous, scale_y_continuous @@ -182,6 +184,21 @@ def test_coord_polar_draw_uses_polar_axes_and_hides_blank_border(): plt.close(fig) +def test_coord_projection_creates_projected_axes(): + class coord_custom(coord_cartesian): + _projection = "polar" + + data = pd.DataFrame({"x": [1, 2], "y": [1, 2]}) + p = ggplot(data, aes("x", "y")) + geom_point() + coord_custom() + + fig = p.draw() + + try: + assert fig.axes[0].name == "polar" + finally: + plt.close(fig) + + def test_coord_radial_arc_uses_end_or_direction(): assert coord_radial(start=1, end=4)._arc == 3 assert coord_radial(direction=-1)._arc == -2 * np.pi From 530cfbc510e41968b05d19b6789d0ad1d1fe6087 Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Wed, 27 May 2026 13:49:31 -0400 Subject: [PATCH 019/126] Move axes setup to coord classes --- plotnine/coords/coord.py | 8 -------- plotnine/coords/coord_radial.py | 12 ++++++++---- tests/test_coord_polar.py | 17 +++++++++++------ 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/plotnine/coords/coord.py b/plotnine/coords/coord.py index ff7360b80d..7d281c11f6 100644 --- a/plotnine/coords/coord.py +++ b/plotnine/coords/coord.py @@ -310,14 +310,6 @@ def draw(self, axs: list) -> None: add elements such as polar grid lines. """ - def post_setup_ax(self, ax: Any) -> None: - """ - Hook called for each axes after set_limits_breaks_and_labels. - - Override in subclasses to apply per-axes settings that must - run after the facet has set tick positions and label padding. - """ - def labels(self, cur_labels: labels_view) -> labels_view: """ Modify labels diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 302d4624c8..6a06d5a3a1 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -12,6 +12,7 @@ from matplotlib.axes import Axes from matplotlib.projections.polar import PolarAxes + from plotnine import theme from plotnine.iapi import panel_view from plotnine.scales.scale import scale @@ -221,8 +222,8 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: x_updates["labels"] = theta_labels # Partial arc: x panel range must match [arc_lo, arc_hi] so that - # set_limits_breaks_and_labels calls ax.set_xlim(arc_lo, arc_hi) rather - # than ax.set_xlim(0, 2π), which would override set_thetalim. + # coord.setup_ax calls ax.set_xlim(arc_lo, arc_hi) rather than + # ax.set_xlim(0, 2π), which would override set_thetalim. if arc_lo is not None: x_updates["limits"] = (arc_lo, arc_hi) x_updates["range"] = (arc_lo, arc_hi) @@ -324,10 +325,13 @@ def draw(self, axs: list[Axes]) -> None: np.degrees(float(self.r_axis_inside)) ) - def post_setup_ax(self, ax: Axes) -> None: + def setup_ax( + self, ax: Axes, panel_params: panel_view, theme: theme + ) -> None: """ - Apply theta label pad after facet has set tick positions and padding. + Apply theta label pad after setting tick positions and padding. """ + super().setup_ax(ax, panel_params, theme) if self.theta_labels or self.end is not None: ax.tick_params(axis="x", pad=self.theta_label_pad) # Allow geom_text labels to extend past the polar axes bounding box diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index 0183a6def3..8fc0815223 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -10,6 +10,7 @@ element_line, geom_col, geom_point, + geom_text, ggplot, theme, ) @@ -291,14 +292,18 @@ def test_coord_radial_draw_float_r_axis_position(): plt.close(fig) -def test_coord_radial_post_setup_ax_sets_pad_and_unclips_text(): - coord = coord_radial(theta_label_pad=17, theta_labels=True) - fig, ax = plt.subplots(subplot_kw={"projection": "polar"}) - text = ax.text(0, 1, "label", clip_on=True) +def test_coord_radial_setup_ax_sets_pad_and_unclips_text(): + data = pd.DataFrame({"x": [1], "y": [1], "label": ["label"]}) + p = ( + ggplot(data, aes("x", "y", label="label")) + + geom_text() + + coord_radial(theta_label_pad=17, theta_labels=True) + ) + fig = p.draw() try: - coord.post_setup_ax(ax) + ax = fig.axes[0] assert ax.xaxis.get_tick_params()["pad"] == 17 - assert not text.get_clip_on() + assert not ax.texts[0].get_clip_on() finally: plt.close(fig) From 05a1a4b531d762bf5a654fc1cc2790c04f41bd1b Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Wed, 27 May 2026 13:52:36 -0400 Subject: [PATCH 020/126] Move polar border blanking to themeable --- plotnine/ggplot.py | 5 ----- plotnine/themes/themeable.py | 5 +++++ tests/test_theme.py | 13 +++++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/plotnine/ggplot.py b/plotnine/ggplot.py index 65a516030b..ce7505fe0b 100755 --- a/plotnine/ggplot.py +++ b/plotnine/ggplot.py @@ -520,11 +520,6 @@ def _draw_panel_borders(self): # grid lines below the borders. We leave ax.patch for the # background only. if self.theme.T.is_blank("panel_border"): - # For PolarAxes the default circular spine is separate from the - # panel border Rectangle; hide it explicitly when blank. - for ax in self.axs: - if "polar" in ax.spines: - ax.spines["polar"].set_visible(False) return from matplotlib.patches import Rectangle diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index f0e9420d57..3bc53853c1 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -1943,6 +1943,11 @@ def blank_figure(self, figure: Figure, targets: ThemeTargets): for rect in targets.panel_border: rect.set_visible(False) + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + if "polar" in ax.spines: + ax.spines["polar"].set_visible(False) + class plot_background(themeable): """ diff --git a/tests/test_theme.py b/tests/test_theme.py index c5f025eae5..f1248076d0 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -2,6 +2,7 @@ import matplotlib as mpl import pytest +from matplotlib import pyplot as plt from packaging import version from plotnine import ( @@ -34,6 +35,7 @@ theme_xkcd, ) from plotnine.data import mtcars +from plotnine.themes.themeable import panel_border LT_MPL310 = version.parse(mpl.__version__) < version.parse("3.10") IS_CI = bool(os.environ.get("CI")) @@ -111,6 +113,17 @@ def test_add_element_blank(): assert theme3 == theme4 # blanking cleans the slate +def test_blank_panel_border_hides_polar_spine(): + th = panel_border(element_blank()) + fig, ax = plt.subplots(subplot_kw={"projection": "polar"}) + + try: + th.blank_ax(ax) + assert not ax.spines["polar"].get_visible() + finally: + plt.close(fig) + + def test_element_line_dashed_capstyle(): p = ggplot(mtcars, aes(x="wt", y="mpg")) + theme( panel_grid=element_line( From 65a3d699e81bfd00784c8feeae86161acfe82db9 Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Wed, 27 May 2026 14:15:51 -0400 Subject: [PATCH 021/126] Document custom themeable extension path --- plotnine/themes/theme.py | 3 +++ plotnine/themes/themeable.py | 7 ++++-- tests/test_theme.py | 45 +++++++++++++++++++++++++++++++++++- 3 files changed, 52 insertions(+), 3 deletions(-) diff --git a/plotnine/themes/theme.py b/plotnine/themes/theme.py index 13d3247a72..5a0bd22009 100644 --- a/plotnine/themes/theme.py +++ b/plotnine/themes/theme.py @@ -85,6 +85,9 @@ class theme: These simply bind together all the aspects of a themeable that can be themed. See [](`~plotnine.themes.themeable.themeable`). + Extension packages may provide additional themeables by defining + and importing subclasses of `themeable`, then passing values for + them as keyword arguments to `theme`. Notes ----- diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index 3bc53853c1..35ee1d2fc3 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -99,8 +99,11 @@ class axis_title(axis_title_x, axis_title_y): Notes ----- - A user should never create instances of class - [](`~plotnine.themes.themeable.Themeable`) or subclasses of it. + Most users should not create instances of class + [](`~plotnine.themes.themeable.themeable`) or subclasses of it + directly. Extension authors may define subclasses; they are registered + by class name when their module is imported and can be used through + [](`~plotnine.themes.theme.theme`) keyword arguments. """ def __init__(self, theme_element: element_base | str | float): diff --git a/tests/test_theme.py b/tests/test_theme.py index f1248076d0..335f905123 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -35,7 +35,8 @@ theme_xkcd, ) from plotnine.data import mtcars -from plotnine.themes.themeable import panel_border +from plotnine.coords.coord_cartesian import coord_cartesian +from plotnine.themes.themeable import panel_border, themeable LT_MPL310 = version.parse(mpl.__version__) < version.parse("3.10") IS_CI = bool(os.environ.get("CI")) @@ -124,6 +125,48 @@ def test_blank_panel_border_hides_polar_spine(): plt.close(fig) +def test_extension_themeable_applies_from_theme_kwargs(): + class test_extension_panel_facecolor(themeable): + def apply_ax(self, ax): + super().apply_ax(ax) + ax.set_facecolor(self.properties["value"]) + + p = ( + ggplot(mtcars, aes(x="wt", y="mpg")) + + geom_point() + + theme(test_extension_panel_facecolor="red") + ) + + fig = p.draw() + try: + assert fig.axes[0].get_facecolor() == (1.0, 0.0, 0.0, 1.0) + finally: + plt.close(fig) + + +def test_coord_can_read_extension_themeable(): + class test_extension_coord_title(themeable): + pass + + class coord_reads_themeable(coord_cartesian): + def setup_ax(self, ax, panel_params, theme): + super().setup_ax(ax, panel_params, theme) + ax.set_title(theme.getp("test_extension_coord_title")) + + p = ( + ggplot(mtcars, aes(x="wt", y="mpg")) + + geom_point() + + coord_reads_themeable() + + theme(test_extension_coord_title="coord themeable") + ) + + fig = p.draw() + try: + assert fig.axes[0].get_title() == "coord themeable" + finally: + plt.close(fig) + + def test_element_line_dashed_capstyle(): p = ggplot(mtcars, aes(x="wt", y="mpg")) + theme( panel_grid=element_line( From 0a2cf796b41231ba17031d8adcbe818ff2322b5a Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Wed, 27 May 2026 14:28:25 -0400 Subject: [PATCH 022/126] Added `guide_axis_theta()` (initial pass). --- plotnine/__init__.py | 2 ++ plotnine/coords/coord_radial.py | 14 ++++++++++ plotnine/guides/__init__.py | 9 ++++++- plotnine/guides/guide_axis_theta.py | 40 +++++++++++++++++++++++++++++ plotnine/guides/guides.py | 12 ++++++++- tests/test_coord_polar.py | 24 +++++++++++++++++ 6 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 plotnine/guides/guide_axis_theta.py diff --git a/plotnine/__init__.py b/plotnine/__init__.py index 98507e5ece..4b92a0b8c4 100644 --- a/plotnine/__init__.py +++ b/plotnine/__init__.py @@ -90,6 +90,7 @@ save_as_pdf_pages, ) from .guides import ( + guide_axis_theta, guide_colorbar, guide_colourbar, guide_legend, @@ -352,6 +353,7 @@ "ggplot", "ggsave", "ggtitle", + "guide_axis_theta", "guide_colorbar", "guide_colourbar", "guide_legend", diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 6a06d5a3a1..64dd2b3f39 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -334,7 +334,21 @@ def setup_ax( super().setup_ax(ax, panel_params, theme) if self.theta_labels or self.end is not None: ax.tick_params(axis="x", pad=self.theta_label_pad) + if (angle := self._theta_guide_angle(theme)) is not None: + ax.tick_params(axis="x", labelrotation=angle) + for label in ax.get_xticklabels(): + label.set_rotation(angle) + label.set_rotation_mode("anchor") # Allow geom_text labels to extend past the polar axes bounding box # (e.g. spoke labels placed just beyond the outermost bar tip). for text in ax.texts: text.set_clip_on(False) + + @staticmethod + def _theta_guide_angle(theme: theme) -> float | None: + """ + Return the angle from guides(theta=guide_axis_theta(...)). + """ + guide = getattr(getattr(theme, "owner", None), "guides", None) + guide = getattr(guide, "theta", None) + return getattr(guide, "angle", None) diff --git a/plotnine/guides/__init__.py b/plotnine/guides/__init__.py index d3d18e3b2c..690a8ef0cc 100644 --- a/plotnine/guides/__init__.py +++ b/plotnine/guides/__init__.py @@ -1,5 +1,12 @@ +from .guide_axis_theta import guide_axis_theta from .guide_colorbar import guide_colorbar, guide_colourbar from .guide_legend import guide_legend from .guides import guides -__all__ = ("guide_colorbar", "guide_colourbar", "guide_legend", "guides") +__all__ = ( + "guide_axis_theta", + "guide_colorbar", + "guide_colourbar", + "guide_legend", + "guides", +) diff --git a/plotnine/guides/guide_axis_theta.py b/plotnine/guides/guide_axis_theta.py new file mode 100644 index 0000000000..9435ae85ae --- /dev/null +++ b/plotnine/guides/guide_axis_theta.py @@ -0,0 +1,40 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +from ..themes.theme import theme as Theme + + +@dataclass +class guide_axis_theta: + """ + Theta-axis guide for radial coordinates. + + This guide is consumed by ``coord_radial`` when it draws theta-axis + tick labels. It is not drawn as a legend or colorbar. + """ + + title: str | None = None + """Title of the guide. Currently unused.""" + + theme: Theme = field(default_factory=Theme) + """A theme to style the guide. Currently unused.""" + + angle: float | None = None + """ + Angle in degrees at which to draw theta-axis tick labels. + + If ``None``, Matplotlib's default theta tick label rotation is used. + """ + + minor_ticks: bool | None = None + """Whether to draw minor ticks. Currently unused.""" + + cap: bool | None = None + """Whether to cap the axis line. Currently unused.""" + + order: int = 0 + """Order of this guide among multiple guides. Currently unused.""" + + position: str | None = None + """Guide position. Currently unused.""" diff --git a/plotnine/guides/guides.py b/plotnine/guides/guides.py index d01d6bd7d3..106b0bb599 100644 --- a/plotnine/guides/guides.py +++ b/plotnine/guides/guides.py @@ -27,7 +27,13 @@ from matplotlib.figure import Figure from matplotlib.offsetbox import OffsetBox, PackerBase - from plotnine import ggplot, guide_colorbar, guide_legend, theme + from plotnine import ( + ggplot, + guide_axis_theta, + guide_colorbar, + guide_legend, + theme, + ) from plotnine._mpl.offsetbox import FlexibleAnchoredOffsetbox from plotnine.composition import Compose from plotnine.iapi import labels_view @@ -47,6 +53,7 @@ guide_legend | guide_colorbar | Literal["legend", "colorbar"] ) LegendOnly: TypeAlias = guide_legend | Literal["legend"] + ThetaGuide: TypeAlias = guide_axis_theta class LegendOwner(Protocol): """ @@ -116,6 +123,9 @@ class guides: colour: Optional[LegendOnly | NoGuide] = None """Guide for colour scale.""" + theta: Optional[ThetaGuide | NoGuide] = None + """Guide for theta axis labels in radial coordinates.""" + def __post_init__(self): self.plot: ggplot self.plot_scales: Scales diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index 8fc0815223..60aeb964f1 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -12,6 +12,8 @@ geom_point, geom_text, ggplot, + guide_axis_theta, + guides, theme, ) from plotnine.coords.coord_cartesian import coord_cartesian @@ -307,3 +309,25 @@ def test_coord_radial_setup_ax_sets_pad_and_unclips_text(): assert not ax.texts[0].get_clip_on() finally: plt.close(fig) + + +def test_coord_radial_uses_guide_axis_theta_angle(): + data = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) + p = ( + ggplot(data, aes("x", "y")) + + geom_point() + + coord_radial(theta_labels=True) + + guides(theta=guide_axis_theta(angle=35)) + ) + + fig = p.draw() + try: + rotations = [ + label.get_rotation() + for label in fig.axes[0].get_xticklabels() + if label.get_text() + ] + assert rotations + assert rotations == [35] * len(rotations) + finally: + plt.close(fig) From 99921cad2e2d2bdda148778cffd171e447b96420 Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Wed, 27 May 2026 15:12:11 -0400 Subject: [PATCH 023/126] fix guide_axis_theta: use 'auto' mode so angle is relative to tangent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In Matplotlib, ThetaTick has two label-rotation modes: ('default', N) — absolute N degrees (what tick_params(labelrotation=N) sets) ('auto', N) — tangential direction + N degrees offset ggplot2's guide_axis_theta(angle=0) means labels tangential to the arc (0° offset from the tangent), not horizontal. Patching tick._labelrotation directly is the only way to set 'auto' mode with a user-angle offset since ax.tick_params() always sets 'default' mode. --- plotnine/coords/coord_radial.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 64dd2b3f39..c54e1e9bdb 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -335,10 +335,13 @@ def setup_ax( if self.theta_labels or self.end is not None: ax.tick_params(axis="x", pad=self.theta_label_pad) if (angle := self._theta_guide_angle(theme)) is not None: - ax.tick_params(axis="x", labelrotation=angle) - for label in ax.get_xticklabels(): - label.set_rotation(angle) - label.set_rotation_mode("anchor") + # Use Matplotlib's 'auto' mode so labels orient tangentially + # to the arc, with `angle` as an offset — matching ggplot2's + # guide_axis_theta() semantics where angle=0 means tangential. + # ax.tick_params(labelrotation=...) always sets 'default' mode + # (absolute degrees), so we patch each tick directly instead. + for tick in ax.xaxis.get_major_ticks(): + tick._labelrotation = ("auto", angle) # Allow geom_text labels to extend past the polar axes bounding box # (e.g. spoke labels placed just beyond the outermost bar tip). for text in ax.texts: From c71e9d4f1d258fba347f58fba864d409d059ad54 Mon Sep 17 00:00:00 2001 From: Ian Gow Date: Wed, 27 May 2026 20:06:26 -0400 Subject: [PATCH 024/126] Document guide_axis_theta semantics --- plotnine/guides/guide_axis_theta.py | 88 ++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 8 deletions(-) diff --git a/plotnine/guides/guide_axis_theta.py b/plotnine/guides/guide_axis_theta.py index 9435ae85ae..3af7003228 100644 --- a/plotnine/guides/guide_axis_theta.py +++ b/plotnine/guides/guide_axis_theta.py @@ -10,8 +10,78 @@ class guide_axis_theta: """ Theta-axis guide for radial coordinates. - This guide is consumed by ``coord_radial`` when it draws theta-axis - tick labels. It is not drawn as a legend or colorbar. + A specialized guide used in :class:`~plotnine.coords.coord_radial` to + control how tick labels are rendered on the theta (angular) axis. + Unlike legend-style guides, this guide is not drawn as a box or colorbar; + it is consumed directly by ``coord_radial`` when it positions and rotates + the outer tick labels. + + Parameters + ---------- + title : + Title for the guide. Currently unused; theta-axis labels do not + display a title in Matplotlib's polar axes. + theme : + A :class:`~plotnine.themes.theme.theme` object to style the guide + individually. Currently unused. + angle : + Orientation of the tick labels, in degrees, measured as an offset + from the **tangential direction** at each tick position. + + * ``None`` (default) - labels are not rotated by this guide; the + Matplotlib default (horizontal, i.e. ``0`` absolute) is used. + * ``0`` - labels are oriented tangentially, parallel to the arc. + This matches the ggplot2 heuristic default and is the most common + choice. + * ``90`` - labels point radially outward along the spoke. + * Any other float - a corresponding offset from the tangential + direction. + + .. note:: + This differs from Matplotlib's ``tick_params(labelrotation=N)``, + which treats ``N`` as an **absolute** angle. Here ``angle`` is + always relative to the tangent at each label's position. + + minor_ticks : + Whether to draw minor ticks. Not yet implemented; the value is + stored but has no effect. + cap : + Whether to cap the axis line back to the outermost breaks. Not + yet implemented; the value is stored but has no effect. + order : + Order of this guide among multiple guides. Not yet implemented. + position : + Guide position (``"top"``, ``"bottom"``, ``"left"``, or + ``"right"``). Not yet implemented. + + See Also + -------- + :class:`~plotnine.coords.coord_radial` : The coordinate system that + consumes this guide. + plotnine.guides : Add guides to a plot with ``guides(theta=...)``. + + Examples + -------- + Make theta labels follow the arc (tangential, matching ggplot2's default + heuristic): + + .. code-block:: python + + from plotnine import ggplot, aes, geom_point, coord_radial, guides + from plotnine.guides import guide_axis_theta + from plotnine.data import mtcars + + ( + ggplot(mtcars, aes("disp", "mpg")) + + geom_point() + + coord_radial(theta_labels=True) + + guides(theta=guide_axis_theta(angle=0)) + ) + + .. note:: + ``coord_radial`` hides theta labels on full-circle plots by default + (``theta_labels=False``). You must pass ``theta_labels=True`` to + ``coord_radial`` for this guide to have any visible effect. """ title: str | None = None @@ -22,19 +92,21 @@ class guide_axis_theta: angle: float | None = None """ - Angle in degrees at which to draw theta-axis tick labels. + Offset from the tangential direction in degrees. - If ``None``, Matplotlib's default theta tick label rotation is used. + ``None`` keeps Matplotlib's default (horizontal labels). + ``0`` makes labels tangential (parallel to the arc). + ``90`` makes labels radial (pointing outward). """ minor_ticks: bool | None = None - """Whether to draw minor ticks. Currently unused.""" + """Whether to draw minor ticks. Not yet implemented.""" cap: bool | None = None - """Whether to cap the axis line. Currently unused.""" + """Whether to cap the axis line. Not yet implemented.""" order: int = 0 - """Order of this guide among multiple guides. Currently unused.""" + """Order of this guide among multiple guides. Not yet implemented.""" position: str | None = None - """Guide position. Currently unused.""" + """Guide position. Not yet implemented.""" From b373f12af5cf626c381da02c46cd0e15416ce162 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 17 Jun 2026 19:09:16 +0300 Subject: [PATCH 025/126] fix(coord_polar): swap axis titles when theta='y' --- plotnine/coords/coord_polar.py | 12 +++++++++++- tests/test_coord_polar.py | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py index 489a8da57e..4d30362f84 100644 --- a/plotnine/coords/coord_polar.py +++ b/plotnine/coords/coord_polar.py @@ -13,7 +13,7 @@ from matplotlib.axes import Axes from matplotlib.projections.polar import PolarAxes - from plotnine.iapi import panel_view + from plotnine.iapi import labels_view, panel_view from plotnine.scales.scale import scale @@ -127,6 +127,16 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: return replace(pv_exp, x=new_x, y=new_y) + def labels(self, cur_labels: labels_view) -> labels_view: + # When theta="y" the data x/y columns are swapped in transform so that + # PolarAxes sees x=theta, y=r. Swap the axis titles to match, the same + # way coord_flip does for its flipped axes. + if self.theta == "y": + from .coord_flip import flip_labels + + return flip_labels(super().labels(cur_labels)) + return super().labels(cur_labels) + # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index 60aeb964f1..19c56d7cce 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -18,6 +18,7 @@ ) from plotnine.coords.coord_cartesian import coord_cartesian from plotnine.coords.coord_polar import coord_polar +from plotnine.iapi import labels_view from plotnine.scales import scale_x_continuous, scale_y_continuous @@ -168,6 +169,20 @@ def test_coord_polar_aspect_is_square(): assert coord_polar().aspect(None) == 1 +def test_coord_polar_swaps_labels_when_theta_y(): + coord = coord_polar(theta="y") + out = coord.labels(labels_view(x="xlab", y="ylab")) + assert out.x == "ylab" + assert out.y == "xlab" + + +def test_coord_polar_keeps_labels_when_theta_x(): + coord = coord_polar(theta="x") + out = coord.labels(labels_view(x="xlab", y="ylab")) + assert out.x == "xlab" + assert out.y == "ylab" + + def test_coord_polar_draw_uses_polar_axes_and_hides_blank_border(): data = pd.DataFrame({"x": ["a", "b"], "y": [1, 2]}) p = ( From 0ec324e9538484a1bdc267b5a273f77ace78e120 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 17 Jun 2026 19:09:29 +0300 Subject: [PATCH 026/126] fix(coord_polar): correct polar coordinate setup - apply direction once via set_theta_direction so the default is clockwise and direction=-1 reverses - derive per-panel r-limits from setup_ax instead of cached shared state - default the radial axis to the left - recompute thetalim/rlim breaks over the zoomed range - route theta-axis text through the theme: drop theta_labels and theta_label_pad (theta labels show by default, controlled via theme(axis_text_x=...)) and guard axis_text_y on PolarAxes --- plotnine/coords/coord_polar.py | 16 +-- plotnine/coords/coord_radial.py | 175 ++++++++++++++-------------- plotnine/guides/guide_axis_theta.py | 7 +- plotnine/themes/themeable.py | 1 - tests/test_coord_polar.py | 136 ++++++--------------- tests/test_theme.py | 15 +-- 6 files changed, 140 insertions(+), 210 deletions(-) diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py index 4d30362f84..4f29835deb 100644 --- a/plotnine/coords/coord_polar.py +++ b/plotnine/coords/coord_polar.py @@ -148,7 +148,9 @@ def _to_radians(self, vals: np.ndarray) -> np.ndarray: if denom == 0: return np.zeros_like(vals, dtype=float) norm = (np.asarray(vals, dtype=float) - float(t_min)) / denom - return self.start + self.direction * norm * 2.0 * np.pi + # Rotation direction is a PolarAxes property set in draw via + # set_theta_direction; it is not baked into these radian values. + return self.start + norm * 2.0 * np.pi # ------------------------------------------------------------------ # Data transformation @@ -236,18 +238,16 @@ def backtransform_range(self, panel_params: panel_view) -> panel_ranges: # ------------------------------------------------------------------ def draw(self, axs: list[Axes]) -> None: - """Configure each PolarAxes: zero location, direction, r limits.""" - r_min, r_max = self.params.get("r_range", (0.0, 1.0)) + """Configure each PolarAxes: zero location and rotation direction - # Matplotlib PolarAxes theta_direction: -1 = clockwise, 1 = counter-CW. + R-limits are set per panel by setup_ax. + """ + # PolarAxes theta_direction: -1 = clockwise, +1 = counter-clockwise. mpl_direction = -1 if self.direction == 1 else 1 - for ax in axs: polar_ax = cast("PolarAxes", ax) - polar_ax.set_theta_zero_location("N") # 12 o'clock = 0 + polar_ax.set_theta_zero_location("N") # 12 o'clock polar_ax.set_theta_direction(mpl_direction) - if np.isfinite(r_min) and np.isfinite(r_max) and r_min != r_max: - polar_ax.set_rlim(float(r_min), float(r_max)) # ------------------------------------------------------------------ # Misc diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index c54e1e9bdb..dddaf41c3f 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import replace -from typing import TYPE_CHECKING, Sequence, cast +from typing import TYPE_CHECKING, cast import numpy as np @@ -69,17 +69,14 @@ class coord_radial(coord_polar): Data-space limits for the r axis as ``(lo, hi)``. Only data within this range is shown; equivalent to zooming on the radial axis. ``None`` (default) uses the full data range. - theta_labels : - If ``True``, show theta axis tick labels on the outer edge of the - circle for full-circle plots, using the breaks and labels from the - theta scale. Default ``False``. Partial-arc plots always show - theta labels (filtered to the visible arc) regardless of this flag. - theta_label_pad : - Distance in points between the outer circle spine and the theta tick - labels. Default ``8``. Only applied when theta labels are shown. Notes ----- + Theta-axis tick labels are shown by default, matching ggplot2. Since a + polar axes' x-axis *is* the theta axis, they are styled through the + theme: hide them with ``theme(axis_text_x=element_blank())`` and adjust + the gap to the outer circle through the ``axis_text_x`` margin. + The Python API uses snake_case names for arguments that are dotted in ggplot2: ``inner_radius``, ``r_axis_inside``, and ``rotate_angle``. @@ -137,8 +134,6 @@ def __init__( rotate_angle: bool = False, thetalim: tuple[float, float] | None = None, rlim: tuple[float, float] | None = None, - theta_labels: bool = False, - theta_label_pad: float = 8, ) -> None: super().__init__( theta=theta, @@ -152,8 +147,6 @@ def __init__( self.rotate_angle = rotate_angle self.thetalim = thetalim self.rlim = rlim - self.theta_labels = theta_labels - self.theta_label_pad = theta_label_pad # ------------------------------------------------------------------ # Panel params @@ -176,25 +169,36 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: pv = super().setup_panel_params(scale_x, scale_y) # thetalim: zoom the theta data range — only this slice maps to the - # arc. + # arc. Recompute nice breaks over the zoomed range so labels are not + # reduced to sparse endpoints, matching ggplot2. if self.thetalim is not None: self.params["theta_range"] = tuple(self.thetalim) - - # rlim: zoom the r data range — update params, panel view y axis, and - # filter breaks/labels to within rlim so set_yticks doesn't force the - # PolarAxes r-axis to expand beyond the requested limits. + theta_scale = scale_x if self.theta == "x" else scale_y + theta_breaks = [ + b + for b in theta_scale.get_bounded_breaks(self.thetalim) + if np.isfinite(b) + ] + theta_labels = list(theta_scale.get_labels(theta_breaks)) + + # rlim: zoom the r data range — update params and recompute nice breaks + # over the zoomed range (rather than filtering the full-range breaks, + # which leaves sparse endpoint-only labels), matching ggplot2. if self.rlim is not None: self.params["r_range"] = tuple(self.rlim) - rlo, rhi = self.rlim - breaks = cast("Sequence[float]", pv.y.breaks) - labels = pv.y.labels - mask = [rlo <= b <= rhi for b in breaks] + r_scale = scale_y if self.theta == "x" else scale_x + breaks = [ + b + for b in r_scale.get_bounded_breaks(self.rlim) + if np.isfinite(b) + ] + labels = r_scale.get_labels(breaks) new_y = replace( pv.y, limits=tuple(self.rlim), range=tuple(self.rlim), - breaks=[b for b, m in zip(breaks, mask) if m], - labels=[l for l, m in zip(labels, mask) if m], + breaks=breaks, + labels=labels, ) pv = replace(pv, y=new_y) @@ -206,11 +210,10 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: arc_hi = max(self.start, self.start + arc) # Convert data-space theta breaks to radian positions and restore them - # as theta axis tick labels on the outer edge. Always done for partial - # arcs; for full circles only when theta_labels=True (opt-in, so that - # pac-man / coxcomb charts keep breaks=[] as set by super()). + # as theta axis tick labels on the outer edge. Theta labels show by + # default (matching ggplot2); hide them via theme(axis_text_x=...). x_updates: dict = {} - if theta_breaks and (arc_lo is not None or self.theta_labels): + if theta_breaks: radian_pos = list( self._to_radians(np.asarray(theta_breaks, dtype=float)) ) @@ -240,13 +243,16 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: @property def _arc(self) -> float: """ - Total arc in radians. + Total angular span of the plotted region, in radians - A positive value represents clockwise movement when ``direction=1``. + For a partial radial plot this is the distance between ``start`` and + ``end``; otherwise it is a full turn. The sense of rotation + (clockwise vs counter-clockwise) is a PolarAxes property applied at + draw time and is not encoded in this magnitude. """ if self.end is not None: return self.end - self.start - return self.direction * 2.0 * np.pi + return 2.0 * np.pi def _to_radians(self, vals: np.ndarray) -> np.ndarray: """Normalize theta values to [start, start + arc].""" @@ -282,66 +288,65 @@ def transform( # Draw decorations on PolarAxes # ------------------------------------------------------------------ - def draw(self, axs: list[Axes]) -> None: - """Configure PolarAxes: arc limits, inner radius, axis placement.""" - super().draw(axs) - - r_min, r_max = self.params.get("r_range", (0.0, 1.0)) - arc = self._arc - - for ax in axs: - polar_ax = cast("PolarAxes", ax) - # Restrict visible theta range for partial arcs. - if self.end is not None: - theta_lo = min(self.start, self.start + arc) - theta_hi = max(self.start, self.start + arc) - polar_ax.set_thetalim(theta_lo, theta_hi) - - # Inner radius: push the data away from the centre by setting a - # virtual r-origin below r_min. Formula: solve - # inner_radius = (r_min - r_origin) / (r_max - r_origin) - if ( - self.inner_radius > 0 - and np.isfinite(r_min) - and np.isfinite(r_max) - and r_max > r_min - and self.inner_radius < 1.0 - ): - r_origin = (r_min - self.inner_radius * r_max) / ( - 1.0 - self.inner_radius - ) - polar_ax.set_rorigin(r_origin) - - # Radial axis label placement. - if self.r_axis_inside is not None: - if isinstance(self.r_axis_inside, bool): - if self.r_axis_inside: - # Just inside the start angle keeps it out of the data. - polar_ax.set_rlabel_position( - np.degrees(self.start) + 10 - ) - else: - polar_ax.set_rlabel_position( - np.degrees(float(self.r_axis_inside)) - ) - def setup_ax( self, ax: Axes, panel_params: panel_view, theme: theme ) -> None: """ - Apply theta label pad after setting tick positions and padding. + Configure each PolarAxes from this panel's limits + + Sets the arc limits, inner radius, and radial-axis placement using + ``panel_params`` so faceted panels with free scales each get their + own radial range. """ super().setup_ax(ax, panel_params, theme) - if self.theta_labels or self.end is not None: - ax.tick_params(axis="x", pad=self.theta_label_pad) - if (angle := self._theta_guide_angle(theme)) is not None: - # Use Matplotlib's 'auto' mode so labels orient tangentially - # to the arc, with `angle` as an offset — matching ggplot2's - # guide_axis_theta() semantics where angle=0 means tangential. - # ax.tick_params(labelrotation=...) always sets 'default' mode - # (absolute degrees), so we patch each tick directly instead. - for tick in ax.xaxis.get_major_ticks(): - tick._labelrotation = ("auto", angle) + polar_ax = cast("PolarAxes", ax) + arc = self._arc + + # Restrict visible theta range for partial arcs. + if self.end is not None: + polar_ax.set_thetalim( + min(self.start, self.start + arc), + max(self.start, self.start + arc), + ) + + # Inner radius: push the data away from the centre by setting a + # virtual r-origin below r_min. Formula: solve + # inner_radius = (r_min - r_origin) / (r_max - r_origin) + r_min, r_max = panel_params.y.range + if ( + self.inner_radius > 0 + and np.isfinite(r_min) + and np.isfinite(r_max) + and r_max > r_min + and self.inner_radius < 1.0 + ): + r_origin = (r_min - self.inner_radius * r_max) / ( + 1.0 - self.inner_radius + ) + polar_ax.set_rorigin(r_origin) + + # Radial axis label placement. + if self.r_axis_inside is None: + # Left spoke, matching ggplot2's default. + polar_ax.set_rlabel_position(270) + elif isinstance(self.r_axis_inside, bool): + if self.r_axis_inside: + # Just inside the start angle keeps it out of the data. + polar_ax.set_rlabel_position(np.degrees(self.start) + 10) + else: + polar_ax.set_rlabel_position(270) + else: + polar_ax.set_rlabel_position(np.degrees(float(self.r_axis_inside))) + + ax.tick_params(axis="x", which="major", direction="out") + if (angle := self._theta_guide_angle(theme)) is not None: + # Use Matplotlib's 'auto' mode so labels orient tangentially + # to the arc, with `angle` as an offset — matching ggplot2's + # guide_axis_theta() semantics where angle=0 means tangential. + # ax.tick_params(labelrotation=...) always sets 'default' mode + # (absolute degrees), so we patch each tick directly instead. + for tick in ax.xaxis.get_major_ticks(): + tick._labelrotation = ("auto", angle) # Allow geom_text labels to extend past the polar axes bounding box # (e.g. spoke labels placed just beyond the outermost bar tip). for text in ax.texts: diff --git a/plotnine/guides/guide_axis_theta.py b/plotnine/guides/guide_axis_theta.py index 3af7003228..e1d536423d 100644 --- a/plotnine/guides/guide_axis_theta.py +++ b/plotnine/guides/guide_axis_theta.py @@ -74,14 +74,13 @@ class guide_axis_theta: ( ggplot(mtcars, aes("disp", "mpg")) + geom_point() - + coord_radial(theta_labels=True) + + coord_radial() + guides(theta=guide_axis_theta(angle=0)) ) .. note:: - ``coord_radial`` hides theta labels on full-circle plots by default - (``theta_labels=False``). You must pass ``theta_labels=True`` to - ``coord_radial`` for this guide to have any visible effect. + ``coord_radial`` shows theta-axis labels by default. Hide them with + ``theme(axis_text_x=element_blank())`` if a plot needs no theta axis. """ title: str | None = None diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index 35ee1d2fc3..f661b87372 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -1208,7 +1208,6 @@ class axis_text_y(axis_text_y_left, axis_text_y_right): theme_element : element_text """ - class axis_text(axis_text_x, axis_text_y): """ Axis tick labels diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index 19c56d7cce..99ab67a3f7 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -10,10 +10,7 @@ element_line, geom_col, geom_point, - geom_text, ggplot, - guide_axis_theta, - guides, theme, ) from plotnine.coords.coord_cartesian import coord_cartesian @@ -68,6 +65,17 @@ def test_coord_polar_setup_panel_params_theta_y(): assert panel_params.y.breaks == [0, 2, 5, 10] +def test_coord_polar_setup_panel_params_per_panel_independent(): + # one instance reused across panels (faceting) + coord = coord_polar(expand=False) + sx1, sy1 = trained_scales(y=(0, 10)) + pv1 = coord.setup_panel_params(sx1, sy1) + sx2, sy2 = trained_scales(y=(0, 100)) + pv2 = coord.setup_panel_params(sx2, sy2) + assert pv1.y.range == (0, 10) + assert pv2.y.range == (0, 100) + + def test_coord_polar_to_radians_zero_width_range(): coord = coord_polar() coord.params = {"theta_range": (1, 1)} @@ -152,19 +160,6 @@ def test_coord_polar_distance_and_backtransform_theta_y(): assert coord.backtransform_range(None).y == (0, 10) -def test_coord_polar_draw_sets_polar_axis(): - coord = coord_polar(direction=-1) - coord.params = {"r_range": (2, 8)} - fig, ax = plt.subplots(subplot_kw={"projection": "polar"}) - - try: - coord.draw([ax]) - assert ax.get_theta_direction() == 1 - assert_allclose(ax.get_ylim(), (2, 8)) - finally: - plt.close(fig) - - def test_coord_polar_aspect_is_square(): assert coord_polar().aspect(None) == 1 @@ -217,9 +212,9 @@ class coord_custom(coord_cartesian): plt.close(fig) -def test_coord_radial_arc_uses_end_or_direction(): +def test_coord_radial_arc_uses_end_or_full_turn(): assert coord_radial(start=1, end=4)._arc == 3 - assert coord_radial(direction=-1)._arc == -2 * np.pi + assert coord_radial()._arc == 2 * np.pi def test_coord_radial_setup_panel_params_for_partial_arc(): @@ -247,17 +242,30 @@ def test_coord_radial_setup_panel_params_for_partial_arc(): assert panel_params.y.labels == ["2", "4", "8"] -def test_coord_radial_setup_panel_params_theta_y_with_labels(): - scale_x, scale_y = trained_scales( - y_breaks=(0, 5, 10), - y_labels=("low", "mid", "high"), - ) - coord = coord_radial(theta="y", theta_labels=True, expand=False) - - panel_params = coord.setup_panel_params(scale_x, scale_y) - - assert_allclose(panel_params.x.breaks, [0, np.pi, 2 * np.pi]) - assert panel_params.x.labels == ["low", "mid", "high"] +def test_coord_radial_rlim_recomputes_breaks(): + # Auto breaks (breaks=True), so the scale recomputes nice breaks over the + # zoomed range. Full-range breaks would be [0, 5, 10]; the naive filter to + # (2, 8) would leave only [5]. + scale_x = scale_x_continuous() + scale_y = scale_y_continuous() + scale_x.train((0, 10)) + scale_y.train((0, 10)) + coord = coord_radial(rlim=(2, 8), expand=False) + pv = coord.setup_panel_params(scale_x, scale_y) + breaks = list(pv.y.breaks) + # Recomputed nice breaks over (2, 8) — NOT the naive filter, which would + # leave only [5]. Must be within the zoom and contain more than one break. + assert len(breaks) > 1 + assert all(2 <= b <= 8 for b in breaks) + assert len(pv.y.labels) == len(breaks) + + +def test_coord_radial_shows_theta_labels_by_default(): + scale_x, scale_y = trained_scales() + coord = coord_radial(expand=False) # no theta_labels arg + pv = coord.setup_panel_params(scale_x, scale_y) + assert list(pv.x.breaks) # theta breaks present by default + assert list(pv.x.labels) def test_coord_radial_to_radians_zero_width_range(): @@ -276,73 +284,3 @@ def test_coord_radial_transform_rotates_angle(): assert_allclose(out["x"], [0, np.pi]) assert_allclose(out["angle"], [10, 200]) - - -def test_coord_radial_draw_sets_arc_inner_radius_and_axis_position(): - coord = coord_radial( - start=np.pi / 4, - end=3 * np.pi / 4, - inner_radius=0.5, - r_axis_inside=True, - ) - coord.params = {"r_range": (2, 10)} - fig, ax = plt.subplots(subplot_kw={"projection": "polar"}) - - try: - coord.draw([ax]) - assert_allclose(ax.get_xlim(), (np.pi / 4, 3 * np.pi / 4)) - assert_allclose(ax.get_rorigin(), -6) - assert ax.get_rlabel_position() == 55 - finally: - plt.close(fig) - - -def test_coord_radial_draw_float_r_axis_position(): - coord = coord_radial(r_axis_inside=np.pi / 2) - coord.params = {"r_range": (0, 10)} - fig, ax = plt.subplots(subplot_kw={"projection": "polar"}) - - try: - coord.draw([ax]) - assert ax.get_rlabel_position() == 90 - finally: - plt.close(fig) - - -def test_coord_radial_setup_ax_sets_pad_and_unclips_text(): - data = pd.DataFrame({"x": [1], "y": [1], "label": ["label"]}) - p = ( - ggplot(data, aes("x", "y", label="label")) - + geom_text() - + coord_radial(theta_label_pad=17, theta_labels=True) - ) - - fig = p.draw() - try: - ax = fig.axes[0] - assert ax.xaxis.get_tick_params()["pad"] == 17 - assert not ax.texts[0].get_clip_on() - finally: - plt.close(fig) - - -def test_coord_radial_uses_guide_axis_theta_angle(): - data = pd.DataFrame({"x": [1, 2, 3], "y": [1, 2, 3]}) - p = ( - ggplot(data, aes("x", "y")) - + geom_point() - + coord_radial(theta_labels=True) - + guides(theta=guide_axis_theta(angle=35)) - ) - - fig = p.draw() - try: - rotations = [ - label.get_rotation() - for label in fig.axes[0].get_xticklabels() - if label.get_text() - ] - assert rotations - assert rotations == [35] * len(rotations) - finally: - plt.close(fig) diff --git a/tests/test_theme.py b/tests/test_theme.py index 335f905123..ede692e332 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -34,9 +34,9 @@ theme_void, theme_xkcd, ) -from plotnine.data import mtcars from plotnine.coords.coord_cartesian import coord_cartesian -from plotnine.themes.themeable import panel_border, themeable +from plotnine.data import mtcars +from plotnine.themes.themeable import themeable LT_MPL310 = version.parse(mpl.__version__) < version.parse("3.10") IS_CI = bool(os.environ.get("CI")) @@ -114,17 +114,6 @@ def test_add_element_blank(): assert theme3 == theme4 # blanking cleans the slate -def test_blank_panel_border_hides_polar_spine(): - th = panel_border(element_blank()) - fig, ax = plt.subplots(subplot_kw={"projection": "polar"}) - - try: - th.blank_ax(ax) - assert not ax.spines["polar"].get_visible() - finally: - plt.close(fig) - - def test_extension_themeable_applies_from_theme_kwargs(): class test_extension_panel_facecolor(themeable): def apply_ax(self, ax): From 05b6ce13574d47779dab6fdb9dc9acdd2af32372 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 17 Jun 2026 19:09:40 +0300 Subject: [PATCH 027/126] fix(coord_radial): align rotate_angle labels to spokes and keep them upright --- plotnine/coords/coord_radial.py | 17 +++++++++++++---- tests/test_coord_polar.py | 20 +++++++++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index dddaf41c3f..92a056c97a 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -58,9 +58,10 @@ class coord_radial(coord_polar): Unlike ggplot2's ``r.axis.inside``, a length-2 value for separate primary and secondary axis placement is not supported. rotate_angle : - If ``True``, automatically add the local theta angle (in degrees) to - the ``angle`` aesthetic so that text or other rotated marks align with - the spoke direction. Default ``False``. + If ``True``, rotate the ``angle`` aesthetic so that text or other + rotated marks align tangentially with the arc at their spoke. The + rotation is folded so labels stay upright (readable) rather than + appearing upside-down in the lower half. Default ``False``. thetalim : Data-space limits for the theta axis as ``(lo, hi)``. Only data within this range is mapped to the arc; equivalent to zooming on the @@ -281,7 +282,15 @@ def transform( and "x" in data.columns ): data = data.copy() - data["angle"] = data["angle"] + np.degrees(data["x"]) + # Align marks tangentially to their spoke. The PolarAxes places + # a data theta t at on-screen angle (deg, CCW from East) + # screen = 90 + mpl_dir * degrees(t), mpl_dir = -1 if cw else 1 + # Tangential text rotation is screen - 90; folding into (-90, 90] + # keeps labels upright (a bottom label reads "6", not "9"). + mpl_dir = -1 if self.direction == 1 else 1 + rot = mpl_dir * np.degrees(data["x"].to_numpy()) + rot = (rot + 90.0) % 180.0 - 90.0 + data["angle"] = data["angle"] + rot return data # ------------------------------------------------------------------ diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index 99ab67a3f7..4f692d9660 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -282,5 +282,23 @@ def test_coord_radial_transform_rotates_angle(): out = coord.transform(data, None) + # x=0 -> top spoke (rotation 0); x=5 -> bottom spoke (180deg) which + # folds back to a horizontal, readable label (rotation 0). assert_allclose(out["x"], [0, np.pi]) - assert_allclose(out["angle"], [10, 200]) + assert_allclose(out["angle"], [10, 20]) + + +def test_coord_radial_rotate_angle_aligns_upright(): + coord = coord_radial(rotate_angle=True) + coord.params = {"theta_range": (0, 3), "r_range": (0, 10)} + # theta data 0 -> top, 1 -> 120deg clockwise, 2 -> 240deg clockwise. + data = pd.DataFrame( + {"x": [0.0, 1.0, 2.0], "y": [1, 1, 1], "angle": [0.0, 0.0, 0.0]} + ) + + out = coord.transform(data, None) + + # Labels align tangentially to the arc and are folded into (-90, 90] + # so they stay upright (a bottom label reads "6", not "9"). + assert all(-90 <= a <= 90 for a in out["angle"]) + assert_allclose(out["angle"], [0.0, 60.0, -60.0], atol=1e-6) From 2f5a75dab8d2d8c9bf141c4750266ef5d51ab921 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 17 Jun 2026 19:09:41 +0300 Subject: [PATCH 028/126] fix(coord_radial): draw radial axis labels above the geoms --- plotnine/coords/coord_radial.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 92a056c97a..a814848eea 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -347,6 +347,13 @@ def setup_ax( else: polar_ax.set_rlabel_position(np.degrees(float(self.r_axis_inside))) + # Draw the radial-axis tick labels above the geoms. By default the + # r-axis paints at a low zorder, so opaque geoms (e.g. coxcomb bars) + # hide the count labels; ggplot2 keeps them on top. + ax.yaxis.set_zorder(10000) + for label in ax.yaxis.get_ticklabels(): + label.set_zorder(10000) + ax.tick_params(axis="x", which="major", direction="out") if (angle := self._theta_guide_angle(theme)) is not None: # Use Matplotlib's 'auto' mode so labels orient tangentially From 7e1d66e92e59d4b84d68c6829ab1b763a6bfa841 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 17 Jun 2026 19:09:42 +0300 Subject: [PATCH 029/126] feat(coord_radial): add reverse argument and per-panel range derivation - add reverse=none/theta/r/thetar for ggplot2 parity - derive per-panel theta/r ranges from panel_params, removing the shared mutable self.params - keep the inner_radius hole working under reverse='r' --- plotnine/coords/coord_polar.py | 52 +++++++++++---- plotnine/coords/coord_radial.py | 74 ++++++++++++++++----- plotnine/iapi.py | 7 ++ tests/test_coord_polar.py | 114 +++++++++++++++++++++++--------- 4 files changed, 187 insertions(+), 60 deletions(-) diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py index 4f29835deb..afdc99879c 100644 --- a/plotnine/coords/coord_polar.py +++ b/plotnine/coords/coord_polar.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import replace -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Literal, cast import numpy as np @@ -78,7 +78,6 @@ def __init__( self.start = start self.direction = direction self.expand = expand - self.params: dict = {} # ------------------------------------------------------------------ # Panel params @@ -103,8 +102,7 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: theta_range = pv_no_exp.y.range r_sv = pv_exp.x - self.params["theta_range"] = theta_range - self.params["r_range"] = r_sv.range + r_range = r_sv.range empty = np.array([], dtype=float) @@ -125,7 +123,13 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: # breaks. new_y = replace(r_sv) - return replace(pv_exp, x=new_x, y=new_y) + return replace( + pv_exp, + x=new_x, + y=new_y, + theta_range=tuple(theta_range), + r_range=tuple(r_range), + ) def labels(self, cur_labels: labels_view) -> labels_view: # When theta="y" the data x/y columns are swapped in transform so that @@ -141,9 +145,11 @@ def labels(self, cur_labels: labels_view) -> labels_view: # Helpers # ------------------------------------------------------------------ - def _to_radians(self, vals: np.ndarray) -> np.ndarray: + def _to_radians( + self, vals: np.ndarray, theta_range: tuple[float, float] + ) -> np.ndarray: """Normalise data-space theta values to [start, start + 2π].""" - t_min, t_max = self.params["theta_range"] + t_min, t_max = theta_range denom = float(t_max) - float(t_min) if denom == 0: return np.zeros_like(vals, dtype=float) @@ -177,14 +183,19 @@ def transform( if theta_col not in data.columns or r_col not in data.columns: return data + theta_range = panel_params.theta_range + assert theta_range is not None + data = data.copy() - data[theta_col] = self._to_radians(data[theta_col].to_numpy()) + data[theta_col] = self._to_radians( + data[theta_col].to_numpy(), theta_range + ) has_endpoints = ( theta_end_col in data.columns and r_end_col in data.columns ) if has_endpoints: data[theta_end_col] = self._to_radians( - data[theta_end_col].to_numpy() + data[theta_end_col].to_numpy(), theta_range ) # PolarAxes always expects x = theta (radians) and y = r. @@ -210,8 +221,10 @@ def distance( panel_params: panel_view, ) -> np.ndarray: # Normalise theta and r to [0, 1] then compute Euclidean distance. - t_min, t_max = self.params["theta_range"] - r_min, r_max = self.params["r_range"] + assert panel_params.theta_range is not None + assert panel_params.r_range is not None + t_min, t_max = panel_params.theta_range + r_min, r_max = panel_params.r_range t_denom = float(t_max - t_min) or 1.0 r_denom = float(r_max - r_min) or 1.0 @@ -227,8 +240,10 @@ def distance( return dist_euclidean(theta_norm, r_norm) def backtransform_range(self, panel_params: panel_view) -> panel_ranges: - t_range = tuple(self.params["theta_range"]) - r_range = tuple(self.params["r_range"]) + assert panel_params.theta_range is not None + assert panel_params.r_range is not None + t_range = panel_params.theta_range + r_range = panel_params.r_range if self.theta == "x": return panel_ranges(x=t_range, y=r_range) return panel_ranges(x=r_range, y=t_range) @@ -237,13 +252,22 @@ def backtransform_range(self, panel_params: panel_view) -> panel_ranges: # Draw decorations on PolarAxes # ------------------------------------------------------------------ + def _mpl_theta_direction(self) -> Literal[-1, 1]: + """ + Matplotlib theta direction for this coordinate system + + ``-1`` draws clockwise and ``+1`` counter-clockwise, the opposite of + plotnine's own ``direction`` convention. + """ + return -1 if self.direction == 1 else 1 + def draw(self, axs: list[Axes]) -> None: """Configure each PolarAxes: zero location and rotation direction R-limits are set per panel by setup_ax. """ # PolarAxes theta_direction: -1 = clockwise, +1 = counter-clockwise. - mpl_direction = -1 if self.direction == 1 else 1 + mpl_direction = self._mpl_theta_direction() for ax in axs: polar_ax = cast("PolarAxes", ax) polar_ax.set_theta_zero_location("N") # 12 o'clock diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index a814848eea..ddcec1e5b6 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import replace -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Literal, cast import numpy as np @@ -37,8 +37,9 @@ class coord_radial(coord_polar): Ending angle in radians, measured clockwise from 12 o'clock. ``None`` (default) gives a full circle (``start + 2π * direction``). direction : - ``1`` = clockwise (default), ``-1`` = counter-clockwise. - Only used when *end* is ``None``. + Angular rotation sense: ``1`` = clockwise (default), ``-1`` = + counter-clockwise. Applied regardless of *end*, so it also sets + the sweep direction of a partial arc. expand : Add a small buffer around the data on the radius axis. Default ``True``. @@ -70,6 +71,14 @@ class coord_radial(coord_polar): Data-space limits for the r axis as ``(lo, hi)``. Only data within this range is shown; equivalent to zooming on the radial axis. ``None`` (default) uses the full data range. + reverse : + Which axes run in the opposite direction. + + * ``"none"`` (default) — neither axis is reversed. + * ``"theta"`` — the angular axis runs the other way around. + * ``"r"`` — the radial axis is inverted, so large values sit + toward the centre. + * ``"thetar"`` — both the angular and radial axes are reversed. Notes ----- @@ -81,10 +90,6 @@ class coord_radial(coord_polar): The Python API uses snake_case names for arguments that are dotted in ggplot2: ``inner_radius``, ``r_axis_inside``, and ``rotate_angle``. - Unlike ggplot2, plotnine coordinate systems do not currently expose a - ``clip`` argument. The ggplot2 ``reverse`` argument is not currently - implemented. - Examples -------- A donut chart is a stacked bar chart with an inner radius. @@ -135,6 +140,7 @@ def __init__( rotate_angle: bool = False, thetalim: tuple[float, float] | None = None, rlim: tuple[float, float] | None = None, + reverse: str = "none", ) -> None: super().__init__( theta=theta, @@ -142,12 +148,18 @@ def __init__( direction=direction, expand=expand, ) + if reverse not in {"none", "theta", "r", "thetar"}: + raise ValueError( + "reverse must be one of 'none', 'theta', 'r', 'thetar'; " + f"got {reverse!r}." + ) self.end = end self.inner_radius = inner_radius self.r_axis_inside = r_axis_inside self.rotate_angle = rotate_angle self.thetalim = thetalim self.rlim = rlim + self.reverse = reverse # ------------------------------------------------------------------ # Panel params @@ -173,7 +185,7 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: # arc. Recompute nice breaks over the zoomed range so labels are not # reduced to sparse endpoints, matching ggplot2. if self.thetalim is not None: - self.params["theta_range"] = tuple(self.thetalim) + pv = replace(pv, theta_range=tuple(self.thetalim)) theta_scale = scale_x if self.theta == "x" else scale_y theta_breaks = [ b @@ -182,11 +194,11 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: ] theta_labels = list(theta_scale.get_labels(theta_breaks)) - # rlim: zoom the r data range — update params and recompute nice breaks + # rlim: zoom the r data range — update ranges and recompute nice breaks # over the zoomed range (rather than filtering the full-range breaks, # which leaves sparse endpoint-only labels), matching ggplot2. if self.rlim is not None: - self.params["r_range"] = tuple(self.rlim) + pv = replace(pv, r_range=tuple(self.rlim)) r_scale = scale_y if self.theta == "x" else scale_x breaks = [ b @@ -215,8 +227,11 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: # default (matching ggplot2); hide them via theme(axis_text_x=...). x_updates: dict = {} if theta_breaks: + assert pv.theta_range is not None radian_pos = list( - self._to_radians(np.asarray(theta_breaks, dtype=float)) + self._to_radians( + np.asarray(theta_breaks, dtype=float), pv.theta_range + ) ) if arc_lo is not None: keep = [arc_lo <= r <= arc_hi for r in radian_pos] @@ -235,6 +250,16 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: if x_updates: pv = replace(pv, x=replace(pv.x, **x_updates)) + # reverse="r"/"thetar": flip the displayed radial range so large r + # values sit toward the centre. set_ylim(hi, lo) inverts the axis. + # pv.r_range stays in natural (lo, hi) order, so setup_ax reads it + # (not y.range) for the inner_radius origin: reversing only y.range + # inverts the display without breaking the hole, and distance() is + # likewise unaffected. + if self.reverse in ("r", "thetar"): + lo, hi = pv.y.range + pv = replace(pv, y=replace(pv.y, range=(hi, lo))) + return pv # ------------------------------------------------------------------ @@ -255,9 +280,24 @@ def _arc(self) -> float: return self.end - self.start return 2.0 * np.pi - def _to_radians(self, vals: np.ndarray) -> np.ndarray: + def _mpl_theta_direction(self) -> Literal[-1, 1]: + """ + Matplotlib theta direction, flipped when reverse acts on theta + + ``reverse="theta"``/``"thetar"`` runs the angular axis the other way + by folding into the PolarAxes direction at draw time, rather than into + the radian mapping. + """ + mpl_direction = super()._mpl_theta_direction() + if self.reverse in ("theta", "thetar"): + return 1 if mpl_direction == -1 else -1 + return mpl_direction + + def _to_radians( + self, vals: np.ndarray, theta_range: tuple[float, float] + ) -> np.ndarray: """Normalize theta values to [start, start + arc].""" - t_min, t_max = self.params["theta_range"] + t_min, t_max = theta_range denom = float(t_max) - float(t_min) if denom == 0: return np.zeros_like(vals, dtype=float) @@ -287,7 +327,7 @@ def transform( # screen = 90 + mpl_dir * degrees(t), mpl_dir = -1 if cw else 1 # Tangential text rotation is screen - 90; folding into (-90, 90] # keeps labels upright (a bottom label reads "6", not "9"). - mpl_dir = -1 if self.direction == 1 else 1 + mpl_dir = self._mpl_theta_direction() rot = mpl_dir * np.degrees(data["x"].to_numpy()) rot = (rot + 90.0) % 180.0 - 90.0 data["angle"] = data["angle"] + rot @@ -321,7 +361,11 @@ def setup_ax( # Inner radius: push the data away from the centre by setting a # virtual r-origin below r_min. Formula: solve # inner_radius = (r_min - r_origin) / (r_max - r_origin) - r_min, r_max = panel_params.y.range + # Use the natural r range (always (lo, hi), reflects rlim) rather + # than y.range, which reverse="r"/"thetar" inverts to (hi, lo) and + # would fail the r_max > r_min guard, dropping the donut hole. + assert panel_params.r_range is not None + r_min, r_max = panel_params.r_range if ( self.inner_radius > 0 and np.isfinite(r_min) diff --git a/plotnine/iapi.py b/plotnine/iapi.py index eaca78e7a3..f124cf9b94 100644 --- a/plotnine/iapi.py +++ b/plotnine/iapi.py @@ -166,6 +166,13 @@ class panel_view: x: scale_position_view y: scale_position_view + # Polar coordinates store the per-panel data-space ranges of the theta + # and r variables here so that transform-time methods read the range + # belonging to the panel they are given, not shared instance state. + # These are None for non-polar coordinate systems. + theta_range: Optional[tuple[float, float]] = None + r_range: Optional[tuple[float, float]] = None + @dataclass class panel_ranges: diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index 4f692d9660..59d8511b03 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -15,10 +15,34 @@ ) from plotnine.coords.coord_cartesian import coord_cartesian from plotnine.coords.coord_polar import coord_polar -from plotnine.iapi import labels_view +from plotnine.iapi import labels_view, panel_view, scale_view from plotnine.scales import scale_x_continuous, scale_y_continuous +def _dummy_scale_view() -> scale_view: + """A minimal scale_view; polar transforms only read theta/r ranges.""" + return scale_view( + scale=None, # type: ignore[arg-type] + aesthetics=[], + name=None, + limits=(0, 1), + range=(0, 1), + breaks=[], + minor_breaks=np.array([], dtype=float), + labels=[], + ) + + +def make_panel_view(theta_range, r_range) -> panel_view: + """A panel_view carrying just the per-panel theta/r ranges.""" + return panel_view( + x=_dummy_scale_view(), + y=_dummy_scale_view(), + theta_range=theta_range, + r_range=r_range, + ) + + def trained_scales( x=(0, 10), y=(0, 10), @@ -43,8 +67,8 @@ def test_coord_polar_setup_panel_params_theta_x(): panel_params = coord.setup_panel_params(scale_x, scale_y) - assert coord.params["theta_range"] == (0, 10) - assert coord.params["r_range"] == (0, 10) + assert panel_params.theta_range == (0, 10) + assert panel_params.r_range == (0, 10) assert panel_params.x.range == (np.pi / 4, np.pi / 4 + 2 * np.pi) assert panel_params.x.breaks == [] assert panel_params.x.labels == [] @@ -60,8 +84,8 @@ def test_coord_polar_setup_panel_params_theta_y(): panel_params = coord.setup_panel_params(scale_x, scale_y) - assert coord.params["theta_range"] == (0, 10) - assert coord.params["r_range"] == (0, 10) + assert panel_params.theta_range == (0, 10) + assert panel_params.r_range == (0, 10) assert panel_params.y.breaks == [0, 2, 5, 10] @@ -76,19 +100,38 @@ def test_coord_polar_setup_panel_params_per_panel_independent(): assert pv2.y.range == (0, 100) +def test_coord_polar_transform_uses_per_panel_range_not_last(): + coord = coord_polar() # one instance reused across panels (faceting) + sx1, sy1 = trained_scales(x=(0, 10), y=(0, 10)) + pv1 = coord.setup_panel_params(sx1, sy1) + sx2, sy2 = trained_scales(x=(0, 20), y=(0, 20)) + coord.setup_panel_params(sx2, sy2) # would clobber shared state + # Transform a point against panel 1 AFTER panel 2 was set up. + out = coord.transform(pd.DataFrame({"x": [5], "y": [5]}), pv1) + # x=5 in panel 1's theta range (0,10) -> norm 0.5 -> pi (half turn). + assert_allclose(out.loc[0, "x"], np.pi) + + def test_coord_polar_to_radians_zero_width_range(): coord = coord_polar() - coord.params = {"theta_range": (1, 1)} + pv = panel_view( + x=_dummy_scale_view(), + y=_dummy_scale_view(), + theta_range=(1, 1), + r_range=(0, 10), + ) - assert_allclose(coord._to_radians(np.array([1, 2, 3])), [0, 0, 0]) + assert_allclose( + coord._to_radians(np.array([1, 2, 3]), pv.theta_range), [0, 0, 0] + ) def test_coord_polar_transforms_segment_endpoints_theta_x(): coord = coord_polar(theta="x") - coord.params = {"theta_range": (0, 10), "r_range": (0, 10)} + pv = make_panel_view((0, 10), (0, 10)) data = pd.DataFrame({"x": [0], "y": [1], "xend": [10], "yend": [2]}) - out = coord.transform(data, None) + out = coord.transform(data, pv) assert out.loc[0, "x"] == 0 assert out.loc[0, "y"] == 1 @@ -98,10 +141,10 @@ def test_coord_polar_transforms_segment_endpoints_theta_x(): def test_coord_polar_transforms_segment_endpoints_theta_y(): coord = coord_polar(theta="y") - coord.params = {"theta_range": (0, 10), "r_range": (0, 10)} + pv = make_panel_view((0, 10), (0, 10)) data = pd.DataFrame({"x": [1], "y": [0], "xend": [2], "yend": [10]}) - out = coord.transform(data, None) + out = coord.transform(data, pv) assert out.loc[0, "x"] == 0 assert out.loc[0, "y"] == 1 @@ -111,10 +154,10 @@ def test_coord_polar_transforms_segment_endpoints_theta_y(): def test_coord_polar_transforms_theta_y_without_endpoints(): coord = coord_polar(theta="y") - coord.params = {"theta_range": (0, 10), "r_range": (0, 10)} + pv = make_panel_view((0, 10), (0, 10)) data = pd.DataFrame({"x": [1], "y": [5]}) - out = coord.transform(data, None) + out = coord.transform(data, pv) assert_allclose(out.loc[0, "x"], np.pi) assert out.loc[0, "y"] == 1 @@ -122,10 +165,10 @@ def test_coord_polar_transforms_theta_y_without_endpoints(): def test_coord_polar_munches_before_radian_transform(): coord = coord_polar() - coord.params = {"theta_range": (0, 10), "r_range": (0, 10)} + pv = make_panel_view((0, 10), (0, 10)) data = pd.DataFrame({"x": [0, 10], "y": [1, 2], "group": [1, 1]}) - out = coord.transform(data, None, munch=True) + out = coord.transform(data, pv, munch=True) assert len(out) > len(data) assert out["x"].between(0, 2 * np.pi).all() @@ -140,24 +183,24 @@ def test_coord_polar_leaves_non_position_data_unchanged(): def test_coord_polar_distance_and_backtransform_theta_x(): coord = coord_polar() - coord.params = {"theta_range": (0, 10), "r_range": (0, 20)} + pv = make_panel_view((0, 10), (0, 20)) - distance = coord.distance(pd.Series([0, 10]), pd.Series([0, 10]), None) + distance = coord.distance(pd.Series([0, 10]), pd.Series([0, 10]), pv) assert_allclose(distance, [np.sqrt(1.25)]) - assert coord.backtransform_range(None).x == (0, 10) - assert coord.backtransform_range(None).y == (0, 20) + assert coord.backtransform_range(pv).x == (0, 10) + assert coord.backtransform_range(pv).y == (0, 20) def test_coord_polar_distance_and_backtransform_theta_y(): coord = coord_polar(theta="y") - coord.params = {"theta_range": (0, 10), "r_range": (0, 20)} + pv = make_panel_view((0, 10), (0, 20)) - distance = coord.distance(pd.Series([0, 10]), pd.Series([0, 10]), None) + distance = coord.distance(pd.Series([0, 10]), pd.Series([0, 10]), pv) assert_allclose(distance, [np.sqrt(1.25)]) - assert coord.backtransform_range(None).x == (0, 20) - assert coord.backtransform_range(None).y == (0, 10) + assert coord.backtransform_range(pv).x == (0, 20) + assert coord.backtransform_range(pv).y == (0, 10) def test_coord_polar_aspect_is_square(): @@ -232,8 +275,8 @@ def test_coord_radial_setup_panel_params_for_partial_arc(): panel_params = coord.setup_panel_params(scale_x, scale_y) - assert coord.params["theta_range"] == (0, 10) - assert coord.params["r_range"] == (2, 8) + assert panel_params.theta_range == (0, 10) + assert panel_params.r_range == (2, 8) assert_allclose(panel_params.x.breaks, [0, np.pi / 2, np.pi]) assert panel_params.x.labels == ["0", "5", "10"] assert panel_params.x.range == (0, np.pi) @@ -260,6 +303,16 @@ def test_coord_radial_rlim_recomputes_breaks(): assert len(pv.y.labels) == len(breaks) +def test_coord_radial_reverse_r_inverts_radial_range(): + scale_x, scale_y = trained_scales(y=(0, 10)) + base = coord_radial(expand=False) + rev = coord_radial(reverse="r", expand=False) + pv_base = base.setup_panel_params(scale_x, scale_y) + pv_rev = rev.setup_panel_params(scale_x, scale_y) + assert tuple(pv_base.y.range) == (0, 10) + assert tuple(pv_rev.y.range) == (10, 0) # reversed: large r toward centre + + def test_coord_radial_shows_theta_labels_by_default(): scale_x, scale_y = trained_scales() coord = coord_radial(expand=False) # no theta_labels arg @@ -270,17 +323,16 @@ def test_coord_radial_shows_theta_labels_by_default(): def test_coord_radial_to_radians_zero_width_range(): coord = coord_radial() - coord.params = {"theta_range": (1, 1)} - assert_allclose(coord._to_radians(np.array([1, 2, 3])), [0, 0, 0]) + assert_allclose(coord._to_radians(np.array([1, 2, 3]), (1, 1)), [0, 0, 0]) def test_coord_radial_transform_rotates_angle(): coord = coord_radial(rotate_angle=True) - coord.params = {"theta_range": (0, 10), "r_range": (0, 10)} + pv = make_panel_view((0, 10), (0, 10)) data = pd.DataFrame({"x": [0, 5], "y": [1, 1], "angle": [10, 20]}) - out = coord.transform(data, None) + out = coord.transform(data, pv) # x=0 -> top spoke (rotation 0); x=5 -> bottom spoke (180deg) which # folds back to a horizontal, readable label (rotation 0). @@ -290,13 +342,13 @@ def test_coord_radial_transform_rotates_angle(): def test_coord_radial_rotate_angle_aligns_upright(): coord = coord_radial(rotate_angle=True) - coord.params = {"theta_range": (0, 3), "r_range": (0, 10)} + pv = make_panel_view((0, 3), (0, 10)) # theta data 0 -> top, 1 -> 120deg clockwise, 2 -> 240deg clockwise. data = pd.DataFrame( {"x": [0.0, 1.0, 2.0], "y": [1, 1, 1], "angle": [0.0, 0.0, 0.0]} ) - out = coord.transform(data, None) + out = coord.transform(data, pv) # Labels align tangentially to the arc and are folded into (-90, 90] # so they stay upright (a bottom label reads "6", not "9"). From e20617978b2207fbb48f8eafb4af896fe69753a9 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 17 Jun 2026 23:55:43 +0300 Subject: [PATCH 030/126] fix(coord_polar): layer gridlines below geoms, axis labels above --- plotnine/_mpl/_polar_axes.py | 47 +++++++++++++++++++++++++++++++++ plotnine/coords/coord_polar.py | 3 ++- plotnine/coords/coord_radial.py | 7 ----- 3 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 plotnine/_mpl/_polar_axes.py diff --git a/plotnine/_mpl/_polar_axes.py b/plotnine/_mpl/_polar_axes.py new file mode 100644 index 0000000000..3bc799f39b --- /dev/null +++ b/plotnine/_mpl/_polar_axes.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +from matplotlib.projections import register_projection +from matplotlib.projections.polar import PolarAxes + +if TYPE_CHECKING: + from matplotlib.backend_bases import RendererBase + from matplotlib.projections.polar import RadialAxis, ThetaAxis + + +class p9PolarAxes(PolarAxes): + """ + PolarAxes that keeps the axis tick labels above the geom layers + """ + + name = "p9polar" + + def draw(self, renderer: RendererBase) -> None: + """ + Draw the axes, then lift the tick labels above the geoms + + With the default `panel_ontop=False` (`axisbelow=True`) the normal + pass paints the gridlines and the tick labels below the geoms. + Re-drawing the radial-axis and theta-axis tick labels keeps them + visible on top of opaque geoms. + When `panel_ontop=True` everything is already above the geoms, so + the re-draw is a visual no-op. + """ + super().draw(renderer) + + for axis in (self.raxis, self.thetaaxis): + for label in axis.get_ticklabels(): + if label.get_visible(): + label.draw(renderer) + + @property + def thetaaxis(self) -> ThetaAxis: + return cast("ThetaAxis", self.xaxis) + + @property + def raxis(self) -> RadialAxis: + return cast("RadialAxis", self.yaxis) + + +register_projection(p9PolarAxes) diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py index afdc99879c..a8941ffa52 100644 --- a/plotnine/coords/coord_polar.py +++ b/plotnine/coords/coord_polar.py @@ -5,6 +5,7 @@ import numpy as np +from .._mpl._polar_axes import p9PolarAxes # noqa: F401 (registers "p9polar") from ..iapi import panel_ranges from .coord import coord, dist_euclidean @@ -65,7 +66,7 @@ class coord_polar(coord): """ is_linear = False - _projection = "polar" + _projection = "p9polar" def __init__( self, diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index ddcec1e5b6..98f93ce8f2 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -391,13 +391,6 @@ def setup_ax( else: polar_ax.set_rlabel_position(np.degrees(float(self.r_axis_inside))) - # Draw the radial-axis tick labels above the geoms. By default the - # r-axis paints at a low zorder, so opaque geoms (e.g. coxcomb bars) - # hide the count labels; ggplot2 keeps them on top. - ax.yaxis.set_zorder(10000) - for label in ax.yaxis.get_ticklabels(): - label.set_zorder(10000) - ax.tick_params(axis="x", which="major", direction="out") if (angle := self._theta_guide_angle(theme)) is not None: # Use Matplotlib's 'auto' mode so labels orient tangentially From 0c68a36ba35a6f4129c920580b2ad0d4c5f25697 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 8 Jul 2026 17:32:45 +0300 Subject: [PATCH 031/126] test(coord_polar): fix stale p9polar assertion, drop introspective test --- tests/test_coord_polar.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index 59d8511b03..bf31dee151 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -9,11 +9,9 @@ element_blank, element_line, geom_col, - geom_point, ggplot, theme, ) -from plotnine.coords.coord_cartesian import coord_cartesian from plotnine.coords.coord_polar import coord_polar from plotnine.iapi import labels_view, panel_view, scale_view from plotnine.scales import scale_x_continuous, scale_y_continuous @@ -234,27 +232,12 @@ def test_coord_polar_draw_uses_polar_axes_and_hides_blank_border(): try: ax = fig.axes[0] - assert ax.name == "polar" + assert ax.name == "p9polar" assert not ax.spines["polar"].get_visible() finally: plt.close(fig) -def test_coord_projection_creates_projected_axes(): - class coord_custom(coord_cartesian): - _projection = "polar" - - data = pd.DataFrame({"x": [1, 2], "y": [1, 2]}) - p = ggplot(data, aes("x", "y")) + geom_point() + coord_custom() - - fig = p.draw() - - try: - assert fig.axes[0].name == "polar" - finally: - plt.close(fig) - - def test_coord_radial_arc_uses_end_or_full_turn(): assert coord_radial(start=1, end=4)._arc == 3 assert coord_radial()._arc == 2 * np.pi From a50f31f2715c7310710ed05ca7e73deeb181dcd6 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 8 Jul 2026 17:36:34 +0300 Subject: [PATCH 032/126] fix(coord_polar): skip cartesian per-side axis setup --- plotnine/coords/coord_polar.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py index a8941ffa52..52f4e4e2e9 100644 --- a/plotnine/coords/coord_polar.py +++ b/plotnine/coords/coord_polar.py @@ -14,7 +14,7 @@ from matplotlib.axes import Axes from matplotlib.projections.polar import PolarAxes - from plotnine.iapi import labels_view, panel_view + from plotnine.iapi import labels_view, layout_details, panel_view from plotnine.scales.scale import scale @@ -142,6 +142,22 @@ def labels(self, cur_labels: labels_view) -> labels_view: return flip_labels(super().labels(cur_labels)) return super().labels(cur_labels) + def setup_ax( + self, + ax: Axes, + panel_params: panel_view, + layout_info: layout_details, + ) -> None: + """ + Limits, breaks and tick labels for a polar panel + + Skips `coord`'s per-side spine/axis-line setup: the theta axis + wraps the full circle and the r axis runs along one spoke, + neither of which maps onto a cartesian left/right/top/bottom + side or a `p9Axes`-style spine. + """ + self._setup_ticks_labels(ax, panel_params) + # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ From c13b860569b0d88ef37cb52bb44c46ef659d287e Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 8 Jul 2026 17:41:32 +0300 Subject: [PATCH 033/126] fix(theme): guard axis_line blank_ax against non-cartesian spines --- plotnine/themes/themeable.py | 13 +++++++++---- tests/test_coord_polar.py | 7 +++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index f661b87372..fd5afaf894 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -1208,6 +1208,7 @@ class axis_text_y(axis_text_y_left, axis_text_y_right): theme_element : element_text """ + class axis_text(axis_text_x, axis_text_y): """ Axis tick labels @@ -1290,7 +1291,8 @@ def apply_ax(self, ax: Axes): def blank_ax(self, ax: Axes): super().blank_ax(ax) - ax.spines["bottom"].set_visible(False) + if "bottom" in ax.spines: + ax.spines["bottom"].set_visible(False) class axis_line_x_top(themeable): @@ -1304,7 +1306,8 @@ def apply_ax(self, ax: Axes): def blank_ax(self, ax: Axes): super().blank_ax(ax) - ax.spines["top"].set_visible(False) + if "top" in ax.spines: + ax.spines["top"].set_visible(False) class axis_line_x(axis_line_x_top, axis_line_x_bottom): @@ -1328,7 +1331,8 @@ def apply_ax(self, ax: Axes): def blank_ax(self, ax: Axes): super().blank_ax(ax) - ax.spines["left"].set_visible(False) + if "left" in ax.spines: + ax.spines["left"].set_visible(False) class axis_line_y_right(themeable): @@ -1342,7 +1346,8 @@ def apply_ax(self, ax: Axes): def blank_ax(self, ax: Axes): super().blank_ax(ax) - ax.spines["right"].set_visible(False) + if "right" in ax.spines: + ax.spines["right"].set_visible(False) class axis_line_y(axis_line_y_left, axis_line_y_right): diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index bf31dee151..ca80634869 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -238,6 +238,13 @@ def test_coord_polar_draw_uses_polar_axes_and_hides_blank_border(): plt.close(fig) +def test_coord_polar_default_theme_does_not_crash(): + data = pd.DataFrame({"x": ["a", "b"], "y": [1, 2]}) + p = ggplot(data, aes("x", "y")) + geom_col() + coord_polar() + + p.draw_test() + + def test_coord_radial_arc_uses_end_or_full_turn(): assert coord_radial(start=1, end=4)._arc == 3 assert coord_radial()._arc == 2 * np.pi From 2146c5d896451aee03a60861ddf0304d357118ed Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 8 Jul 2026 17:46:53 +0300 Subject: [PATCH 034/126] feat(coord): bind coord._owner to the plot at draw setup --- plotnine/coords/coord.py | 25 +++++++++++++++++++++++++ plotnine/ggplot.py | 1 + 2 files changed, 26 insertions(+) diff --git a/plotnine/coords/coord.py b/plotnine/coords/coord.py index 7d281c11f6..2d01c13d96 100644 --- a/plotnine/coords/coord.py +++ b/plotnine/coords/coord.py @@ -129,6 +129,12 @@ class coord: # if the coordinate system needs them params: dict[str, Any] + # The ggplot this coord belongs to. Bound in `_setup`, not + # `__init__`/`__radd__`: a ggplot is copied on every `+`, so any + # earlier binding would point at a stale, intermediate copy rather + # than the final plot that's actually drawn. + _owner: ggplot | None = None + def __radd__(self, other: ggplot) -> ggplot: """ Add coordinates to ggplot object @@ -136,6 +142,25 @@ def __radd__(self, other: ggplot) -> ggplot: other.coordinates = copy(self) return other + def _setup(self, plot: ggplot) -> None: + """ + Bind this coord to the plot that owns it + + Unlike `theme._setup`/`guides._setup`, whose owner can be a + `ggplot`, a `Compose`, or (for `theme`) a `guide`, a coord is + never collected/shared across a `Compose` — it's always local to + exactly one `ggplot` — so the parameter is named for what it + concretely always is. + + Parameters + ---------- + plot : + The final `ggplot` being drawn. Called once per draw, after + all copying from `+` operators is done, mirroring + `guides._setup`/`theme._setup`. + """ + self._owner = plot + def setup_data(self, data: list[pd.DataFrame]) -> list[pd.DataFrame]: """ Allow the coordinate system to manipulate the layer data diff --git a/plotnine/ggplot.py b/plotnine/ggplot.py index ce7505fe0b..1fb030bc10 100755 --- a/plotnine/ggplot.py +++ b/plotnine/ggplot.py @@ -384,6 +384,7 @@ def draw(self, *, show: bool = False) -> Figure: self._build() # setup + self.coordinates._setup(self) self.guides._setup(self) self.theme._setup(self) From 1d4fb299730894e2ddf25628323bccd04f4a53ca Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 8 Jul 2026 17:52:35 +0300 Subject: [PATCH 035/126] fix(coord_radial): read guide_axis_theta angle via coord._owner --- plotnine/coords/coord_radial.py | 24 +++++++++++++----------- tests/test_theme.py | 24 ------------------------ 2 files changed, 13 insertions(+), 35 deletions(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 98f93ce8f2..93323b595a 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -12,8 +12,7 @@ from matplotlib.axes import Axes from matplotlib.projections.polar import PolarAxes - from plotnine import theme - from plotnine.iapi import panel_view + from plotnine.iapi import layout_details, panel_view from plotnine.scales.scale import scale @@ -338,7 +337,10 @@ def transform( # ------------------------------------------------------------------ def setup_ax( - self, ax: Axes, panel_params: panel_view, theme: theme + self, + ax: Axes, + panel_params: panel_view, + layout_info: layout_details, ) -> None: """ Configure each PolarAxes from this panel's limits @@ -347,7 +349,7 @@ def setup_ax( ``panel_params`` so faceted panels with free scales each get their own radial range. """ - super().setup_ax(ax, panel_params, theme) + super().setup_ax(ax, panel_params, layout_info) polar_ax = cast("PolarAxes", ax) arc = self._arc @@ -392,7 +394,7 @@ def setup_ax( polar_ax.set_rlabel_position(np.degrees(float(self.r_axis_inside))) ax.tick_params(axis="x", which="major", direction="out") - if (angle := self._theta_guide_angle(theme)) is not None: + if (angle := self._theta_guide_angle()) is not None: # Use Matplotlib's 'auto' mode so labels orient tangentially # to the arc, with `angle` as an offset — matching ggplot2's # guide_axis_theta() semantics where angle=0 means tangential. @@ -405,11 +407,11 @@ def setup_ax( for text in ax.texts: text.set_clip_on(False) - @staticmethod - def _theta_guide_angle(theme: theme) -> float | None: + def _theta_guide_angle(self) -> float | None: """ - Return the angle from guides(theta=guide_axis_theta(...)). + Return the angle from guides(theta=guide_axis_theta(...)) """ - guide = getattr(getattr(theme, "owner", None), "guides", None) - guide = getattr(guide, "theta", None) - return getattr(guide, "angle", None) + try: + return self._owner.guides.theta.angle + except AttributeError: + return None diff --git a/tests/test_theme.py b/tests/test_theme.py index ede692e332..2bb0631706 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -34,7 +34,6 @@ theme_void, theme_xkcd, ) -from plotnine.coords.coord_cartesian import coord_cartesian from plotnine.data import mtcars from plotnine.themes.themeable import themeable @@ -133,29 +132,6 @@ def apply_ax(self, ax): plt.close(fig) -def test_coord_can_read_extension_themeable(): - class test_extension_coord_title(themeable): - pass - - class coord_reads_themeable(coord_cartesian): - def setup_ax(self, ax, panel_params, theme): - super().setup_ax(ax, panel_params, theme) - ax.set_title(theme.getp("test_extension_coord_title")) - - p = ( - ggplot(mtcars, aes(x="wt", y="mpg")) - + geom_point() - + coord_reads_themeable() - + theme(test_extension_coord_title="coord themeable") - ) - - fig = p.draw() - try: - assert fig.axes[0].get_title() == "coord themeable" - finally: - plt.close(fig) - - def test_element_line_dashed_capstyle(): p = ggplot(mtcars, aes(x="wt", y="mpg")) + theme( panel_grid=element_line( From b0a9fe274df6555572d327a9dba387141673bbb9 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 8 Jul 2026 17:57:15 +0300 Subject: [PATCH 036/126] feat(layout): reserve polar panel clearance for theta/r axis decorations --- .../_mpl/layout_manager/_plot_layout_items.py | 85 +++++++++++++++++++ .../_mpl/layout_manager/_plot_side_space.py | 9 +- tests/test_coord_polar.py | 12 +++ 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/plotnine/_mpl/layout_manager/_plot_layout_items.py b/plotnine/_mpl/layout_manager/_plot_layout_items.py index 22073e3fef..e601e6e231 100644 --- a/plotnine/_mpl/layout_manager/_plot_layout_items.py +++ b/plotnine/_mpl/layout_manager/_plot_layout_items.py @@ -903,6 +903,91 @@ def _position_strip_text(self, st: StripText): st.set_position((x, y)) +class PolarPlotLayoutItems(PlotLayoutItems): + """ + `PlotLayoutItems` for a polar panel + + A theta axis wraps the full circle and an r axis runs along one + spoke, so neither has a single side to measure a tick-label + bounding box from (`axis_at` never resolves one for either). These + four methods return a fixed clearance estimate instead, applied to + every side alike. + """ + + def axis_ticks_x_max_height(self, ax: Axes, side: Side) -> float: + return _polar_ticks_clearance_x(self.plot.theme) + + def axis_text_x_max_height(self, ax: Axes, side: Side) -> float: + return _polar_text_clearance_x(self.plot.theme, side) + + def axis_ticks_y_max_width(self, ax: Axes, side: Side) -> float: + return _polar_ticks_clearance_y(self.plot.theme) + + def axis_text_y_max_width(self, ax: Axes, side: Side) -> float: + return _polar_text_clearance_y(self.plot.theme, side) + + +def _polar_text_clearance_x(theme: theme, side: Side) -> float: + """ + Fixed/estimated figure-height clearance for `axis_text_x_` + on a polar panel + + Not measured from rendered artists: a theta axis wraps the full + circle, so `axis_at` never resolves a side to measure a tick-label + bounding box from. Estimated instead from theme metrics — the + `axis_text_x_` font size (standing in for label height) plus + its margin — the same rough idea `_strip_switch_pad` uses to turn a + themed point value into a figure fraction. + """ + name = f"axis_text_x_{side}" + if theme.T.is_blank(name): + return 0 + _, H = theme.getp("figure_size") + size_pt = theme.getp((name, "size")) or 0 + margin = theme.getp((name, "margin")) + margin.setup(theme, name) + return size_pt / 72 / H + margin.fig.t + margin.fig.b + + +def _polar_text_clearance_y(theme: theme, side: Side) -> float: + """ + Fixed/estimated figure-width clearance for `axis_text_y_` on + a polar panel + + Mirrors `_polar_text_clearance_x` for the r axis: it runs along one + spoke rather than wrapping the circle, but the same "no side to + measure" problem applies. + """ + name = f"axis_text_y_{side}" + if theme.T.is_blank(name): + return 0 + W, _ = theme.getp("figure_size") + size_pt = theme.getp((name, "size")) or 0 + margin = theme.getp((name, "margin")) + margin.setup(theme, name) + return size_pt / 72 / W + margin.fig.l + margin.fig.r + + +def _polar_ticks_clearance_x(theme: theme) -> float: + """ + Fixed/estimated figure-height clearance for the major x tick + length on a polar panel + """ + _, H = theme.getp("figure_size") + tick_pt = abs(theme.getp("axis_ticks_length_major_x") or 0) + return tick_pt / 72 / H + + +def _polar_ticks_clearance_y(theme: theme) -> float: + """ + Fixed/estimated figure-width clearance for the major y tick length + on a polar panel + """ + W, _ = theme.getp("figure_size") + tick_pt = abs(theme.getp("axis_ticks_length_major_y") or 0) + return tick_pt / 72 / W + + def _spine_set_position_outward(spine: Spine, axis: Axis, distance: float): """ Move a spine and its tick marks outward, keeping the theme's tick styling diff --git a/plotnine/_mpl/layout_manager/_plot_side_space.py b/plotnine/_mpl/layout_manager/_plot_side_space.py index 0d7166794f..20412e53b6 100644 --- a/plotnine/_mpl/layout_manager/_plot_side_space.py +++ b/plotnine/_mpl/layout_manager/_plot_side_space.py @@ -18,7 +18,7 @@ from plotnine.exceptions import PlotnineError from plotnine.facets import facet_grid, facet_null, facet_wrap -from ._plot_layout_items import PlotLayoutItems +from ._plot_layout_items import PlotLayoutItems, PolarPlotLayoutItems from ._side_space import GridSpecParams, _side_space if TYPE_CHECKING: @@ -909,7 +909,12 @@ def __init__(self, plot: ggplot): self.plot = plot self.gridspec = plot._gridspec self.sub_gridspec = plot._sub_gridspec - self.items = PlotLayoutItems(plot) + items_cls = ( + PlotLayoutItems + if plot.coordinates._projection is None + else PolarPlotLayoutItems + ) + self.items = items_cls(plot) self.l = left_space(self.items) """All subspaces to the left of the panels""" diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index ca80634869..0e071fb150 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -344,3 +344,15 @@ def test_coord_radial_rotate_angle_aligns_upright(): # so they stay upright (a bottom label reads "6", not "9"). assert all(-90 <= a <= 90 for a in out["angle"]) assert_allclose(out["angle"], [0.0, 60.0, -60.0], atol=1e-6) + + +def test_coord_polar_axis_clearance(): + data = pd.DataFrame( + {"x": ["a", "a", "a"], "y": [2, 3, 5], "group": ["a", "b", "c"]} + ) + p = ( + ggplot(data, aes("x", "y", fill="group")) + + geom_col() + + coord_polar("y") + ) + assert p == "coord_polar_axis_clearance" From 0c299f6e462580079bc0cf8d59eaf468a6bafbfe Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 00:59:06 +0300 Subject: [PATCH 037/126] feat(theme): add axis_line_theta/axis_line_r for polar spines --- doc/_quartodoc.yml | 6 ++ plotnine/themes/theme.py | 6 ++ plotnine/themes/themeable.py | 118 ++++++++++++++++++++++++++++++++--- 3 files changed, 123 insertions(+), 7 deletions(-) diff --git a/doc/_quartodoc.yml b/doc/_quartodoc.yml index 76c8625b6c..4f922fa4c3 100644 --- a/doc/_quartodoc.yml +++ b/doc/_quartodoc.yml @@ -403,6 +403,12 @@ quartodoc: contents: - aspect_ratio - axis_line + - axis_line_r + - axis_line_r_end + - axis_line_r_start + - axis_line_theta + - axis_line_theta_inside + - axis_line_theta_outside - axis_line_x - axis_line_x_bottom - axis_line_x_top diff --git a/plotnine/themes/theme.py b/plotnine/themes/theme.py index 5a0bd22009..b38ee2e203 100644 --- a/plotnine/themes/theme.py +++ b/plotnine/themes/theme.py @@ -158,9 +158,15 @@ def __init__( text=None, axis_line_x_bottom=None, axis_line_x_top=None, + axis_line_theta_inside=None, + axis_line_theta_outside=None, + axis_line_theta=None, axis_line_x=None, axis_line_y_left=None, axis_line_y_right=None, + axis_line_r_start=None, + axis_line_r_end=None, + axis_line_r=None, axis_line_y=None, axis_line=None, axis_ticks_minor_x_bottom=None, diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index fd5afaf894..7b0439ef69 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -1280,6 +1280,39 @@ def _style_axis_line(themeable, ax, side): ax.spines[side].set(**properties) +def _style_polar_axis_line(themeable, ax, spine): + """ + Style a polar spine and show it + + Mirrors `_style_axis_line`: an explicitly themed (non-blank) spine + is shown (`visible=True`) even if matplotlib's own geometry-driven + default would hide it (e.g. a full circle has no `start`/`end` + spokes). `p9PolarAxes.draw` is what makes this choice stick across + draws. + """ + if spine not in ax.spines: + return + properties = themeable._get_properties(omit=("solid_capstyle",)) + properties.setdefault("visible", True) + if "zorder" not in properties: + properties["zorder"] = 10000 + ax.spines[spine].set(**properties) + + +def _blank_polar_axis_line(ax, spine): + """ + Hide a polar spine + + A plain `Axes` has no matching spine name, so this is a safe no-op + for cartesian axes. It's reachable from every `blank_ax` call, polar + or not, since `axis_line_theta`/`axis_line_r` nest under + `axis_line_x`/`axis_line_y`. `p9PolarAxes.draw` is what makes this + choice stick across draws. + """ + if spine in ax.spines: + ax.spines[spine].set_visible(False) + + class axis_line_x_bottom(themeable): """ x-axis line on the bottom @@ -1310,7 +1343,45 @@ def blank_ax(self, ax: Axes): ax.spines["top"].set_visible(False) -class axis_line_x(axis_line_x_top, axis_line_x_bottom): +class axis_line_theta_inside(themeable): + """ + theta-axis line on the inner boundary + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + _style_polar_axis_line(self, ax, "inner") + + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + _blank_polar_axis_line(ax, "inner") + + +class axis_line_theta_outside(themeable): + """ + theta-axis line on the outer boundary + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + _style_polar_axis_line(self, ax, "polar") + + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + _blank_polar_axis_line(ax, "polar") + + +class axis_line_theta(axis_line_theta_inside, axis_line_theta_outside): + """ + theta-axis line + + Parameters + ---------- + theme_element : element_line + """ + + +class axis_line_x(axis_line_x_top, axis_line_x_bottom, axis_line_theta): """ x-axis line @@ -1350,7 +1421,45 @@ def blank_ax(self, ax: Axes): ax.spines["right"].set_visible(False) -class axis_line_y(axis_line_y_left, axis_line_y_right): +class axis_line_r_start(themeable): + """ + r-axis line at the start angle + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + _style_polar_axis_line(self, ax, "start") + + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + _blank_polar_axis_line(ax, "start") + + +class axis_line_r_end(themeable): + """ + r-axis line at the end angle + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + _style_polar_axis_line(self, ax, "end") + + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + _blank_polar_axis_line(ax, "end") + + +class axis_line_r(axis_line_r_start, axis_line_r_end): + """ + r-axis line + + Parameters + ---------- + theme_element : element_line + """ + + +class axis_line_y(axis_line_y_left, axis_line_y_right, axis_line_r): """ y-axis line @@ -1950,11 +2059,6 @@ def blank_figure(self, figure: Figure, targets: ThemeTargets): for rect in targets.panel_border: rect.set_visible(False) - def blank_ax(self, ax: Axes): - super().blank_ax(ax) - if "polar" in ax.spines: - ax.spines["polar"].set_visible(False) - class plot_background(themeable): """ From 862e96471b55efb83e3f29dc2ccd42847004da41 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 01:06:43 +0300 Subject: [PATCH 038/126] fix(coord_radial): persist themed theta/r spine visibility across draws --- plotnine/_mpl/_polar_axes.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plotnine/_mpl/_polar_axes.py b/plotnine/_mpl/_polar_axes.py index 3bc799f39b..d636f3394b 100644 --- a/plotnine/_mpl/_polar_axes.py +++ b/plotnine/_mpl/_polar_axes.py @@ -27,9 +27,24 @@ def draw(self, renderer: RendererBase) -> None: visible on top of opaque geoms. When `panel_ontop=True` everything is already above the geoms, so the re-draw is a visual no-op. + + `PolarAxes.draw` also derives `inner`/`start`/`end` spine + visibility from pure geometry (donut hole present, arc partial), + discarding whatever axis_line theming chose. Theming always sets + every one of these three spines (blank or not) before the first + real draw, so saving their visibility beforehand and restoring + it right after `super().draw()` makes that choice stick. """ + spine_names = ("inner", "start", "end") + visible = { + name: self.spines[name].get_visible() for name in spine_names + } + super().draw(renderer) + for name, is_visible in visible.items(): + self.spines[name].set_visible(is_visible) + for axis in (self.raxis, self.thetaaxis): for label in axis.get_ticklabels(): if label.get_visible(): From d2560ee985b77d12ba4f73c8ad20c03b4b05ec94 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 01:11:30 +0300 Subject: [PATCH 039/126] test(coord_polar): cover axis_line control of polar/radial spines --- tests/test_coord_polar.py | 44 ++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index 0e071fb150..c286a7474d 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -1,6 +1,5 @@ import numpy as np import pandas as pd -from matplotlib import pyplot as plt from numpy.testing import assert_allclose from plotnine import ( @@ -9,10 +8,12 @@ element_blank, element_line, geom_col, + geom_point, ggplot, theme, ) from plotnine.coords.coord_polar import coord_polar +from plotnine.data import mtcars from plotnine.iapi import labels_view, panel_view, scale_view from plotnine.scales import scale_x_continuous, scale_y_continuous @@ -219,23 +220,46 @@ def test_coord_polar_keeps_labels_when_theta_x(): assert out.y == "ylab" -def test_coord_polar_draw_uses_polar_axes_and_hides_blank_border(): +def test_coord_radial_default_theme_hides_theta_and_r_boundaries(): + # theme_gray blanks axis_line_x/axis_line_y; axis_line_theta and + # axis_line_r nest under them, so a default plot shows none of the + # wedge/donut boundary lines without any explicit theme() call. + p = ( + ggplot(mtcars, aes("disp", "mpg")) + + geom_point() + + coord_radial(start=-0.4 * np.pi, end=0.4 * np.pi, inner_radius=0.3) + ) + assert p == "coord_radial_default_theme_hides_theta_and_r_boundaries" + + +def test_coord_polar_axis_line_controls_polar_spine(): + # panel_border no longer owns the outer circle; axis_line does, and + # unlike panel_border it can style it, not just hide it. data = pd.DataFrame({"x": ["a", "b"], "y": [1, 2]}) p = ( ggplot(data, aes("x", "y")) + geom_col() + coord_polar() - + theme(panel_border=element_blank(), axis_line=element_line()) + + theme( + panel_border=element_blank(), + axis_line=element_line(color="red", size=2), + ) ) + assert p == "coord_polar_axis_line_controls_polar_spine" - fig = p.draw() - try: - ax = fig.axes[0] - assert ax.name == "p9polar" - assert not ax.spines["polar"].get_visible() - finally: - plt.close(fig) +def test_coord_polar_axis_line_r_start_shows_on_full_circle(): + # coord_polar() is a full circle, so matplotlib's own default hides + # the 'start'/'end' spokes. Explicitly theming axis_line_r_start + # must show it anyway, and the choice must survive the real draw. + data = pd.DataFrame({"x": ["a", "b"], "y": [1, 2]}) + p = ( + ggplot(data, aes("x", "y")) + + geom_col() + + coord_polar() + + theme(axis_line_r_start=element_line(color="blue", size=2)) + ) + assert p == "coord_polar_axis_line_r_start_shows_on_full_circle" def test_coord_polar_default_theme_does_not_crash(): From dd1e5de5f777b2a73817b366cf1bee9c486c07ad Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 01:19:44 +0300 Subject: [PATCH 040/126] docs(coord_radial): tighten p9PolarAxes.draw docstring precision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final review nit: the save/restore runs on every draw, not just the first — correctness depends on theming preceding the final rendering draw, not literally the first one. --- plotnine/_mpl/_polar_axes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plotnine/_mpl/_polar_axes.py b/plotnine/_mpl/_polar_axes.py index d636f3394b..e8a29369ab 100644 --- a/plotnine/_mpl/_polar_axes.py +++ b/plotnine/_mpl/_polar_axes.py @@ -31,9 +31,9 @@ def draw(self, renderer: RendererBase) -> None: `PolarAxes.draw` also derives `inner`/`start`/`end` spine visibility from pure geometry (donut hole present, arc partial), discarding whatever axis_line theming chose. Theming always sets - every one of these three spines (blank or not) before the first - real draw, so saving their visibility beforehand and restoring - it right after `super().draw()` makes that choice stick. + every one of these three spines (blank or not) before each real + draw, so saving their visibility beforehand and restoring it + right after `super().draw()` makes that choice stick. """ spine_names = ("inner", "start", "end") visible = { From 89dc0a12026d05f0b3d597fac44017b95bf00e61 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 01:50:34 +0300 Subject: [PATCH 041/126] fix(coord_radial): make axis_line polar spine locking re-entrant Task 2's persistence fix (save/restore around super().draw()) never actually worked: PolarAxes.draw() paints spines in the same call that recomputes their geometry-driven visibility, so restoring the flag afterward was always too late for the pixels already on the canvas. Confirmed by rendering actual images, not just checking spine.get_visible() (which reports correctly right up until you look at the pixels). Replace it with p9PolarAxes.set_spine_visible: it replaces the spine's own set_visible so matplotlib's internal call, made moments before it paints, reapplies the locked value instead of the geometry-driven one. --- plotnine/_mpl/_polar_axes.py | 36 +++++++++++++++++++++--------------- plotnine/themes/themeable.py | 14 ++++++++------ 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/plotnine/_mpl/_polar_axes.py b/plotnine/_mpl/_polar_axes.py index e8a29369ab..c3e455485b 100644 --- a/plotnine/_mpl/_polar_axes.py +++ b/plotnine/_mpl/_polar_axes.py @@ -27,24 +27,9 @@ def draw(self, renderer: RendererBase) -> None: visible on top of opaque geoms. When `panel_ontop=True` everything is already above the geoms, so the re-draw is a visual no-op. - - `PolarAxes.draw` also derives `inner`/`start`/`end` spine - visibility from pure geometry (donut hole present, arc partial), - discarding whatever axis_line theming chose. Theming always sets - every one of these three spines (blank or not) before each real - draw, so saving their visibility beforehand and restoring it - right after `super().draw()` makes that choice stick. """ - spine_names = ("inner", "start", "end") - visible = { - name: self.spines[name].get_visible() for name in spine_names - } - super().draw(renderer) - for name, is_visible in visible.items(): - self.spines[name].set_visible(is_visible) - for axis in (self.raxis, self.thetaaxis): for label in axis.get_ticklabels(): if label.get_visible(): @@ -58,5 +43,26 @@ def thetaaxis(self) -> ThetaAxis: def raxis(self) -> RadialAxis: return cast("RadialAxis", self.yaxis) + def set_spine_visible(self, name: str, visible: bool) -> None: + """ + Set a spine's visibility and block matplotlib's from overwriting it + + `PolarAxes.draw` derives `inner`/`start`/`end` spine visibility + from pure geometry (donut hole present, arc partial) and paints + the spine in that same call, so a plain `set_visible` on a spine + is overwritten and never matters. + """ + # This method makes the spine visible and replaces the spine's + # own `set_visible` with a no-op function to block matplotlib's + # internal call before it paints. + spine = self.spines[name] + spine_cls = type(spine) + spine_cls.set_visible(spine, visible) + + def noop(b): + pass + + spine.set_visible = noop + register_projection(p9PolarAxes) diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index 7b0439ef69..d19bb66851 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -1287,16 +1287,18 @@ def _style_polar_axis_line(themeable, ax, spine): Mirrors `_style_axis_line`: an explicitly themed (non-blank) spine is shown (`visible=True`) even if matplotlib's own geometry-driven default would hide it (e.g. a full circle has no `start`/`end` - spokes). `p9PolarAxes.draw` is what makes this choice stick across - draws. + spokes). Visibility goes through `p9PolarAxes.set_spine_visible` + rather than the `properties` dict, since that's what makes the + choice stick across draws. """ if spine not in ax.spines: return properties = themeable._get_properties(omit=("solid_capstyle",)) - properties.setdefault("visible", True) + visible = properties.pop("visible", True) if "zorder" not in properties: properties["zorder"] = 10000 ax.spines[spine].set(**properties) + ax.set_spine_visible(spine, visible) def _blank_polar_axis_line(ax, spine): @@ -1306,11 +1308,11 @@ def _blank_polar_axis_line(ax, spine): A plain `Axes` has no matching spine name, so this is a safe no-op for cartesian axes. It's reachable from every `blank_ax` call, polar or not, since `axis_line_theta`/`axis_line_r` nest under - `axis_line_x`/`axis_line_y`. `p9PolarAxes.draw` is what makes this - choice stick across draws. + `axis_line_x`/`axis_line_y`. `p9PolarAxes.set_spine_visible` is what + makes this choice stick across draws. """ if spine in ax.spines: - ax.spines[spine].set_visible(False) + ax.set_spine_visible(spine, False) class axis_line_x_bottom(themeable): From 5d39f43536861e6bd62a49742d0e9c1a7b0d4bc8 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 16:48:54 +0300 Subject: [PATCH 042/126] feat(typing): add PolarSide alias for theta/r side names --- plotnine/typing.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plotnine/typing.py b/plotnine/typing.py index 200f95bc7b..7583fce099 100644 --- a/plotnine/typing.py +++ b/plotnine/typing.py @@ -114,6 +114,16 @@ def to_pandas(self) -> pd.DataFrame: # Guide Side: TypeAlias = Literal["left", "right", "top", "bottom"] +PolarSide: TypeAlias = Literal[ + "theta_inside", "theta_outside", "r_start", "r_end" +] +""" +A side of a polar panel's theta or r axis + +`"theta_inside"`/`"theta_outside"` are the donut-hole and rim boundaries +of the angular axis; `"r_start"`/`"r_end"` are the start-angle and +end-angle spokes of the radial axis. +""" LegendPosition: TypeAlias = ( Literal["left", "right", "top", "bottom", "inside"] | tuple[float, float] ) From 8dff09eb5a4a28352a5a59efa5b7068179fef31f Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 16:53:43 +0300 Subject: [PATCH 043/126] feat(utils): widen side_artists/MARGIN_SIDE for polar sides --- plotnine/_utils/__init__.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/plotnine/_utils/__init__.py b/plotnine/_utils/__init__.py index 0aebd502b1..9839d1d360 100644 --- a/plotnine/_utils/__init__.py +++ b/plotnine/_utils/__init__.py @@ -35,6 +35,7 @@ FloatArray, FloatArrayLike, HorizontalJustification, + PolarSide, Side, VerticalJustification, ) @@ -61,14 +62,15 @@ to_rgba = color_utils.to_rgba -def side_artists(side: str) -> tuple[str, str]: +def side_artists(side: Side | PolarSide) -> tuple[str, str]: """ - Return the `(tickline, label)` tick-attribute names for an axis side + Return the (tickline, label) attribute names for one side of an axis - The bottom/left side maps to `tick1line`/`label1` and the top/right side - to `tick2line`/`label2`. + `"top"`/`"right"` (and their polar counterparts `"theta_outside"`/ + `"r_end"`) use the second tick/label pair; every other side uses the + first. """ - if side in ("top", "right"): + if side in ("top", "right", "theta_outside", "r_end"): return ("tick2line", "label2") return ("tick1line", "label1") @@ -84,12 +86,18 @@ def side_artists(side: str) -> tuple[str, str]: # The margin side that faces inward for an element on each side: for an axis # the side facing the panel (bottom axis -> top "t", top -> "b", left -> right # "r", right -> "l"); for a legend title/text the side facing the keys. It is -# the initial of the opposite side; cf. OPPOSITE_SIDE. -MARGIN_SIDE: dict[Side, str] = { +# the initial of the opposite side; cf. OPPOSITE_SIDE. Polar boundaries reuse +# the cartesian side their tick/label pair matches in side_artists: outside +# and start behave like bottom/left, inside and end like top/right. +MARGIN_SIDE: dict[Side | PolarSide, str] = { "bottom": "t", "top": "b", "left": "r", "right": "l", + "theta_outside": "t", + "theta_inside": "b", + "r_start": "r", + "r_end": "l", } From 61125f80e9d95b2de8ada41fb23a692ec835b540 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 17:02:30 +0300 Subject: [PATCH 044/126] feat(mpl): make axis_at resolve polar theta/r sides --- plotnine/_mpl/axes.py | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/plotnine/_mpl/axes.py b/plotnine/_mpl/axes.py index 3fc712b968..0a780df1e1 100644 --- a/plotnine/_mpl/axes.py +++ b/plotnine/_mpl/axes.py @@ -8,7 +8,7 @@ from matplotlib.projections import register_projection if TYPE_CHECKING: - from plotnine.typing import Side + from plotnine.typing import PolarSide, Side AxisT = TypeVar("AxisT", XAxis, YAxis) @@ -116,34 +116,43 @@ def _make_sec_axis(self, cls: type[AxisT]) -> AxisT: return axis -def axis_at(ax: Axes, side: Side) -> XAxis | YAxis | None: +def axis_at(ax: Axes, side: Side | PolarSide) -> XAxis | YAxis | None: """ - Return the axis of `ax` whose ticks occupy `side`, if any + Return the axis whose ticks occupy `side`, if any - Considers the primary axis of the side's dimension and, on a - `p9Axes`, the secondary one. + For a cartesian `side`, considers the primary axis of the side's + dimension and, on a `p9Axes`, the secondary one, via mpl's own + tick-flag state — reliable for `XAxis`/`YAxis`. For a polar `side` + (a theta/r side), reads `p9PolarAxes.axis_at_side` instead: + `ThetaAxis`/`RadialAxis` can't self-report which side they + occupy through `get_tick_params()`, so plotnine tracks it + explicitly there. Parameters ---------- ax : Panel axes. side : - Side of the panel. + Side of a cartesian panel, or side of a polar one. Returns ------- : The axis with active ticks on `side`, or `None` when that side - shows no ticks (e.g. an interior facet panel). + shows no ticks (e.g. an interior facet panel, or a polar + side nothing occupies). """ - if side in ("top", "bottom"): - candidates = (ax.xaxis, getattr(ax, "sec_xaxis", None)) - else: - candidates = (ax.yaxis, getattr(ax, "sec_yaxis", None)) - for axis in candidates: - if axis and axis.get_tick_params(which="major").get(side): - return axis - return None + if side in ("top", "bottom", "left", "right"): + candidates = ( + (ax.xaxis, getattr(ax, "sec_xaxis", None)) + if side in ("top", "bottom") + else (ax.yaxis, getattr(ax, "sec_yaxis", None)) + ) + for axis in candidates: + if axis and axis.get_tick_params(which="major").get(side): + return axis + return None + return getattr(ax, "axis_at_side", {}).get(side) register_projection(p9Axes) From 123e8d3282eae06f9d9dfff5cd32a61323f31178 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 17:07:11 +0300 Subject: [PATCH 045/126] feat(mpl): add axis_at_side bookkeeping to p9PolarAxes --- plotnine/_mpl/_polar_axes.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/plotnine/_mpl/_polar_axes.py b/plotnine/_mpl/_polar_axes.py index c3e455485b..6b709176e4 100644 --- a/plotnine/_mpl/_polar_axes.py +++ b/plotnine/_mpl/_polar_axes.py @@ -9,6 +9,8 @@ from matplotlib.backend_bases import RendererBase from matplotlib.projections.polar import RadialAxis, ThetaAxis + from plotnine.typing import PolarSide + class p9PolarAxes(PolarAxes): """ @@ -17,6 +19,17 @@ class p9PolarAxes(PolarAxes): name = "p9polar" + _axis_at_side: dict[PolarSide, ThetaAxis | RadialAxis] | None = None + + @property + def axis_at_side(self) -> dict[PolarSide, ThetaAxis | RadialAxis]: + """ + The theta/r axis artist occupying each active polar side + """ + if self._axis_at_side is None: + self._axis_at_side = {} + return self._axis_at_side + def draw(self, renderer: RendererBase) -> None: """ Draw the axes, then lift the tick labels above the geoms From 96e5df19c6d5ab947a5cee183926016a43b2c3b1 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 17:16:02 +0300 Subject: [PATCH 046/126] fix(coord_polar): activate theta/r tick sides from scale position --- plotnine/coords/coord_polar.py | 62 +++++++++++++++++++++++++++++----- tests/test_coord_polar.py | 9 +++++ 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py index 52f4e4e2e9..f5bf6c1478 100644 --- a/plotnine/coords/coord_polar.py +++ b/plotnine/coords/coord_polar.py @@ -2,20 +2,45 @@ from dataclasses import replace from typing import TYPE_CHECKING, Literal, cast +from warnings import warn import numpy as np -from .._mpl._polar_axes import p9PolarAxes # noqa: F401 (registers "p9polar") +from .._mpl._polar_axes import p9PolarAxes # noqa: TCH001 +from ..exceptions import PlotnineWarning from ..iapi import panel_ranges -from .coord import coord, dist_euclidean +from .coord import _activate_axis, coord, dist_euclidean if TYPE_CHECKING: import pandas as pd from matplotlib.axes import Axes - from matplotlib.projections.polar import PolarAxes + from matplotlib.projections.polar import PolarAxes, RadialAxis, ThetaAxis from plotnine.iapi import labels_view, layout_details, panel_view from plotnine.scales.scale import scale + from plotnine.typing import PolarSide, Side + + +def _resolve_theta_side(position: Side) -> PolarSide: + """ + Return which theta side a scale's x position resolves to + + `"bottom"` (the default `scale_x_*` position) is the outer rim, + matching today's only visible placement; `"top"` moves theta to the + inner (donut-hole) side. + """ + return "theta_outside" if position == "bottom" else "theta_inside" + + +def _resolve_r_side(position: Side) -> PolarSide: + """ + Return which r side a scale's y position resolves to + + `"left"` (the default `scale_y_*` position) is the start-angle + spoke, matching the existing 270-degree default placement on a full + circle; `"right"` moves r to the end-angle spoke. + """ + return "r_start" if position == "left" else "r_end" class coord_polar(coord): @@ -149,14 +174,35 @@ def setup_ax( layout_info: layout_details, ) -> None: """ - Limits, breaks and tick labels for a polar panel + Limits, breaks, tick labels, and active side for a polar panel - Skips `coord`'s per-side spine/axis-line setup: the theta axis - wraps the full circle and the r axis runs along one spoke, - neither of which maps onto a cartesian left/right/top/bottom - side or a `p9Axes`-style spine. + Activates exactly one theta side (from `panel_params.x.position`) + and one r side (from `panel_params.y.position`). It records the + choice on `p9PolarAxes.axis_at_side` so theming can find it. """ + if panel_params.x.sec is not None: + warn( + f"{self.__class__.__name__}() does not support a secondary " + "theta axis.", + PlotnineWarning, + ) + self._setup_ticks_labels(ax, panel_params) + polar_ax = cast("p9PolarAxes", ax) + theta_side = _resolve_theta_side(panel_params.x.position) + r_side = _resolve_r_side(panel_params.y.position) + + _activate_axis( + ax.xaxis, + "top" if theta_side == "theta_outside" else "bottom", + True, + ) + _activate_axis( + ax.yaxis, "left" if r_side == "r_start" else "right", True + ) + + polar_ax.axis_at_side[theta_side] = cast("ThetaAxis", ax.xaxis) + polar_ax.axis_at_side[r_side] = cast("RadialAxis", ax.yaxis) # ------------------------------------------------------------------ # Helpers diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index c286a7474d..b796da67a5 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -262,6 +262,15 @@ def test_coord_polar_axis_line_r_start_shows_on_full_circle(): assert p == "coord_polar_axis_line_r_start_shows_on_full_circle" +def test_coord_polar_ticks_visible_by_default(): + # Ticks were previously invisible on every polar panel regardless of + # theme, because activation was skipped entirely; this is the + # regression test for that fix. + data = pd.DataFrame({"x": ["a", "b"], "y": [1, 2]}) + p = ggplot(data, aes("x", "y")) + geom_col() + coord_polar() + assert p == "coord_polar_ticks_visible_by_default" + + def test_coord_polar_default_theme_does_not_crash(): data = pd.DataFrame({"x": ["a", "b"], "y": [1, 2]}) p = ggplot(data, aes("x", "y")) + geom_col() + coord_polar() From 22b0780f228e94358616fab693ffcb0990a94e7d Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 17:23:56 +0300 Subject: [PATCH 047/126] refactor(coord_polar): reuse p9PolarAxes.thetaaxis/raxis instead of ad hoc casts --- plotnine/coords/coord_polar.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py index f5bf6c1478..f42a79b6c5 100644 --- a/plotnine/coords/coord_polar.py +++ b/plotnine/coords/coord_polar.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: import pandas as pd from matplotlib.axes import Axes - from matplotlib.projections.polar import PolarAxes, RadialAxis, ThetaAxis + from matplotlib.projections.polar import PolarAxes from plotnine.iapi import labels_view, layout_details, panel_view from plotnine.scales.scale import scale @@ -201,8 +201,8 @@ def setup_ax( ax.yaxis, "left" if r_side == "r_start" else "right", True ) - polar_ax.axis_at_side[theta_side] = cast("ThetaAxis", ax.xaxis) - polar_ax.axis_at_side[r_side] = cast("RadialAxis", ax.yaxis) + polar_ax.axis_at_side[theta_side] = polar_ax.thetaaxis + polar_ax.axis_at_side[r_side] = polar_ax.raxis # ------------------------------------------------------------------ # Helpers From 997f64c743c15c2d0f0782bc089d9073220d68ab Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 17:29:27 +0300 Subject: [PATCH 048/126] feat(coord_radial): remove r_axis_inside, drive r placement from scale_y position r_axis_inside and scale_y_*(position=...) were two competing knobs for the same decision. scale_y_*(position=...) already resolves the r side via coord_polar's _resolve_r_side, so drop the redundant parameter and gate rlabel_position on that resolution instead. Warn rather than silently no-op when a full circle resolves to r_end, since matplotlib hides that boundary outright on a full circle. --- plotnine/coords/coord_radial.py | 45 +++++++++++++++------------------ tests/test_coord_polar.py | 22 ++++++++++++++++ 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 93323b595a..8f6f5f4fea 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -2,10 +2,12 @@ from dataclasses import replace from typing import TYPE_CHECKING, Literal, cast +from warnings import warn import numpy as np -from .coord_polar import coord_polar +from ..exceptions import PlotnineWarning +from .coord_polar import _resolve_r_side, coord_polar if TYPE_CHECKING: import pandas as pd @@ -46,17 +48,6 @@ class coord_radial(coord_polar): Size of the inner hole as a fraction of the outer radius, in ``[0, 1)``. ``0`` (default) means no hole; ``0.3`` creates a 30 % donut hole, useful for gauge and donut charts. - r_axis_inside : - Where to place the radial (r) axis tick labels. - - * ``None`` (default) — let Matplotlib decide (usually outside). - * ``True`` — force inside, aligned just past the *start* angle. - * ``False`` — force outside (Matplotlib default). - * *float* — place at this theta angle in radians (clockwise from - North). - - Unlike ggplot2's ``r.axis.inside``, a length-2 value for separate - primary and secondary axis placement is not supported. rotate_angle : If ``True``, rotate the ``angle`` aesthetic so that text or other rotated marks align tangentially with the arc at their spoke. The @@ -87,7 +78,7 @@ class coord_radial(coord_polar): the gap to the outer circle through the ``axis_text_x`` margin. The Python API uses snake_case names for arguments that are dotted in - ggplot2: ``inner_radius``, ``r_axis_inside``, and ``rotate_angle``. + ggplot2: ``inner_radius`` and ``rotate_angle``. Examples -------- @@ -135,7 +126,6 @@ def __init__( direction: int = 1, expand: bool = True, inner_radius: float = 0, - r_axis_inside: bool | float | None = None, rotate_angle: bool = False, thetalim: tuple[float, float] | None = None, rlim: tuple[float, float] | None = None, @@ -154,7 +144,6 @@ def __init__( ) self.end = end self.inner_radius = inner_radius - self.r_axis_inside = r_axis_inside self.rotate_angle = rotate_angle self.thetalim = thetalim self.rlim = rlim @@ -380,18 +369,24 @@ def setup_ax( ) polar_ax.set_rorigin(r_origin) - # Radial axis label placement. - if self.r_axis_inside is None: - # Left spoke, matching ggplot2's default. - polar_ax.set_rlabel_position(270) - elif isinstance(self.r_axis_inside, bool): - if self.r_axis_inside: - # Just inside the start angle keeps it out of the data. - polar_ax.set_rlabel_position(np.degrees(self.start) + 10) + # Radial axis label placement. RadialTick.update_position (mpl) + # reads rlabel_position only for a full circle; a partial arc + # ignores it and always uses thetamin/thetamax directly, so this + # only matters here. + if self.end is None: + r_side = _resolve_r_side(panel_params.y.position) + if r_side == "r_end": + warn( + "coord_radial(): a full circle has no 'end' " + "boundary distinct from 'start' for the r axis " + "(matplotlib always hides it there); " + "scale_y_*(position='right') has no visible " + "effect here. Set `end` for a partial arc to show " + "the r axis on the opposite spoke.", + PlotnineWarning, + ) else: polar_ax.set_rlabel_position(270) - else: - polar_ax.set_rlabel_position(np.degrees(float(self.r_axis_inside))) ax.tick_params(axis="x", which="major", direction="out") if (angle := self._theta_guide_angle()) is not None: diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index b796da67a5..79184716e7 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -283,6 +283,28 @@ def test_coord_radial_arc_uses_end_or_full_turn(): assert coord_radial()._arc == 2 * np.pi +def test_coord_radial_no_longer_has_r_axis_inside(): + import pytest + + with pytest.raises(TypeError, match="r_axis_inside"): + coord_radial(r_axis_inside=True) # type: ignore[call-arg] + + +def test_coord_radial_full_circle_position_right_warns(): + import pytest + + from plotnine.exceptions import PlotnineWarning + + p = ( + ggplot(mtcars, aes("disp", "mpg")) + + geom_point() + + coord_radial() + + scale_y_continuous(position="right") + ) + with pytest.warns(PlotnineWarning, match="end.*boundary"): + p.draw_test() + + def test_coord_radial_setup_panel_params_for_partial_arc(): scale_x, scale_y = trained_scales( y_breaks=(0, 2, 4, 8, 10), From e5d3d430f35bc891baa5b07d3f6f8eb3245557f9 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 17:51:00 +0300 Subject: [PATCH 049/126] feat(theme): add axis_text_theta/axis_text_r themeables for polar panels axis_text_theta_outside/_inside and axis_text_r_start/_end nest under the existing axis_text_x/axis_text_y so a plain axis_text= theme now reaches a coord_radial() panel's tick labels, not just cartesian ones. --- plotnine/themes/theme_gray.py | 4 ++ plotnine/themes/themeable.py | 128 ++++++++++++++++++++++++++++++++-- tests/test_coord_polar.py | 14 ++++ 3 files changed, 142 insertions(+), 4 deletions(-) diff --git a/plotnine/themes/theme_gray.py b/plotnine/themes/theme_gray.py index afb43ab683..c4fec59f7b 100644 --- a/plotnine/themes/theme_gray.py +++ b/plotnine/themes/theme_gray.py @@ -68,6 +68,10 @@ def __init__(self, base_size=11, base_family=None): axis_text_y_right=element_text( ha="left", margin=margin(l=fifth_line) ), + axis_text_theta_outside=element_text(margin=margin(t=fifth_line)), + axis_text_theta_inside=element_text(margin=margin(b=fifth_line)), + axis_text_r_start=element_text(margin=margin(r=fifth_line)), + axis_text_r_end=element_text(margin=margin(l=fifth_line)), axis_ticks=element_line(color="#333333"), axis_ticks_length=0, axis_ticks_length_major=quarter_line, diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index d19bb66851..cae44ba05a 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -35,7 +35,7 @@ from plotnine import theme from plotnine.themes.targets import ThemeTargets - from plotnine.typing import Side + from plotnine.typing import PolarSide, Side class themeable(metaclass=RegistryHierarchyMeta): @@ -533,7 +533,7 @@ def blend_alpha( return properties -def _set_axis_text_margin(themeable, ax, side: Side): +def _set_axis_text_margin(themeable, ax, side: Side | PolarSide): """ Set the gap between axis tick and axis text """ @@ -1139,7 +1139,67 @@ def blank_ax(self, ax: Axes): t.label2.set_visible(False) -class axis_text_x(axis_text_x_top, axis_text_x_bottom): +class axis_text_theta_outside(MixinSequenceOfValues): + """ + theta-axis tick labels at the outer (rim) boundary + + Parameters + ---------- + theme_element : element_text + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + if (axis := axis_at(ax, "theta_outside")) is None: + return + labels = [t.label2 for t in axis.get_major_ticks()] + self.set(labels, self._get_properties(omit=("margin", "va"))) + _set_axis_text_margin(self, ax, "theta_outside") + + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + if (axis := axis_at(ax, "theta_outside")) is None: + return + for t in axis.get_major_ticks(): + t.label2.set_visible(False) + + +class axis_text_theta_inside(MixinSequenceOfValues): + """ + theta-axis tick labels at the inner (donut-hole) boundary + + Parameters + ---------- + theme_element : element_text + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + if (axis := axis_at(ax, "theta_inside")) is None: + return + labels = [t.label1 for t in axis.get_major_ticks()] + self.set(labels, self._get_properties(omit=("margin", "va"))) + _set_axis_text_margin(self, ax, "theta_inside") + + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + if (axis := axis_at(ax, "theta_inside")) is None: + return + for t in axis.get_major_ticks(): + t.label1.set_visible(False) + + +class axis_text_theta(axis_text_theta_outside, axis_text_theta_inside): + """ + theta-axis tick labels + + Parameters + ---------- + theme_element : element_text + """ + + +class axis_text_x(axis_text_x_top, axis_text_x_bottom, axis_text_theta): """ x-axis tick labels @@ -1199,7 +1259,67 @@ def blank_ax(self, ax: Axes): t.label2.set_visible(False) -class axis_text_y(axis_text_y_left, axis_text_y_right): +class axis_text_r_start(MixinSequenceOfValues): + """ + r-axis tick labels at the start-angle spoke + + Parameters + ---------- + theme_element : element_text + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + if (axis := axis_at(ax, "r_start")) is None: + return + labels = [t.label1 for t in axis.get_major_ticks()] + self.set(labels, self._get_properties(omit=("margin", "ha"))) + _set_axis_text_margin(self, ax, "r_start") + + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + if (axis := axis_at(ax, "r_start")) is None: + return + for t in axis.get_major_ticks(): + t.label1.set_visible(False) + + +class axis_text_r_end(MixinSequenceOfValues): + """ + r-axis tick labels at the end-angle spoke + + Parameters + ---------- + theme_element : element_text + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + if (axis := axis_at(ax, "r_end")) is None: + return + labels = [t.label2 for t in axis.get_major_ticks()] + self.set(labels, self._get_properties(omit=("margin", "ha"))) + _set_axis_text_margin(self, ax, "r_end") + + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + if (axis := axis_at(ax, "r_end")) is None: + return + for t in axis.get_major_ticks(): + t.label2.set_visible(False) + + +class axis_text_r(axis_text_r_start, axis_text_r_end): + """ + r-axis tick labels + + Parameters + ---------- + theme_element : element_text + """ + + +class axis_text_y(axis_text_y_left, axis_text_y_right, axis_text_r): """ y-axis tick labels diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index 79184716e7..b9d29d4976 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -7,6 +7,7 @@ coord_radial, element_blank, element_line, + element_text, geom_col, geom_point, ggplot, @@ -411,3 +412,16 @@ def test_coord_polar_axis_clearance(): + coord_polar("y") ) assert p == "coord_polar_axis_clearance" + + +def test_coord_radial_axis_text_theming(): + # axis_text used to be a no-op on a polar panel's tick labels; + # axis_text_theta_* / axis_text_r_* now nest under axis_text_x/y + # so a plain axis_text= theme reaches both the theta and r labels. + p = ( + ggplot(mtcars, aes("disp", "mpg")) + + geom_point() + + coord_radial(start=0.5 * np.pi, end=-0.5 * np.pi, inner_radius=0.3) + + theme(axis_text=element_text(color="red", size=5)) + ) + assert p == "coord_radial_axis_text_theming" From 186fb48d26490616c00876823984626d8305aed6 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 18:09:50 +0300 Subject: [PATCH 050/126] fix(coord_radial): preserve themed r-axis tick style across mpl reset_ticks matplotlib's PolarAxes.draw calls yaxis.reset_ticks() on the first real render pass, deleting the lazily-cached tick lists; the regenerated ticks carry matplotlib's default styling, wiping the tick and tick-label properties the theme set directly on the r-axis. Because the reset and the paint that consumes it happen inside one PolarAxes.draw call, restoring after draw() returns is too late. Wrap raxis.reset_ticks so it re-copies the pre-reset tick[0] styling onto the fresh tick[0] (via mpl's own _copy_tick_props) before the paint; mpl then propagates that look to every tick it grows. Covers label and tick-line properties for major and minor ticks. The theta-axis has no such reset and is unaffected. --- plotnine/_mpl/_polar_axes.py | 47 ++++++++++++++++++++++++++++++++++++ plotnine/ggplot.py | 9 +++++++ 2 files changed, 56 insertions(+) diff --git a/plotnine/_mpl/_polar_axes.py b/plotnine/_mpl/_polar_axes.py index 6b709176e4..f701399f80 100644 --- a/plotnine/_mpl/_polar_axes.py +++ b/plotnine/_mpl/_polar_axes.py @@ -21,6 +21,11 @@ class p9PolarAxes(PolarAxes): _axis_at_side: dict[PolarSide, ThetaAxis | RadialAxis] | None = None + # Whether the r-axis ticks already keep their themed styling across + # matplotlib's tick resets. Guards `lock_raxis_tick_style` so repeated + # calls do not stack wrappers on top of each other. + _raxis_tick_style_locked: bool = False + @property def axis_at_side(self) -> dict[PolarSide, ThetaAxis | RadialAxis]: """ @@ -77,5 +82,47 @@ def noop(b): spine.set_visible = noop + def lock_raxis_tick_style(self) -> None: + """ + Keep the r-axis ticks looking as the theme styled them across draws + + The theme styles the r-axis ticks and tick labels once, before any + real render pass. On the first render `PolarAxes.draw` calls + `self.yaxis.reset_ticks()`, which drops the lazily-cached tick lists; + the next access rebuilds fresh `Tick` objects with matplotlib's + default styling, so the themed colours, sizes and tick lengths are + gone by the time those ticks are painted. This makes the r-axis ticks + immune to that wipe. Only the r-axis needs it; `PolarAxes.draw` has no + matching reset for the theta-axis. + """ + # The reset and the paint that consumes it both happen inside a single + # `PolarAxes.draw` call, so restoring styling after `draw` returns is + # too late -- the default-styled ticks are already painted. Wrapping + # `reset_ticks` instead closes the gap synchronously: the wrapper + # re-copies the styling from the pre-reset ticks onto the fresh ones + # before control returns to `PolarAxes.draw`, so every tick the axis + # then grows and paints inherits the themed look. Capturing the source + # styling live on each reset (rather than from a one-off snapshot) + # keeps this re-entrant across any number of redraws. + if self._raxis_tick_style_locked: + return + self._raxis_tick_style_locked = True + + axis = self.raxis + reset_ticks = axis.reset_ticks + + def reset_ticks_and_restyle() -> None: + # `_copy_tick_props` is matplotlib's own routine for propagating + # the look of `majorTicks[0]` onto the sibling ticks it grows, so + # reusing it leaves the restored ticks identical to freshly grown + # ones -- covering label1/label2, tick1line/tick2line and the tick + # length, for both major and minor ticks. + major, minor = axis.majorTicks[0], axis.minorTicks[0] + reset_ticks() + axis._copy_tick_props(major, axis.majorTicks[0]) # type: ignore + axis._copy_tick_props(minor, axis.minorTicks[0]) # type: ignore + + axis.reset_ticks = reset_ticks_and_restyle + register_projection(p9PolarAxes) diff --git a/plotnine/ggplot.py b/plotnine/ggplot.py index 1fb030bc10..de56db0bdb 100755 --- a/plotnine/ggplot.py +++ b/plotnine/ggplot.py @@ -403,6 +403,15 @@ def draw(self, *, show: bool = False) -> Figure: # Artist object theming self.theme.apply() + # Now that the themed tick styling is final, shield the polar + # r-axis ticks from matplotlib's first-draw tick reset, which + # would otherwise rebuild them with default styling. + from ._mpl._polar_axes import p9PolarAxes + + for ax in self.axs: + if isinstance(ax, p9PolarAxes): + ax.lock_raxis_tick_style() + self._insets.draw(which="above") return figure From ad59ea3f83f44ebf6b7da8f89d1ece4ab4d73a52 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 18:21:49 +0300 Subject: [PATCH 051/126] feat(theme): add axis_ticks_major_theta/axis_ticks_major_r themeables --- plotnine/themes/themeable.py | 90 ++++++++++++++++++++++++++++++++++-- tests/test_coord_polar.py | 14 ++++++ 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index cae44ba05a..4bd1c4e3ef 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -1601,7 +1601,7 @@ class axis_line(axis_line_x, axis_line_y): """ -def _style_axis_ticks(themeable, ax, which, side): +def _style_axis_ticks(themeable, ax, which, side: Side | PolarSide): """ Style the tick lines on one side of an axis @@ -1633,7 +1633,7 @@ def _style_axis_ticks(themeable, ax, which, side): themeable.set([getattr(t, attr) for t in ticks], properties) -def _blank_axis_ticks(ax, which, side): +def _blank_axis_ticks(ax, which, side: Side | PolarSide): """ Hide the tick lines on one side of an axis """ @@ -1751,7 +1751,49 @@ def blank_ax(self, ax: Axes): _blank_axis_ticks(ax, "major", "top") -class axis_ticks_major_x(axis_ticks_major_x_top, axis_ticks_major_x_bottom): +class axis_ticks_major_theta_outside(MixinSequenceOfValues): + """ + theta-axis major tick lines at the outer (rim) boundary + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + _style_axis_ticks(self, ax, "major", "theta_outside") + + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + _blank_axis_ticks(ax, "major", "theta_outside") + + +class axis_ticks_major_theta_inside(MixinSequenceOfValues): + """ + theta-axis major tick lines at the inner (donut-hole) boundary + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + _style_axis_ticks(self, ax, "major", "theta_inside") + + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + _blank_axis_ticks(ax, "major", "theta_inside") + + +class axis_ticks_major_theta( + axis_ticks_major_theta_outside, axis_ticks_major_theta_inside +): + """ + theta-axis major tick lines + + Parameters + ---------- + theme_element : element_line + """ + + +class axis_ticks_major_x( + axis_ticks_major_x_top, axis_ticks_major_x_bottom, axis_ticks_major_theta +): """ x-axis major tick lines @@ -1789,7 +1831,47 @@ def blank_ax(self, ax: Axes): _blank_axis_ticks(ax, "major", "right") -class axis_ticks_major_y(axis_ticks_major_y_left, axis_ticks_major_y_right): +class axis_ticks_major_r_start(MixinSequenceOfValues): + """ + r-axis major tick lines at the start-angle spoke + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + _style_axis_ticks(self, ax, "major", "r_start") + + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + _blank_axis_ticks(ax, "major", "r_start") + + +class axis_ticks_major_r_end(MixinSequenceOfValues): + """ + r-axis major tick lines at the end-angle spoke + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + _style_axis_ticks(self, ax, "major", "r_end") + + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + _blank_axis_ticks(ax, "major", "r_end") + + +class axis_ticks_major_r(axis_ticks_major_r_start, axis_ticks_major_r_end): + """ + r-axis major tick lines + + Parameters + ---------- + theme_element : element_line + """ + + +class axis_ticks_major_y( + axis_ticks_major_y_left, axis_ticks_major_y_right, axis_ticks_major_r +): """ y-axis major tick lines diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index b9d29d4976..eb270f0398 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -425,3 +425,17 @@ def test_coord_radial_axis_text_theming(): + theme(axis_text=element_text(color="red", size=5)) ) assert p == "coord_radial_axis_text_theming" + + +def test_coord_radial_axis_ticks_major_theming(): + # axis_ticks used to be a no-op on a polar panel's theta tick + # marks; axis_ticks_major_theta_* / axis_ticks_major_r_* now nest + # under axis_ticks_major_x/y so a plain axis_ticks= theme reaches + # both the theta and r tick marks. + p = ( + ggplot(mtcars, aes("disp", "mpg")) + + geom_point() + + coord_radial(start=0.5 * np.pi, end=-0.5 * np.pi, inner_radius=0.3) + + theme(axis_ticks=element_line(color="blue", size=2)) + ) + assert p == "coord_radial_axis_ticks_major_theming" From dd6359f46b90bd6d2a26786fd465358a08d4856a Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 18:33:08 +0300 Subject: [PATCH 052/126] feat(theme): add axis_ticks_minor_theta/axis_ticks_minor_r themeables --- plotnine/themes/themeable.py | 86 +++++++++++++++++++++++++++++++++++- tests/test_coord_polar.py | 17 +++++++ 2 files changed, 101 insertions(+), 2 deletions(-) diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index 4bd1c4e3ef..cc609abe1e 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -1675,7 +1675,49 @@ def blank_ax(self, ax: Axes): _blank_axis_ticks(ax, "minor", "top") -class axis_ticks_minor_x(axis_ticks_minor_x_top, axis_ticks_minor_x_bottom): +class axis_ticks_minor_theta_outside(MixinSequenceOfValues): + """ + theta-axis minor tick lines at the outer (rim) boundary + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + _style_axis_ticks(self, ax, "minor", "theta_outside") + + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + _blank_axis_ticks(ax, "minor", "theta_outside") + + +class axis_ticks_minor_theta_inside(MixinSequenceOfValues): + """ + theta-axis minor tick lines at the inner (donut-hole) boundary + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + _style_axis_ticks(self, ax, "minor", "theta_inside") + + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + _blank_axis_ticks(ax, "minor", "theta_inside") + + +class axis_ticks_minor_theta( + axis_ticks_minor_theta_outside, axis_ticks_minor_theta_inside +): + """ + theta-axis minor tick lines + + Parameters + ---------- + theme_element : element_line + """ + + +class axis_ticks_minor_x( + axis_ticks_minor_x_top, axis_ticks_minor_x_bottom, axis_ticks_minor_theta +): """ x-axis minor tick lines @@ -1713,7 +1755,47 @@ def blank_ax(self, ax: Axes): _blank_axis_ticks(ax, "minor", "right") -class axis_ticks_minor_y(axis_ticks_minor_y_left, axis_ticks_minor_y_right): +class axis_ticks_minor_r_start(MixinSequenceOfValues): + """ + r-axis minor tick lines at the start-angle spoke + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + _style_axis_ticks(self, ax, "minor", "r_start") + + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + _blank_axis_ticks(ax, "minor", "r_start") + + +class axis_ticks_minor_r_end(MixinSequenceOfValues): + """ + r-axis minor tick lines at the end-angle spoke + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + _style_axis_ticks(self, ax, "minor", "r_end") + + def blank_ax(self, ax: Axes): + super().blank_ax(ax) + _blank_axis_ticks(ax, "minor", "r_end") + + +class axis_ticks_minor_r(axis_ticks_minor_r_start, axis_ticks_minor_r_end): + """ + r-axis minor tick lines + + Parameters + ---------- + theme_element : element_line + """ + + +class axis_ticks_minor_y( + axis_ticks_minor_y_left, axis_ticks_minor_y_right, axis_ticks_minor_r +): """ y-axis minor tick lines diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index eb270f0398..c5a1ea8486 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -439,3 +439,20 @@ def test_coord_radial_axis_ticks_major_theming(): + theme(axis_ticks=element_line(color="blue", size=2)) ) assert p == "coord_radial_axis_ticks_major_theming" + + +def test_coord_radial_axis_ticks_minor_theming(): + # theme_gray blanks axis_ticks_minor globally, so minor ticks stay + # invisible everywhere until a user overrides axis_ticks_minor + # explicitly. axis_ticks_minor_theta_* / axis_ticks_minor_r_* now + # nest under axis_ticks_minor_x/y so a plain axis_ticks_minor= + # theme reaches both the theta and r minor tick marks. + p = ( + ggplot(mtcars, aes("disp", "mpg")) + + geom_point() + + coord_radial(start=0.5 * np.pi, end=-0.5 * np.pi, inner_radius=0.3) + + theme( + axis_ticks_minor=element_line(color="green", size=1), + ) + ) + assert p == "coord_radial_axis_ticks_minor_theming" From 48fa6b707062c4dfccff56d039fe9a87bae836b2 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 18:39:22 +0300 Subject: [PATCH 053/126] docs(tests): clarify axis_ticks_minor_theming test only exercises r-axis --- tests/test_coord_polar.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index c5a1ea8486..13e9420808 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -446,7 +446,9 @@ def test_coord_radial_axis_ticks_minor_theming(): # invisible everywhere until a user overrides axis_ticks_minor # explicitly. axis_ticks_minor_theta_* / axis_ticks_minor_r_* now # nest under axis_ticks_minor_x/y so a plain axis_ticks_minor= - # theme reaches both the theta and r minor tick marks. + # theme reaches minor ticks on both axes. This plot only exercises + # the r-axis half: coord_radial doesn't currently populate theta + # minor breaks, so there are no theta minor ticks to render here. p = ( ggplot(mtcars, aes("disp", "mpg")) + geom_point() From ea7910e9e312ca52df2265ce8635a495ca93d7ab Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 18:44:14 +0300 Subject: [PATCH 054/126] fix(theme): stop zeroing polar tick length, add axis_ticks_length_theta/r axis_ticks_length_major_x/_y zeroed the tick length whenever the first major tick's tick1line/tick2line wasn't visible - a reasonable cartesian guard, but on a polar panel it fired unconditionally before Task 5's fix and now fires on the wrong axis type after it. Guard the cartesian leaves to skip polar panels, and add axis_ticks_length_major_theta/_r and _minor_theta/_r as the leaves that actually apply length to polar axes. --- plotnine/themes/themeable.py | 122 ++++++++++++++++++++++++++++++++++- tests/test_coord_polar.py | 28 ++++++++ 2 files changed, 148 insertions(+), 2 deletions(-) diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index cc609abe1e..4992577770 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -2478,6 +2478,8 @@ class axis_ticks_length_major_x(themeable): def apply_ax(self, ax: Axes): super().apply_ax(ax) + if getattr(ax, "axis_at_side", None) is not None: + return for axis in (ax.xaxis, getattr(ax, "sec_xaxis", None)): if axis is None: continue @@ -2519,6 +2521,8 @@ class axis_ticks_length_major_y(themeable): def apply_ax(self, ax: Axes): super().apply_ax(ax) + if getattr(ax, "axis_at_side", None) is not None: + return for axis in (ax.yaxis, getattr(ax, "sec_yaxis", None)): if axis is None: continue @@ -2546,8 +2550,63 @@ def apply_ax(self, ax: Axes): ) +class axis_ticks_length_major_theta(themeable): + """ + theta-axis major-tick length + + Parameters + ---------- + theme_element : float | complex + Value in points. A negative value creates the ticks + inside the plot panel. A complex value (e.g. `3j`) + creates ticks that span both in and out of the panel. + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + value: float | complex = self.properties["value"] + if isinstance(value, (float, int)): + tickdir = "in" if value < 0 else "out" + else: + tickdir = "inout" + for side in ("theta_inside", "theta_outside"): + if (axis := axis_at(ax, side)) is not None: + axis.set_tick_params( + which="major", length=abs(value), tickdir=tickdir + ) + + +class axis_ticks_length_major_r(themeable): + """ + r-axis major-tick length + + Parameters + ---------- + theme_element : float | complex + Value in points. A negative value creates the ticks + inside the plot panel. A complex value (e.g. `3j`) + creates ticks that span both in and out of the panel. + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + value: float | complex = self.properties["value"] + if isinstance(value, (float, int)): + tickdir = "in" if value < 0 else "out" + else: + tickdir = "inout" + for side in ("r_start", "r_end"): + if (axis := axis_at(ax, side)) is not None: + axis.set_tick_params( + which="major", length=abs(value), tickdir=tickdir + ) + + class axis_ticks_length_major( - axis_ticks_length_major_x, axis_ticks_length_major_y + axis_ticks_length_major_x, + axis_ticks_length_major_y, + axis_ticks_length_major_theta, + axis_ticks_length_major_r, ): """ Axis major-tick length @@ -2575,6 +2634,8 @@ class axis_ticks_length_minor_x(themeable): def apply_ax(self, ax: Axes): super().apply_ax(ax) + if getattr(ax, "axis_at_side", None) is not None: + return value: float | complex = self.properties["value"] if isinstance(value, (float, int)): @@ -2604,6 +2665,8 @@ class axis_ticks_length_minor_y(themeable): def apply_ax(self, ax: Axes): super().apply_ax(ax) + if getattr(ax, "axis_at_side", None) is not None: + return value: float | complex = self.properties["value"] if isinstance(value, (float, int)): @@ -2619,8 +2682,63 @@ def apply_ax(self, ax: Axes): ) +class axis_ticks_length_minor_theta(themeable): + """ + theta-axis minor-tick length + + Parameters + ---------- + theme_element : float | complex + Value in points. A negative value creates the ticks + inside the plot panel. A complex value (e.g. `3j`) + creates ticks that span both in and out of the panel. + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + value: float | complex = self.properties["value"] + if isinstance(value, (float, int)): + tickdir = "in" if value < 0 else "out" + else: + tickdir = "inout" + for side in ("theta_inside", "theta_outside"): + if (axis := axis_at(ax, side)) is not None: + axis.set_tick_params( + which="minor", length=abs(value), tickdir=tickdir + ) + + +class axis_ticks_length_minor_r(themeable): + """ + r-axis minor-tick length + + Parameters + ---------- + theme_element : float | complex + Value in points. A negative value creates the ticks + inside the plot panel. A complex value (e.g. `3j`) + creates ticks that span both in and out of the panel. + """ + + def apply_ax(self, ax: Axes): + super().apply_ax(ax) + value: float | complex = self.properties["value"] + if isinstance(value, (float, int)): + tickdir = "in" if value < 0 else "out" + else: + tickdir = "inout" + for side in ("r_start", "r_end"): + if (axis := axis_at(ax, side)) is not None: + axis.set_tick_params( + which="minor", length=abs(value), tickdir=tickdir + ) + + class axis_ticks_length_minor( - axis_ticks_length_minor_x, axis_ticks_length_minor_y + axis_ticks_length_minor_x, + axis_ticks_length_minor_y, + axis_ticks_length_minor_theta, + axis_ticks_length_minor_r, ): """ Axis minor-tick length diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index 13e9420808..a1eb81129f 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -458,3 +458,31 @@ def test_coord_radial_axis_ticks_minor_theming(): ) ) assert p == "coord_radial_axis_ticks_minor_theming" + + +def test_coord_radial_ticks_length_themed_nonzero(): + # axis_ticks_length_major_x/_y used to zero the tick length on a + # polar panel because the first major tick's tick1line/tick2line + # visibility check never held before Task 5 activated polar tick + # visibility. axis_ticks_length_major_theta/_r are the leaves that + # now apply length on a polar panel, so a themed nonzero length + # actually renders long tick marks. + p = ( + ggplot(mtcars, aes("disp", "mpg")) + + geom_point() + + coord_radial(start=0.5 * np.pi, end=-0.5 * np.pi, inner_radius=0.3) + + theme(axis_ticks_length=10) + ) + assert p == "coord_radial_ticks_length_themed_nonzero" + + +def test_coord_radial_ticks_length_themed_zero_stays_invisible(): + # Counterpart to the nonzero case above: a themed length of 0 + # should still render with no visible tick marks. + p = ( + ggplot(mtcars, aes("disp", "mpg")) + + geom_point() + + coord_radial(start=0.5 * np.pi, end=-0.5 * np.pi, inner_radius=0.3) + + theme(axis_ticks_length=0) + ) + assert p == "coord_radial_ticks_length_themed_zero_stays_invisible" From 57854000d163da4f2e6d9134c7c8082993ccb630 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 18:53:07 +0300 Subject: [PATCH 055/126] docs(theme): explain why axis_ticks_length_theta/r don't split by boundary --- plotnine/themes/themeable.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index 4992577770..bfc4f6c7fb 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -2554,6 +2554,10 @@ class axis_ticks_length_major_theta(themeable): """ theta-axis major-tick length + Applies uniformly to whichever theta boundary is active — unlike + `axis_ticks_major_theta_outside`/`_inside`, length was never split + by boundary even for the cartesian `axis_ticks_length_major_x`. + Parameters ---------- theme_element : float | complex @@ -2580,6 +2584,10 @@ class axis_ticks_length_major_r(themeable): """ r-axis major-tick length + Applies uniformly to whichever r boundary is active — unlike + `axis_ticks_major_r_start`/`_end`, length was never split + by boundary even for the cartesian `axis_ticks_length_major_y`. + Parameters ---------- theme_element : float | complex @@ -2686,6 +2694,10 @@ class axis_ticks_length_minor_theta(themeable): """ theta-axis minor-tick length + Applies uniformly to whichever theta boundary is active — unlike + `axis_ticks_minor_theta_outside`/`_inside`, length was never split + by boundary even for the cartesian `axis_ticks_length_minor_x`. + Parameters ---------- theme_element : float | complex @@ -2712,6 +2724,10 @@ class axis_ticks_length_minor_r(themeable): """ r-axis minor-tick length + Applies uniformly to whichever r boundary is active — unlike + `axis_ticks_minor_r_start`/`_end`, length was never split + by boundary even for the cartesian `axis_ticks_length_minor_y`. + Parameters ---------- theme_element : float | complex From 7b452ac6910bb2f08061f4c4811056b5f2bac1e1 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 18:56:27 +0300 Subject: [PATCH 056/126] test(coord_polar): cover scale position driving theta/r side choice --- tests/test_coord_polar.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index a1eb81129f..4fa27d26d0 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -486,3 +486,31 @@ def test_coord_radial_ticks_length_themed_zero_stays_invisible(): + theme(axis_ticks_length=0) ) assert p == "coord_radial_ticks_length_themed_zero_stays_invisible" + + +def test_coord_radial_scale_x_position_top_moves_theta_inside(): + # scale_x_continuous(position="top") must move the resolved theta + # side from theta_outside to theta_inside, not just leave theming + # to work on whichever side happens to be primary by default. + p = ( + ggplot(mtcars, aes("disp", "mpg")) + + geom_point() + + coord_radial(start=-1.0, end=1.0, inner_radius=0.3) + + scale_x_continuous(position="top") + + theme(axis_text_theta_inside=element_text(color="red")) + ) + assert p == "coord_radial_scale_x_position_top_moves_theta_inside" + + +def test_coord_radial_scale_y_position_right_moves_r_end(): + # scale_y_continuous(position="right") must move the resolved r + # side from r_start to r_end, not just leave theming to work on + # whichever side happens to be primary by default. + p = ( + ggplot(mtcars, aes("disp", "mpg")) + + geom_point() + + coord_radial(start=-1.0, end=1.0, inner_radius=0.3) + + scale_y_continuous(position="right") + + theme(axis_text_r_end=element_text(color="blue")) + ) + assert p == "coord_radial_scale_y_position_right_moves_r_end" From 6e8cf7c7ef9cbbfa636e34ac537794d97af7d636 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 19:26:32 +0300 Subject: [PATCH 057/126] feat(theme): expose polar theta/r themeables in theme signature and docs --- doc/_quartodoc.yml | 22 ++++++++++++++++++++++ plotnine/themes/theme.py | 24 +++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/doc/_quartodoc.yml b/doc/_quartodoc.yml index 4f922fa4c3..e5251d7107 100644 --- a/doc/_quartodoc.yml +++ b/doc/_quartodoc.yml @@ -416,6 +416,12 @@ quartodoc: - axis_line_y_left - axis_line_y_right - axis_text + - axis_text_r + - axis_text_r_end + - axis_text_r_start + - axis_text_theta + - axis_text_theta_inside + - axis_text_theta_outside - axis_text_x - axis_text_x_bottom - axis_text_x_top @@ -425,12 +431,22 @@ quartodoc: - axis_ticks - axis_ticks_length - axis_ticks_length_major + - axis_ticks_length_major_r + - axis_ticks_length_major_theta - axis_ticks_length_major_x - axis_ticks_length_major_y - axis_ticks_length_minor + - axis_ticks_length_minor_r + - axis_ticks_length_minor_theta - axis_ticks_length_minor_x - axis_ticks_length_minor_y - axis_ticks_major + - axis_ticks_major_r + - axis_ticks_major_r_end + - axis_ticks_major_r_start + - axis_ticks_major_theta + - axis_ticks_major_theta_inside + - axis_ticks_major_theta_outside - axis_ticks_major_x - axis_ticks_major_x_bottom - axis_ticks_major_x_top @@ -438,6 +454,12 @@ quartodoc: - axis_ticks_major_y_left - axis_ticks_major_y_right - axis_ticks_minor + - axis_ticks_minor_r + - axis_ticks_minor_r_end + - axis_ticks_minor_r_start + - axis_ticks_minor_theta + - axis_ticks_minor_theta_inside + - axis_ticks_minor_theta_outside - axis_ticks_minor_x - axis_ticks_minor_x_bottom - axis_ticks_minor_x_top diff --git a/plotnine/themes/theme.py b/plotnine/themes/theme.py index b38ee2e203..63d2346bd3 100644 --- a/plotnine/themes/theme.py +++ b/plotnine/themes/theme.py @@ -117,7 +117,7 @@ def __init__( # python -c " # from plotnine.themes.themeable import themeable # for name in themeable.registry(): - # print(f'{name}=None,') + # print(f' {name}=None,') # " axis_title_x_bottom=None, axis_title_x_top=None, @@ -150,9 +150,15 @@ def __init__( title=None, axis_text_x_bottom=None, axis_text_x_top=None, + axis_text_theta_outside=None, + axis_text_theta_inside=None, + axis_text_theta=None, axis_text_x=None, axis_text_y_left=None, axis_text_y_right=None, + axis_text_r_start=None, + axis_text_r_end=None, + axis_text_r=None, axis_text_y=None, axis_text=None, text=None, @@ -171,15 +177,27 @@ def __init__( axis_line=None, axis_ticks_minor_x_bottom=None, axis_ticks_minor_x_top=None, + axis_ticks_minor_theta_outside=None, + axis_ticks_minor_theta_inside=None, + axis_ticks_minor_theta=None, axis_ticks_minor_x=None, axis_ticks_minor_y_left=None, axis_ticks_minor_y_right=None, + axis_ticks_minor_r_start=None, + axis_ticks_minor_r_end=None, + axis_ticks_minor_r=None, axis_ticks_minor_y=None, axis_ticks_major_x_bottom=None, axis_ticks_major_x_top=None, + axis_ticks_major_theta_outside=None, + axis_ticks_major_theta_inside=None, + axis_ticks_major_theta=None, axis_ticks_major_x=None, axis_ticks_major_y_left=None, axis_ticks_major_y_right=None, + axis_ticks_major_r_start=None, + axis_ticks_major_r_end=None, + axis_ticks_major_r=None, axis_ticks_major_y=None, axis_ticks_major=None, axis_ticks_minor=None, @@ -210,9 +228,13 @@ def __init__( rect=None, axis_ticks_length_major_x=None, axis_ticks_length_major_y=None, + axis_ticks_length_major_theta=None, + axis_ticks_length_major_r=None, axis_ticks_length_major=None, axis_ticks_length_minor_x=None, axis_ticks_length_minor_y=None, + axis_ticks_length_minor_theta=None, + axis_ticks_length_minor_r=None, axis_ticks_length_minor=None, axis_ticks_length=None, panel_spacing_x=None, From 024731fdf4b8b895855c144eb7c5103c6b110736 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 9 Jul 2026 19:52:53 +0300 Subject: [PATCH 058/126] refactor(coord_polar): lock r-axis tick style from setup_ax Move the polar r-axis tick-style locking out of ggplot.draw, where it was a polar-specific special case at the wrong abstraction level, into coord_polar.setup_ax. The live per-reset style capture makes early installation safe: a pre-theme reset copies the default look forward harmlessly, while the draw-time reset copies the themed look forward. Output is unchanged. --- plotnine/_mpl/_polar_axes.py | 41 ++++++++++++++-------------------- plotnine/coords/coord_polar.py | 4 ++++ plotnine/ggplot.py | 9 -------- 3 files changed, 21 insertions(+), 33 deletions(-) diff --git a/plotnine/_mpl/_polar_axes.py b/plotnine/_mpl/_polar_axes.py index f701399f80..30f4020cb1 100644 --- a/plotnine/_mpl/_polar_axes.py +++ b/plotnine/_mpl/_polar_axes.py @@ -84,26 +84,20 @@ def noop(b): def lock_raxis_tick_style(self) -> None: """ - Keep the r-axis ticks looking as the theme styled them across draws - - The theme styles the r-axis ticks and tick labels once, before any - real render pass. On the first render `PolarAxes.draw` calls - `self.yaxis.reset_ticks()`, which drops the lazily-cached tick lists; - the next access rebuilds fresh `Tick` objects with matplotlib's - default styling, so the themed colours, sizes and tick lengths are - gone by the time those ticks are painted. This makes the r-axis ticks - immune to that wipe. Only the r-axis needs it; `PolarAxes.draw` has no - matching reset for the theta-axis. + Keep the r-axis tick labels styled by the theme across draws + + The theme styles the tick labels by setting the font on their label + artists directly, so matplotlib's tick reset -- which `PolarAxes.draw` + runs on the r-axis -- would otherwise repaint them with its defaults. + The tick marks are not at risk: their colour, width and length go + through `set_tick_params`, which the reset preserves. Only the r-axis + is reset; the theta-axis is not. """ - # The reset and the paint that consumes it both happen inside a single - # `PolarAxes.draw` call, so restoring styling after `draw` returns is - # too late -- the default-styled ticks are already painted. Wrapping - # `reset_ticks` instead closes the gap synchronously: the wrapper - # re-copies the styling from the pre-reset ticks onto the fresh ones - # before control returns to `PolarAxes.draw`, so every tick the axis - # then grows and paints inherits the themed look. Capturing the source - # styling live on each reset (rather than from a one-off snapshot) - # keeps this re-entrant across any number of redraws. + # Wrapping `reset_ticks` restores the styling that lives on the tick + # artists (rather than in `set_tick_params` state) after every reset. + # Reading it live from the pre-reset ticks on each call keeps this + # correct however many resets run: a reset before the theme is applied + # simply carries the default look forward. if self._raxis_tick_style_locked: return self._raxis_tick_style_locked = True @@ -112,11 +106,10 @@ def lock_raxis_tick_style(self) -> None: reset_ticks = axis.reset_ticks def reset_ticks_and_restyle() -> None: - # `_copy_tick_props` is matplotlib's own routine for propagating - # the look of `majorTicks[0]` onto the sibling ticks it grows, so - # reusing it leaves the restored ticks identical to freshly grown - # ones -- covering label1/label2, tick1line/tick2line and the tick - # length, for both major and minor ticks. + # `_copy_tick_props` `update_from`s each label and tick line, so it + # carries the full look of the pre-reset `majorTicks[0]` / + # `minorTicks[0]` onto the ticks the reset grows -- the label font + # included, which is the part `set_tick_params` cannot record. major, minor = axis.majorTicks[0], axis.minorTicks[0] reset_ticks() axis._copy_tick_props(major, axis.majorTicks[0]) # type: ignore diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py index f42a79b6c5..22bea2006b 100644 --- a/plotnine/coords/coord_polar.py +++ b/plotnine/coords/coord_polar.py @@ -204,6 +204,10 @@ def setup_ax( polar_ax.axis_at_side[theta_side] = polar_ax.thetaaxis polar_ax.axis_at_side[r_side] = polar_ax.raxis + # The theme styles these tick objects later; keep matplotlib's + # tick resets from replacing their styling with the default look. + polar_ax.lock_raxis_tick_style() + # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ diff --git a/plotnine/ggplot.py b/plotnine/ggplot.py index de56db0bdb..1fb030bc10 100755 --- a/plotnine/ggplot.py +++ b/plotnine/ggplot.py @@ -403,15 +403,6 @@ def draw(self, *, show: bool = False) -> Figure: # Artist object theming self.theme.apply() - # Now that the themed tick styling is final, shield the polar - # r-axis ticks from matplotlib's first-draw tick reset, which - # would otherwise rebuild them with default styling. - from ._mpl._polar_axes import p9PolarAxes - - for ax in self.axs: - if isinstance(ax, p9PolarAxes): - ax.lock_raxis_tick_style() - self._insets.draw(which="above") return figure From 960356ff93d1aae818e2252750602ca30c03df80 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 10 Jul 2026 18:40:19 +0300 Subject: [PATCH 059/126] fix(coord_polar): put theta tick labels on the mark's side --- plotnine/_mpl/_polar_axes.py | 53 ++++++++++++++++++++++++++++++++++-- tests/test_coord_polar.py | 9 ++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/plotnine/_mpl/_polar_axes.py b/plotnine/_mpl/_polar_axes.py index 30f4020cb1..863297c1c1 100644 --- a/plotnine/_mpl/_polar_axes.py +++ b/plotnine/_mpl/_polar_axes.py @@ -2,16 +2,55 @@ from typing import TYPE_CHECKING, cast +import numpy as np from matplotlib.projections import register_projection -from matplotlib.projections.polar import PolarAxes +from matplotlib.projections.polar import PolarAxes, ThetaAxis, ThetaTick if TYPE_CHECKING: from matplotlib.backend_bases import RendererBase - from matplotlib.projections.polar import RadialAxis, ThetaAxis + from matplotlib.projections.polar import RadialAxis + from matplotlib.transforms import Transform from plotnine.typing import PolarSide +class p9ThetaTick(ThetaTick): + """ + ThetaTick whose labels sit on the same rim as the matching tick marks + """ + + def _get_text1_transform(self) -> tuple[Transform, str, str]: + # matplotlib anchors the theta labels through a flipr-reversed + # transform, sending label1 to tick2line's radius and label2 to + # tick1line's. Use the unflipped tick transform so label1 shares + # tick1line's inner radius instead. + return self.axes.get_xaxis_transform("tick1"), "center", "center" + + def _get_text2_transform(self) -> tuple[Transform, str, str]: + # Counterpart to _get_text1_transform: label2 shares tick2line's + # outer radius. + return self.axes.get_xaxis_transform("tick2"), "center", "center" + + def _update_padding(self, pad: float, angle: float) -> None: + # With the base radii corrected above, flip matplotlib's pad signs + # so the clearance pushes label1 further inward and label2 further + # outward -- toward the correct side of each now-repaired label. + padx = pad * np.cos(angle) / 72 + pady = pad * np.sin(angle) / 72 + self._text1_translate._t = (-padx, -pady) # type: ignore + self._text1_translate.invalidate() # type: ignore + self._text2_translate._t = (padx, pady) # type: ignore + self._text2_translate.invalidate() # type: ignore + + +class p9ThetaAxis(ThetaAxis): + """ + ThetaAxis that builds ticks whose labels follow the tick marks + """ + + _tick_class = p9ThetaTick + + class p9PolarAxes(PolarAxes): """ PolarAxes that keeps the axis tick labels above the geom layers @@ -26,6 +65,16 @@ class p9PolarAxes(PolarAxes): # calls do not stack wrappers on top of each other. _raxis_tick_style_locked: bool = False + def _init_axis(self) -> None: + """ + Build the panel's axes, using the theta axis with corrected labels + """ + # `super()` creates the r-axis and registers it with the polar/inner + # spines. The theta axis carries no spine registration, so replacing + # it afterwards leaves nothing stale; the stock ThetaAxis is discarded. + super()._init_axis() # type: ignore + self.xaxis = p9ThetaAxis(self, clear=False) + @property def axis_at_side(self) -> dict[PolarSide, ThetaAxis | RadialAxis]: """ diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index 4fa27d26d0..56a805eb84 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -514,3 +514,12 @@ def test_coord_radial_scale_y_position_right_moves_r_end(): + theme(axis_text_r_end=element_text(color="blue")) ) assert p == "coord_radial_scale_y_position_right_moves_r_end" + + +def test_p9_theta_axis_builds_swapped_tick(): + from plotnine._mpl._polar_axes import p9ThetaAxis, p9ThetaTick + + # The theta axis builds ticks whose labels follow the tick marks: + # label1 inner with tick1line, label2 outer with tick2line -- unlike + # matplotlib's ThetaTick, which pads the labels the opposite way. + assert p9ThetaAxis._tick_class is p9ThetaTick From 5ae1c97b21ae74e27c5e940eeafffa732865e0cd Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 14 Jul 2026 20:44:56 +0300 Subject: [PATCH 060/126] fix(coord_polar): let scale position move only the axis title On a polar panel, scale_x/scale_y position moved the axis line, ticks, and text as well as the title, and the two did not move in harmony. Fix the primary theta axis on the outside and the primary r axis on r_start regardless of position; the title still follows position through the existing coord-agnostic mechanism. Drop the now-unused _resolve_theta_side/_resolve_r_side helpers and the full-circle position="right" no-op warning. --- plotnine/coords/coord_polar.py | 46 ++++++--------------------------- plotnine/coords/coord_radial.py | 18 ++----------- tests/test_coord_polar.py | 39 +++++++++++++++++----------- 3 files changed, 34 insertions(+), 69 deletions(-) diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py index 22bea2006b..5d58c05ec6 100644 --- a/plotnine/coords/coord_polar.py +++ b/plotnine/coords/coord_polar.py @@ -18,29 +18,6 @@ from plotnine.iapi import labels_view, layout_details, panel_view from plotnine.scales.scale import scale - from plotnine.typing import PolarSide, Side - - -def _resolve_theta_side(position: Side) -> PolarSide: - """ - Return which theta side a scale's x position resolves to - - `"bottom"` (the default `scale_x_*` position) is the outer rim, - matching today's only visible placement; `"top"` moves theta to the - inner (donut-hole) side. - """ - return "theta_outside" if position == "bottom" else "theta_inside" - - -def _resolve_r_side(position: Side) -> PolarSide: - """ - Return which r side a scale's y position resolves to - - `"left"` (the default `scale_y_*` position) is the start-angle - spoke, matching the existing 270-degree default placement on a full - circle; `"right"` moves r to the end-angle spoke. - """ - return "r_start" if position == "left" else "r_end" class coord_polar(coord): @@ -176,9 +153,10 @@ def setup_ax( """ Limits, breaks, tick labels, and active side for a polar panel - Activates exactly one theta side (from `panel_params.x.position`) - and one r side (from `panel_params.y.position`). It records the - choice on `p9PolarAxes.axis_at_side` so theming can find it. + The primary theta axis always renders on the outside and the + primary r axis always on r_start, independent of `scale.position` + (which moves only the axis title). It records the fixed choice on + `p9PolarAxes.axis_at_side` so theming can find it. """ if panel_params.x.sec is not None: warn( @@ -189,20 +167,12 @@ def setup_ax( self._setup_ticks_labels(ax, panel_params) polar_ax = cast("p9PolarAxes", ax) - theta_side = _resolve_theta_side(panel_params.x.position) - r_side = _resolve_r_side(panel_params.y.position) - _activate_axis( - ax.xaxis, - "top" if theta_side == "theta_outside" else "bottom", - True, - ) - _activate_axis( - ax.yaxis, "left" if r_side == "r_start" else "right", True - ) + _activate_axis(ax.xaxis, "top", True) + _activate_axis(ax.yaxis, "left", True) - polar_ax.axis_at_side[theta_side] = polar_ax.thetaaxis - polar_ax.axis_at_side[r_side] = polar_ax.raxis + polar_ax.axis_at_side["theta_outside"] = polar_ax.thetaaxis + polar_ax.axis_at_side["r_start"] = polar_ax.raxis # The theme styles these tick objects later; keep matplotlib's # tick resets from replacing their styling with the default look. diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 8f6f5f4fea..192726f60a 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -2,12 +2,10 @@ from dataclasses import replace from typing import TYPE_CHECKING, Literal, cast -from warnings import warn import numpy as np -from ..exceptions import PlotnineWarning -from .coord_polar import _resolve_r_side, coord_polar +from .coord_polar import coord_polar if TYPE_CHECKING: import pandas as pd @@ -374,19 +372,7 @@ def setup_ax( # ignores it and always uses thetamin/thetamax directly, so this # only matters here. if self.end is None: - r_side = _resolve_r_side(panel_params.y.position) - if r_side == "r_end": - warn( - "coord_radial(): a full circle has no 'end' " - "boundary distinct from 'start' for the r axis " - "(matplotlib always hides it there); " - "scale_y_*(position='right') has no visible " - "effect here. Set `end` for a partial arc to show " - "the r axis on the opposite spoke.", - PlotnineWarning, - ) - else: - polar_ax.set_rlabel_position(270) + polar_ax.set_rlabel_position(270) ax.tick_params(axis="x", which="major", direction="out") if (angle := self._theta_guide_angle()) is not None: diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index 56a805eb84..f603b0a179 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -291,18 +291,21 @@ def test_coord_radial_no_longer_has_r_axis_inside(): coord_radial(r_axis_inside=True) # type: ignore[call-arg] -def test_coord_radial_full_circle_position_right_warns(): - import pytest +def test_coord_radial_full_circle_position_right_no_warning(): + import warnings from plotnine.exceptions import PlotnineWarning + # position="right" now moves only the r title, so a full circle no + # longer warns that it has no visible effect. p = ( ggplot(mtcars, aes("disp", "mpg")) + geom_point() + coord_radial() + scale_y_continuous(position="right") ) - with pytest.warns(PlotnineWarning, match="end.*boundary"): + with warnings.catch_warnings(): + warnings.simplefilter("error", PlotnineWarning) p.draw_test() @@ -488,32 +491,38 @@ def test_coord_radial_ticks_length_themed_zero_stays_invisible(): assert p == "coord_radial_ticks_length_themed_zero_stays_invisible" -def test_coord_radial_scale_x_position_top_moves_theta_inside(): - # scale_x_continuous(position="top") must move the resolved theta - # side from theta_outside to theta_inside, not just leave theming - # to work on whichever side happens to be primary by default. +def test_coord_radial_scale_x_position_top_keeps_theta_outside(): + # On a polar panel scale.position moves only the axis title; the + # primary theta axis stays on the outside regardless of position. + from plotnine._mpl.axes import axis_at + p = ( ggplot(mtcars, aes("disp", "mpg")) + geom_point() + coord_radial(start=-1.0, end=1.0, inner_radius=0.3) + scale_x_continuous(position="top") - + theme(axis_text_theta_inside=element_text(color="red")) ) - assert p == "coord_radial_scale_x_position_top_moves_theta_inside" + p.draw_test() + ax = p.axs[0] + assert axis_at(ax, "theta_outside") is ax.xaxis + assert axis_at(ax, "theta_inside") is None -def test_coord_radial_scale_y_position_right_moves_r_end(): - # scale_y_continuous(position="right") must move the resolved r - # side from r_start to r_end, not just leave theming to work on - # whichever side happens to be primary by default. +def test_coord_radial_scale_y_position_right_keeps_r_start(): + # On a polar panel scale.position moves only the axis title; the + # primary r axis stays on r_start regardless of position. + from plotnine._mpl.axes import axis_at + p = ( ggplot(mtcars, aes("disp", "mpg")) + geom_point() + coord_radial(start=-1.0, end=1.0, inner_radius=0.3) + scale_y_continuous(position="right") - + theme(axis_text_r_end=element_text(color="blue")) ) - assert p == "coord_radial_scale_y_position_right_moves_r_end" + p.draw_test() + ax = p.axs[0] + assert axis_at(ax, "r_start") is ax.yaxis + assert axis_at(ax, "r_end") is None def test_p9_theta_axis_builds_swapped_tick(): From 4b22230cb824cb029c79e83d162e5a2074e692bc Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 15 Jul 2026 15:05:47 +0300 Subject: [PATCH 061/126] fix(coord_radial): restore theta minor grid breaks coord_polar clears the theta axis minor breaks because data-space ticks are meaningless once mapped onto the arc. coord_radial restored the major breaks and labels as radian positions but never the minor breaks, so the theta minor grid never rendered. Capture the data-space theta minor breaks, convert them to radian positions, and clip them to a partial arc, mirroring the major-break handling. Under thetalim, recompute them over the zoomed range only for a continuous theta scale, since a discrete scale has no minor breaks. --- plotnine/coords/coord_radial.py | 47 +++++++++++++++++++++++++-------- tests/test_coord_polar.py | 31 ++++++++++++++++++++++ 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 192726f60a..0f1e75a24f 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -70,13 +70,10 @@ class coord_radial(coord_polar): Notes ----- - Theta-axis tick labels are shown by default, matching ggplot2. Since a - polar axes' x-axis *is* the theta axis, they are styled through the - theme: hide them with ``theme(axis_text_x=element_blank())`` and adjust - the gap to the outer circle through the ``axis_text_x`` margin. - - The Python API uses snake_case names for arguments that are dotted in - ggplot2: ``inner_radius`` and ``rotate_angle``. + Theta-axis tick labels are shown by default. Since a polar axes' x-axis + *is* the theta axis, they are styled through the theme: hide them with + ``theme(axis_text_x=element_blank())`` and adjust the gap to the outer + circle through the ``axis_text_x`` margin. Examples -------- @@ -152,6 +149,7 @@ def __init__( # ------------------------------------------------------------------ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: + from ..scales.scale_continuous import scale_continuous from .coord_cartesian import coord_cartesian # Capture data-space theta breaks before super() clears them. @@ -161,15 +159,17 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: if self.theta == "x": theta_breaks = list(pv_data.x.breaks) theta_labels = list(pv_data.x.labels) + theta_minor_breaks = list(pv_data.x.minor_breaks) else: theta_breaks = list(pv_data.y.breaks) theta_labels = list(pv_data.y.labels) + theta_minor_breaks = list(pv_data.y.minor_breaks) pv = super().setup_panel_params(scale_x, scale_y) # thetalim: zoom the theta data range — only this slice maps to the # arc. Recompute nice breaks over the zoomed range so labels are not - # reduced to sparse endpoints, matching ggplot2. + # reduced to sparse endpoints. if self.thetalim is not None: pv = replace(pv, theta_range=tuple(self.thetalim)) theta_scale = scale_x if self.theta == "x" else scale_y @@ -179,10 +179,22 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: if np.isfinite(b) ] theta_labels = list(theta_scale.get_labels(theta_breaks)) + # Only a continuous theta scale has minor breaks; a discrete one + # has none, so leave theta_minor_breaks empty. + if isinstance(theta_scale, scale_continuous): + theta_minor_breaks = [ + b + for b in theta_scale.get_minor_breaks( + theta_breaks, self.thetalim + ) + if np.isfinite(b) + ] + else: + theta_minor_breaks = [] # rlim: zoom the r data range — update ranges and recompute nice breaks # over the zoomed range (rather than filtering the full-range breaks, - # which leaves sparse endpoint-only labels), matching ggplot2. + # which leaves sparse endpoint-only labels). if self.rlim is not None: pv = replace(pv, r_range=tuple(self.rlim)) r_scale = scale_y if self.theta == "x" else scale_x @@ -209,8 +221,7 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: arc_hi = max(self.start, self.start + arc) # Convert data-space theta breaks to radian positions and restore them - # as theta axis tick labels on the outer edge. Theta labels show by - # default (matching ggplot2); hide them via theme(axis_text_x=...). + # as theta axis tick labels on the outer edge. x_updates: dict = {} if theta_breaks: assert pv.theta_range is not None @@ -226,6 +237,20 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: x_updates["breaks"] = radian_pos x_updates["labels"] = theta_labels + if theta_minor_breaks: + assert pv.theta_range is not None + minor_radian_pos = list( + self._to_radians( + np.asarray(theta_minor_breaks, dtype=float), + pv.theta_range, + ) + ) + if arc_lo is not None: + minor_radian_pos = [ + r for r in minor_radian_pos if arc_lo <= r <= arc_hi + ] + x_updates["minor_breaks"] = minor_radian_pos + # Partial arc: x panel range must match [arc_lo, arc_hi] so that # coord.setup_ax calls ax.set_xlim(arc_lo, arc_hi) rather than # ax.set_xlim(0, 2π), which would override set_thetalim. diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index f603b0a179..d7640083a2 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -370,6 +370,37 @@ def test_coord_radial_shows_theta_labels_by_default(): assert list(pv.x.labels) +def test_coord_radial_restores_theta_minor_breaks(): + # coord_polar clears theta minor breaks (data-space ticks are meaningless + # on the arc); coord_radial must restore them as radian positions, the + # same way it restores the major breaks, so the theta minor grid renders. + scale_x, scale_y = trained_scales() + coord = coord_radial(expand=False) + pv = coord.setup_panel_params(scale_x, scale_y) + minor = list(pv.x.minor_breaks) + assert minor # not empty + assert all(0 <= b <= 2 * np.pi for b in minor) + + +def test_coord_radial_clips_theta_minor_breaks_to_partial_arc(): + scale_x, scale_y = trained_scales() + coord = coord_radial(start=0, end=np.pi, expand=False) + pv = coord.setup_panel_params(scale_x, scale_y) + minor = list(pv.x.minor_breaks) + assert minor + assert all(0 <= b <= np.pi for b in minor) + + +def test_coord_radial_discrete_theta_has_no_minor_breaks(): + # A discrete theta scale has no minor breaks and no get_minor_breaks + # method, so setting one up must not crash and leaves the theta minor + # grid empty. + df = pd.DataFrame({"cat": ["a", "b", "c"], "y": [2, 3, 5]}) + p = ggplot(df, aes("cat", "y")) + geom_col() + coord_radial(theta="x") + pv = p.build_test().layout.panel_params[0] + assert list(pv.x.minor_breaks) == [] + + def test_coord_radial_to_radians_zero_width_range(): coord = coord_radial() From 857d74b8635a2ba1b0363ceb71fbe4ab728ac669 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 15 Jul 2026 23:13:20 +0300 Subject: [PATCH 062/126] fix(coord_polar): expand theta axis per the expand flag Theta was pinned to the unexpanded data range, so on a partial arc the outermost geoms sat flush against the arc ends. Make theta honor the expand flag like every other axis, in both coord_polar and coord_radial: read theta_range from the expanded view, capture theta breaks over the expanded range, and expand the thetalim zoom on top of its bounds. With expand=True a full circle opens a small gap at the 12-o'clock seam; expand=False keeps it flush so a pie still closes cleanly. --- plotnine/coords/coord_polar.py | 16 +++++++--------- plotnine/coords/coord_radial.py | 31 ++++++++++++++++++++++++------- tests/test_coord_polar.py | 12 ++++++++++++ 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py index 5d58c05ec6..65244b1f67 100644 --- a/plotnine/coords/coord_polar.py +++ b/plotnine/coords/coord_polar.py @@ -89,21 +89,19 @@ def __init__( def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: from .coord_cartesian import coord_cartesian - # Theta fills exactly one full revolution — no expansion on that axis. - # R uses the caller-controlled expand flag. - pv_no_exp = coord_cartesian(expand=False).setup_panel_params( - scale_x, scale_y - ) + # Theta follows the caller-controlled expand flag, like every other + # axis: expand=True buffers the data off the arc ends (and, for a full + # circle, opens a small gap at the 12-o'clock seam); expand=False keeps + # it flush so a pie closes cleanly. This theta_range is only the domain + # of the data → angle map; new_x.range below is the fixed display span. pv_exp = coord_cartesian(expand=self.expand).setup_panel_params( scale_x, scale_y ) if self.theta == "x": - theta_range = pv_no_exp.x.range - r_sv = pv_exp.y + theta_range, r_sv = pv_exp.x.range, pv_exp.y else: - theta_range = pv_no_exp.y.range - r_sv = pv_exp.x + theta_range, r_sv = pv_exp.y.range, pv_exp.x r_range = r_sv.range diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 0f1e75a24f..5b9ffeae34 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -152,8 +152,10 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: from ..scales.scale_continuous import scale_continuous from .coord_cartesian import coord_cartesian - # Capture data-space theta breaks before super() clears them. - pv_data = coord_cartesian(expand=False).setup_panel_params( + # Capture theta breaks over the expanded range before super() clears + # them, the same way coord_cartesian chooses breaks within an expanded + # window. They map through the matching expanded theta_range below. + pv_data = coord_cartesian(expand=self.expand).setup_panel_params( scale_x, scale_y ) if self.theta == "x": @@ -168,14 +170,29 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: pv = super().setup_panel_params(scale_x, scale_y) # thetalim: zoom the theta data range — only this slice maps to the - # arc. Recompute nice breaks over the zoomed range so labels are not - # reduced to sparse endpoints. + # arc. Expand on top of the zoom (like coord_cartesian expands on top + # of xlim) and recompute nice breaks over the expanded range so labels + # are not reduced to sparse endpoints. if self.thetalim is not None: - pv = replace(pv, theta_range=tuple(self.thetalim)) + from mizani.transforms import identity_trans + theta_scale = scale_x if self.theta == "x" else scale_y + coord_limits = ( + theta_scale.transform(self.thetalim) + if isinstance(theta_scale, scale_continuous) + else self.thetalim + ) + expansion = theta_scale.default_expansion(expand=self.expand) + theta_zoom = theta_scale.expand_limits( + theta_scale.final_limits, + expansion, + coord_limits, + identity_trans(), + ).range + pv = replace(pv, theta_range=tuple(theta_zoom)) theta_breaks = [ b - for b in theta_scale.get_bounded_breaks(self.thetalim) + for b in theta_scale.get_bounded_breaks(theta_zoom) if np.isfinite(b) ] theta_labels = list(theta_scale.get_labels(theta_breaks)) @@ -185,7 +202,7 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: theta_minor_breaks = [ b for b in theta_scale.get_minor_breaks( - theta_breaks, self.thetalim + theta_breaks, theta_zoom ) if np.isfinite(b) ] diff --git a/tests/test_coord_polar.py b/tests/test_coord_polar.py index d7640083a2..aee2c5e862 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_polar.py @@ -233,6 +233,18 @@ def test_coord_radial_default_theme_hides_theta_and_r_boundaries(): assert p == "coord_radial_default_theme_hides_theta_and_r_boundaries" +def test_coord_radial_partial_arc_expands_off_ends(): + # Regression: outermost bars used to sit flush against the arc ends + # (angles 0 and π). With the default expand=True the theta axis is + # buffered, so the bars sit inside the arc. + p = ( + ggplot(mtcars, aes("cyl", "mpg")) + + geom_col() + + coord_radial(start=0, end=np.pi, inner_radius=0.1) + ) + assert p == "coord_radial_partial_arc_expands_off_ends" + + def test_coord_polar_axis_line_controls_polar_spine(): # panel_border no longer owns the outer circle; axis_line does, and # unlike panel_border it can style it, not just hide it. From 729499060b1bbf98918c555bc99b649f66ff26eb Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 16 Jul 2026 00:56:16 +0300 Subject: [PATCH 063/126] refactor(coord): make coord_radial the base, coord_polar its alias coord_radial now owns the polar logic directly (parent bodies inlined, theta breaks computed once) and coord_polar is an @alias subclass. coord_polar() now shows theta tick labels by default. --- plotnine/coords/__init__.py | 3 +- plotnine/coords/coord_polar.py | 316 -------------------------------- plotnine/coords/coord_radial.py | 253 +++++++++++++++++++------ 3 files changed, 202 insertions(+), 370 deletions(-) delete mode 100644 plotnine/coords/coord_polar.py diff --git a/plotnine/coords/__init__.py b/plotnine/coords/__init__.py index ae14967bef..6c16635bff 100644 --- a/plotnine/coords/__init__.py +++ b/plotnine/coords/__init__.py @@ -5,8 +5,7 @@ from .coord_cartesian import coord_cartesian from .coord_fixed import coord_equal, coord_fixed from .coord_flip import coord_flip -from .coord_polar import coord_polar -from .coord_radial import coord_radial +from .coord_radial import coord_polar, coord_radial from .coord_trans import coord_trans __all__ = ( diff --git a/plotnine/coords/coord_polar.py b/plotnine/coords/coord_polar.py deleted file mode 100644 index 65244b1f67..0000000000 --- a/plotnine/coords/coord_polar.py +++ /dev/null @@ -1,316 +0,0 @@ -from __future__ import annotations - -from dataclasses import replace -from typing import TYPE_CHECKING, Literal, cast -from warnings import warn - -import numpy as np - -from .._mpl._polar_axes import p9PolarAxes # noqa: TCH001 -from ..exceptions import PlotnineWarning -from ..iapi import panel_ranges -from .coord import _activate_axis, coord, dist_euclidean - -if TYPE_CHECKING: - import pandas as pd - from matplotlib.axes import Axes - from matplotlib.projections.polar import PolarAxes - - from plotnine.iapi import labels_view, layout_details, panel_view - from plotnine.scales.scale import scale - - -class coord_polar(coord): - """ - Polar coordinate system - - `coord_polar` maps one position aesthetic to the angle and the other - to the radius. It is commonly used for pie charts, which are stacked - bar charts in polar coordinates. - - Parameters - ---------- - theta : - Which variable maps to the angle axis, ``"x"`` (default) or ``"y"``. - start : - Starting angle in radians, measured clockwise from 12 o'clock - (i.e. from the positive-y axis). Default 0. - direction : - ``1`` = clockwise (default), ``-1`` = counter-clockwise. - expand : - Add a small buffer around the data on the radius axis. - Default ``True``. - - Notes - ----- - Unlike ggplot2, plotnine coordinate systems do not currently expose a - ``clip`` argument. - - For partial arcs, donut charts, and theta/radius limits, use - ``coord_radial``. - - Examples - -------- - A pie chart is a stacked bar chart with the y position mapped to angle. - - ```python - import pandas as pd - from plotnine import aes, coord_polar, geom_col, ggplot - - df = pd.DataFrame({ - "x": [1, 1, 1], - "y": [2, 3, 5], - "group": ["a", "b", "c"], - }) - - ggplot(df, aes("x", "y", fill="group")) + geom_col() + coord_polar("y") - ``` - """ - - is_linear = False - _projection = "p9polar" - - def __init__( - self, - theta: str = "x", - start: float = 0, - direction: int = 1, - expand: bool = True, - ) -> None: - self.theta = theta - self.start = start - self.direction = direction - self.expand = expand - - # ------------------------------------------------------------------ - # Panel params - # ------------------------------------------------------------------ - - def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: - from .coord_cartesian import coord_cartesian - - # Theta follows the caller-controlled expand flag, like every other - # axis: expand=True buffers the data off the arc ends (and, for a full - # circle, opens a small gap at the 12-o'clock seam); expand=False keeps - # it flush so a pie closes cleanly. This theta_range is only the domain - # of the data → angle map; new_x.range below is the fixed display span. - pv_exp = coord_cartesian(expand=self.expand).setup_panel_params( - scale_x, scale_y - ) - - if self.theta == "x": - theta_range, r_sv = pv_exp.x.range, pv_exp.y - else: - theta_range, r_sv = pv_exp.y.range, pv_exp.x - - r_range = r_sv.range - - empty = np.array([], dtype=float) - - # x → theta axis: data ticks are in original units (not radians), so - # suppress them. Limits span [start, start+2π] so that bars rotated - # by a non-zero start angle stay within the displayed theta range. - theta_start = float(self.start) - new_x = replace( - pv_exp.x, - limits=(theta_start, theta_start + 2 * np.pi), - range=(theta_start, theta_start + 2 * np.pi), - breaks=[], - minor_breaks=empty, - labels=[], - ) - - # y → r axis: use the scale for the r dimension with its natural - # breaks. - new_y = replace(r_sv) - - return replace( - pv_exp, - x=new_x, - y=new_y, - theta_range=tuple(theta_range), - r_range=tuple(r_range), - ) - - def labels(self, cur_labels: labels_view) -> labels_view: - # When theta="y" the data x/y columns are swapped in transform so that - # PolarAxes sees x=theta, y=r. Swap the axis titles to match, the same - # way coord_flip does for its flipped axes. - if self.theta == "y": - from .coord_flip import flip_labels - - return flip_labels(super().labels(cur_labels)) - return super().labels(cur_labels) - - def setup_ax( - self, - ax: Axes, - panel_params: panel_view, - layout_info: layout_details, - ) -> None: - """ - Limits, breaks, tick labels, and active side for a polar panel - - The primary theta axis always renders on the outside and the - primary r axis always on r_start, independent of `scale.position` - (which moves only the axis title). It records the fixed choice on - `p9PolarAxes.axis_at_side` so theming can find it. - """ - if panel_params.x.sec is not None: - warn( - f"{self.__class__.__name__}() does not support a secondary " - "theta axis.", - PlotnineWarning, - ) - - self._setup_ticks_labels(ax, panel_params) - polar_ax = cast("p9PolarAxes", ax) - - _activate_axis(ax.xaxis, "top", True) - _activate_axis(ax.yaxis, "left", True) - - polar_ax.axis_at_side["theta_outside"] = polar_ax.thetaaxis - polar_ax.axis_at_side["r_start"] = polar_ax.raxis - - # The theme styles these tick objects later; keep matplotlib's - # tick resets from replacing their styling with the default look. - polar_ax.lock_raxis_tick_style() - - # ------------------------------------------------------------------ - # Helpers - # ------------------------------------------------------------------ - - def _to_radians( - self, vals: np.ndarray, theta_range: tuple[float, float] - ) -> np.ndarray: - """Normalise data-space theta values to [start, start + 2π].""" - t_min, t_max = theta_range - denom = float(t_max) - float(t_min) - if denom == 0: - return np.zeros_like(vals, dtype=float) - norm = (np.asarray(vals, dtype=float) - float(t_min)) / denom - # Rotation direction is a PolarAxes property set in draw via - # set_theta_direction; it is not baked into these radian values. - return self.start + norm * 2.0 * np.pi - - # ------------------------------------------------------------------ - # Data transformation - # ------------------------------------------------------------------ - - def transform( - self, - data: pd.DataFrame, - panel_params: panel_view, - munch: bool = False, - ) -> pd.DataFrame: - # Munch first (in original data space) so curved edges get enough - # interpolation points before we convert theta → radians. - if munch: - data = self.munch(data, panel_params) - - if self.theta == "x": - theta_col, r_col = "x", "y" - theta_end_col, r_end_col = "xend", "yend" - else: - theta_col, r_col = "y", "x" - theta_end_col, r_end_col = "yend", "xend" - - if theta_col not in data.columns or r_col not in data.columns: - return data - - theta_range = panel_params.theta_range - assert theta_range is not None - - data = data.copy() - data[theta_col] = self._to_radians( - data[theta_col].to_numpy(), theta_range - ) - has_endpoints = ( - theta_end_col in data.columns and r_end_col in data.columns - ) - if has_endpoints: - data[theta_end_col] = self._to_radians( - data[theta_end_col].to_numpy(), theta_range - ) - - # PolarAxes always expects x = theta (radians) and y = r. - # When theta = "y" we need to swap the columns. - if self.theta == "y": - data["x"], data["y"] = data["y"].copy(), data["x"].copy() - if has_endpoints: - data["xend"], data["yend"] = ( - data["yend"].copy(), - data["xend"].copy(), - ) - - return data - - # ------------------------------------------------------------------ - # Distance (used by munch, called before transform) - # ------------------------------------------------------------------ - - def distance( - self, - x: pd.Series, - y: pd.Series, - panel_params: panel_view, - ) -> np.ndarray: - # Normalise theta and r to [0, 1] then compute Euclidean distance. - assert panel_params.theta_range is not None - assert panel_params.r_range is not None - t_min, t_max = panel_params.theta_range - r_min, r_max = panel_params.r_range - t_denom = float(t_max - t_min) or 1.0 - r_denom = float(r_max - r_min) or 1.0 - - if self.theta == "x": - theta_vals = np.asarray(x, dtype=float) - r_vals = np.asarray(y, dtype=float) - else: - theta_vals = np.asarray(y, dtype=float) - r_vals = np.asarray(x, dtype=float) - - theta_norm = (theta_vals - float(t_min)) / t_denom - r_norm = (r_vals - float(r_min)) / r_denom - return dist_euclidean(theta_norm, r_norm) - - def backtransform_range(self, panel_params: panel_view) -> panel_ranges: - assert panel_params.theta_range is not None - assert panel_params.r_range is not None - t_range = panel_params.theta_range - r_range = panel_params.r_range - if self.theta == "x": - return panel_ranges(x=t_range, y=r_range) - return panel_ranges(x=r_range, y=t_range) - - # ------------------------------------------------------------------ - # Draw decorations on PolarAxes - # ------------------------------------------------------------------ - - def _mpl_theta_direction(self) -> Literal[-1, 1]: - """ - Matplotlib theta direction for this coordinate system - - ``-1`` draws clockwise and ``+1`` counter-clockwise, the opposite of - plotnine's own ``direction`` convention. - """ - return -1 if self.direction == 1 else 1 - - def draw(self, axs: list[Axes]) -> None: - """Configure each PolarAxes: zero location and rotation direction - - R-limits are set per panel by setup_ax. - """ - # PolarAxes theta_direction: -1 = clockwise, +1 = counter-clockwise. - mpl_direction = self._mpl_theta_direction() - for ax in axs: - polar_ax = cast("PolarAxes", ax) - polar_ax.set_theta_zero_location("N") # 12 o'clock - polar_ax.set_theta_direction(mpl_direction) - - # ------------------------------------------------------------------ - # Misc - # ------------------------------------------------------------------ - - def aspect(self, panel_params: panel_view) -> float: - return 1.0 diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 5b9ffeae34..1a37d879b4 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -2,28 +2,33 @@ from dataclasses import replace from typing import TYPE_CHECKING, Literal, cast +from warnings import warn import numpy as np -from .coord_polar import coord_polar +from .._mpl._polar_axes import p9PolarAxes # noqa: TCH001 +from .._utils.registry import alias +from ..exceptions import PlotnineWarning +from ..iapi import panel_ranges +from .coord import _activate_axis, coord, dist_euclidean if TYPE_CHECKING: import pandas as pd from matplotlib.axes import Axes from matplotlib.projections.polar import PolarAxes - from plotnine.iapi import layout_details, panel_view + from plotnine.iapi import labels_view, layout_details, panel_view from plotnine.scales.scale import scale -class coord_radial(coord_polar): +class coord_radial(coord): """ Radial coordinate system `coord_radial` maps one position aesthetic to the angle and the other - to the radius. Compared with ``coord_polar``, it adds support for - partial arcs, inner radius holes, theta/radius limits, radial-axis - placement, and rotation of the ``angle`` aesthetic. + to the radius. It supports full and partial arcs, inner radius holes, + theta/radius limits, radial-axis placement, and rotation of the + ``angle`` aesthetic. It supersedes `coord_polar`. Parameters ---------- @@ -75,6 +80,9 @@ class coord_radial(coord_polar): ``theme(axis_text_x=element_blank())`` and adjust the gap to the outer circle through the ``axis_text_x`` margin. + Unlike ggplot2, plotnine coordinate systems do not currently expose a + ``clip`` argument. + Examples -------- A donut chart is a stacked bar chart with an inner radius. @@ -113,6 +121,9 @@ class coord_radial(coord_polar): ``` """ + is_linear = False + _projection = "p9polar" + def __init__( self, theta: str = "x", @@ -126,48 +137,69 @@ def __init__( rlim: tuple[float, float] | None = None, reverse: str = "none", ) -> None: - super().__init__( - theta=theta, - start=start, - direction=direction, - expand=expand, - ) if reverse not in {"none", "theta", "r", "thetar"}: raise ValueError( "reverse must be one of 'none', 'theta', 'r', 'thetar'; " f"got {reverse!r}." ) + self.theta = theta + self.start = start self.end = end + self.direction = direction + self.expand = expand self.inner_radius = inner_radius self.rotate_angle = rotate_angle self.thetalim = thetalim self.rlim = rlim self.reverse = reverse - # ------------------------------------------------------------------ - # Panel params - # ------------------------------------------------------------------ - def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: from ..scales.scale_continuous import scale_continuous from .coord_cartesian import coord_cartesian - # Capture theta breaks over the expanded range before super() clears - # them, the same way coord_cartesian chooses breaks within an expanded - # window. They map through the matching expanded theta_range below. - pv_data = coord_cartesian(expand=self.expand).setup_panel_params( + # One expanded cartesian view supplies both the theta/r ranges and + # the theta breaks. Theta follows the expand flag like every other + # axis: expand=True buffers the data off the arc ends; expand=False + # keeps it flush so a pie closes cleanly. + pv_exp = coord_cartesian(expand=self.expand).setup_panel_params( scale_x, scale_y ) + if self.theta == "x": - theta_breaks = list(pv_data.x.breaks) - theta_labels = list(pv_data.x.labels) - theta_minor_breaks = list(pv_data.x.minor_breaks) + theta_range, r_sv = pv_exp.x.range, pv_exp.y + theta_breaks = list(pv_exp.x.breaks) + theta_labels = list(pv_exp.x.labels) + theta_minor_breaks = list(pv_exp.x.minor_breaks) else: - theta_breaks = list(pv_data.y.breaks) - theta_labels = list(pv_data.y.labels) - theta_minor_breaks = list(pv_data.y.minor_breaks) - - pv = super().setup_panel_params(scale_x, scale_y) + theta_range, r_sv = pv_exp.y.range, pv_exp.x + theta_breaks = list(pv_exp.y.breaks) + theta_labels = list(pv_exp.y.labels) + theta_minor_breaks = list(pv_exp.y.minor_breaks) + + r_range = r_sv.range + empty = np.array([], dtype=float) + + # theta axis: [start, start+2π] display span so bars rotated by a + # non-zero start stay within range. Data ticks (original units) are + # converted to radian positions and applied at the end of this method. + theta_start = float(self.start) + new_x = replace( + pv_exp.x, + limits=(theta_start, theta_start + 2 * np.pi), + range=(theta_start, theta_start + 2 * np.pi), + breaks=[], + minor_breaks=empty, + labels=[], + ) + new_y = replace(r_sv) + + pv = replace( + pv_exp, + x=new_x, + y=new_y, + theta_range=tuple(theta_range), + r_range=tuple(r_range), + ) # thetalim: zoom the theta data range — only this slice maps to the # arc. Expand on top of the zoom (like coord_cartesian expands on top @@ -290,9 +322,15 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: return pv - # ------------------------------------------------------------------ - # Helpers - # ------------------------------------------------------------------ + def labels(self, cur_labels: labels_view) -> labels_view: + # When theta="y" the data x/y columns are swapped in transform so that + # PolarAxes sees x=theta, y=r. Swap the axis titles to match, the same + # way coord_flip does for its flipped axes. + if self.theta == "y": + from .coord_flip import flip_labels + + return flip_labels(super().labels(cur_labels)) + return super().labels(cur_labels) @property def _arc(self) -> float: @@ -310,13 +348,14 @@ def _arc(self) -> float: def _mpl_theta_direction(self) -> Literal[-1, 1]: """ - Matplotlib theta direction, flipped when reverse acts on theta + Matplotlib theta direction for this coordinate system - ``reverse="theta"``/``"thetar"`` runs the angular axis the other way - by folding into the PolarAxes direction at draw time, rather than into - the radian mapping. + ``-1`` draws clockwise and ``+1`` counter-clockwise, the opposite of + plotnine's own ``direction`` convention. ``reverse="theta"``/ + ``"thetar"`` runs the angular axis the other way by folding into the + PolarAxes direction here, rather than into the radian mapping. """ - mpl_direction = super()._mpl_theta_direction() + mpl_direction: Literal[-1, 1] = -1 if self.direction == 1 else 1 if self.reverse in ("theta", "thetar"): return 1 if mpl_direction == -1 else -1 return mpl_direction @@ -324,32 +363,68 @@ def _mpl_theta_direction(self) -> Literal[-1, 1]: def _to_radians( self, vals: np.ndarray, theta_range: tuple[float, float] ) -> np.ndarray: - """Normalize theta values to [start, start + arc].""" + """Normalize theta values to [start, start + arc]""" t_min, t_max = theta_range denom = float(t_max) - float(t_min) if denom == 0: return np.zeros_like(vals, dtype=float) norm = (np.asarray(vals, dtype=float) - float(t_min)) / denom + # Rotation direction is a PolarAxes property set in draw via + # set_theta_direction; it is not baked into these radian values. return self.start + norm * self._arc - # ------------------------------------------------------------------ - # Data transformation - # ------------------------------------------------------------------ - def transform( self, data: pd.DataFrame, panel_params: panel_view, munch: bool = False, ) -> pd.DataFrame: - data = super().transform(data, panel_params, munch=munch) - # After super().transform(), data["x"] is always theta in radians. + # Munch first (in original data space) so curved edges get enough + # interpolation points before we convert theta → radians. + if munch: + data = self.munch(data, panel_params) + + if self.theta == "x": + theta_col, r_col = "x", "y" + theta_end_col, r_end_col = "xend", "yend" + else: + theta_col, r_col = "y", "x" + theta_end_col, r_end_col = "yend", "xend" + + if theta_col not in data.columns or r_col not in data.columns: + return data + + theta_range = panel_params.theta_range + assert theta_range is not None + + data = data.copy() + data[theta_col] = self._to_radians( + data[theta_col].to_numpy(), theta_range + ) + has_endpoints = ( + theta_end_col in data.columns and r_end_col in data.columns + ) + if has_endpoints: + data[theta_end_col] = self._to_radians( + data[theta_end_col].to_numpy(), theta_range + ) + + # PolarAxes always expects x = theta (radians) and y = r. + # When theta = "y" we need to swap the columns. + if self.theta == "y": + data["x"], data["y"] = data["y"].copy(), data["x"].copy() + if has_endpoints: + data["xend"], data["yend"] = ( + data["yend"].copy(), + data["xend"].copy(), + ) + + # After the swap, data["x"] is always theta in radians. if ( self.rotate_angle and "angle" in data.columns and "x" in data.columns ): - data = data.copy() # Align marks tangentially to their spoke. The PolarAxes places # a data theta t at on-screen angle (deg, CCW from East) # screen = 90 + mpl_dir * degrees(t), mpl_dir = -1 if cw else 1 @@ -359,11 +434,42 @@ def transform( rot = mpl_dir * np.degrees(data["x"].to_numpy()) rot = (rot + 90.0) % 180.0 - 90.0 data["angle"] = data["angle"] + rot + return data - # ------------------------------------------------------------------ - # Draw decorations on PolarAxes - # ------------------------------------------------------------------ + def distance( + self, + x: pd.Series, + y: pd.Series, + panel_params: panel_view, + ) -> np.ndarray: + # Normalise theta and r to [0, 1] then compute Euclidean distance. + assert panel_params.theta_range is not None + assert panel_params.r_range is not None + t_min, t_max = panel_params.theta_range + r_min, r_max = panel_params.r_range + t_denom = float(t_max - t_min) or 1.0 + r_denom = float(r_max - r_min) or 1.0 + + if self.theta == "x": + theta_vals = np.asarray(x, dtype=float) + r_vals = np.asarray(y, dtype=float) + else: + theta_vals = np.asarray(y, dtype=float) + r_vals = np.asarray(x, dtype=float) + + theta_norm = (theta_vals - float(t_min)) / t_denom + r_norm = (r_vals - float(r_min)) / r_denom + return dist_euclidean(theta_norm, r_norm) + + def backtransform_range(self, panel_params: panel_view) -> panel_ranges: + assert panel_params.theta_range is not None + assert panel_params.r_range is not None + t_range = panel_params.theta_range + r_range = panel_params.r_range + if self.theta == "x": + return panel_ranges(x=t_range, y=r_range) + return panel_ranges(x=r_range, y=t_range) def setup_ax( self, @@ -374,12 +480,35 @@ def setup_ax( """ Configure each PolarAxes from this panel's limits - Sets the arc limits, inner radius, and radial-axis placement using - ``panel_params`` so faceted panels with free scales each get their - own radial range. + Sets limits, breaks, tick labels, the fixed active side, arc limits, + inner radius, and radial-axis placement using ``panel_params`` so + faceted panels with free scales each get their own radial range. + + The primary theta axis always renders on the outside and the primary + r axis always on r_start, independent of `scale.position` (which moves + only the axis title). The fixed choice is recorded on + `p9PolarAxes.axis_at_side` so theming can find it. """ - super().setup_ax(ax, panel_params, layout_info) - polar_ax = cast("PolarAxes", ax) + if panel_params.x.sec is not None: + warn( + f"{self.__class__.__name__}() does not support a secondary " + "theta axis.", + PlotnineWarning, + ) + + self._setup_ticks_labels(ax, panel_params) + polar_ax = cast("p9PolarAxes", ax) + + _activate_axis(ax.xaxis, "top", True) + _activate_axis(ax.yaxis, "left", True) + + polar_ax.axis_at_side["theta_outside"] = polar_ax.thetaaxis + polar_ax.axis_at_side["r_start"] = polar_ax.raxis + + # The theme styles these tick objects later; keep matplotlib's + # tick resets from replacing their styling with the default look. + polar_ax.lock_raxis_tick_style() + arc = self._arc # Restrict visible theta range for partial arcs. @@ -438,3 +567,23 @@ def _theta_guide_angle(self) -> float | None: return self._owner.guides.theta.angle except AttributeError: return None + + def draw(self, axs: list[Axes]) -> None: + """Configure each PolarAxes: zero location and rotation direction + + R-limits are set per panel by setup_ax. + """ + # PolarAxes theta_direction: -1 = clockwise, +1 = counter-clockwise. + mpl_direction = self._mpl_theta_direction() + for ax in axs: + polar_ax = cast("PolarAxes", ax) + polar_ax.set_theta_zero_location("N") # 12 o'clock + polar_ax.set_theta_direction(mpl_direction) + + def aspect(self, panel_params: panel_view) -> float: + return 1.0 + + +@alias +class coord_polar(coord_radial): + pass From 1904c62b3815270ab4195028b843799c771b09d1 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 16 Jul 2026 01:05:41 +0300 Subject: [PATCH 064/126] test(coord): rename polar tests to coord_radial, keep one alias test Every test now drives coord_radial; a single test verifies coord_polar is exported as an alias of it. --- ...st_coord_polar.py => test_coord_radial.py} | 127 +++++++++--------- 1 file changed, 66 insertions(+), 61 deletions(-) rename tests/{test_coord_polar.py => test_coord_radial.py} (84%) diff --git a/tests/test_coord_polar.py b/tests/test_coord_radial.py similarity index 84% rename from tests/test_coord_polar.py rename to tests/test_coord_radial.py index aee2c5e862..f99282e4c3 100644 --- a/tests/test_coord_polar.py +++ b/tests/test_coord_radial.py @@ -13,7 +13,6 @@ ggplot, theme, ) -from plotnine.coords.coord_polar import coord_polar from plotnine.data import mtcars from plotnine.iapi import labels_view, panel_view, scale_view from plotnine.scales import scale_x_continuous, scale_y_continuous @@ -58,29 +57,36 @@ def trained_scales( return scale_x, scale_y -def test_coord_polar_setup_panel_params_theta_x(): +def test_coord_radial_setup_panel_params_theta_x(): scale_x, scale_y = trained_scales( + x_breaks=(0, 5, 10), + x_labels=("0", "5", "10"), y_breaks=(0, 2, 5, 10), y_labels=("0", "2", "5", "10"), ) - coord = coord_polar(theta="x", start=np.pi / 4, expand=False) + coord = coord_radial(theta="x", start=np.pi / 4, expand=False) panel_params = coord.setup_panel_params(scale_x, scale_y) assert panel_params.theta_range == (0, 10) assert panel_params.r_range == (0, 10) assert panel_params.x.range == (np.pi / 4, np.pi / 4 + 2 * np.pi) - assert panel_params.x.breaks == [] - assert panel_params.x.labels == [] + # Theta breaks are kept as radian positions (0,5,10 over [0,10] -> + # start + fraction * 2pi), not cleared the way coord_polar used to. + assert_allclose( + panel_params.x.breaks, + [np.pi / 4, np.pi / 4 + np.pi, np.pi / 4 + 2 * np.pi], + ) + assert panel_params.x.labels == ["0", "5", "10"] assert panel_params.y.breaks == [0, 2, 5, 10] -def test_coord_polar_setup_panel_params_theta_y(): +def test_coord_radial_setup_panel_params_theta_y(): scale_x, scale_y = trained_scales( x_breaks=(0, 2, 5, 10), x_labels=("0", "2", "5", "10"), ) - coord = coord_polar(theta="y", expand=False) + coord = coord_radial(theta="y", expand=False) panel_params = coord.setup_panel_params(scale_x, scale_y) @@ -89,9 +95,9 @@ def test_coord_polar_setup_panel_params_theta_y(): assert panel_params.y.breaks == [0, 2, 5, 10] -def test_coord_polar_setup_panel_params_per_panel_independent(): +def test_coord_radial_setup_panel_params_per_panel_independent(): # one instance reused across panels (faceting) - coord = coord_polar(expand=False) + coord = coord_radial(expand=False) sx1, sy1 = trained_scales(y=(0, 10)) pv1 = coord.setup_panel_params(sx1, sy1) sx2, sy2 = trained_scales(y=(0, 100)) @@ -100,8 +106,8 @@ def test_coord_polar_setup_panel_params_per_panel_independent(): assert pv2.y.range == (0, 100) -def test_coord_polar_transform_uses_per_panel_range_not_last(): - coord = coord_polar() # one instance reused across panels (faceting) +def test_coord_radial_transform_uses_per_panel_range_not_last(): + coord = coord_radial() # one instance reused across panels (faceting) sx1, sy1 = trained_scales(x=(0, 10), y=(0, 10)) pv1 = coord.setup_panel_params(sx1, sy1) sx2, sy2 = trained_scales(x=(0, 20), y=(0, 20)) @@ -112,22 +118,8 @@ def test_coord_polar_transform_uses_per_panel_range_not_last(): assert_allclose(out.loc[0, "x"], np.pi) -def test_coord_polar_to_radians_zero_width_range(): - coord = coord_polar() - pv = panel_view( - x=_dummy_scale_view(), - y=_dummy_scale_view(), - theta_range=(1, 1), - r_range=(0, 10), - ) - - assert_allclose( - coord._to_radians(np.array([1, 2, 3]), pv.theta_range), [0, 0, 0] - ) - - -def test_coord_polar_transforms_segment_endpoints_theta_x(): - coord = coord_polar(theta="x") +def test_coord_radial_transforms_segment_endpoints_theta_x(): + coord = coord_radial(theta="x") pv = make_panel_view((0, 10), (0, 10)) data = pd.DataFrame({"x": [0], "y": [1], "xend": [10], "yend": [2]}) @@ -139,8 +131,8 @@ def test_coord_polar_transforms_segment_endpoints_theta_x(): assert out.loc[0, "yend"] == 2 -def test_coord_polar_transforms_segment_endpoints_theta_y(): - coord = coord_polar(theta="y") +def test_coord_radial_transforms_segment_endpoints_theta_y(): + coord = coord_radial(theta="y") pv = make_panel_view((0, 10), (0, 10)) data = pd.DataFrame({"x": [1], "y": [0], "xend": [2], "yend": [10]}) @@ -152,8 +144,8 @@ def test_coord_polar_transforms_segment_endpoints_theta_y(): assert out.loc[0, "yend"] == 2 -def test_coord_polar_transforms_theta_y_without_endpoints(): - coord = coord_polar(theta="y") +def test_coord_radial_transforms_theta_y_without_endpoints(): + coord = coord_radial(theta="y") pv = make_panel_view((0, 10), (0, 10)) data = pd.DataFrame({"x": [1], "y": [5]}) @@ -163,8 +155,8 @@ def test_coord_polar_transforms_theta_y_without_endpoints(): assert out.loc[0, "y"] == 1 -def test_coord_polar_munches_before_radian_transform(): - coord = coord_polar() +def test_coord_radial_munches_before_radian_transform(): + coord = coord_radial() pv = make_panel_view((0, 10), (0, 10)) data = pd.DataFrame({"x": [0, 10], "y": [1, 2], "group": [1, 1]}) @@ -174,15 +166,15 @@ def test_coord_polar_munches_before_radian_transform(): assert out["x"].between(0, 2 * np.pi).all() -def test_coord_polar_leaves_non_position_data_unchanged(): - coord = coord_polar() +def test_coord_radial_leaves_non_position_data_unchanged(): + coord = coord_radial() data = pd.DataFrame({"label": ["A"]}) assert coord.transform(data, None) is data -def test_coord_polar_distance_and_backtransform_theta_x(): - coord = coord_polar() +def test_coord_radial_distance_and_backtransform_theta_x(): + coord = coord_radial() pv = make_panel_view((0, 10), (0, 20)) distance = coord.distance(pd.Series([0, 10]), pd.Series([0, 10]), pv) @@ -192,8 +184,8 @@ def test_coord_polar_distance_and_backtransform_theta_x(): assert coord.backtransform_range(pv).y == (0, 20) -def test_coord_polar_distance_and_backtransform_theta_y(): - coord = coord_polar(theta="y") +def test_coord_radial_distance_and_backtransform_theta_y(): + coord = coord_radial(theta="y") pv = make_panel_view((0, 10), (0, 20)) distance = coord.distance(pd.Series([0, 10]), pd.Series([0, 10]), pv) @@ -203,19 +195,19 @@ def test_coord_polar_distance_and_backtransform_theta_y(): assert coord.backtransform_range(pv).y == (0, 10) -def test_coord_polar_aspect_is_square(): - assert coord_polar().aspect(None) == 1 +def test_coord_radial_aspect_is_square(): + assert coord_radial().aspect(None) == 1 -def test_coord_polar_swaps_labels_when_theta_y(): - coord = coord_polar(theta="y") +def test_coord_radial_swaps_labels_when_theta_y(): + coord = coord_radial(theta="y") out = coord.labels(labels_view(x="xlab", y="ylab")) assert out.x == "ylab" assert out.y == "xlab" -def test_coord_polar_keeps_labels_when_theta_x(): - coord = coord_polar(theta="x") +def test_coord_radial_keeps_labels_when_theta_x(): + coord = coord_radial(theta="x") out = coord.labels(labels_view(x="xlab", y="ylab")) assert out.x == "xlab" assert out.y == "ylab" @@ -235,7 +227,7 @@ def test_coord_radial_default_theme_hides_theta_and_r_boundaries(): def test_coord_radial_partial_arc_expands_off_ends(): # Regression: outermost bars used to sit flush against the arc ends - # (angles 0 and π). With the default expand=True the theta axis is + # (angles 0 and pi). With the default expand=True the theta axis is # buffered, so the bars sit inside the arc. p = ( ggplot(mtcars, aes("cyl", "mpg")) @@ -245,48 +237,48 @@ def test_coord_radial_partial_arc_expands_off_ends(): assert p == "coord_radial_partial_arc_expands_off_ends" -def test_coord_polar_axis_line_controls_polar_spine(): +def test_coord_radial_axis_line_controls_polar_spine(): # panel_border no longer owns the outer circle; axis_line does, and # unlike panel_border it can style it, not just hide it. data = pd.DataFrame({"x": ["a", "b"], "y": [1, 2]}) p = ( ggplot(data, aes("x", "y")) + geom_col() - + coord_polar() + + coord_radial() + theme( panel_border=element_blank(), axis_line=element_line(color="red", size=2), ) ) - assert p == "coord_polar_axis_line_controls_polar_spine" + assert p == "coord_radial_axis_line_controls_polar_spine" -def test_coord_polar_axis_line_r_start_shows_on_full_circle(): - # coord_polar() is a full circle, so matplotlib's own default hides +def test_coord_radial_axis_line_r_start_shows_on_full_circle(): + # coord_radial() is a full circle, so matplotlib's own default hides # the 'start'/'end' spokes. Explicitly theming axis_line_r_start # must show it anyway, and the choice must survive the real draw. data = pd.DataFrame({"x": ["a", "b"], "y": [1, 2]}) p = ( ggplot(data, aes("x", "y")) + geom_col() - + coord_polar() + + coord_radial() + theme(axis_line_r_start=element_line(color="blue", size=2)) ) - assert p == "coord_polar_axis_line_r_start_shows_on_full_circle" + assert p == "coord_radial_axis_line_r_start_shows_on_full_circle" -def test_coord_polar_ticks_visible_by_default(): +def test_coord_radial_ticks_visible_by_default(): # Ticks were previously invisible on every polar panel regardless of # theme, because activation was skipped entirely; this is the # regression test for that fix. data = pd.DataFrame({"x": ["a", "b"], "y": [1, 2]}) - p = ggplot(data, aes("x", "y")) + geom_col() + coord_polar() - assert p == "coord_polar_ticks_visible_by_default" + p = ggplot(data, aes("x", "y")) + geom_col() + coord_radial() + assert p == "coord_radial_ticks_visible_by_default" -def test_coord_polar_default_theme_does_not_crash(): +def test_coord_radial_default_theme_does_not_crash(): data = pd.DataFrame({"x": ["a", "b"], "y": [1, 2]}) - p = ggplot(data, aes("x", "y")) + geom_col() + coord_polar() + p = ggplot(data, aes("x", "y")) + geom_col() + coord_radial() p.draw_test() @@ -448,16 +440,16 @@ def test_coord_radial_rotate_angle_aligns_upright(): assert_allclose(out["angle"], [0.0, 60.0, -60.0], atol=1e-6) -def test_coord_polar_axis_clearance(): +def test_coord_radial_axis_clearance(): data = pd.DataFrame( {"x": ["a", "a", "a"], "y": [2, 3, 5], "group": ["a", "b", "c"]} ) p = ( ggplot(data, aes("x", "y", fill="group")) + geom_col() - + coord_polar("y") + + coord_radial("y") ) - assert p == "coord_polar_axis_clearance" + assert p == "coord_radial_axis_clearance" def test_coord_radial_axis_text_theming(): @@ -575,3 +567,16 @@ def test_p9_theta_axis_builds_swapped_tick(): # label1 inner with tick1line, label2 outer with tick2line -- unlike # matplotlib's ThetaTick, which pads the labels the opposite way. assert p9ThetaAxis._tick_class is p9ThetaTick + + +def test_coord_polar_is_alias_of_coord_radial(): + # coord_polar is superseded; it is exported only as an alias so old + # code keeps working. It IS coord_radial with the same signature. + from plotnine import coord_polar + + assert issubclass(coord_polar, coord_radial) + c = coord_polar(theta="y", inner_radius=0.4, end=3.14) + assert isinstance(c, coord_radial) + assert coord_polar.__doc__ == ( + "alias of [coord_radial](`plotnine.coords.coord_radial.coord_radial`)" + ) From e21fded09bb83b05933d123250c3e229a81ac98e Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 16 Jul 2026 01:13:49 +0300 Subject: [PATCH 065/126] refactor(coords): separate radial data and display axes Keep trained theta and radius views distinct from the Matplotlib display axes, preserving coordinate transforms and discrete bar placement. Rename the polar axes integration to radial axes and update radial tests accordingly. --- .../_mpl/{_polar_axes.py => _radial_axes.py} | 13 +- plotnine/_mpl/axes.py | 2 +- plotnine/coords/coord_radial.py | 507 ++++++------------ plotnine/iapi.py | 19 +- plotnine/themes/themeable.py | 4 +- tests/test_coord_radial.py | 72 ++- 6 files changed, 256 insertions(+), 361 deletions(-) rename plotnine/_mpl/{_polar_axes.py => _radial_axes.py} (95%) diff --git a/plotnine/_mpl/_polar_axes.py b/plotnine/_mpl/_radial_axes.py similarity index 95% rename from plotnine/_mpl/_polar_axes.py rename to plotnine/_mpl/_radial_axes.py index 863297c1c1..8b47c7b9e9 100644 --- a/plotnine/_mpl/_polar_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -51,12 +51,12 @@ class p9ThetaAxis(ThetaAxis): _tick_class = p9ThetaTick -class p9PolarAxes(PolarAxes): +class p9RadialAxes(PolarAxes): """ - PolarAxes that keeps the axis tick labels above the geom layers + Polar axes that keeps the axis tick labels above the geom layers """ - name = "p9polar" + name = "p9radial" _axis_at_side: dict[PolarSide, ThetaAxis | RadialAxis] | None = None @@ -65,6 +65,11 @@ class p9PolarAxes(PolarAxes): # calls do not stack wrappers on top of each other. _raxis_tick_style_locked: bool = False + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.set_theta_zero_location("N") # 12 o'clock + def _init_axis(self) -> None: """ Build the panel's axes, using the theta axis with corrected labels @@ -167,4 +172,4 @@ def reset_ticks_and_restyle() -> None: axis.reset_ticks = reset_ticks_and_restyle -register_projection(p9PolarAxes) +register_projection(p9RadialAxes) diff --git a/plotnine/_mpl/axes.py b/plotnine/_mpl/axes.py index 0a780df1e1..f22f9ff160 100644 --- a/plotnine/_mpl/axes.py +++ b/plotnine/_mpl/axes.py @@ -123,7 +123,7 @@ def axis_at(ax: Axes, side: Side | PolarSide) -> XAxis | YAxis | None: For a cartesian `side`, considers the primary axis of the side's dimension and, on a `p9Axes`, the secondary one, via mpl's own tick-flag state — reliable for `XAxis`/`YAxis`. For a polar `side` - (a theta/r side), reads `p9PolarAxes.axis_at_side` instead: + (a theta/r side), reads `p9RadialAxes.axis_at_side` instead: `ThetaAxis`/`RadialAxis` can't self-report which side they occupy through `get_tick_params()`, so plotnine tracks it explicitly there. diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 1a37d879b4..4608f1d817 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -1,135 +1,97 @@ from __future__ import annotations from dataclasses import replace -from typing import TYPE_CHECKING, Literal, cast +from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast from warnings import warn import numpy as np -from .._mpl._polar_axes import p9PolarAxes # noqa: TCH001 +from .._mpl._radial_axes import p9RadialAxes # noqa: TCH001 from .._utils.registry import alias from ..exceptions import PlotnineWarning -from ..iapi import panel_ranges +from ..iapi import panel_ranges, radial_panel_view from .coord import _activate_axis, coord, dist_euclidean if TYPE_CHECKING: + import numpy.typing as npt import pandas as pd from matplotlib.axes import Axes - from matplotlib.projections.polar import PolarAxes from plotnine.iapi import labels_view, layout_details, panel_view from plotnine.scales.scale import scale + from plotnine.typing import FloatArrayLike, FloatSeries + +T1 = TypeVar("T1") +T2 = TypeVar("T2") class coord_radial(coord): """ Radial coordinate system - `coord_radial` maps one position aesthetic to the angle and the other - to the radius. It supports full and partial arcs, inner radius holes, - theta/radius limits, radial-axis placement, and rotation of the - ``angle`` aesthetic. It supersedes `coord_polar`. + A system where position aesthetic is mapped to the angle and the other + to the radius. Parameters ---------- theta : - Which variable maps to the angle axis, ``"x"`` (default) or ``"y"``. + Which variable maps to the angle axis, `"x"` (default) or `"y"`. start : Starting angle in radians, measured clockwise from 12 o'clock. Default 0. end : Ending angle in radians, measured clockwise from 12 o'clock. - ``None`` (default) gives a full circle (``start + 2π * direction``). + `None` (default) gives a full circle (`start + 2π * direction`). direction : - Angular rotation sense: ``1`` = clockwise (default), ``-1`` = + Angular rotation sense: `1` = clockwise (default), `-1` = counter-clockwise. Applied regardless of *end*, so it also sets the sweep direction of a partial arc. expand : Add a small buffer around the data on the radius axis. - Default ``True``. + Default `True`. inner_radius : Size of the inner hole as a fraction of the outer radius, in - ``[0, 1)``. ``0`` (default) means no hole; ``0.3`` creates a 30 % + `[0, 1)`. `0` (default) means no hole; `0.3` creates a 30 % donut hole, useful for gauge and donut charts. rotate_angle : - If ``True``, rotate the ``angle`` aesthetic so that text or other + If `True`, rotate the `angle` aesthetic so that text or other rotated marks align tangentially with the arc at their spoke. The rotation is folded so labels stay upright (readable) rather than - appearing upside-down in the lower half. Default ``False``. + appearing upside-down in the lower half. Default `False`. thetalim : - Data-space limits for the theta axis as ``(lo, hi)``. Only data + Data-space limits for the theta axis as `(lo, hi)`. Only data within this range is mapped to the arc; equivalent to zooming on the - angular axis. ``None`` (default) uses the full data range. + angular axis. `None` (default) uses the full data range. rlim : - Data-space limits for the r axis as ``(lo, hi)``. Only data within + Data-space limits for the r axis as `(lo, hi)`. Only data within this range is shown; equivalent to zooming on the radial axis. - ``None`` (default) uses the full data range. + `None` (default) uses the full data range. reverse : Which axes run in the opposite direction. - * ``"none"`` (default) — neither axis is reversed. - * ``"theta"`` — the angular axis runs the other way around. - * ``"r"`` — the radial axis is inverted, so large values sit + * `"none"` (default) — neither axis is reversed. + * `"theta"` — the angular axis runs the other way around. + * `"r"` — the radial axis is inverted, so large values sit toward the centre. - * ``"thetar"`` — both the angular and radial axes are reversed. + * `"thetar"` — both the angular and radial axes are reversed. Notes ----- Theta-axis tick labels are shown by default. Since a polar axes' x-axis *is* the theta axis, they are styled through the theme: hide them with - ``theme(axis_text_x=element_blank())`` and adjust the gap to the outer - circle through the ``axis_text_x`` margin. - - Unlike ggplot2, plotnine coordinate systems do not currently expose a - ``clip`` argument. - - Examples - -------- - A donut chart is a stacked bar chart with an inner radius. - - ```python - import pandas as pd - from plotnine import aes, coord_radial, geom_col, ggplot - - df = pd.DataFrame({ - "x": [1, 1, 1], - "y": [2, 3, 5], - "group": ["a", "b", "c"], - }) - - ( - ggplot(df, aes("x", "y", fill="group")) - + geom_col() - + coord_radial(theta="y", inner_radius=0.4) - ) - ``` - - Partial arcs can be used for gauge-like displays. - - ```python - import numpy as np - import pandas as pd - from plotnine import aes, coord_radial, geom_point, ggplot - - df = pd.DataFrame({"x": [1, 2, 3], "y": [2, 4, 3]}) - - ggplot(df, aes("x", "y")) + geom_point() + coord_radial( - start=-0.4 * np.pi, - end=0.4 * np.pi, - inner_radius=0.3, - ) - ``` + `theme(axis_text_x=element_blank())` and adjust the gap to the outer + circle through the `axis_text_x` margin. """ is_linear = False - _projection = "p9polar" + _projection = "p9radial" def __init__( self, theta: str = "x", start: float = 0, end: float | None = None, - direction: int = 1, + direction: Literal[-1, 1] = 1, expand: bool = True, inner_radius: float = 0, rotate_angle: bool = False, @@ -145,7 +107,7 @@ def __init__( self.theta = theta self.start = start self.end = end - self.direction = direction + self.direction: Literal[-1, 1] = direction self.expand = expand self.inner_radius = inner_radius self.rotate_angle = rotate_angle @@ -153,114 +115,52 @@ def __init__( self.rlim = rlim self.reverse = reverse - def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: - from ..scales.scale_continuous import scale_continuous + @property + def is_default_axes(self) -> bool: + return self.theta == "x" + + def _flip(self, p1: T1, p2: T2) -> tuple[T1, T2] | tuple[T2, T1]: + return (p1, p2) if self.is_default_axes else (p2, p1) + + def setup_panel_params( + self, scale_x: scale, scale_y: scale + ) -> radial_panel_view: + """ + Compute the range and break information for the panel + + The panel_params are created in only this method. If the axes + have been switched (i.e. `theta = "y"`) from the default + (`theta = "x"` is the default), the panel_params are adjusted + accordingly. + """ from .coord_cartesian import coord_cartesian # One expanded cartesian view supplies both the theta/r ranges and # the theta breaks. Theta follows the expand flag like every other # axis: expand=True buffers the data off the arc ends; expand=False # keeps it flush so a pie closes cleanly. - pv_exp = coord_cartesian(expand=self.expand).setup_panel_params( - scale_x, scale_y - ) - - if self.theta == "x": - theta_range, r_sv = pv_exp.x.range, pv_exp.y - theta_breaks = list(pv_exp.x.breaks) - theta_labels = list(pv_exp.x.labels) - theta_minor_breaks = list(pv_exp.x.minor_breaks) - else: - theta_range, r_sv = pv_exp.y.range, pv_exp.x - theta_breaks = list(pv_exp.y.breaks) - theta_labels = list(pv_exp.y.labels) - theta_minor_breaks = list(pv_exp.y.minor_breaks) - - r_range = r_sv.range - empty = np.array([], dtype=float) + xlim, ylim = self._flip(self.thetalim, self.rlim) + cartesian_view = coord_cartesian( + xlim=xlim, ylim=ylim, expand=self.expand + ).setup_panel_params(scale_x, scale_y) + theta, r = self._flip(cartesian_view.x, cartesian_view.y) + theta.breaks = cast("list[float]", theta.breaks) # theta axis: [start, start+2π] display span so bars rotated by a # non-zero start stay within range. Data ticks (original units) are # converted to radian positions and applied at the end of this method. - theta_start = float(self.start) - new_x = replace( - pv_exp.x, - limits=(theta_start, theta_start + 2 * np.pi), - range=(theta_start, theta_start + 2 * np.pi), - breaks=[], - minor_breaks=empty, - labels=[], + _start, _end = self.start, self.start + 2 * np.pi + x = replace( + theta, + limits=(_start, _end), + range=(_start, _end), + breaks=self._to_radians(theta.breaks, theta.range), + minor_breaks=np.asarray( + self._to_radians(theta.minor_breaks, theta.range) + ), + labels=list(theta.labels), ) - new_y = replace(r_sv) - - pv = replace( - pv_exp, - x=new_x, - y=new_y, - theta_range=tuple(theta_range), - r_range=tuple(r_range), - ) - - # thetalim: zoom the theta data range — only this slice maps to the - # arc. Expand on top of the zoom (like coord_cartesian expands on top - # of xlim) and recompute nice breaks over the expanded range so labels - # are not reduced to sparse endpoints. - if self.thetalim is not None: - from mizani.transforms import identity_trans - - theta_scale = scale_x if self.theta == "x" else scale_y - coord_limits = ( - theta_scale.transform(self.thetalim) - if isinstance(theta_scale, scale_continuous) - else self.thetalim - ) - expansion = theta_scale.default_expansion(expand=self.expand) - theta_zoom = theta_scale.expand_limits( - theta_scale.final_limits, - expansion, - coord_limits, - identity_trans(), - ).range - pv = replace(pv, theta_range=tuple(theta_zoom)) - theta_breaks = [ - b - for b in theta_scale.get_bounded_breaks(theta_zoom) - if np.isfinite(b) - ] - theta_labels = list(theta_scale.get_labels(theta_breaks)) - # Only a continuous theta scale has minor breaks; a discrete one - # has none, so leave theta_minor_breaks empty. - if isinstance(theta_scale, scale_continuous): - theta_minor_breaks = [ - b - for b in theta_scale.get_minor_breaks( - theta_breaks, theta_zoom - ) - if np.isfinite(b) - ] - else: - theta_minor_breaks = [] - - # rlim: zoom the r data range — update ranges and recompute nice breaks - # over the zoomed range (rather than filtering the full-range breaks, - # which leaves sparse endpoint-only labels). - if self.rlim is not None: - pv = replace(pv, r_range=tuple(self.rlim)) - r_scale = scale_y if self.theta == "x" else scale_x - breaks = [ - b - for b in r_scale.get_bounded_breaks(self.rlim) - if np.isfinite(b) - ] - labels = r_scale.get_labels(breaks) - new_y = replace( - pv.y, - limits=tuple(self.rlim), - range=tuple(self.rlim), - breaks=breaks, - labels=labels, - ) - pv = replace(pv, y=new_y) + y = replace(r) # Compute arc bounds for partial-arc plots (None means full circle). arc_lo = arc_hi = None @@ -269,76 +169,58 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: arc_lo = min(self.start, self.start + arc) arc_hi = max(self.start, self.start + arc) - # Convert data-space theta breaks to radian positions and restore them - # as theta axis tick labels on the outer edge. - x_updates: dict = {} - if theta_breaks: - assert pv.theta_range is not None - radian_pos = list( - self._to_radians( - np.asarray(theta_breaks, dtype=float), pv.theta_range - ) - ) - if arc_lo is not None: - keep = [arc_lo <= r <= arc_hi for r in radian_pos] - radian_pos = [r for r, k in zip(radian_pos, keep) if k] - theta_labels = [l for l, k in zip(theta_labels, keep) if k] - x_updates["breaks"] = radian_pos - x_updates["labels"] = theta_labels - - if theta_minor_breaks: - assert pv.theta_range is not None - minor_radian_pos = list( - self._to_radians( - np.asarray(theta_minor_breaks, dtype=float), - pv.theta_range, - ) - ) - if arc_lo is not None: - minor_radian_pos = [ - r for r in minor_radian_pos if arc_lo <= r <= arc_hi - ] - x_updates["minor_breaks"] = minor_radian_pos - # Partial arc: x panel range must match [arc_lo, arc_hi] so that # coord.setup_ax calls ax.set_xlim(arc_lo, arc_hi) rather than # ax.set_xlim(0, 2π), which would override set_thetalim. - if arc_lo is not None: - x_updates["limits"] = (arc_lo, arc_hi) - x_updates["range"] = (arc_lo, arc_hi) - - if x_updates: - pv = replace(pv, x=replace(pv.x, **x_updates)) + if arc_lo is not None and arc_hi is not None: + x_breaks = cast("list[float]", x.breaks) + keep = [arc_lo <= value <= arc_hi for value in x_breaks] + x = replace( + x, + limits=(arc_lo, arc_hi), + range=(arc_lo, arc_hi), + breaks=[ + value for value, include in zip(x_breaks, keep) if include + ], + minor_breaks=[ + value + for value in x.minor_breaks + if arc_lo <= value <= arc_hi + ], + labels=[ + label for label, include in zip(x.labels, keep) if include + ], + ) # reverse="r"/"thetar": flip the displayed radial range so large r # values sit toward the centre. set_ylim(hi, lo) inverts the axis. - # pv.r_range stays in natural (lo, hi) order, so setup_ax reads it + # r.range stays in natural (lo, hi) order, so setup_ax reads it # (not y.range) for the inner_radius origin: reversing only y.range # inverts the display without breaking the hole, and distance() is # likewise unaffected. if self.reverse in ("r", "thetar"): - lo, hi = pv.y.range - pv = replace(pv, y=replace(pv.y, range=(hi, lo))) + y = replace(y, range=(y.range[1], y.range[0])) - return pv + return radial_panel_view(x=x, y=y, theta=theta, r=r) def labels(self, cur_labels: labels_view) -> labels_view: - # When theta="y" the data x/y columns are swapped in transform so that - # PolarAxes sees x=theta, y=r. Swap the axis titles to match, the same - # way coord_flip does for its flipped axes. - if self.theta == "y": - from .coord_flip import flip_labels + labels = super().labels(cur_labels) + if self.theta == "x": + return labels + + # The default theta="x", so when theta="y", we also swap the labels + # and axis titles. + from .coord_flip import flip_labels - return flip_labels(super().labels(cur_labels)) - return super().labels(cur_labels) + return flip_labels(labels) @property def _arc(self) -> float: """ Total angular span of the plotted region, in radians - For a partial radial plot this is the distance between ``start`` and - ``end``; otherwise it is a full turn. The sense of rotation + For a partial radial plot this is the distance between `start` and + `end`; otherwise it is a full turn. The sense of rotation (clockwise vs counter-clockwise) is a PolarAxes property applied at draw time and is not encoded in this magnitude. """ @@ -346,32 +228,29 @@ def _arc(self) -> float: return self.end - self.start return 2.0 * np.pi - def _mpl_theta_direction(self) -> Literal[-1, 1]: + @property + def _mpl_direction(self) -> Literal[-1, 1]: """ Matplotlib theta direction for this coordinate system - ``-1`` draws clockwise and ``+1`` counter-clockwise, the opposite of - plotnine's own ``direction`` convention. ``reverse="theta"``/ - ``"thetar"`` runs the angular axis the other way by folding into the + `-1` draws clockwise and `+1` counter-clockwise, the opposite of + plotnine's own `direction` convention. `reverse="theta"`/ + `"thetar"` runs the angular axis the other way by folding into the PolarAxes direction here, rather than into the radian mapping. """ - mpl_direction: Literal[-1, 1] = -1 if self.direction == 1 else 1 - if self.reverse in ("theta", "thetar"): - return 1 if mpl_direction == -1 else -1 - return mpl_direction + reverse = -1 if self.reverse in ("theta", "thetar") else 1 + return self.direction * reverse * -1 def _to_radians( - self, vals: np.ndarray, theta_range: tuple[float, float] - ) -> np.ndarray: + self, vals: FloatArrayLike, theta_range: tuple[float, float] + ) -> list[float]: """Normalize theta values to [start, start + arc]""" - t_min, t_max = theta_range - denom = float(t_max) - float(t_min) - if denom == 0: - return np.zeros_like(vals, dtype=float) - norm = (np.asarray(vals, dtype=float) - float(t_min)) / denom - # Rotation direction is a PolarAxes property set in draw via - # set_theta_direction; it is not baked into these radian values. - return self.start + norm * self._arc + lo, hi = theta_range + span = hi - lo + if span == 0: + return [0] * len(vals) + norm = (np.asarray(vals) - lo) / span + return list(self.start + norm * self._arc) def transform( self, @@ -384,92 +263,67 @@ def transform( if munch: data = self.munch(data, panel_params) - if self.theta == "x": - theta_col, r_col = "x", "y" - theta_end_col, r_end_col = "xend", "yend" - else: - theta_col, r_col = "y", "x" - theta_end_col, r_end_col = "yend", "xend" - - if theta_col not in data.columns or r_col not in data.columns: + if "x" not in data or "y" not in data: return data - theta_range = panel_params.theta_range - assert theta_range is not None + t = self._flip("x", "y")[0] + tend = self._flip("xend", "yend")[0] + view = cast("radial_panel_view", panel_params) data = data.copy() - data[theta_col] = self._to_radians( - data[theta_col].to_numpy(), theta_range - ) - has_endpoints = ( - theta_end_col in data.columns and r_end_col in data.columns - ) - if has_endpoints: - data[theta_end_col] = self._to_radians( - data[theta_end_col].to_numpy(), theta_range - ) + data[t] = self._to_radians(data[t], view.theta.range) + if tend in data.columns: + data[tend] = self._to_radians(data[tend], view.theta.range) # PolarAxes always expects x = theta (radians) and y = r. # When theta = "y" we need to swap the columns. - if self.theta == "y": - data["x"], data["y"] = data["y"].copy(), data["x"].copy() - if has_endpoints: - data["xend"], data["yend"] = ( - data["yend"].copy(), - data["xend"].copy(), - ) + if not self.is_default_axes: + data["x"], data["y"] = data["y"], data["x"] + if "xend" in data and "yend" in data: + data["xend"], data["yend"] = data["yend"], data["xend"] # After the swap, data["x"] is always theta in radians. - if ( - self.rotate_angle - and "angle" in data.columns - and "x" in data.columns - ): + if self.rotate_angle and "angle" in data and "x" in data: # Align marks tangentially to their spoke. The PolarAxes places # a data theta t at on-screen angle (deg, CCW from East) # screen = 90 + mpl_dir * degrees(t), mpl_dir = -1 if cw else 1 # Tangential text rotation is screen - 90; folding into (-90, 90] # keeps labels upright (a bottom label reads "6", not "9"). - mpl_dir = self._mpl_theta_direction() - rot = mpl_dir * np.degrees(data["x"].to_numpy()) + rot = self._mpl_direction * np.degrees(data["x"]) rot = (rot + 90.0) % 180.0 - 90.0 data["angle"] = data["angle"] + rot - return data def distance( self, - x: pd.Series, - y: pd.Series, + x: FloatSeries, + y: FloatSeries, panel_params: panel_view, - ) -> np.ndarray: + ) -> npt.NDArray[Any]: # Normalise theta and r to [0, 1] then compute Euclidean distance. - assert panel_params.theta_range is not None - assert panel_params.r_range is not None - t_min, t_max = panel_params.theta_range - r_min, r_max = panel_params.r_range - t_denom = float(t_max - t_min) or 1.0 - r_denom = float(r_max - r_min) or 1.0 + view = cast("radial_panel_view", panel_params) - if self.theta == "x": - theta_vals = np.asarray(x, dtype=float) - r_vals = np.asarray(y, dtype=float) - else: - theta_vals = np.asarray(y, dtype=float) - r_vals = np.asarray(x, dtype=float) + t_lo, t_hi = view.theta.range + r_lo, r_hi = view.r.range + + t_span = (t_hi - t_lo) or 1 + r_span = (r_hi - r_lo) or 1 - theta_norm = (theta_vals - float(t_min)) / t_denom - r_norm = (r_vals - float(r_min)) / r_denom - return dist_euclidean(theta_norm, r_norm) + # While the panel_params are flipped, the x and y parameters + # have not be reoriented. + x, y = self._flip(x, y) + t_vals = np.asarray(x, dtype=float) + r_vals = np.asarray(y, dtype=float) + + t_norm = (t_vals - t_lo) / t_span + r_norm = (r_vals - r_lo) / r_span + + return dist_euclidean(t_norm, r_norm) def backtransform_range(self, panel_params: panel_view) -> panel_ranges: - assert panel_params.theta_range is not None - assert panel_params.r_range is not None - t_range = panel_params.theta_range - r_range = panel_params.r_range - if self.theta == "x": - return panel_ranges(x=t_range, y=r_range) - return panel_ranges(x=r_range, y=t_range) + view = cast("radial_panel_view", panel_params) + x, y = self._flip(view.theta.range, view.r.range) + return panel_ranges(x=x, y=y) def setup_ax( self, @@ -478,72 +332,76 @@ def setup_ax( layout_info: layout_details, ) -> None: """ - Configure each PolarAxes from this panel's limits + Configure each polar axes from this panel's limits Sets limits, breaks, tick labels, the fixed active side, arc limits, - inner radius, and radial-axis placement using ``panel_params`` so + inner radius, and radial-axis placement using `panel_params` so faceted panels with free scales each get their own radial range. The primary theta axis always renders on the outside and the primary r axis always on r_start, independent of `scale.position` (which moves only the axis title). The fixed choice is recorded on - `p9PolarAxes.axis_at_side` so theming can find it. + `p9RadialAxes.axis_at_side` so theming can find it. """ - if panel_params.x.sec is not None: + view = cast("radial_panel_view", panel_params) + radial_ax = cast("p9RadialAxes", ax) + + radial_ax.set_theta_direction(self._mpl_direction) + + if view.x.sec is not None: warn( f"{self.__class__.__name__}() does not support a secondary " "theta axis.", PlotnineWarning, ) - self._setup_ticks_labels(ax, panel_params) - polar_ax = cast("p9PolarAxes", ax) + self._setup_ticks_labels(ax, view) _activate_axis(ax.xaxis, "top", True) _activate_axis(ax.yaxis, "left", True) - polar_ax.axis_at_side["theta_outside"] = polar_ax.thetaaxis - polar_ax.axis_at_side["r_start"] = polar_ax.raxis + radial_ax.axis_at_side["theta_outside"] = radial_ax.thetaaxis + radial_ax.axis_at_side["r_start"] = radial_ax.raxis # The theme styles these tick objects later; keep matplotlib's # tick resets from replacing their styling with the default look. - polar_ax.lock_raxis_tick_style() + radial_ax.lock_raxis_tick_style() arc = self._arc # Restrict visible theta range for partial arcs. if self.end is not None: - polar_ax.set_thetalim( + radial_ax.set_thetalim( min(self.start, self.start + arc), max(self.start, self.start + arc), ) # Inner radius: push the data away from the centre by setting a # virtual r-origin below r_min. Formula: solve - # inner_radius = (r_min - r_origin) / (r_max - r_origin) + # inner_radius = (r_lo - r_origin) / (r_hi - r_origin) # Use the natural r range (always (lo, hi), reflects rlim) rather # than y.range, which reverse="r"/"thetar" inverts to (hi, lo) and - # would fail the r_max > r_min guard, dropping the donut hole. - assert panel_params.r_range is not None - r_min, r_max = panel_params.r_range + # would fail the r_lo < r_hi guard, dropping the donut hole. + r_lo, r_hi = view.r.range + if ( self.inner_radius > 0 - and np.isfinite(r_min) - and np.isfinite(r_max) - and r_max > r_min + and np.isfinite(r_lo) + and np.isfinite(r_hi) + and r_lo < r_hi and self.inner_radius < 1.0 ): - r_origin = (r_min - self.inner_radius * r_max) / ( + r_origin = (r_lo - self.inner_radius * r_hi) / ( 1.0 - self.inner_radius ) - polar_ax.set_rorigin(r_origin) + radial_ax.set_rorigin(r_origin) # Radial axis label placement. RadialTick.update_position (mpl) # reads rlabel_position only for a full circle; a partial arc # ignores it and always uses thetamin/thetamax directly, so this # only matters here. if self.end is None: - polar_ax.set_rlabel_position(270) + radial_ax.set_rlabel_position(270) ax.tick_params(axis="x", which="major", direction="out") if (angle := self._theta_guide_angle()) is not None: @@ -553,7 +411,10 @@ def setup_ax( # ax.tick_params(labelrotation=...) always sets 'default' mode # (absolute degrees), so we patch each tick directly instead. for tick in ax.xaxis.get_major_ticks(): - tick._labelrotation = ("auto", angle) + tick._labelrotation = ( # pyright: ignore[reportAttributeAccessIssue] + "auto", + angle, + ) # Allow geom_text labels to extend past the polar axes bounding box # (e.g. spoke labels placed just beyond the outermost bar tip). for text in ax.texts: @@ -563,22 +424,10 @@ def _theta_guide_angle(self) -> float | None: """ Return the angle from guides(theta=guide_axis_theta(...)) """ - try: - return self._owner.guides.theta.angle - except AttributeError: + if self._owner is None: return None - - def draw(self, axs: list[Axes]) -> None: - """Configure each PolarAxes: zero location and rotation direction - - R-limits are set per panel by setup_ax. - """ - # PolarAxes theta_direction: -1 = clockwise, +1 = counter-clockwise. - mpl_direction = self._mpl_theta_direction() - for ax in axs: - polar_ax = cast("PolarAxes", ax) - polar_ax.set_theta_zero_location("N") # 12 o'clock - polar_ax.set_theta_direction(mpl_direction) + guide = cast("Any", self._owner.guides.theta) + return getattr(guide, "angle", None) def aspect(self, panel_params: panel_view) -> float: return 1.0 diff --git a/plotnine/iapi.py b/plotnine/iapi.py index f124cf9b94..d1375bc746 100644 --- a/plotnine/iapi.py +++ b/plotnine/iapi.py @@ -160,19 +160,12 @@ def __repr__(self) -> str: @dataclass class panel_view: """ - Information from the trained position scales in a panel + Position-scale state for one panel """ x: scale_position_view y: scale_position_view - # Polar coordinates store the per-panel data-space ranges of the theta - # and r variables here so that transform-time methods read the range - # belonging to the panel they are given, not shared instance state. - # These are None for non-polar coordinate systems. - theta_range: Optional[tuple[float, float]] = None - r_range: Optional[tuple[float, float]] = None - @dataclass class panel_ranges: @@ -184,6 +177,16 @@ class panel_ranges: y: tuple[float, float] +@dataclass +class radial_panel_view(panel_view): + """ + Data-space and display-space scales for one radial panel + """ + + theta: scale_position_view + r: scale_position_view + + @dataclass class pos_scales: """ diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index bfc4f6c7fb..6059cb272d 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -1407,7 +1407,7 @@ def _style_polar_axis_line(themeable, ax, spine): Mirrors `_style_axis_line`: an explicitly themed (non-blank) spine is shown (`visible=True`) even if matplotlib's own geometry-driven default would hide it (e.g. a full circle has no `start`/`end` - spokes). Visibility goes through `p9PolarAxes.set_spine_visible` + spokes). Visibility goes through `p9RadialAxes.set_spine_visible` rather than the `properties` dict, since that's what makes the choice stick across draws. """ @@ -1428,7 +1428,7 @@ def _blank_polar_axis_line(ax, spine): A plain `Axes` has no matching spine name, so this is a safe no-op for cartesian axes. It's reachable from every `blank_ax` call, polar or not, since `axis_line_theta`/`axis_line_r` nest under - `axis_line_x`/`axis_line_y`. `p9PolarAxes.set_spine_visible` is what + `axis_line_x`/`axis_line_y`. `p9RadialAxes.set_spine_visible` is what makes this choice stick across draws. """ if spine in ax.spines: diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index f99282e4c3..0606fa48ed 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1,3 +1,5 @@ +from dataclasses import replace + import numpy as np import pandas as pd from numpy.testing import assert_allclose @@ -14,13 +16,17 @@ theme, ) from plotnine.data import mtcars -from plotnine.iapi import labels_view, panel_view, scale_view +from plotnine.iapi import ( + labels_view, + radial_panel_view, + scale_position_view, +) from plotnine.scales import scale_x_continuous, scale_y_continuous -def _dummy_scale_view() -> scale_view: - """A minimal scale_view; polar transforms only read theta/r ranges.""" - return scale_view( +def _dummy_scale_view() -> scale_position_view: + """Minimal position-scale state for radial coordinate unit tests""" + return scale_position_view( scale=None, # type: ignore[arg-type] aesthetics=[], name=None, @@ -29,16 +35,22 @@ def _dummy_scale_view() -> scale_view: breaks=[], minor_breaks=np.array([], dtype=float), labels=[], + position="bottom", ) -def make_panel_view(theta_range, r_range) -> panel_view: - """A panel_view carrying just the per-panel theta/r ranges.""" - return panel_view( - x=_dummy_scale_view(), - y=_dummy_scale_view(), - theta_range=theta_range, - r_range=r_range, +def make_panel_view( + theta_range: tuple[float, float], + r_range: tuple[float, float], +) -> radial_panel_view: + """Radial panel state with distinct data and display ranges""" + theta = replace(_dummy_scale_view(), range=theta_range) + r = replace(_dummy_scale_view(), range=r_range) + return radial_panel_view( + x=replace(theta, range=(0, 2 * np.pi)), + y=replace(r), + theta=theta, + r=r, ) @@ -68,9 +80,12 @@ def test_coord_radial_setup_panel_params_theta_x(): panel_params = coord.setup_panel_params(scale_x, scale_y) - assert panel_params.theta_range == (0, 10) - assert panel_params.r_range == (0, 10) + assert panel_params.theta.range == (0, 10) + assert panel_params.r.range == (0, 10) assert panel_params.x.range == (np.pi / 4, np.pi / 4 + 2 * np.pi) + assert panel_params.theta is not panel_params.x + assert panel_params.r is not panel_params.y + assert panel_params.theta.breaks == [0, 5, 10] # Theta breaks are kept as radian positions (0,5,10 over [0,10] -> # start + fraction * 2pi), not cleared the way coord_polar used to. assert_allclose( @@ -90,11 +105,34 @@ def test_coord_radial_setup_panel_params_theta_y(): panel_params = coord.setup_panel_params(scale_x, scale_y) - assert panel_params.theta_range == (0, 10) - assert panel_params.r_range == (0, 10) + assert panel_params.theta.range == (0, 10) + assert panel_params.r.range == (0, 10) assert panel_params.y.breaks == [0, 2, 5, 10] +def test_coord_radial_discrete_theta_spans_full_circle(): + p = ( + ggplot(mtcars, aes("factor(cyl)", "mpg", fill="factor(cyl)")) + + geom_col() + + coord_radial(start=0, end=2 * np.pi, inner_radius=0) + ) + built = p.build_test() + panel_params = built.layout.panel_params[0] + transformed = built.coordinates.transform( + built.layers[0].data.copy(), + panel_params, + ) + + expected = built.coordinates._to_radians( + [1, 2, 3], + panel_params.theta.range, + ) + actual = np.sort(transformed["x"].unique()) + + assert_allclose(actual, expected) + assert np.ptp(actual) > np.pi + + def test_coord_radial_setup_panel_params_per_panel_independent(): # one instance reused across panels (faceting) coord = coord_radial(expand=False) @@ -328,8 +366,8 @@ def test_coord_radial_setup_panel_params_for_partial_arc(): panel_params = coord.setup_panel_params(scale_x, scale_y) - assert panel_params.theta_range == (0, 10) - assert panel_params.r_range == (2, 8) + assert panel_params.theta.range == (0, 10) + assert panel_params.r.range == (2, 8) assert_allclose(panel_params.x.breaks, [0, np.pi / 2, np.pi]) assert panel_params.x.labels == ["0", "5", "10"] assert panel_params.x.range == (0, np.pi) From 2d6bbf2dc91473e0ce2e69a506e85c36959993d1 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 17 Jul 2026 15:07:51 +0300 Subject: [PATCH 066/126] fix(coord_radial): align r axis with start --- plotnine/_mpl/_radial_axes.py | 61 ++++-------------- plotnine/_mpl/_radial_axis.py | 111 ++++++++++++++++++++++++++++++++ plotnine/coords/coord_radial.py | 8 +-- tests/test_coord_radial.py | 90 ++++++++++++++++++++++++-- 4 files changed, 212 insertions(+), 58 deletions(-) create mode 100644 plotnine/_mpl/_radial_axis.py diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index 8b47c7b9e9..25657cd2a8 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -2,55 +2,17 @@ from typing import TYPE_CHECKING, cast -import numpy as np from matplotlib.projections import register_projection -from matplotlib.projections.polar import PolarAxes, ThetaAxis, ThetaTick +from matplotlib.projections.polar import PolarAxes, RadialAxis, ThetaAxis + +from ._radial_axis import p9RadialAxis, p9ThetaAxis if TYPE_CHECKING: from matplotlib.backend_bases import RendererBase - from matplotlib.projections.polar import RadialAxis - from matplotlib.transforms import Transform from plotnine.typing import PolarSide -class p9ThetaTick(ThetaTick): - """ - ThetaTick whose labels sit on the same rim as the matching tick marks - """ - - def _get_text1_transform(self) -> tuple[Transform, str, str]: - # matplotlib anchors the theta labels through a flipr-reversed - # transform, sending label1 to tick2line's radius and label2 to - # tick1line's. Use the unflipped tick transform so label1 shares - # tick1line's inner radius instead. - return self.axes.get_xaxis_transform("tick1"), "center", "center" - - def _get_text2_transform(self) -> tuple[Transform, str, str]: - # Counterpart to _get_text1_transform: label2 shares tick2line's - # outer radius. - return self.axes.get_xaxis_transform("tick2"), "center", "center" - - def _update_padding(self, pad: float, angle: float) -> None: - # With the base radii corrected above, flip matplotlib's pad signs - # so the clearance pushes label1 further inward and label2 further - # outward -- toward the correct side of each now-repaired label. - padx = pad * np.cos(angle) / 72 - pady = pad * np.sin(angle) / 72 - self._text1_translate._t = (-padx, -pady) # type: ignore - self._text1_translate.invalidate() # type: ignore - self._text2_translate._t = (padx, pady) # type: ignore - self._text2_translate.invalidate() # type: ignore - - -class p9ThetaAxis(ThetaAxis): - """ - ThetaAxis that builds ticks whose labels follow the tick marks - """ - - _tick_class = p9ThetaTick - - class p9RadialAxes(PolarAxes): """ Polar axes that keeps the axis tick labels above the geom layers @@ -72,13 +34,13 @@ def __init__(self, *args, **kwargs): def _init_axis(self) -> None: """ - Build the panel's axes, using the theta axis with corrected labels + Polar panel axes with plotnine's theta and radial tick geometry """ - # `super()` creates the r-axis and registers it with the polar/inner - # spines. The theta axis carries no spine registration, so replacing - # it afterwards leaves nothing stale; the stock ThetaAxis is discarded. - super()._init_axis() # type: ignore self.xaxis = p9ThetaAxis(self, clear=False) + self.yaxis = p9RadialAxis(self, clear=False) + self.spines["polar"].register_axis(self.yaxis) + if (inner_spine := self.spines.get("inner")) is not None: + inner_spine.register_axis(self.yaxis) @property def axis_at_side(self) -> dict[PolarSide, ThetaAxis | RadialAxis]: @@ -100,6 +62,9 @@ def draw(self, renderer: RendererBase) -> None: When `panel_ontop=True` everything is already above the geoms, so the re-draw is a visual no-op. """ + for tick in (*self.raxis.majorTicks, *self.raxis.minorTicks): + tick.update_position(tick.get_loc()) + super().draw(renderer) for axis in (self.raxis, self.thetaaxis): @@ -112,8 +77,8 @@ def thetaaxis(self) -> ThetaAxis: return cast("ThetaAxis", self.xaxis) @property - def raxis(self) -> RadialAxis: - return cast("RadialAxis", self.yaxis) + def raxis(self) -> p9RadialAxis: + return cast("p9RadialAxis", self.yaxis) def set_spine_visible(self, name: str, visible: bool) -> None: """ diff --git a/plotnine/_mpl/_radial_axis.py b/plotnine/_mpl/_radial_axis.py new file mode 100644 index 0000000000..93a3279675 --- /dev/null +++ b/plotnine/_mpl/_radial_axis.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +import numpy as np +from matplotlib import markers as mmarkers +from matplotlib.projections.polar import ( + PolarAxes, + RadialAxis, + RadialTick, + ThetaAxis, + ThetaTick, +) +from matplotlib.transforms import Affine2D + +if TYPE_CHECKING: + from matplotlib.transforms import Transform + + +class p9ThetaTick(ThetaTick): + """ + Theta tick whose labels sit on the rim of their matching tick marks + """ + + def _get_text1_transform(self) -> tuple[Transform, str, str]: + # matplotlib anchors the theta labels through a flipr-reversed + # transform, sending label1 to tick2line's radius and label2 to + # tick1line's. Use the unflipped tick transform so label1 shares + # tick1line's inner radius instead. + return self.axes.get_xaxis_transform("tick1"), "center", "center" + + def _get_text2_transform(self) -> tuple[Transform, str, str]: + # Counterpart to _get_text1_transform: label2 shares tick2line's + # outer radius. + return self.axes.get_xaxis_transform("tick2"), "center", "center" + + def _update_padding(self, pad: float, angle: float) -> None: + # With the base radii corrected above, flip matplotlib's pad signs + # so the clearance pushes label1 further inward and label2 further + # outward -- toward the correct side of each now-repaired label. + padx = pad * np.cos(angle) / 72 + pady = pad * np.sin(angle) / 72 + self._text1_translate._t = (-padx, -pady) # pyright: ignore[reportAttributeAccessIssue] + self._text1_translate.invalidate() # pyright: ignore[reportAttributeAccessIssue] + self._text2_translate._t = (padx, pady) # pyright: ignore[reportAttributeAccessIssue] + self._text2_translate.invalidate() # pyright: ignore[reportAttributeAccessIssue] + + +class p9ThetaAxis(ThetaAxis): + """ + Theta axis whose labels follow their matching tick marks + """ + + _tick_class = p9ThetaTick + + +class p9RadialTick(RadialTick): + """ + Radial tick whose mark and label share the start-spoke sweep side + """ + + def update_position(self, loc: float) -> None: + # Matplotlib already handles the two radial axes at the ends of a + # partial arc. For a full circle it places the labels at + # `rlabel_position`, but leaves the tick marker unrotated. Convert + # that position to its rendered screen angle, then turn the marker + # perpendicular to the spoke, opposite the effective theta sweep. + super().update_position(loc) + + axes = cast("PolarAxes", self.axes) + thetamin = axes.get_thetamin() + thetamax = axes.get_thetamax() + if abs(abs(thetamax - thetamin) - 360.0) >= 1e-12: + return + + direction = axes.get_theta_direction() + offset = np.rad2deg(axes.get_theta_offset()) + spoke_angle = (axes.get_rlabel_position() * direction + offset) % 360 + marker_angle = np.deg2rad(spoke_angle - direction * 90) + + # Replace the marker's base transform so its one-sided tick points + # along the computed tangent. `MarkerStyle.transformed()` would + # compose with the existing TICKLEFT/TICKRIGHT transform instead. + marker = self.tick1line.get_marker() + if marker == mmarkers.TICKLEFT: + transform = Affine2D().rotate(marker_angle) + elif marker == mmarkers.TICKRIGHT: + transform = Affine2D().scale(-1, 1).rotate(marker_angle) + elif marker == "_": + transform = Affine2D().rotate(marker_angle + np.pi / 2) + else: + transform = self.tick1line._marker._transform # pyright: ignore[reportAttributeAccessIssue] + self.tick1line._marker._transform = transform # pyright: ignore[reportAttributeAccessIssue] + + mode, _ = self._labelrotation # pyright: ignore[reportAttributeAccessIssue] + angle = spoke_angle - 90 + ha, va = self._determine_anchor(mode, angle, direction > 0) # pyright: ignore[reportAttributeAccessIssue] + self.label1.set_horizontalalignment(ha) + self.label1.set_verticalalignment(va) + + +class p9RadialAxis(RadialAxis): + """ + Radial axis whose ticks follow plotnine's start-spoke geometry + """ + + _tick_class = p9RadialTick + + def _copy_tick_props(self, src: p9RadialTick, dest: p9RadialTick) -> None: + super()._copy_tick_props(src, dest) # pyright: ignore[reportAttributeAccessIssue] + dest.update_position(dest.get_loc()) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 4608f1d817..d605d85aaf 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -396,12 +396,10 @@ def setup_ax( ) radial_ax.set_rorigin(r_origin) - # Radial axis label placement. RadialTick.update_position (mpl) - # reads rlabel_position only for a full circle; a partial arc - # ignores it and always uses thetamin/thetamax directly, so this - # only matters here. + # Full-circle r ticks and labels share the start spoke. Partial arcs + # use matplotlib's distinct start/end radial axes. if self.end is None: - radial_ax.set_rlabel_position(270) + radial_ax.set_rlabel_position(np.degrees(self.start)) ax.tick_params(axis="x", which="major", direction="out") if (angle := self._theta_guide_angle()) is not None: diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 0606fa48ed..5ed414d91d 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1,4 +1,5 @@ from dataclasses import replace +from typing import Literal import numpy as np import pandas as pd @@ -69,6 +70,54 @@ def trained_scales( return scale_x, scale_y +def radial_axis_sides( + start: float, + *, + direction: Literal[-1, 1] = 1, +) -> tuple[float, float]: + """ + Signed start-spoke sides occupied by the first r tick and label + """ + p = ( + ggplot(mtcars, aes("factor(cyl)", "mpg", fill="factor(cyl)")) + + geom_col() + + coord_radial( + start=start, + direction=direction, + inner_radius=0.1, + ) + + theme(axis_line_y=element_line()) + ) + p.draw_test() + ax = p.axs[0] + ax.figure.draw_without_rendering() + tick = next( + t + for t in ax.yaxis.get_major_ticks() + if t.get_loc() > 0 and t.label1.get_text() + ) + radius = tick.get_loc() + spoke_point = ax.transData.transform((start, radius)) + centre = ax.transData.transform((start, 0)) + label_box = tick.label1.get_window_extent( + renderer=ax.figure._get_renderer() # pyright: ignore + ) + label_point = np.asarray(label_box.get_points()).mean(axis=0) + spoke = spoke_point - centre + label_offset = label_point - spoke_point + label_side = spoke[0] * label_offset[1] - spoke[1] * label_offset[0] + + marker = tick.tick1line._marker # pyright: ignore[reportPrivateUsage] + marker_vertices = ( + marker.get_path().transformed(marker.get_transform()).vertices + ) + tick_offset = marker_vertices[ + np.argmax(np.linalg.norm(marker_vertices, axis=1)) + ] + tick_side = spoke[0] * tick_offset[1] - spoke[1] * tick_offset[0] + return float(tick_side), float(label_side) + + def test_coord_radial_setup_panel_params_theta_x(): scale_x, scale_y = trained_scales( x_breaks=(0, 5, 10), @@ -598,13 +647,44 @@ def test_coord_radial_scale_y_position_right_keeps_r_start(): assert axis_at(ax, "r_end") is None -def test_p9_theta_axis_builds_swapped_tick(): - from plotnine._mpl._polar_axes import p9ThetaAxis, p9ThetaTick +def test_p9_radial_axes_build_plotnine_ticks(): + from plotnine._mpl._radial_axis import ( + p9RadialAxis, + p9RadialTick, + p9ThetaAxis, + p9ThetaTick, + ) - # The theta axis builds ticks whose labels follow the tick marks: - # label1 inner with tick1line, label2 outer with tick2line -- unlike - # matplotlib's ThetaTick, which pads the labels the opposite way. assert p9ThetaAxis._tick_class is p9ThetaTick + assert p9RadialAxis._tick_class is p9RadialTick + + p = ggplot(mtcars, aes("disp", "mpg")) + geom_point() + coord_radial() + p.draw_test() + assert isinstance(p.axs[0].yaxis, p9RadialAxis) + + +def test_coord_radial_r_axis_uses_pre_sweep_side(): + tick_side, label_side = radial_axis_sides(0) + + assert tick_side > 0 + assert label_side > 0 + + +def test_coord_radial_r_axis_rotates_with_start(): + tick_side, label_side = radial_axis_sides(np.pi / 2) + + assert tick_side > 0 + assert label_side > 0 + + +def test_coord_radial_r_axis_flips_with_effective_sweep(): + clockwise_tick, clockwise_label = radial_axis_sides(0) + counter_tick, counter_label = radial_axis_sides(0, direction=-1) + + assert clockwise_tick > 0 + assert clockwise_label > 0 + assert counter_tick < 0 + assert counter_label < 0 def test_coord_polar_is_alias_of_coord_radial(): From ad6d132f48b9db5042bc892af5edc3ee075361a7 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 17 Jul 2026 16:46:19 +0300 Subject: [PATCH 067/126] fix(coord_radial): align radial axis text --- plotnine/_mpl/_radial_axis.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/plotnine/_mpl/_radial_axis.py b/plotnine/_mpl/_radial_axis.py index 93a3279675..9a97e44d7d 100644 --- a/plotnine/_mpl/_radial_axis.py +++ b/plotnine/_mpl/_radial_axis.py @@ -11,7 +11,7 @@ ThetaAxis, ThetaTick, ) -from matplotlib.transforms import Affine2D +from matplotlib.transforms import Affine2D, ScaledTranslation if TYPE_CHECKING: from matplotlib.transforms import Transform @@ -92,9 +92,29 @@ def update_position(self, loc: float) -> None: transform = self.tick1line._marker._transform # pyright: ignore[reportAttributeAccessIssue] self.tick1line._marker._transform = transform # pyright: ignore[reportAttributeAccessIssue] + # A constant theta offset fans labels farther from the spoke as the + # radius grows. Keep every label on the spoke and apply its padding + # in display space so their edges remain aligned. mode, _ = self._labelrotation # pyright: ignore[reportAttributeAccessIssue] angle = spoke_angle - 90 ha, va = self._determine_anchor(mode, angle, direction > 0) # pyright: ignore[reportAttributeAccessIssue] + # self._pad includes the tick length and the gap between + # the tick and text. + shift = self._pad # pyright: ignore[reportAttributeAccessIssue] + text_transform = ( + # Place the text flush with the spoke. For a full circle, + # matplotlib ignores the pad passed to this method. + axes.get_yaxis_text1_transform(0)[0] + # Move the text by a fixed physical distance independent of the + # data radius. + + ScaledTranslation( + np.cos(marker_angle) * shift / 72, + np.sin(marker_angle) * shift / 72, + axes.figure.dpi_scale_trans, + ) + ) + self.label1.set_x(0) + self.label1.set_transform(text_transform) self.label1.set_horizontalalignment(ha) self.label1.set_verticalalignment(va) From 0de57c3f5d00ac880872e6d046d9bf0fe9870120 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 17 Jul 2026 17:25:36 +0300 Subject: [PATCH 068/126] fix(coord_radial): hide unoccupied radial axis line --- plotnine/_mpl/_radial_axes.py | 2 ++ plotnine/themes/themeable.py | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index 25657cd2a8..8c4569af98 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -31,6 +31,8 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.set_theta_zero_location("N") # 12 o'clock + self.set_spine_visible("start", False) + self.set_spine_visible("end", False) def _init_axis(self) -> None: """ diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index 6059cb272d..7a4b31a0c7 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -1550,6 +1550,8 @@ class axis_line_r_start(themeable): def apply_ax(self, ax: Axes): super().apply_ax(ax) + if axis_at(ax, "r_start") is None: + return _style_polar_axis_line(self, ax, "start") def blank_ax(self, ax: Axes): @@ -1564,6 +1566,8 @@ class axis_line_r_end(themeable): def apply_ax(self, ax: Axes): super().apply_ax(ax) + if axis_at(ax, "r_end") is None: + return _style_polar_axis_line(self, ax, "end") def blank_ax(self, ax: Axes): From 5852f1bb5bf7d0cb7a62bda8e83e313f50879d48 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 17 Jul 2026 19:12:30 +0300 Subject: [PATCH 069/126] fix(coord_radial): keep radial axis at start --- plotnine/coords/coord_radial.py | 79 ++++++++++++++++----------------- tests/test_coord_radial.py | 76 +++++++++++++++++++++++++++++-- 2 files changed, 111 insertions(+), 44 deletions(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index d605d85aaf..51aefe80df 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -1,6 +1,7 @@ from __future__ import annotations from dataclasses import replace +from functools import cached_property from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast from warnings import warn @@ -41,10 +42,11 @@ class coord_radial(coord): Default 0. end : Ending angle in radians, measured clockwise from 12 o'clock. - `None` (default) gives a full circle (`start + 2π * direction`). + Equivalent angles are interpreted as the same endpoint. `None` + (default) gives a full circle. direction : Angular rotation sense: `1` = clockwise (default), `-1` = - counter-clockwise. Applied regardless of *end*, so it also sets + counter-clockwise. Applied regardless of `end`, so it also sets the sweep direction of a partial arc. expand : Add a small buffer around the data on the radius axis. @@ -145,15 +147,14 @@ def setup_panel_params( ).setup_panel_params(scale_x, scale_y) theta, r = self._flip(cartesian_view.x, cartesian_view.y) theta.breaks = cast("list[float]", theta.breaks) + arc_range = self._arc_range - # theta axis: [start, start+2π] display span so bars rotated by a - # non-zero start stay within range. Data ticks (original units) are - # converted to radian positions and applied at the end of this method. - _start, _end = self.start, self.start + 2 * np.pi + # The display range keeps its requested order; a full circle ends + # one turn after start. Data ticks are converted to radians below. x = replace( theta, - limits=(_start, _end), - range=(_start, _end), + limits=arc_range, + range=arc_range, breaks=self._to_radians(theta.breaks, theta.range), minor_breaks=np.asarray( self._to_radians(theta.minor_breaks, theta.range) @@ -162,23 +163,13 @@ def setup_panel_params( ) y = replace(r) - # Compute arc bounds for partial-arc plots (None means full circle). - arc_lo = arc_hi = None + # Sorted partial-arc bounds are only for clipping breaks. if self.end is not None: - arc = self._arc - arc_lo = min(self.start, self.start + arc) - arc_hi = max(self.start, self.start + arc) - - # Partial arc: x panel range must match [arc_lo, arc_hi] so that - # coord.setup_ax calls ax.set_xlim(arc_lo, arc_hi) rather than - # ax.set_xlim(0, 2π), which would override set_thetalim. - if arc_lo is not None and arc_hi is not None: + arc_lo, arc_hi = sorted(arc_range) x_breaks = cast("list[float]", x.breaks) keep = [arc_lo <= value <= arc_hi for value in x_breaks] x = replace( x, - limits=(arc_lo, arc_hi), - range=(arc_lo, arc_hi), breaks=[ value for value, include in zip(x_breaks, keep) if include ], @@ -214,29 +205,37 @@ def labels(self, cur_labels: labels_view) -> labels_view: return flip_labels(labels) - @property - def _arc(self) -> float: + @cached_property + def _arc_range(self) -> tuple[float, float]: """ - Total angular span of the plotted region, in radians + Forward angular limits of the displayed arc - For a partial radial plot this is the distance between `start` and - `end`; otherwise it is a full turn. The sense of rotation - (clockwise vs counter-clockwise) is a PolarAxes property applied at - draw time and is not encoded in this magnitude. + Equivalent endpoints select the same clockwise arc. An explicit + non-zero whole turn remains a full circle, while equal endpoints + remain a zero-width arc. """ - if self.end is not None: - return self.end - self.start - return 2.0 * np.pi + turn = 2 * np.pi + if self.end is None: + return (self.start, self.start + turn) + + delta = self.end - self.start + if delta == 0: + return (self.start, self.start) + + span = delta % turn + if span == 0: + span = turn + return (self.start, self.start + span) @property def _mpl_direction(self) -> Literal[-1, 1]: """ Matplotlib theta direction for this coordinate system - `-1` draws clockwise and `+1` counter-clockwise, the opposite of - plotnine's own `direction` convention. `reverse="theta"`/ - `"thetar"` runs the angular axis the other way by folding into the - PolarAxes direction here, rather than into the radian mapping. + For matplotlib -1 is clockwise and +1 is counter-clockwise, the + opposite of plotnine's own `direction` convention. + Also, `reverse="theta"`/ `"thetar"` run the angular axis the other + way. """ reverse = -1 if self.reverse in ("theta", "thetar") else 1 return self.direction * reverse * -1 @@ -247,10 +246,13 @@ def _to_radians( """Normalize theta values to [start, start + arc]""" lo, hi = theta_range span = hi - lo + if span == 0: return [0] * len(vals) + + arc_start, arc_end = self._arc_range norm = (np.asarray(vals) - lo) / span - return list(self.start + norm * self._arc) + return list(arc_start + norm * (arc_end - arc_start)) def transform( self, @@ -367,14 +369,9 @@ def setup_ax( # tick resets from replacing their styling with the default look. radial_ax.lock_raxis_tick_style() - arc = self._arc - # Restrict visible theta range for partial arcs. if self.end is not None: - radial_ax.set_thetalim( - min(self.start, self.start + arc), - max(self.start, self.start + arc), - ) + radial_ax.set_thetalim(*self._arc_range) # Inner radius: push the data away from the centre by setting a # virtual r-origin below r_min. Formula: solve diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 5ed414d91d..dc4d3bb164 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -3,6 +3,7 @@ import numpy as np import pandas as pd +import pytest from numpy.testing import assert_allclose from plotnine import ( @@ -370,9 +371,41 @@ def test_coord_radial_default_theme_does_not_crash(): p.draw_test() -def test_coord_radial_arc_uses_end_or_full_turn(): - assert coord_radial(start=1, end=4)._arc == 3 - assert coord_radial()._arc == 2 * np.pi +def test_coord_radial_arc_range_normalizes_end_forward(): + assert coord_radial(start=1, end=4)._arc_range == (1, 4) + assert_allclose( + coord_radial(start=np.pi, end=0)._arc_range, + (np.pi, 2 * np.pi), + ) + assert_allclose( + coord_radial(start=np.pi, end=2 * np.pi)._arc_range, + (np.pi, 2 * np.pi), + ) + assert_allclose( + coord_radial(start=0, end=2 * np.pi)._arc_range, + (0, 2 * np.pi), + ) + assert coord_radial(start=1, end=1)._arc_range == (1, 1) + assert coord_radial(start=1)._arc_range == (1, 1 + 2 * np.pi) + + +@pytest.mark.parametrize("direction", [1, -1]) +def test_coord_radial_equivalent_endpoints_have_same_sweep( + direction: Literal[-1, 1], +): + pi = 3.14159 + coords = [ + coord_radial(start=pi, end=end, direction=direction) + for end in (0, 2 * pi) + ] + angles = [coord._to_radians([1, 2, 3], (0.4, 3.6)) for coord in coords] + + assert_allclose(angles[0], angles[1], rtol=1e-5) + assert all(np.all(np.diff(values) > 0) for values in angles) + assert [coord._mpl_direction for coord in coords] == [ + -direction, + -direction, + ] def test_coord_radial_no_longer_has_r_axis_inside(): @@ -687,6 +720,43 @@ def test_coord_radial_r_axis_flips_with_effective_sweep(): assert counter_label < 0 +@pytest.mark.parametrize( + ("start", "end", "expected_end"), + [(0, np.pi, np.pi), (np.pi, 0, 2 * np.pi)], +) +def test_coord_radial_partial_r_axis_uses_start( + start: float, + end: float, + expected_end: float, +): + p = ( + ggplot(mtcars, aes("factor(cyl)", "mpg")) + + geom_col() + + coord_radial(start=start, end=end, inner_radius=0.1) + + theme(axis_line_y=element_line()) + ) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] + ax.figure.draw_without_rendering() # pyright: ignore[reportAttributeAccessIssue] + tick = next( + tick + for tick in ax.yaxis.get_major_ticks() + if tick.get_loc() > 0 and tick.label1.get_text() + ) + radius = tick.get_loc() + tick_point = tick.tick1line.get_transform().transform((0, radius)) + expected = ax.transData.transform((start, radius)) + + assert_allclose(ax.get_xlim(), (start, expected_end)) + assert_allclose(tick_point, expected) + assert tick.tick1line.get_visible() + assert tick.label1.get_visible() + assert not tick.tick2line.get_visible() + assert not tick.label2.get_visible() + assert ax.spines["start"].get_visible() + assert not ax.spines["end"].get_visible() + + def test_coord_polar_is_alias_of_coord_radial(): # coord_polar is superseded; it is exported only as an alias so old # code keeps working. It IS coord_radial with the same signature. From 9699665d5658dc850d63d0c8b63d7147dfb50e91 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Sat, 18 Jul 2026 00:41:59 +0300 Subject: [PATCH 070/126] fix(coord_radial): align explicit full-circle axes Classify full circles from canonical arc geometry so explicit and implicit full turns share theta limits, radial tick placement, and labels. --- plotnine/coords/coord_radial.py | 14 +++++-- tests/test_coord_radial.py | 66 +++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 51aefe80df..3d3a5b72a9 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -163,8 +163,8 @@ def setup_panel_params( ) y = replace(r) - # Sorted partial-arc bounds are only for clipping breaks. - if self.end is not None: + # Partial arcs keep only breaks inside their visible bounds. + if not self._is_full_circle: arc_lo, arc_hi = sorted(arc_range) x_breaks = cast("list[float]", x.breaks) keep = [arc_lo <= value <= arc_hi for value in x_breaks] @@ -227,6 +227,12 @@ def _arc_range(self) -> tuple[float, float]: span = turn return (self.start, self.start + span) + @cached_property + def _is_full_circle(self) -> bool: + """True when the displayed arc covers one complete turn""" + start, end = self._arc_range + return bool(np.isclose(end - start, 2 * np.pi)) + @property def _mpl_direction(self) -> Literal[-1, 1]: """ @@ -370,7 +376,7 @@ def setup_ax( radial_ax.lock_raxis_tick_style() # Restrict visible theta range for partial arcs. - if self.end is not None: + if not self._is_full_circle: radial_ax.set_thetalim(*self._arc_range) # Inner radius: push the data away from the centre by setting a @@ -395,7 +401,7 @@ def setup_ax( # Full-circle r ticks and labels share the start spoke. Partial arcs # use matplotlib's distinct start/end radial axes. - if self.end is None: + if self._is_full_circle: radial_ax.set_rlabel_position(np.degrees(self.start)) ax.tick_params(axis="x", which="major", direction="out") diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index dc4d3bb164..f2feba1fda 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -389,6 +389,72 @@ def test_coord_radial_arc_range_normalizes_end_forward(): assert coord_radial(start=1)._arc_range == (1, 1 + 2 * np.pi) +def test_coord_radial_classifies_full_circle_from_arc_range(): + assert coord_radial()._is_full_circle + assert coord_radial(start=0, end=2 * np.pi)._is_full_circle + assert coord_radial(start=1, end=1 + 2 * np.pi)._is_full_circle + assert coord_radial( + end=2 * np.pi, + direction=-1, + reverse="theta", + )._is_full_circle + assert not coord_radial(start=0, end=np.pi)._is_full_circle + assert not coord_radial(start=1, end=1)._is_full_circle + + +@pytest.mark.parametrize("end", [None, 2 * np.pi]) +def test_coord_radial_full_circle_r_axis_uses_start( + end: float | None, +): + p = ( + ggplot(mtcars, aes("cyl", "mpg")) + + geom_col() + + coord_radial(start=0, end=end, inner_radius=0.1) + + theme(axis_line_y=element_line()) + ) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] + ax.figure.draw_without_rendering() # pyright: ignore[reportAttributeAccessIssue] + tick = next( + tick + for tick in ax.yaxis.get_major_ticks() + if tick.get_loc() > 0 and tick.label1.get_text() + ) + renderer = ax.figure._get_renderer() # pyright: ignore[reportAttributeAccessIssue] + radius = tick.get_loc() + tick_point = tick.tick1line.get_transform().transform((0, radius)) + label_point = np.asarray( + tick.label1.get_window_extent(renderer=renderer).get_points() + ).mean(axis=0) + start_point = ax.transData.transform((0, radius)) + + assert_allclose(ax.get_xlim(), (0, 2 * np.pi)) + assert_allclose((ax.get_thetamin(), ax.get_thetamax()), (0, 360)) + assert ax.get_rlabel_position() == 0 + assert_allclose(tick_point, start_point) + assert_allclose(label_point[1], start_point[1]) + + +def test_coord_radial_full_circle_panel_params_ignore_end_representation(): + views = [] + for end in (None, 2 * np.pi): + scale_x, scale_y = trained_scales( + x_breaks=(0, 5, 10), + x_labels=("0", "5", "10"), + ) + views.append( + coord_radial(start=0, end=end, expand=False).setup_panel_params( + scale_x, + scale_y, + ) + ) + + assert_allclose(views[0].x.range, views[1].x.range) + assert_allclose(views[0].x.breaks, views[1].x.breaks) + assert_allclose(views[0].x.minor_breaks, views[1].x.minor_breaks) + assert views[0].x.labels == views[1].x.labels + + @pytest.mark.parametrize("direction", [1, -1]) def test_coord_radial_equivalent_endpoints_have_same_sweep( direction: Literal[-1, 1], From 3a009158a51a5bf9f6a491158c9e70d62843066f Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Sat, 18 Jul 2026 02:57:00 +0300 Subject: [PATCH 071/126] fix(coord_radial): equalize theta label clearance Measure theta text margins from rendered label edges instead of Matplotlib's fixed center anchors. Ignore blank tick lengths and use the largest margin edge as the radial gap. --- plotnine/_mpl/_radial_axes.py | 13 +- plotnine/_mpl/_radial_axis.py | 69 ++++++++- plotnine/themes/elements/margin.py | 5 + plotnine/themes/themeable.py | 9 +- tests/test_coord_radial.py | 224 ++++++++++++++++++++++++++++- tests/test_margin.py | 12 ++ 6 files changed, 318 insertions(+), 14 deletions(-) diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index 8c4569af98..1bba7236d3 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -5,7 +5,7 @@ from matplotlib.projections import register_projection from matplotlib.projections.polar import PolarAxes, RadialAxis, ThetaAxis -from ._radial_axis import p9RadialAxis, p9ThetaAxis +from ._radial_axis import p9RadialAxis, p9ThetaAxis, p9ThetaTick if TYPE_CHECKING: from matplotlib.backend_bases import RendererBase @@ -69,8 +69,15 @@ def draw(self, renderer: RendererBase) -> None: super().draw(renderer) - for axis in (self.raxis, self.thetaaxis): - for label in axis.get_ticklabels(): + for label in self.raxis.get_ticklabels(): + if label.get_visible(): + label.draw(renderer) + + for tick in (*self.thetaaxis.majorTicks, *self.thetaaxis.minorTicks): + if not isinstance(tick, p9ThetaTick): + continue + tick._position_labels(renderer) + for label in (tick.label1, tick.label2): if label.get_visible(): label.draw(renderer) diff --git a/plotnine/_mpl/_radial_axis.py b/plotnine/_mpl/_radial_axis.py index 9a97e44d7d..f26786de3d 100644 --- a/plotnine/_mpl/_radial_axis.py +++ b/plotnine/_mpl/_radial_axis.py @@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, cast import numpy as np +from matplotlib import artist as martist from matplotlib import markers as mmarkers from matplotlib.projections.polar import ( PolarAxes, @@ -14,6 +15,7 @@ from matplotlib.transforms import Affine2D, ScaledTranslation if TYPE_CHECKING: + from matplotlib.backend_bases import RendererBase from matplotlib.transforms import Transform @@ -35,16 +37,69 @@ def _get_text2_transform(self) -> tuple[Transform, str, str]: return self.axes.get_xaxis_transform("tick2"), "center", "center" def _update_padding(self, pad: float, angle: float) -> None: - # With the base radii corrected above, flip matplotlib's pad signs - # so the clearance pushes label1 further inward and label2 further - # outward -- toward the correct side of each now-repaired label. - padx = pad * np.cos(angle) / 72 - pady = pad * np.sin(angle) / 72 - self._text1_translate._t = (-padx, -pady) # pyright: ignore[reportAttributeAccessIssue] + # The rendered label extent replaces matplotlib's fixed allowance. + self._radial_angle = angle + self._text1_translate._t = (0, 0) # pyright: ignore[reportAttributeAccessIssue] self._text1_translate.invalidate() # pyright: ignore[reportAttributeAccessIssue] - self._text2_translate._t = (padx, pady) # pyright: ignore[reportAttributeAccessIssue] + self._text2_translate._t = (0, 0) # pyright: ignore[reportAttributeAccessIssue] self._text2_translate.invalidate() # pyright: ignore[reportAttributeAccessIssue] + def _position_labels(self, renderer: RendererBase) -> None: + # Matplotlib pads theta labels from their centres by a fixed amount, + # which leaves angle-dependent gaps at the edges. We override to place + # each nearest edge at the themed margin beyond its tick or the axis + # boundary. + axes = cast("PolarAxes", self.axes) + radial = np.array( + [np.cos(self._radial_angle), np.sin(self._radial_angle)] + ) + + for label, tickline, translate, direction in ( + ( + self.label1, + self.tick1line, + self._text1_translate, # pyright: ignore[reportAttributeAccessIssue] + -1, + ), + ( + self.label2, + self.tick2line, + self._text2_translate, # pyright: ignore[reportAttributeAccessIssue] + 1, + ), + ): + if not label.get_visible() or not label.get_text(): + continue + + label.set_verticalalignment("center") + label.set_verticalalignment("center") + tick_length = ( + self._size # pyright: ignore[reportAttributeAccessIssue] + if tickline.get_visible() + else 0 + ) + base_pad = ( + self._base_pad # pyright: ignore[reportAttributeAccessIssue] + + tick_length + ) + unit = radial * direction + translate._t = tuple(unit * base_pad / 72) + translate.invalidate() + + anchor = label.get_transform().transform(label.get_position()) + corners = label.get_window_extent(renderer).corners() + # Offset the label by its radial extent so the margin starts at + # the nearest text edge, not at the label centre. + edge_offset = np.min((corners - anchor) @ unit) + corrected = base_pad - edge_offset * 72 / axes.figure.dpi + translate._t = tuple(unit * corrected / 72) + translate.invalidate() + + @martist.allow_rasterization + def draw(self, renderer: RendererBase) -> None: + self._position_labels(renderer) + super().draw(renderer) + class p9ThetaAxis(ThetaAxis): """ diff --git a/plotnine/themes/elements/margin.py b/plotnine/themes/elements/margin.py index e53702753e..baddebee2e 100644 --- a/plotnine/themes/elements/margin.py +++ b/plotnine/themes/elements/margin.py @@ -86,6 +86,11 @@ class margin: init=False, default=False, repr=False, compare=False ) + @property + def max(self) -> float: + """Largest edge value in the margin's current unit""" + return max(self.t, self.r, self.b, self.l) + def setup(self, theme: theme, themeable_name: str): """ Setup the margin to be used in the layout diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index 7a4b31a0c7..820232c128 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -542,7 +542,10 @@ def _set_axis_text_margin(themeable, ax, side: Side | PolarSide): return if (axis := axis_at(ax, side)) is None: return - pad = getattr(margin.pt, MARGIN_SIDE[side]) + if side in ("theta_outside", "theta_inside"): + pad = margin.pt.max + else: + pad = getattr(margin.pt, MARGIN_SIDE[side]) axis.set_tick_params(which="major", pad=pad) @@ -1153,7 +1156,7 @@ def apply_ax(self, ax: Axes): if (axis := axis_at(ax, "theta_outside")) is None: return labels = [t.label2 for t in axis.get_major_ticks()] - self.set(labels, self._get_properties(omit=("margin", "va"))) + self.set(labels, self._get_properties(omit=("margin", "ha", "va"))) _set_axis_text_margin(self, ax, "theta_outside") def blank_ax(self, ax: Axes): @@ -1178,7 +1181,7 @@ def apply_ax(self, ax: Axes): if (axis := axis_at(ax, "theta_inside")) is None: return labels = [t.label1 for t in axis.get_major_ticks()] - self.set(labels, self._get_properties(omit=("margin", "va"))) + self.set(labels, self._get_properties(omit=("margin", "ha", "va"))) _set_axis_text_margin(self, ax, "theta_inside") def blank_ax(self, ax: Axes): diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index f2feba1fda..27f9dcfe7f 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1,9 +1,11 @@ from dataclasses import replace -from typing import Literal +from typing import Any, Literal import numpy as np import pandas as pd import pytest +from matplotlib.axes import Axes +from matplotlib.backends.backend_agg import FigureCanvasAgg from numpy.testing import assert_allclose from plotnine import ( @@ -15,6 +17,8 @@ geom_col, geom_point, ggplot, + guide_axis_theta, + guides, theme, ) from plotnine.data import mtcars @@ -24,6 +28,7 @@ scale_position_view, ) from plotnine.scales import scale_x_continuous, scale_y_continuous +from plotnine.themes.elements import margin def _dummy_scale_view() -> scale_position_view: @@ -119,6 +124,223 @@ def radial_axis_sides( return float(tick_side), float(label_side) +def _theta_margin_plot( + text_margin: (margin | dict[Literal["t", "r", "b", "l", "unit"], Any]), + *, + inner_radius: float = 0, + **text_properties: Any, +) -> ggplot: + data = pd.DataFrame({"x": [0, 120, 330], "y": [1, 2, 3]}) + return ( + ggplot(data, aes("x", "y")) + + geom_point() + + scale_x_continuous( + breaks=[0, 120, 330], + limits=(0, 360), + expand=(0, 0), + ) + + coord_radial(inner_radius=inner_radius) + + theme( + axis_text_x=element_text( + margin=text_margin, + **text_properties, + ), + axis_text_y=element_blank(), + axis_ticks_y=element_blank(), + axis_title=element_blank(), + ) + ) + + +@pytest.mark.parametrize( + ("text_margin", "expected"), + [ + ({"t": 2, "r": 4, "b": 1, "l": 3}, 4), + ({"t": -2, "r": 0, "b": 0, "l": 0}, 0), + ({"t": -5, "r": -2, "b": -8, "l": -4}, -2), + (margin(t=1 / 72, r=3 / 72, unit="in"), 3), + ], +) +def test_coord_radial_theta_margin_uses_largest_side( + text_margin: (margin | dict[Literal["t", "r", "b", "l", "unit"], Any]), + expected: float, +): + p = _theta_margin_plot(text_margin) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + + assert p.axs[0].xaxis.get_major_ticks()[0].get_pad() == expected + + +def _theta_label_clearance( + ax: Axes, + text: str, + side: Literal["inside", "outside"] = "outside", +) -> float: + canvas = FigureCanvasAgg(ax.figure) + canvas.draw() + renderer = canvas.get_renderer() + label_name = "label1" if side == "inside" else "label2" + tickline_name = "tick1line" if side == "inside" else "tick2line" + tick = next( + tick + for tick in ax.xaxis.get_major_ticks() + if getattr(tick, label_name).get_text() == text + ) + tick.draw(renderer) + + label = getattr(tick, label_name) + tickline = getattr(tick, tickline_name) + boundary = tickline.get_transform().transform(tickline.get_xydata())[0] + centre = ax.transData.transform((tick.get_loc(), ax.get_rorigin())) + outward = boundary - centre + outward /= np.linalg.norm(outward) + if side == "inside": + outward *= -1 + + tick_length = ( + tick._size # pyright: ignore[reportPrivateUsage] + * ax.figure.dpi + / 72 + if tickline.get_visible() + else 0 + ) + reference = boundary + outward * tick_length + corners = label.get_window_extent(renderer).corners() + return float(np.min((corners - reference) @ outward)) + + +def test_coord_radial_theta_label_clearance_is_uniform(): + p = _theta_margin_plot({}) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] + + assert_allclose( + [ + _theta_label_clearance(ax, "120"), + _theta_label_clearance(ax, "330"), + ], + 0, + atol=0.01, + ) + + +def test_coord_radial_theta_label_ignores_alignment(): + p = _theta_margin_plot({}, ha="left", va="top") + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] + + assert_allclose( + [ + _theta_label_clearance(ax, "120"), + _theta_label_clearance(ax, "330"), + ], + 0, + atol=0.01, + ) + + +@pytest.mark.parametrize( + ("text_margin", "expected"), + [ + ({"t": 2, "r": 4, "b": 1, "l": 3}, 4), + ({"t": -2, "r": 0, "b": 0, "l": 0}, 0), + ({"t": -5, "r": -2, "b": -8, "l": -4}, -2), + ], +) +def test_coord_radial_theta_label_clearance_uses_margin( + text_margin: dict[Literal["t", "r", "b", "l", "unit"], Any], + expected: float, +): + p = _theta_margin_plot(text_margin, size=16) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] + expected_pixels = expected * ax.figure.dpi / 72 + + assert_allclose( + [ + _theta_label_clearance(ax, "120"), + _theta_label_clearance(ax, "330"), + ], + expected_pixels, + atol=0.01, + ) + + +def test_coord_radial_theta_label_clearance_ignores_blank_tick_length(): + p = _theta_margin_plot({}) + theme( + axis_ticks_x=element_blank(), + axis_ticks_length_major_x=20, + ) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] + + assert_allclose( + [ + _theta_label_clearance(ax, "120"), + _theta_label_clearance(ax, "330"), + ], + 0, + atol=0.01, + ) + + +def test_coord_radial_rotated_theta_label_clearance(): + p = _theta_margin_plot({}) + guides(theta=guide_axis_theta(angle=35)) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] + + assert_allclose( + [ + _theta_label_clearance(ax, "120"), + _theta_label_clearance(ax, "330"), + ], + 0, + atol=0.01, + ) + + +def test_coord_radial_theta_label_clearance_does_not_accumulate(): + p = _theta_margin_plot({}) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] + first = _theta_label_clearance(ax, "330") + + ax.figure.draw_without_rendering() # pyright: ignore[reportAttributeAccessIssue] + second = _theta_label_clearance(ax, "330") + ax.figure.set_size_inches(8, 5) + ax.figure.draw_without_rendering() # pyright: ignore[reportAttributeAccessIssue] + resized = _theta_label_clearance(ax, "330") + + assert_allclose([first, second, resized], 0, atol=0.01) + + +def test_coord_radial_inside_theta_label_clearance(): + p = _theta_margin_plot({}, inner_radius=0.3) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] + ax.tick_params( + axis="x", + bottom=True, + labelbottom=True, + top=False, + labeltop=False, + ) + + assert_allclose( + [ + _theta_label_clearance(ax, "120", "inside"), + _theta_label_clearance(ax, "330", "inside"), + ], + 0, + atol=0.01, + ) + + +def test_coord_radial_theta_label_clearance(): + p = _theta_margin_plot({}) + assert p == "coord_radial_theta_label_clearance" + + def test_coord_radial_setup_panel_params_theta_x(): scale_x, scale_y = trained_scales( x_breaks=(0, 5, 10), diff --git a/tests/test_margin.py b/tests/test_margin.py index 472e2907b0..e935fef729 100644 --- a/tests/test_margin.py +++ b/tests/test_margin.py @@ -23,6 +23,18 @@ def test_conversion_before_setup_raises(): m.to("fig") +@pytest.mark.parametrize( + ("m", "expected"), + [ + (margin(t=2, r=4, b=1, l=3), 4), + (margin(t=-2, r=0, b=0, l=0), 0), + (margin(t=-5, r=-2, b=-8, l=-4), -2), + ], +) +def test_max(m: margin, expected: float): + assert m.max == expected + + def test_conversion_same_unit_after_setup_succeeds(): m = setup_margin(margin(t=5, r=5, b=5, l=5, unit="pt")) result = m.to("pt") From 337cca4522559de5bdeb60751255c473cc2ebf0c Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Mon, 20 Jul 2026 19:49:27 +0300 Subject: [PATCH 072/126] fix(coord_radial): trim unused theta label descent --- plotnine/_mpl/_radial_axis.py | 41 ++++++++++++++++++++-- tests/test_coord_radial.py | 66 +++++++++++++++++++++++++++++++++-- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/plotnine/_mpl/_radial_axis.py b/plotnine/_mpl/_radial_axis.py index f26786de3d..f5126743d7 100644 --- a/plotnine/_mpl/_radial_axis.py +++ b/plotnine/_mpl/_radial_axis.py @@ -12,13 +12,17 @@ ThetaAxis, ThetaTick, ) -from matplotlib.transforms import Affine2D, ScaledTranslation +from matplotlib.transforms import Affine2D, Bbox, ScaledTranslation if TYPE_CHECKING: from matplotlib.backend_bases import RendererBase + from matplotlib.text import Text from matplotlib.transforms import Transform +_NON_DESCENDING_NUMERIC_CHARS = frozenset("0123456789.+-\N{MINUS SIGN}eE") + + class p9ThetaTick(ThetaTick): """ Theta tick whose labels sit on the rim of their matching tick marks @@ -44,6 +48,39 @@ def _update_padding(self, pad: float, angle: float) -> None: self._text2_translate._t = (0, 0) # pyright: ignore[reportAttributeAccessIssue] self._text2_translate.invalidate() # pyright: ignore[reportAttributeAccessIssue] + def _label_bounds(self, label: Text, renderer: RendererBase) -> Bbox: + """ + Return label bounds without unused numeric descent + + Matplotlib reserves the font's full descent even when a numeric label + has no descenders. Removing that unused space prevents the visible + gap between theta labels and ticks from varying around the circle. + """ + bbox, parts, _ = label._get_layout(renderer) # pyright: ignore[reportAttributeAccessIssue] + descent = parts[-1][1][2] + x, y = label.get_unitless_position() + x, y = label.get_transform().transform((x, y)) + bbox = bbox.translated(x, y) + + text = label.get_text() + _, ismath = label._preprocess_math(text) # pyright: ignore[reportAttributeAccessIssue] + # Common numeric ticks have no descenders, so exclude Matplotlib's + # unused descent without measuring rendered ink. + if ( + text + and "\n" not in text + and ismath is False + and set(text) <= _NON_DESCENDING_NUMERIC_CHARS + ): + bbox = Bbox.from_extents( + bbox.x0, + bbox.y0 + descent, + bbox.x1, + bbox.y1, + ) + + return bbox + def _position_labels(self, renderer: RendererBase) -> None: # Matplotlib pads theta labels from their centres by a fixed amount, # which leaves angle-dependent gaps at the edges. We override to place @@ -87,7 +124,7 @@ def _position_labels(self, renderer: RendererBase) -> None: translate.invalidate() anchor = label.get_transform().transform(label.get_position()) - corners = label.get_window_extent(renderer).corners() + corners = self._label_bounds(label, renderer).corners() # Offset the label by its radial extent so the margin starts at # the nearest text edge, not at the label centre. edge_offset = np.min((corners - anchor) @ unit) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 27f9dcfe7f..c24f819f4d 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -21,6 +21,7 @@ guides, theme, ) +from plotnine._mpl._radial_axis import p9ThetaTick from plotnine.data import mtcars from plotnine.iapi import ( labels_view, @@ -128,6 +129,7 @@ def _theta_margin_plot( text_margin: (margin | dict[Literal["t", "r", "b", "l", "unit"], Any]), *, inner_radius: float = 0, + labels: tuple[str, str, str] = ("0", "120", "330"), **text_properties: Any, ) -> ggplot: data = pd.DataFrame({"x": [0, 120, 330], "y": [1, 2, 3]}) @@ -136,6 +138,7 @@ def _theta_margin_plot( + geom_point() + scale_x_continuous( breaks=[0, 120, 330], + labels=labels, limits=(0, 360), expand=(0, 0), ) @@ -205,10 +208,54 @@ def _theta_label_clearance( else 0 ) reference = boundary + outward * tick_length - corners = label.get_window_extent(renderer).corners() + assert isinstance(tick, p9ThetaTick) + corners = tick._label_bounds(label, renderer).corners() return float(np.min((corners - reference) @ outward)) +def _theta_label_bounds( + text: str, +) -> tuple[np.ndarray, np.ndarray, float]: + p = _theta_margin_plot({}) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] + canvas = FigureCanvasAgg(ax.figure) + canvas.draw() + renderer = canvas.get_renderer() + tick = ax.xaxis.get_major_ticks()[0] + assert isinstance(tick, p9ThetaTick) + label = tick.label2 + label.set_text(text) + + anchor = label.get_transform().transform(label.get_position()) + logical, parts, _ = label._get_layout(renderer) + descent = parts[-1][1][2] + logical = logical.translated(*anchor) + corrected = tick._label_bounds(label, renderer) + return logical.get_points(), corrected.get_points(), descent + + +@pytest.mark.parametrize( + "text", + ["300", "-3.0", "+3E0", "3e-2", "\N{MINUS SIGN}300"], +) +def test_coord_radial_numeric_theta_label_bounds_remove_descent(text: str): + logical, corrected, descent = _theta_label_bounds(text) + + assert_allclose(corrected[0], logical[0] + [0, descent]) + assert_allclose(corrected[1], logical[1]) + + +@pytest.mark.parametrize( + "text", + ["3g0", "angle", "3\n0", "$300$", "", "٣٠٠"], +) +def test_coord_radial_other_theta_label_bounds_are_unchanged(text: str): + logical, corrected, _ = _theta_label_bounds(text) + + assert_allclose(corrected, logical) + + def test_coord_radial_theta_label_clearance_is_uniform(): p = _theta_margin_plot({}) p.draw_test() # pyright: ignore[reportAttributeAccessIssue] @@ -224,6 +271,21 @@ def test_coord_radial_theta_label_clearance_is_uniform(): ) +def test_coord_radial_theta_label_descent_correction(): + p = _theta_margin_plot({}, labels=("0", "300", "3g0")) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] + + assert_allclose( + [ + _theta_label_clearance(ax, "300"), + _theta_label_clearance(ax, "3g0"), + ], + 0, + atol=0.01, + ) + + def test_coord_radial_theta_label_ignores_alignment(): p = _theta_margin_plot({}, ha="left", va="top") p.draw_test() # pyright: ignore[reportAttributeAccessIssue] @@ -337,7 +399,7 @@ def test_coord_radial_inside_theta_label_clearance(): def test_coord_radial_theta_label_clearance(): - p = _theta_margin_plot({}) + p = _theta_margin_plot({}, labels=("0", "300", "3g0")) assert p == "coord_radial_theta_label_clearance" From b7f2aa6375c091e9d2a082f8d7dafc0b1388755a Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 21 Jul 2026 15:12:31 +0300 Subject: [PATCH 073/126] fix(coord_radial): reverse theta in place, not by mirroring reverse="theta" flipped the physical draw direction while holding the arc region and offset fixed, which mirrored the wedge onto the opposite side instead of running the data the other way around the same arc. Reverse the data along the arc in _to_radians and drop the reverse factor from _mpl_direction so the wedge stays put. Since the data now begins at end, move the radial axis to the end spoke for a partial arc (_r_axis_side); a full circle shares one spoke and stays at the start. --- plotnine/coords/coord_radial.py | 47 ++++++++++++++++++++++++--------- tests/test_coord_radial.py | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 3d3a5b72a9..71f41a82ab 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -72,7 +72,8 @@ class coord_radial(coord): Which axes run in the opposite direction. * `"none"` (default) — neither axis is reversed. - * `"theta"` — the angular axis runs the other way around. + * `"theta"` — the angular axis runs the other way around the + same arc, so the data sweeps from `end` back to `start`. * `"r"` — the radial axis is inverted, so large values sit toward the centre. * `"thetar"` — both the angular and radial axes are reversed. @@ -99,7 +100,7 @@ def __init__( rotate_angle: bool = False, thetalim: tuple[float, float] | None = None, rlim: tuple[float, float] | None = None, - reverse: str = "none", + reverse: Literal["none", "theta", "r", "thetar"] = "none", ) -> None: if reverse not in {"none", "theta", "r", "thetar"}: raise ValueError( @@ -239,12 +240,25 @@ def _mpl_direction(self) -> Literal[-1, 1]: Matplotlib theta direction for this coordinate system For matplotlib -1 is clockwise and +1 is counter-clockwise, the - opposite of plotnine's own `direction` convention. - Also, `reverse="theta"`/ `"thetar"` run the angular axis the other - way. + opposite of plotnine's own `direction` convention. `reverse="theta"` + does not enter here: it reverses the data along the arc (in + `_to_radians`), not the physical sweep, so the wedge stays put. """ - reverse = -1 if self.reverse in ("theta", "thetar") else 1 - return self.direction * reverse * -1 + return self.direction * -1 + + @cached_property + def _r_axis_side(self) -> Literal["r_start", "r_end"]: + """ + Polar side the radial axis occupies + + The radial axis sits where the data begins. `reverse="theta"` runs + the data from `end` back to `start`, so a partial arc moves the axis + to the end spoke. A full circle has a single shared spoke, so it + stays at the start. + """ + if self.reverse in ("theta", "thetar") and not self._is_full_circle: + return "r_end" + return "r_start" def _to_radians( self, vals: FloatArrayLike, theta_range: tuple[float, float] @@ -258,6 +272,10 @@ def _to_radians( arc_start, arc_end = self._arc_range norm = (np.asarray(vals) - lo) / span + # Traverse the same arc the other way, so data runs from + # end back to start. + if self.reverse in ("theta", "thetar"): + norm = 1 - norm return list(arc_start + norm * (arc_end - arc_start)) def transform( @@ -346,9 +364,11 @@ def setup_ax( inner radius, and radial-axis placement using `panel_params` so faceted panels with free scales each get their own radial range. - The primary theta axis always renders on the outside and the primary - r axis always on r_start, independent of `scale.position` (which moves - only the axis title). The fixed choice is recorded on + The primary theta axis always renders on the outside. The primary r + axis sits on the spoke where the data begins (`_r_axis_side`): the + start spoke normally, the end spoke when `reverse="theta"` runs a + partial arc the other way. Neither follows `scale.position`, which + moves only the axis title. The fixed choice is recorded on `p9RadialAxes.axis_at_side` so theming can find it. """ view = cast("radial_panel_view", panel_params) @@ -365,11 +385,14 @@ def setup_ax( self._setup_ticks_labels(ax, view) + # The radial axis is the yaxis; its start spoke is matplotlib's + # "left" tick pair and its end spoke the "right" pair. + r_side = "right" if self._r_axis_side == "r_end" else "left" _activate_axis(ax.xaxis, "top", True) - _activate_axis(ax.yaxis, "left", True) + _activate_axis(ax.yaxis, r_side, True) radial_ax.axis_at_side["theta_outside"] = radial_ax.thetaaxis - radial_ax.axis_at_side["r_start"] = radial_ax.raxis + radial_ax.axis_at_side[self._r_axis_side] = radial_ax.raxis # The theme styles these tick objects later; keep matplotlib's # tick resets from replacing their styling with the default look. diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index c24f819f4d..7f272a9960 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -836,6 +836,28 @@ def test_coord_radial_reverse_r_inverts_radial_range(): assert tuple(pv_rev.y.range) == (10, 0) # reversed: large r toward centre +def test_coord_radial_reverse_theta_keeps_wedge_flips_order(): + # reverse="theta" runs the data the other way around the SAME arc; it + # must not mirror the wedge onto the opposite side. So the mapped theta + # breaks reflect about the arc midpoint (start + end - break), while the + # arc itself and the physical draw direction are unchanged. + scale_x, scale_y = trained_scales() + base = coord_radial(start=np.pi, end=np.pi / 2, expand=False) + rev = coord_radial( + start=np.pi, end=np.pi / 2, expand=False, reverse="theta" + ) + assert base._arc_range == rev._arc_range + assert base._mpl_direction == rev._mpl_direction + + pv_base = base.setup_panel_params(scale_x, scale_y) + pv_rev = rev.setup_panel_params(scale_x, scale_y) + arc_lo, arc_hi = base._arc_range + assert_allclose( + list(pv_rev.x.breaks), + [arc_lo + arc_hi - b for b in pv_base.x.breaks], + ) + + def test_coord_radial_shows_theta_labels_by_default(): scale_x, scale_y = trained_scales() coord = coord_radial(expand=False) # no theta_labels arg @@ -1030,6 +1052,31 @@ def test_coord_radial_scale_y_position_right_keeps_r_start(): assert axis_at(ax, "r_end") is None +def test_coord_radial_reverse_theta_moves_r_axis_to_end(): + # reverse="theta" runs a partial arc from end back to start, so the + # radial axis follows the data to the end spoke; a full circle has a + # single shared spoke and keeps the axis at the start. + from plotnine._mpl.axes import axis_at + + def r_side(**kwargs) -> ggplot: + p = ( + ggplot(mtcars, aes("factor(cyl)", "mpg")) + + geom_col() + + coord_radial(inner_radius=0.1, **kwargs) + + theme(axis_line_y=element_line()) + ) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + return p.axs[0] + + partial = r_side(start=np.pi, end=np.pi / 2, reverse="theta") + assert axis_at(partial, "r_end") is partial.yaxis + assert axis_at(partial, "r_start") is None + + full = r_side(reverse="theta") + assert axis_at(full, "r_start") is full.yaxis + assert axis_at(full, "r_end") is None + + def test_p9_radial_axes_build_plotnine_ticks(): from plotnine._mpl._radial_axis import ( p9RadialAxis, From 6b06e7c98454be54950bd38dca399e83bcf4518e Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 21 Jul 2026 16:07:45 +0300 Subject: [PATCH 074/126] fix(coord_radial): reverse radial geometry Map reversed radial values into the same display coordinates as a reversed scale, including breaks and inner-radius origins. Add behavioral coverage for partial arcs, theta mappings, limits, and double reversal. --- plotnine/coords/coord_radial.py | 32 +++-- tests/test_coord_radial.py | 210 ++++++++++++++++++++++++++++++-- 2 files changed, 224 insertions(+), 18 deletions(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 71f41a82ab..9f5bdeb61c 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -184,14 +184,20 @@ def setup_panel_params( ], ) - # reverse="r"/"thetar": flip the displayed radial range so large r - # values sit toward the centre. set_ylim(hi, lo) inverts the axis. - # r.range stays in natural (lo, hi) order, so setup_ax reads it - # (not y.range) for the inner_radius origin: reversing only y.range - # inverts the display without breaking the hole, and distance() is - # likewise unaffected. + # Keep r in trained scale space. For radial reversal, make y match + # the display space produced by a reversed scale; descending polar + # limits alone do not reverse radial geometry. if self.reverse in ("r", "thetar"): - y = replace(y, range=(y.range[1], y.range[0])) + r_limits = cast("tuple[float, float]", r.limits) + r_breaks = cast("list[float]", r.breaks) + y = replace( + y, + limits=(-r_limits[1], -r_limits[0]), + range=(-r.range[1], -r.range[0]), + breaks=[-value for value in r_breaks], + minor_breaks=-np.asarray(r.minor_breaks, dtype=float), + labels=list(r.labels), + ) return radial_panel_view(x=x, y=y, theta=theta, r=r) @@ -308,6 +314,11 @@ def transform( if "xend" in data and "yend" in data: data["xend"], data["yend"] = data["yend"], data["xend"] + if self.reverse in ("r", "thetar"): + data["y"] = -data["y"] + if "yend" in data: + data["yend"] = -data["yend"] + # After the swap, data["x"] is always theta in radians. if self.rotate_angle and "angle" in data and "x" in data: # Align marks tangentially to their spoke. The PolarAxes places @@ -405,10 +416,9 @@ def setup_ax( # Inner radius: push the data away from the centre by setting a # virtual r-origin below r_min. Formula: solve # inner_radius = (r_lo - r_origin) / (r_hi - r_origin) - # Use the natural r range (always (lo, hi), reflects rlim) rather - # than y.range, which reverse="r"/"thetar" inverts to (hi, lo) and - # would fail the r_lo < r_hi guard, dropping the donut hole. - r_lo, r_hi = view.r.range + # Use the ordered display range so the origin follows ordinary, + # scale-reversed, and coordinate-reversed radial geometry. + r_lo, r_hi = view.y.range if ( self.inner_radius > 0 diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 7f272a9960..1b7edd6ef9 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -19,6 +19,8 @@ ggplot, guide_axis_theta, guides, + scale_x_reverse, + scale_y_reverse, theme, ) from plotnine._mpl._radial_axis import p9ThetaTick @@ -826,14 +828,208 @@ def test_coord_radial_rlim_recomputes_breaks(): assert len(pv.y.labels) == len(breaks) -def test_coord_radial_reverse_r_inverts_radial_range(): +def test_coord_radial_reverse_r_builds_reversed_display_view(): + scale_x, scale_y = trained_scales( + y=(0, 10), + y_breaks=(0, 5, 10), + y_labels=("0", "5", "10"), + ) + panel_params = coord_radial( + reverse="r", + expand=False, + ).setup_panel_params(scale_x, scale_y) + + assert panel_params.r.range == (0, 10) + assert panel_params.r.limits == (0, 10) + assert panel_params.y.range == (-10, 0) + assert panel_params.y.limits == (-10, 0) + assert_allclose(panel_params.y.breaks, [0, -5, -10]) + assert_allclose( + panel_params.y.minor_breaks, + -np.asarray(panel_params.r.minor_breaks), + ) + assert panel_params.y.labels == ["0", "5", "10"] + + +@pytest.mark.parametrize( + ("theta", "data", "expected_y", "expected_yend"), + [ + ( + "x", + pd.DataFrame( + {"x": [2.0], "y": [3.0], "xend": [4.0], "yend": [5.0]} + ), + [-3.0], + [-5.0], + ), + ( + "y", + pd.DataFrame( + {"x": [3.0], "y": [2.0], "xend": [5.0], "yend": [4.0]} + ), + [-3.0], + [-5.0], + ), + ], +) +def test_coord_radial_reverse_r_negates_display_coordinates( + theta: Literal["x", "y"], + data: pd.DataFrame, + expected_y: list[float], + expected_yend: list[float], +): + panel_params = make_panel_view((0, 10), (0, 10)) + result = coord_radial( + theta=theta, + reverse="r", + expand=False, + ).transform(data, panel_params) + + assert_allclose(result["y"], expected_y) + assert_allclose(result["yend"], expected_yend) + + +def test_coord_radial_reverse_r_matches_reversed_scale_geometry(): + base = ggplot(mtcars, aes("factor(cyl)", "mpg")) + geom_col() + plots = [ + base + + coord_radial( + start=np.pi, + end=np.pi / 2, + inner_radius=0.1, + reverse="r", + ), + base + + scale_y_reverse() + + coord_radial( + start=np.pi, + end=np.pi / 2, + inner_radius=0.1, + ), + ] + + for plot in plots: + plot.draw_test() # pyright: ignore[reportAttributeAccessIssue] + + actual, expected = (plot.axs[0] for plot in plots) + assert_allclose(actual.get_ylim(), expected.get_ylim()) + assert_allclose(actual.get_rorigin(), expected.get_rorigin()) + assert len(actual.collections) == len(expected.collections) + for actual_collection, expected_collection in zip( + actual.collections, + expected.collections, + strict=True, + ): + actual_paths = actual_collection.get_paths() + expected_paths = expected_collection.get_paths() + assert len(actual_paths) == len(expected_paths) + for actual_path, expected_path in zip( + actual_paths, + expected_paths, + strict=True, + ): + distances = np.linalg.norm( + actual_path.vertices[:, None, :] + - expected_path.vertices[None, :, :], + axis=2, + ) + assert_allclose(distances.min(axis=0), 0, atol=1e-12) + assert_allclose(distances.min(axis=1), 0, atol=1e-12) + + theta = actual.collections[0].get_paths()[0].vertices[0, 0] + origin = np.asarray( + actual.transData.transform((theta, actual.get_rorigin())) + ) + baseline = np.asarray(actual.transData.transform((theta, 0))) + inward = np.asarray(actual.transData.transform((theta, -22.8))) + assert np.linalg.norm(baseline - origin) > np.linalg.norm(inward - origin) + + +@pytest.mark.parametrize( + ("theta", "reverse", "inner_radius"), + [ + ("x", "r", 0), + ("x", "r", 0.3), + ("x", "thetar", 0.1), + ("y", "r", 0.1), + ], +) +def test_coord_radial_reverse_r_matches_reversed_scale_cases( + theta: Literal["x", "y"], + reverse: Literal["r", "thetar"], + inner_radius: float, +): + data = pd.DataFrame({"x": [1, 2, 3], "y": [2, 5, 8]}) + mapping = aes("x", "y") if theta == "x" else aes("y", "x") + scale = scale_y_reverse() if theta == "x" else scale_x_reverse() + plots = [ + ggplot(data, mapping) + + geom_point() + + coord_radial( + theta=theta, + start=0, + end=np.pi if reverse == "thetar" else None, + inner_radius=inner_radius, + reverse=reverse, + ), + ggplot(data, mapping) + + geom_point() + + scale + + coord_radial( + theta=theta, + start=0, + end=np.pi if reverse == "thetar" else None, + inner_radius=inner_radius, + reverse="theta" if reverse == "thetar" else "none", + ), + ] + + for plot in plots: + plot.draw_test() # pyright: ignore[reportAttributeAccessIssue] + + actual, expected = (plot.axs[0] for plot in plots) + assert_allclose(actual.get_ylim(), expected.get_ylim()) + assert_allclose(actual.get_rorigin(), expected.get_rorigin()) + assert_allclose( + actual.collections[0].get_offsets(), + expected.collections[0].get_offsets(), + ) + + +def test_coord_radial_reverse_r_respects_rlim(): scale_x, scale_y = trained_scales(y=(0, 10)) - base = coord_radial(expand=False) - rev = coord_radial(reverse="r", expand=False) - pv_base = base.setup_panel_params(scale_x, scale_y) - pv_rev = rev.setup_panel_params(scale_x, scale_y) - assert tuple(pv_base.y.range) == (0, 10) - assert tuple(pv_rev.y.range) == (10, 0) # reversed: large r toward centre + panel_params = coord_radial( + reverse="r", + rlim=(2, 8), + expand=False, + ).setup_panel_params(scale_x, scale_y) + + assert panel_params.r.limits == (2, 8) + assert panel_params.r.range == (2, 8) + assert panel_params.y.limits == (-8, -2) + assert panel_params.y.range == (-8, -2) + + +def test_coord_radial_reverse_r_cancels_reversed_scale_geometry(): + data = pd.DataFrame({"x": [1, 2], "y": [2, 8]}) + plots = [ + ggplot(data, aes("x", "y")) + geom_point() + coord_radial(), + ggplot(data, aes("x", "y")) + + geom_point() + + scale_y_reverse() + + coord_radial(reverse="r"), + ] + + for plot in plots: + plot.draw_test() # pyright: ignore[reportAttributeAccessIssue] + + ordinary, double_reversed = (plot.axs[0] for plot in plots) + assert_allclose(ordinary.get_ylim(), double_reversed.get_ylim()) + assert_allclose(ordinary.get_rorigin(), double_reversed.get_rorigin()) + assert_allclose( + ordinary.collections[0].get_offsets(), + double_reversed.collections[0].get_offsets(), + ) def test_coord_radial_reverse_theta_keeps_wedge_flips_order(): From 2b61f33fc300aa492a04f6ab3838fe0267048787 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 22 Jul 2026 15:44:33 +0300 Subject: [PATCH 075/126] feat(coord_radial): render a second r-axis at the end spoke Add `p9RadialAxes.add_sec_raxis()` which lazily creates and returns a `p9RadialEndAxis` (a new `RadialAxis` subclass whose `p9RadialEndTick` ticks hide the start-spoke pair and expose the end-spoke `label2` / `tick2line` already positioned by matplotlib at `thetamax`). The secondary axis registers under `axis_at_side["r_end"]` and its tick labels are redrawn above geoms alongside the primary r-axis labels on every `draw` call. --- plotnine/_mpl/_radial_axes.py | 51 ++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index 1bba7236d3..a6fef9d480 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -2,6 +2,7 @@ from typing import TYPE_CHECKING, cast +from matplotlib import cbook from matplotlib.projections import register_projection from matplotlib.projections.polar import PolarAxes, RadialAxis, ThetaAxis @@ -20,8 +21,16 @@ class p9RadialAxes(PolarAxes): name = "p9radial" + _shared_axes = { + **PolarAxes._shared_axes, # pyright: ignore[reportAttributeAccessIssue] + "sec_r": cbook.Grouper(), + } + _axis_at_side: dict[PolarSide, ThetaAxis | RadialAxis] | None = None + # Secondary r-axis drawn at the end spoke; None until first requested. + _sec_raxis: p9RadialAxis | None = None + # Whether the r-axis ticks already keep their themed styling across # matplotlib's tick resets. Guards `lock_raxis_tick_style` so repeated # calls do not stack wrappers on top of each other. @@ -29,7 +38,7 @@ class p9RadialAxes(PolarAxes): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - + self._sec_raxis = None self.set_theta_zero_location("N") # 12 o'clock self.set_spine_visible("start", False) self.set_spine_visible("end", False) @@ -67,12 +76,24 @@ def draw(self, renderer: RendererBase) -> None: for tick in (*self.raxis.majorTicks, *self.raxis.minorTicks): tick.update_position(tick.get_loc()) + if self._sec_raxis is not None: + for tick in ( + *self._sec_raxis.majorTicks, + *self._sec_raxis.minorTicks, + ): + tick.update_position(tick.get_loc()) + super().draw(renderer) for label in self.raxis.get_ticklabels(): if label.get_visible(): label.draw(renderer) + if self._sec_raxis is not None: + for label in self._sec_raxis.get_ticklabels(): + if label.get_visible(): + label.draw(renderer) + for tick in (*self.thetaaxis.majorTicks, *self.thetaaxis.minorTicks): if not isinstance(tick, p9ThetaTick): continue @@ -89,6 +110,34 @@ def thetaaxis(self) -> ThetaAxis: def raxis(self) -> p9RadialAxis: return cast("p9RadialAxis", self.yaxis) + def add_sec_raxis(self) -> p9RadialAxis: + """ + Return the secondary r-axis drawn at the end spoke, creating it once + + The axis starts with no ticks or labels; the caller sets them via + `set_ticks` / `set_ticklabels`. It is registered in + `axis_at_side["r_end"]` and its tick labels are redrawn above geoms + on every `draw` call, mirroring the primary r-axis behaviour. + + Returns + ------- + : + The secondary radial axis anchored to the end spoke. + """ + if self._sec_raxis is not None: + return self._sec_raxis + + axis = p9RadialAxis(self, clear=True) + # Register so mpl can resolve _get_axis_name(), and add to the + # draw tree. Mirrors the pattern in p9Axes._make_sec_axis. + self._axis_map["sec_r"] = axis # type: ignore[index] + self.add_artist(axis) + axis.set_clip_on(False) + axis.grid(visible=False) + self._sec_raxis = axis + self.axis_at_side["r_end"] = axis + return axis + def set_spine_visible(self, name: str, visible: bool) -> None: """ Set a spine's visibility and block matplotlib's from overwriting it From 96867ad81639eb8e31385464299598ea009d78c4 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 22 Jul 2026 15:49:14 +0300 Subject: [PATCH 076/126] refactor(iapi): allow a polar side on sec_axis_view.position --- plotnine/iapi.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plotnine/iapi.py b/plotnine/iapi.py index d1375bc746..c41c8de504 100644 --- a/plotnine/iapi.py +++ b/plotnine/iapi.py @@ -22,6 +22,7 @@ CoordRange, FloatArrayLike, HorizontalJustification, + PolarSide, ScaledAestheticsName, Side, StripPosition, @@ -60,8 +61,9 @@ class sec_axis_view: labels: Sequence[str] # Title of the secondary axis; None means the primary axis title name: Optional[str] - # The side opposite the primary axis - position: Side + # The side opposite the primary axis (cartesian), or the polar spoke + # a secondary radial axis occupies + position: Side | PolarSide @dataclass From 59c19b134140414f6e4545bdcee52947ada9a38d Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 22 Jul 2026 15:53:37 +0300 Subject: [PATCH 077/126] feat(coord_radial): wire a secondary r-axis from scale sec_axis Read view.r.sec in setup_ax, call add_sec_raxis(), set fixed ticks, activate the opposite spoke side, and show the end spine. Also narrow sec.position to Side in coord._setup_secondary_axis to fix pyright errors introduced by Task 2's Side | PolarSide widening. --- plotnine/coords/coord.py | 9 +++++---- plotnine/coords/coord_radial.py | 10 +++++++++- tests/test_coord_radial.py | 16 +++++++++++++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/plotnine/coords/coord.py b/plotnine/coords/coord.py index 2d01c13d96..474b2104eb 100644 --- a/plotnine/coords/coord.py +++ b/plotnine/coords/coord.py @@ -320,12 +320,13 @@ def _setup_secondary_axis( if (sec := sv.sec) is None: return + position = cast("Side", sec.position) p9ax = cast("p9Axes", ax) - axis = p9ax.add_sec_axis(sec.position) + axis = p9ax.add_sec_axis(position) _set_fixed_ticks(axis, sec.breaks, sec.labels) - _activate_axis(axis, sec.position, present) - p9ax.sides_with_an_axis.add(sec.position) - ax.spines[sec.position].set_visible(True) + _activate_axis(axis, position, present) + p9ax.sides_with_an_axis.add(position) + ax.spines[position].set_visible(True) def draw(self, axs: list) -> None: """ diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 9f5bdeb61c..dd19bdb64b 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -11,7 +11,7 @@ from .._utils.registry import alias from ..exceptions import PlotnineWarning from ..iapi import panel_ranges, radial_panel_view -from .coord import _activate_axis, coord, dist_euclidean +from .coord import _activate_axis, _set_fixed_ticks, coord, dist_euclidean if TYPE_CHECKING: import numpy.typing as npt @@ -405,6 +405,14 @@ def setup_ax( radial_ax.axis_at_side["theta_outside"] = radial_ax.thetaaxis radial_ax.axis_at_side[self._r_axis_side] = radial_ax.raxis + if (sec := view.r.sec) is not None: + sec_raxis = radial_ax.add_sec_raxis() + _set_fixed_ticks(sec_raxis, sec.breaks, sec.labels) + # The secondary axis occupies the spoke opposite the primary. + sec_side = "left" if r_side == "right" else "right" + _activate_axis(sec_raxis, sec_side, True) + radial_ax.set_spine_visible("end", True) + # The theme styles these tick objects later; keep matplotlib's # tick resets from replacing their styling with the default look. radial_ax.lock_raxis_tick_style() diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 1b7edd6ef9..11fae5f9f1 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1,4 +1,5 @@ from dataclasses import replace +from math import pi from typing import Any, Literal import numpy as np @@ -30,7 +31,7 @@ radial_panel_view, scale_position_view, ) -from plotnine.scales import scale_x_continuous, scale_y_continuous +from plotnine.scales import scale_x_continuous, scale_y_continuous, sec_axis from plotnine.themes.elements import margin @@ -1361,3 +1362,16 @@ def test_coord_polar_is_alias_of_coord_radial(): assert coord_polar.__doc__ == ( "alias of [coord_radial](`plotnine.coords.coord_radial.coord_radial`)" ) + + +def test_secondary_r_axis(): + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + scale_y_continuous( + sec_axis=sec_axis(lambda x: x * 0.354006, name="km/L") + ) + + coord_radial(start=-pi / 2, end=pi / 2, inner_radius=0.1) + + theme(axis_line_y=element_line()) + ) + assert p == "secondary_r_axis" From 724451813ee801c33120dd3a663cc7198b55596f Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 22 Jul 2026 15:59:30 +0300 Subject: [PATCH 078/126] test(coord_radial): verify secondary r-axis theming and title sides --- tests/test_coord_radial.py | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 11fae5f9f1..8f89b1d4c5 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1375,3 +1375,45 @@ def test_secondary_r_axis(): + theme(axis_line_y=element_line()) ) assert p == "secondary_r_axis" + + +def test_secondary_r_axis_themed(): + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + scale_y_continuous( + sec_axis=sec_axis(lambda x: x * 0.354006, name="km/L") + ) + + coord_radial(start=-pi / 2, end=pi / 2, inner_radius=0.1) + + theme( + axis_line_y=element_line(), + axis_text_y=element_text(color="blue"), + ) + ) + assert p == "secondary_r_axis_themed" + + +def test_secondary_r_axis_title_sides(): + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + scale_y_continuous( + sec_axis=sec_axis(lambda x: x * 0.354006, name="km/L") + ) + + coord_radial(start=-pi / 2, end=pi / 2, inner_radius=0.1) + ) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + fig = p.axs[0].figure # pyright: ignore[reportAttributeAccessIssue] + fig.draw_without_rendering() # pyright: ignore[reportAttributeAccessIssue] + targets = p.theme.targets + renderer = fig._get_renderer() # pyright: ignore[reportAttributeAccessIssue] + left = targets.axis_title_y_left + right = targets.axis_title_y_right + assert left is not None, "primary r-axis title not found" + assert right is not None, "secondary r-axis title not found" + left_box = left.get_window_extent(renderer=renderer) + right_box = right.get_window_extent(renderer=renderer) + assert right_box.x0 > left_box.x1, ( + f"secondary title (x0={right_box.x0:.1f}) expected right of " + f"primary title (x1={left_box.x1:.1f})" + ) From 7e5e7f056cacf7f28cbb4c2de0b0b19c0475b785 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 22 Jul 2026 16:04:20 +0300 Subject: [PATCH 079/126] test(coord_radial): secondary r-axis with theta='y' --- tests/test_coord_radial.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 8f89b1d4c5..e6973c6b67 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1393,6 +1393,18 @@ def test_secondary_r_axis_themed(): assert p == "secondary_r_axis_themed" +def test_secondary_r_axis_theta_y(): + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + scale_x_continuous( + sec_axis=sec_axis(lambda x: x * 0.354006, name="scaled") + ) + + coord_radial(theta="y", start=-pi / 2, end=pi / 2) + ) + assert p == "secondary_r_axis_theta_y" + + def test_secondary_r_axis_title_sides(): p = ( ggplot(mtcars, aes("wt", "mpg")) From 15d364c575d0dde3b8dce3d165681d399d20cc33 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 22 Jul 2026 16:11:21 +0300 Subject: [PATCH 080/126] fix(coord_radial): correct secondary r-axis spine spoke and drop dead code M1: remove stale type: ignore[index] on _axis_map assignment (pyright now accepts the key without suppression). M2: drop redundant _sec_raxis = None in __init__; the class-level default covers it. M3: derive the secondary spine from sec_side instead of hard-coding "end"; when the primary sits on the end spoke the secondary lands on start, so showing "end" left the secondary's spoke invisible. --- plotnine/_mpl/_radial_axes.py | 3 +-- plotnine/coords/coord_radial.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index a6fef9d480..ad83329a1b 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -38,7 +38,6 @@ class p9RadialAxes(PolarAxes): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._sec_raxis = None self.set_theta_zero_location("N") # 12 o'clock self.set_spine_visible("start", False) self.set_spine_visible("end", False) @@ -130,7 +129,7 @@ def add_sec_raxis(self) -> p9RadialAxis: axis = p9RadialAxis(self, clear=True) # Register so mpl can resolve _get_axis_name(), and add to the # draw tree. Mirrors the pattern in p9Axes._make_sec_axis. - self._axis_map["sec_r"] = axis # type: ignore[index] + self._axis_map["sec_r"] = axis self.add_artist(axis) axis.set_clip_on(False) axis.grid(visible=False) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index dd19bdb64b..f2a74000f9 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -411,7 +411,8 @@ def setup_ax( # The secondary axis occupies the spoke opposite the primary. sec_side = "left" if r_side == "right" else "right" _activate_axis(sec_raxis, sec_side, True) - radial_ax.set_spine_visible("end", True) + sec_spoke = "end" if sec_side == "right" else "start" + radial_ax.set_spine_visible(sec_spoke, True) # The theme styles these tick objects later; keep matplotlib's # tick resets from replacing their styling with the default look. From 325d3a48e54ef7beceb1a4437b06a917c7b96f07 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 23 Jul 2026 16:47:52 +0300 Subject: [PATCH 081/126] fix(coord_radial): register secondary-axis lim_changed callback signal Setting fixed breaks on a secondary axis makes matplotlib expand its view interval, and when the interval actually changes it fires a `f"{axis_name}lim_changed"` callback. A panel's CallbackRegistry only knows the built-in x/y/z signals, so a secondary axis registered as `sec_x`/`sec_y`/`sec_r` raised ValueError on that lookup. This surfaced with `coord_radial(reverse="r")`, whose negated r view interval forces the change; the identical latent bug existed for the cartesian secondary axes. Register the matching signal when each secondary axis is created so the callback resolves to a no-op. --- plotnine/_mpl/_radial_axes.py | 2 ++ plotnine/_mpl/axes.py | 26 ++++++++++++++++++++++++++ tests/test_coord_radial.py | 15 +++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index ad83329a1b..f3752cd165 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -7,6 +7,7 @@ from matplotlib.projections.polar import PolarAxes, RadialAxis, ThetaAxis from ._radial_axis import p9RadialAxis, p9ThetaAxis, p9ThetaTick +from .axes import register_lim_changed_signal if TYPE_CHECKING: from matplotlib.backend_bases import RendererBase @@ -130,6 +131,7 @@ def add_sec_raxis(self) -> p9RadialAxis: # Register so mpl can resolve _get_axis_name(), and add to the # draw tree. Mirrors the pattern in p9Axes._make_sec_axis. self._axis_map["sec_r"] = axis + register_lim_changed_signal(self, "sec_r") self.add_artist(axis) axis.set_clip_on(False) axis.grid(visible=False) diff --git a/plotnine/_mpl/axes.py b/plotnine/_mpl/axes.py index f22f9ff160..8f4ebab75b 100644 --- a/plotnine/_mpl/axes.py +++ b/plotnine/_mpl/axes.py @@ -108,6 +108,7 @@ def add_sec_axis(self, side: Side) -> XAxis | YAxis: def _make_sec_axis(self, cls: type[AxisT]) -> AxisT: axis = cls(self) + register_lim_changed_signal(self, f"sec_{axis.axis_name}") # Add the axis to the draw tree. Plotnine does not clear the panel # after this point because clearing it would detach the secondary axis. self.add_artist(axis) @@ -116,6 +117,31 @@ def _make_sec_axis(self, cls: type[AxisT]) -> AxisT: return axis +def register_lim_changed_signal(ax: Axes, axis_name: str) -> None: + """ + Register the `lim_changed` callback signal on a panel + + Setting fixed ticks on an axis makes matplotlib expand its view + interval, and when that interval actually changes it fires a + `f"{axis._get_axis_name()}lim_changed"` callback. A panel's + `CallbackRegistry` only knows the built-in `x`/`y`/`z` signals, so a + secondary axis registered under a name like `sec_x` or `sec_r` would + raise `ValueError` on that lookup. Adding the matching signal lets the + callback resolve to a no-op (nothing is connected to it). + + Parameters + ---------- + ax : + Panel axes owning the callback registry. + axis_name : + Name the axis is registered under in `ax._axis_map`. + """ + signal = f"{axis_name}lim_changed" + signals = ax.callbacks._signals # pyright: ignore[reportAttributeAccessIssue] + if signals is not None and signal not in signals: + signals.append(signal) + + def axis_at(ax: Axes, side: Side | PolarSide) -> XAxis | YAxis | None: """ Return the axis whose ticks occupy `side`, if any diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index e6973c6b67..58fe681a42 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1393,6 +1393,21 @@ def test_secondary_r_axis_themed(): assert p == "secondary_r_axis_themed" +def test_secondary_r_axis_reverse_r_builds(): + # reverse="r" negates the r view interval, so setting the secondary + # axis breaks changes its view interval and mpl fires a + # `sec_rlim_changed` callback -- the panel must register that signal. + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + scale_y_continuous( + sec_axis=sec_axis(lambda x: x, breaks=[10, 20, 30]) + ) + + coord_radial(reverse="r") + ) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + + def test_secondary_r_axis_theta_y(): p = ( ggplot(mtcars, aes("wt", "mpg")) From 06c93cb85eebf819c124421210593ab65a849c11 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 23 Jul 2026 16:48:10 +0300 Subject: [PATCH 082/126] fix(coord_radial): negate secondary r-axis breaks under reverse="r" reverse="r"/"thetar" builds the reversed radial display frame by negating the primary axis range, limits, and breaks into `view.y`, but the secondary axis was left in the un-negated frame. Its positive breaks then landed outside the data band, dragging `rmax` outward and squeezing the data into the inner half of the disc. Negate `sec.breaks` alongside the primary breaks and read the secondary axis from `view.y.sec` (the display frame the primary axis renders in) rather than `view.r.sec` (trained-scale space). --- plotnine/coords/coord_radial.py | 12 +++++++++++- tests/test_coord_radial.py | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index f2a74000f9..981f4e522f 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -190,6 +190,15 @@ def setup_panel_params( if self.reverse in ("r", "thetar"): r_limits = cast("tuple[float, float]", r.limits) r_breaks = cast("list[float]", r.breaks) + # The secondary axis lives in the same reversed display frame, + # so its breaks are negated alongside the primary ones; leaving + # them positive would place its ticks outside the data band and + # stretch the radial range. + r_sec = r.sec + if r_sec is not None: + r_sec = replace( + r_sec, breaks=[-value for value in r_sec.breaks] + ) y = replace( y, limits=(-r_limits[1], -r_limits[0]), @@ -197,6 +206,7 @@ def setup_panel_params( breaks=[-value for value in r_breaks], minor_breaks=-np.asarray(r.minor_breaks, dtype=float), labels=list(r.labels), + sec=r_sec, ) return radial_panel_view(x=x, y=y, theta=theta, r=r) @@ -405,7 +415,7 @@ def setup_ax( radial_ax.axis_at_side["theta_outside"] = radial_ax.thetaaxis radial_ax.axis_at_side[self._r_axis_side] = radial_ax.raxis - if (sec := view.r.sec) is not None: + if (sec := view.y.sec) is not None: sec_raxis = radial_ax.add_sec_raxis() _set_fixed_ticks(sec_raxis, sec.breaks, sec.labels) # The secondary axis occupies the spoke opposite the primary. diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 58fe681a42..ed0f5409d4 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1408,6 +1408,29 @@ def test_secondary_r_axis_reverse_r_builds(): p.draw_test() # pyright: ignore[reportAttributeAccessIssue] +def test_secondary_r_axis_reverse_r_keeps_radial_range(): + # The secondary breaks must be negated into the same reversed display + # frame as the primary ones; otherwise their positive positions drag + # `rmax` outward and squeeze the data into the inner half of the disc. + def r_range(with_sec): + sec = ( + {"sec_axis": sec_axis(lambda x: x, breaks=[10, 20, 30])} + if with_sec + else {} + ) + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + scale_y_continuous(**sec) + + coord_radial(reverse="r") + ) + p.draw(show=False) + ax = p.axs[0] # pyright: ignore[reportAttributeAccessIssue] + return ax.get_rmin(), ax.get_rmax() + + assert r_range(True) == r_range(False) + + def test_secondary_r_axis_theta_y(): p = ( ggplot(mtcars, aes("wt", "mpg")) From 3b34bfecab0f4398e6ae8e6c91cd347c82c31fa0 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 23 Jul 2026 19:20:00 +0300 Subject: [PATCH 083/126] feat(coord_radial): render the full-circle secondary r-axis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a full circle the primary and secondary r-axes share one labelling spoke. Matplotlib force-hides the second (`label2`/`tick2line`) tick pair for a full circle, so a plain secondary `p9RadialAxis` drew nothing. Add `p9SecondaryRadialAxis`/`p9SecondaryRadialTick` and a signed `_tick_side` (`-1` primary, `+1` secondary) on `p9RadialTick`. The sign selects the tick/label pair — the secondary draws the end pair that `r_end` theming already resolves — and flips the perpendicular so the secondary mark and label mirror the primary across the shared spoke, keeping the two axes clear of each other. --- plotnine/_mpl/_radial_axes.py | 9 ++++- plotnine/_mpl/_radial_axis.py | 72 ++++++++++++++++++++++++++++++----- tests/test_coord_radial.py | 19 +++++++++ 3 files changed, 88 insertions(+), 12 deletions(-) diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index f3752cd165..cb83b9f1e4 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -6,7 +6,12 @@ from matplotlib.projections import register_projection from matplotlib.projections.polar import PolarAxes, RadialAxis, ThetaAxis -from ._radial_axis import p9RadialAxis, p9ThetaAxis, p9ThetaTick +from ._radial_axis import ( + p9RadialAxis, + p9SecondaryRadialAxis, + p9ThetaAxis, + p9ThetaTick, +) from .axes import register_lim_changed_signal if TYPE_CHECKING: @@ -127,7 +132,7 @@ def add_sec_raxis(self) -> p9RadialAxis: if self._sec_raxis is not None: return self._sec_raxis - axis = p9RadialAxis(self, clear=True) + axis = p9SecondaryRadialAxis(self, clear=True) # Register so mpl can resolve _get_axis_name(), and add to the # draw tree. Mirrors the pattern in p9Axes._make_sec_axis. self._axis_map["sec_r"] = axis diff --git a/plotnine/_mpl/_radial_axis.py b/plotnine/_mpl/_radial_axis.py index f5126743d7..ec42957e75 100644 --- a/plotnine/_mpl/_radial_axis.py +++ b/plotnine/_mpl/_radial_axis.py @@ -151,12 +151,29 @@ class p9RadialTick(RadialTick): Radial tick whose mark and label share the start-spoke sweep side """ + # Which side of the shared full-circle spoke this axis's ticks occupy, + # as a signed perpendicular (`-1` / `+1`). The primary and secondary + # r-axes take opposite sides so their marks and labels never overlap. + # The sign also selects the tick/label pair: the positive side draws the + # end pair (`label2`/`tick2line`, the `r_end` side theming uses), the + # negative side the start pair. + _tick_side: int = -1 + def update_position(self, loc: float) -> None: # Matplotlib already handles the two radial axes at the ends of a # partial arc. For a full circle it places the labels at # `rlabel_position`, but leaves the tick marker unrotated. Convert # that position to its rendered screen angle, then turn the marker # perpendicular to the spoke, opposite the effective theta sweep. + + # On a full circle the two tick/label pairs would land on the same + # spoke, so the `super().update_position` below hides the second + # one. But the secondary r-axis draws that pair, on the opposite + # side of the spoke — so save its visibility now and restore it, + # after the super call, in the `_tick_side > 0` branch. + want_label = self.label2.get_visible() + want_tick = self.tick2line.get_visible() + super().update_position(loc) axes = cast("PolarAxes", self.axes) @@ -165,15 +182,28 @@ def update_position(self, loc: float) -> None: if abs(abs(thetamax - thetamin) - 360.0) >= 1e-12: return + if self._tick_side > 0: + label, tickline = self.label2, self.tick2line + text1_transform = axes.get_yaxis_text2_transform(0)[0] + label.set_visible(want_label) + tickline.set_visible(want_tick) + else: + label, tickline = self.label1, self.tick1line + text1_transform = axes.get_yaxis_text1_transform(0)[0] + direction = axes.get_theta_direction() offset = np.rad2deg(axes.get_theta_offset()) spoke_angle = (axes.get_rlabel_position() * direction + offset) % 360 - marker_angle = np.deg2rad(spoke_angle - direction * 90) + # `_tick_side` flips the perpendicular so a secondary axis mirrors + # the primary across the shared spoke. + marker_angle = np.deg2rad( + spoke_angle + self._tick_side * direction * 90 + ) # Replace the marker's base transform so its one-sided tick points # along the computed tangent. `MarkerStyle.transformed()` would # compose with the existing TICKLEFT/TICKRIGHT transform instead. - marker = self.tick1line.get_marker() + marker = tickline.get_marker() if marker == mmarkers.TICKLEFT: transform = Affine2D().rotate(marker_angle) elif marker == mmarkers.TICKRIGHT: @@ -181,22 +211,23 @@ def update_position(self, loc: float) -> None: elif marker == "_": transform = Affine2D().rotate(marker_angle + np.pi / 2) else: - transform = self.tick1line._marker._transform # pyright: ignore[reportAttributeAccessIssue] - self.tick1line._marker._transform = transform # pyright: ignore[reportAttributeAccessIssue] + transform = tickline._marker._transform # pyright: ignore[reportAttributeAccessIssue] + tickline._marker._transform = transform # pyright: ignore[reportAttributeAccessIssue] # A constant theta offset fans labels farther from the spoke as the # radius grows. Keep every label on the spoke and apply its padding # in display space so their edges remain aligned. mode, _ = self._labelrotation # pyright: ignore[reportAttributeAccessIssue] angle = spoke_angle - 90 - ha, va = self._determine_anchor(mode, angle, direction > 0) # pyright: ignore[reportAttributeAccessIssue] + start = (self._tick_side * direction) < 0 + ha, va = self._determine_anchor(mode, angle, start) # pyright: ignore[reportAttributeAccessIssue] # self._pad includes the tick length and the gap between # the tick and text. shift = self._pad # pyright: ignore[reportAttributeAccessIssue] text_transform = ( # Place the text flush with the spoke. For a full circle, # matplotlib ignores the pad passed to this method. - axes.get_yaxis_text1_transform(0)[0] + text1_transform # Move the text by a fixed physical distance independent of the # data radius. + ScaledTranslation( @@ -205,10 +236,23 @@ def update_position(self, loc: float) -> None: axes.figure.dpi_scale_trans, ) ) - self.label1.set_x(0) - self.label1.set_transform(text_transform) - self.label1.set_horizontalalignment(ha) - self.label1.set_verticalalignment(va) + label.set_x(0) + label.set_transform(text_transform) + label.set_horizontalalignment(ha) + label.set_verticalalignment(va) + + +class p9SecondaryRadialTick(p9RadialTick): + """ + Full-circle secondary r-axis tick mirrored across the shared spoke + + The secondary r-axis shares the primary's single full-circle spoke, so + it sits on the opposite side of it. That side draws the end tick/label + pair — the same pair theming resolves for the `r_end` side — keeping its + mark and label clear of the primary's. + """ + + _tick_side = 1 class p9RadialAxis(RadialAxis): @@ -221,3 +265,11 @@ class p9RadialAxis(RadialAxis): def _copy_tick_props(self, src: p9RadialTick, dest: p9RadialTick) -> None: super()._copy_tick_props(src, dest) # pyright: ignore[reportAttributeAccessIssue] dest.update_position(dest.get_loc()) + + +class p9SecondaryRadialAxis(p9RadialAxis): + """ + Secondary radial axis mirrored across the primary's full-circle spoke + """ + + _tick_class = p9SecondaryRadialTick diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index ed0f5409d4..ff90429612 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1431,6 +1431,25 @@ def r_range(with_sec): assert r_range(True) == r_range(False) +def test_secondary_r_axis_full_circle_labels(): + # A full circle shares one labelling spoke between the primary and + # secondary r-axes. Matplotlib forces the second (`label2`) tick pair + # invisible for a full circle, so the secondary axis must render on the + # same spoke as the primary, offset outward, rather than on its own. + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + scale_y_continuous( + sec_axis=sec_axis(lambda x: x * 2, breaks=[10, 20, 30]) + ) + + coord_radial() + ) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] # pyright: ignore[reportAttributeAccessIssue] + labels = [t.get_text() for t in ax._sec_raxis.get_ticklabels()] + assert labels == ["20", "30"] + + def test_secondary_r_axis_theta_y(): p = ( ggplot(mtcars, aes("wt", "mpg")) From 310db847fc3966ba59969af1fe306f1d7a9a212e Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 23 Jul 2026 19:20:12 +0300 Subject: [PATCH 084/126] fix(coord_radial): keep secondary r-axis marks on the spoke and above gridlines Three rendering glitches remained once the full-circle secondary r-axis drew at all: - The end pair's tick line anchors at theta = thetamax (x = 1), which for a full circle fans off the shared spoke as the radius grows. Anchor it at x = 0 so the mark stays on the spoke. - TICKRIGHT's own `scale(-1, 1)` double-flipped the already-directional `marker_angle`, sending the secondary mark back onto the primary's side. Rotate both TICKLEFT/TICKRIGHT by `marker_angle` alone. - Both r-axes' tick marks were painted below the theta gridlines that cross them on the shared spoke. Redraw each axis's marks (not just its labels) after `super().draw()` via a shared `_redraw_raxis` helper. --- plotnine/_mpl/_radial_axes.py | 47 ++++++++++++++++++++++++----------- plotnine/_mpl/_radial_axis.py | 25 +++++++++++++------ tests/test_coord_radial.py | 19 ++++++++++++++ 3 files changed, 69 insertions(+), 22 deletions(-) diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index cb83b9f1e4..f0706e4755 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -55,7 +55,7 @@ def _init_axis(self) -> None: self.xaxis = p9ThetaAxis(self, clear=False) self.yaxis = p9RadialAxis(self, clear=False) self.spines["polar"].register_axis(self.yaxis) - if (inner_spine := self.spines.get("inner")) is not None: + if inner_spine := self.spines.get("inner"): inner_spine.register_axis(self.yaxis) @property @@ -69,19 +69,20 @@ def axis_at_side(self) -> dict[PolarSide, ThetaAxis | RadialAxis]: def draw(self, renderer: RendererBase) -> None: """ - Draw the axes, then lift the tick labels above the geoms + Draw the axes, then lift the r-axis ticks above the geoms With the default `panel_ontop=False` (`axisbelow=True`) the normal - pass paints the gridlines and the tick labels below the geoms. - Re-drawing the radial-axis and theta-axis tick labels keeps them - visible on top of opaque geoms. + pass paints the gridlines, tick marks and tick labels below the + geoms. Re-drawing each r-axis's tick marks and labels keeps them + visible on top of opaque geoms and above the theta gridlines that + cross them on the shared full-circle spoke. When `panel_ontop=True` everything is already above the geoms, so the re-draw is a visual no-op. """ for tick in (*self.raxis.majorTicks, *self.raxis.minorTicks): tick.update_position(tick.get_loc()) - if self._sec_raxis is not None: + if self._sec_raxis: for tick in ( *self._sec_raxis.majorTicks, *self._sec_raxis.minorTicks, @@ -90,14 +91,9 @@ def draw(self, renderer: RendererBase) -> None: super().draw(renderer) - for label in self.raxis.get_ticklabels(): - if label.get_visible(): - label.draw(renderer) - - if self._sec_raxis is not None: - for label in self._sec_raxis.get_ticklabels(): - if label.get_visible(): - label.draw(renderer) + _redraw_raxis(self.raxis, renderer) + if self._sec_raxis: + _redraw_raxis(self._sec_raxis, renderer) for tick in (*self.thetaaxis.majorTicks, *self.thetaaxis.minorTicks): if not isinstance(tick, p9ThetaTick): @@ -129,7 +125,7 @@ def add_sec_raxis(self) -> p9RadialAxis: : The secondary radial axis anchored to the end spoke. """ - if self._sec_raxis is not None: + if self._sec_raxis: return self._sec_raxis axis = p9SecondaryRadialAxis(self, clear=True) @@ -201,4 +197,25 @@ def reset_ticks_and_restyle() -> None: axis.reset_ticks = reset_ticks_and_restyle +def _redraw_raxis(axis: p9RadialAxis, renderer: RendererBase) -> None: + """ + Redraw an r-axis's visible tick marks and labels on top of the geoms + + Each tick's `_tick_side` selects the mark/label pair it occupies: the + primary r-axis draws the start pair (`tick1line`/`label1`), the secondary + the end pair (`tick2line`/`label2`) on the opposite side of the shared + full-circle spoke. Marks draw before labels so a label never sits under + its own mark. + """ + for tick in (*axis.majorTicks, *axis.minorTicks): + if tick._tick_side > 0: # pyright: ignore[reportAttributeAccessIssue] + mark, label = tick.tick2line, tick.label2 + else: + mark, label = tick.tick1line, tick.label1 + if mark.get_visible(): + mark.draw(renderer) + if label.get_visible(): + label.draw(renderer) + + register_projection(p9RadialAxes) diff --git a/plotnine/_mpl/_radial_axis.py b/plotnine/_mpl/_radial_axis.py index ec42957e75..9901a8a21c 100644 --- a/plotnine/_mpl/_radial_axis.py +++ b/plotnine/_mpl/_radial_axis.py @@ -191,6 +191,12 @@ def update_position(self, loc: float) -> None: label, tickline = self.label1, self.tick1line text1_transform = axes.get_yaxis_text1_transform(0)[0] + # The end pair's tick line anchors at theta = thetamax (x = 1), which + # for a full circle fans away from the shared start spoke as the + # radius grows. Anchor it on the spoke (x = 0), where the start pair + # already sits, so the mirrored marker below stays on the spoke. + tickline.set_xdata([0]) + direction = axes.get_theta_direction() offset = np.rad2deg(axes.get_theta_offset()) spoke_angle = (axes.get_rlabel_position() * direction + offset) % 360 @@ -201,18 +207,23 @@ def update_position(self, loc: float) -> None: ) # Replace the marker's base transform so its one-sided tick points - # along the computed tangent. `MarkerStyle.transformed()` would - # compose with the existing TICKLEFT/TICKRIGHT transform instead. + # along `marker_angle`. `MarkerStyle.transformed()` would compose + # with the existing TICKLEFT/TICKRIGHT transform instead. + # + # `marker_angle` already carries the full direction (the primary and + # secondary sides differ by 180°), and TICKLEFT/TICKRIGHT share the + # same `[[0, 0], [1, 0]]` path, so both rotate by it directly. Adding + # TICKRIGHT's own `scale(-1, 1)` here would undo the `_tick_side` + # mirror and send the secondary mark back onto the primary's side. marker = tickline.get_marker() - if marker == mmarkers.TICKLEFT: + tick_marker = cast("mmarkers.MarkerStyle", tickline._marker) # pyright: ignore[reportAttributeAccessIssue] + if marker in (mmarkers.TICKLEFT, mmarkers.TICKRIGHT): transform = Affine2D().rotate(marker_angle) - elif marker == mmarkers.TICKRIGHT: - transform = Affine2D().scale(-1, 1).rotate(marker_angle) elif marker == "_": transform = Affine2D().rotate(marker_angle + np.pi / 2) else: - transform = tickline._marker._transform # pyright: ignore[reportAttributeAccessIssue] - tickline._marker._transform = transform # pyright: ignore[reportAttributeAccessIssue] + transform = tick_marker._transform # pyright: ignore[reportAttributeAccessIssue] + tick_marker._transform = transform # pyright: ignore[reportAttributeAccessIssue] # A constant theta offset fans labels farther from the spoke as the # radius grows. Keep every label on the spoke and apply its padding diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index ff90429612..f2ada32c7a 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1450,6 +1450,25 @@ def test_secondary_r_axis_full_circle_labels(): assert labels == ["20", "30"] +def test_secondary_r_axis_full_circle_marks(): + # On a full circle the primary and secondary r-axes share one spoke. + # The secondary tick marks must anchor on that spoke (not fan off it as + # the radius grows), point to the opposite side of it from the primary, + # and stay above the gridlines they cross. + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + scale_y_continuous(sec_axis=sec_axis(lambda x: x * 2)) + + coord_radial() + + theme( + axis_line_r=element_line(), + axis_ticks_y=element_line(color="red", size=2), + axis_ticks_length_major=20, + ) + ) + assert p == "secondary_r_axis_full_circle_marks" + + def test_secondary_r_axis_theta_y(): p = ( ggplot(mtcars, aes("wt", "mpg")) From efbee99cca8acb81282f7676301de0cc14bc396b Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 23 Jul 2026 20:01:30 +0300 Subject: [PATCH 085/126] fix(coord_radial): move secondary r-axis to the spoke opposite the primary Under reverse="theta" on a partial arc the primary r-axis correctly moves to the end spoke, but the secondary-axis wiring assumed the primary was always on r_start and the secondary always on r_end. add_sec_raxis() hardcoded axis_at_side["r_end"], clobbering the primary registered there, so axis_at("r_start") returned None (its axis_line/ticks/text theming was lost) and both r sides resolved to the secondary. Resolve the secondary's spoke from the primary's (_sec_r_axis_side), let setup_ax register axis_at_side for the side it occupies, and redraw whichever tick pair is visible so the primary's marks stay above geoms wherever it sits. --- plotnine/_mpl/_radial_axes.py | 38 ++++++++++++++++----------------- plotnine/coords/coord_radial.py | 16 ++++++++++++-- tests/test_coord_radial.py | 20 +++++++++++++++++ 3 files changed, 53 insertions(+), 21 deletions(-) diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index f0706e4755..0bdc42e2f8 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -113,17 +113,18 @@ def raxis(self) -> p9RadialAxis: def add_sec_raxis(self) -> p9RadialAxis: """ - Return the secondary r-axis drawn at the end spoke, creating it once + Return the secondary r-axis, creating it once The axis starts with no ticks or labels; the caller sets them via - `set_ticks` / `set_ticklabels`. It is registered in - `axis_at_side["r_end"]` and its tick labels are redrawn above geoms - on every `draw` call, mirroring the primary r-axis behaviour. + `set_ticks` / `set_ticklabels` and registers it in `axis_at_side` + under the spoke it occupies (opposite the primary). Its tick labels + are redrawn above geoms on every `draw` call, mirroring the primary + r-axis behaviour. Returns ------- : - The secondary radial axis anchored to the end spoke. + The secondary radial axis. """ if self._sec_raxis: return self._sec_raxis @@ -137,7 +138,6 @@ def add_sec_raxis(self) -> p9RadialAxis: axis.set_clip_on(False) axis.grid(visible=False) self._sec_raxis = axis - self.axis_at_side["r_end"] = axis return axis def set_spine_visible(self, name: str, visible: bool) -> None: @@ -201,21 +201,21 @@ def _redraw_raxis(axis: p9RadialAxis, renderer: RendererBase) -> None: """ Redraw an r-axis's visible tick marks and labels on top of the geoms - Each tick's `_tick_side` selects the mark/label pair it occupies: the - primary r-axis draws the start pair (`tick1line`/`label1`), the secondary - the end pair (`tick2line`/`label2`) on the opposite side of the shared - full-circle spoke. Marks draw before labels so a label never sits under - its own mark. + Only the pair on the axis's active spoke is visible, so redrawing every + visible mark/label lifts the right one whichever spoke it occupies: the + primary r-axis moves to the end spoke under `reverse="theta"`, swapping + with the secondary. Each mark draws before its label so a label never + sits under its own mark. """ for tick in (*axis.majorTicks, *axis.minorTicks): - if tick._tick_side > 0: # pyright: ignore[reportAttributeAccessIssue] - mark, label = tick.tick2line, tick.label2 - else: - mark, label = tick.tick1line, tick.label1 - if mark.get_visible(): - mark.draw(renderer) - if label.get_visible(): - label.draw(renderer) + for mark, label in ( + (tick.tick1line, tick.label1), + (tick.tick2line, tick.label2), + ): + if mark.get_visible(): + mark.draw(renderer) + if label.get_visible(): + label.draw(renderer) register_projection(p9RadialAxes) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 981f4e522f..f4e2898493 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -276,6 +276,16 @@ def _r_axis_side(self) -> Literal["r_start", "r_end"]: return "r_end" return "r_start" + @cached_property + def _sec_r_axis_side(self) -> Literal["r_start", "r_end"]: + """ + Polar side the secondary radial axis occupies + + The secondary axis sits on the spoke opposite the primary one, so it + follows the primary when `reverse="theta"` moves it to the end spoke. + """ + return "r_start" if self._r_axis_side == "r_end" else "r_end" + def _to_radians( self, vals: FloatArrayLike, theta_range: tuple[float, float] ) -> list[float]: @@ -418,11 +428,13 @@ def setup_ax( if (sec := view.y.sec) is not None: sec_raxis = radial_ax.add_sec_raxis() _set_fixed_ticks(sec_raxis, sec.breaks, sec.labels) - # The secondary axis occupies the spoke opposite the primary. - sec_side = "left" if r_side == "right" else "right" + # The secondary axis occupies the spoke opposite the primary; that + # spoke's tick pair is its "left"/"right" mpl side and its spine. + sec_side = "right" if self._sec_r_axis_side == "r_end" else "left" _activate_axis(sec_raxis, sec_side, True) sec_spoke = "end" if sec_side == "right" else "start" radial_ax.set_spine_visible(sec_spoke, True) + radial_ax.axis_at_side[self._sec_r_axis_side] = sec_raxis # The theme styles these tick objects later; keep matplotlib's # tick resets from replacing their styling with the default look. diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index f2ada32c7a..b6a2a45f7a 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1393,6 +1393,26 @@ def test_secondary_r_axis_themed(): assert p == "secondary_r_axis_themed" +def test_secondary_r_axis_reverse_theta_swaps_spokes(): + # reverse="theta" moves the primary r-axis to the end spoke, so the + # secondary must follow to the opposite (start) spoke rather than stay + # pinned to r_end. Each side must resolve to its own axis so per-side + # theming (axis_line_r_start/r_end, ticks, text) routes correctly. + from plotnine._mpl.axes import axis_at + + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + scale_y_continuous(sec_axis=sec_axis(lambda x: x * 2)) + + coord_radial(start=-pi / 2, end=pi / 2, reverse="theta") + ) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] # pyright: ignore[reportAttributeAccessIssue] + + assert axis_at(ax, "r_end") is ax.raxis + assert axis_at(ax, "r_start") is ax._sec_raxis + + def test_secondary_r_axis_reverse_r_builds(): # reverse="r" negates the r view interval, so setting the secondary # axis breaks changes its view interval and mpl fires a From e086df91bee507d5a0e6baa314ec1d455d9b2d24 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 23 Jul 2026 22:48:14 +0300 Subject: [PATCH 086/126] fix(coord_radial): reposition axis titles when theta="y" theta="y" transposes the x and y panel views, carrying each view's `position` with it, so the titles registered under sides the layout manager never repositions (axis_title_x_left, axis_title_y_bottom) and stayed stranded at the figure origin. Rotate the transposed positions through coord_flip's _FLIP_POSITION so the angular title lands on bottom/top and the radial title on left/right. --- plotnine/coords/coord_radial.py | 18 +++++++++++++++++- tests/test_coord_radial.py | 13 +++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index f4e2898493..435a7a5333 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -20,7 +20,7 @@ from plotnine.iapi import labels_view, layout_details, panel_view from plotnine.scales.scale import scale - from plotnine.typing import FloatArrayLike, FloatSeries + from plotnine.typing import FloatArrayLike, FloatSeries, Side T1 = TypeVar("T1") T2 = TypeVar("T2") @@ -209,6 +209,22 @@ def setup_panel_params( sec=r_sec, ) + # theta="y" transposes the x and y views, so their axis position + # rotates with them (matching coord_flip): the angular title lands + # on x's bottom/top and the radial title on y's left/right, the + # sides the layout manager repositions. theta="x" keeps them as is. + if not self.is_default_axes: + from .coord_flip import _FLIP_POSITION + + x = replace(x, position=_FLIP_POSITION[x.position]) + y = replace(y, position=_FLIP_POSITION[y.position]) + for sv in (x, y): + if sv.sec: + # A secondary theta axis is rejected in setup_ax, so a + # secondary position here is always cartesian. + pos = cast("Side", sv.sec.position) + sv.sec.position = _FLIP_POSITION[pos] + return radial_panel_view(x=x, y=y, theta=theta, r=r) def labels(self, cur_labels: labels_view) -> labels_view: diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index b6a2a45f7a..2335f35227 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -447,6 +447,19 @@ def test_coord_radial_setup_panel_params_theta_y(): assert panel_params.y.breaks == [0, 2, 5, 10] +def test_coord_radial_theta_y_axis_title_positions_stay_valid(): + # theta="y" transposes the x and y panel views. Their `position` + # must stay on the side the layout manager repositions -- x on + # bottom/top, y on left/right -- or the axis titles never move + # from the figure origin. + scale_x, scale_y = trained_scales() + + panel_params = coord_radial(theta="y").setup_panel_params(scale_x, scale_y) + + assert panel_params.x.position in ("bottom", "top") + assert panel_params.y.position in ("left", "right") + + def test_coord_radial_discrete_theta_spans_full_circle(): p = ( ggplot(mtcars, aes("factor(cyl)", "mpg", fill="factor(cyl)")) From be581e2583bf9cda48f63ed96be9598a79136d8c Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 24 Jul 2026 00:33:32 +0300 Subject: [PATCH 087/126] feat(coord_radial): aspect from tight sector bounding box --- plotnine/coords/coord_radial.py | 41 ++++++++++++++++++++++++++++++++- tests/test_coord_radial.py | 25 ++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 435a7a5333..943bad3a9e 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -26,6 +26,42 @@ T2 = TypeVar("T2") +def polar_bbox( + arc: tuple[float, float], inner_radius: float = 0.0 +) -> tuple[float, float, float, float]: + """ + Tight bounding box of a polar sector in unit-circle coordinates + + The box is expressed as `(left, right, bottom, top)` in the `[0, 1]` + space of the full circle, where the centre is `(0.5, 0.5)` and the + outer radius is `0.5`. A cardinal direction (top, right, bottom, left) + that lies inside the arc extends the box to the circle's full extent + there; otherwise the box is clamped to the arc endpoints. `inner_radius` + pulls the inner edge toward the centre for donut sectors. No margin is + added, so the box matches the wedge the panel is sized to fill. + """ + lo, hi = arc + if abs(hi - lo) >= 2 * np.pi: + return (0.0, 1.0, 0.0, 1.0) + + lo, hi = sorted((lo, hi)) + ends = np.array([lo, hi]) + x_outer = 0.5 * np.sin(ends) + 0.5 + y_outer = 0.5 * np.cos(ends) + 0.5 + x_inner = inner_radius * np.sin(ends) + 0.5 + y_inner = inner_radius * np.cos(ends) + 0.5 + + def in_arc(theta: float) -> bool: + folded = (theta - lo) % (2 * np.pi) + lo + return lo <= folded <= hi + + top = 1.0 if in_arc(0.0) else max(y_outer.max(), y_inner.max()) + right = 1.0 if in_arc(np.pi / 2) else max(x_outer.max(), x_inner.max()) + bottom = 0.0 if in_arc(np.pi) else min(y_outer.min(), y_inner.min()) + left = 0.0 if in_arc(1.5 * np.pi) else min(x_outer.min(), x_inner.min()) + return (float(left), float(right), float(bottom), float(top)) + + class coord_radial(coord): """ Radial coordinate system @@ -511,7 +547,10 @@ def _theta_guide_angle(self) -> float | None: return getattr(guide, "angle", None) def aspect(self, panel_params: panel_view) -> float: - return 1.0 + left, right, bottom, top = polar_bbox( + self._arc_range, self.inner_radius + ) + return (top - bottom) / (right - left) @alias diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 2335f35227..6ad95e7daf 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -25,6 +25,7 @@ theme, ) from plotnine._mpl._radial_axis import p9ThetaTick +from plotnine.coords.coord_radial import polar_bbox from plotnine.data import mtcars from plotnine.iapi import ( labels_view, @@ -1538,3 +1539,27 @@ def test_secondary_r_axis_title_sides(): f"secondary title (x0={right_box.x0:.1f}) expected right of " f"primary title (x1={left_box.x1:.1f})" ) + + +def test_polar_bbox_full_circle_is_unit_square(): + assert polar_bbox((0.0, 2 * pi)) == (0.0, 1.0, 0.0, 1.0) + + +@pytest.mark.parametrize( + "start,end,expected", + [ + (-pi / 2, pi / 2, 0.5), # right half-disc: wide box + (0.0, 0.25, pytest.approx(4.041, abs=1e-2)), # thin sliver: tall box + (-pi / 2, 0.0, 1.0), # quarter: square + (0.0, 2 * pi, 1.0), # full circle: square + ], +) +def test_coord_radial_aspect_matches_wedge(start, end, expected): + c = coord_radial(start=start, end=end) + assert c.aspect(None) == expected + + +def test_coord_radial_aspect_uses_zero_margin_for_half_disc(): + # margin 0 (not ggplot2's 0.05) -> exactly 0.5, so the panel matches the + # tight wedge and the sector is not stretched. + assert coord_radial(start=-pi / 2, end=pi / 2).aspect(None) == 0.5 From e535eb67cb39be43582176d27d15ab637a916454 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 24 Jul 2026 01:05:29 +0300 Subject: [PATCH 088/126] fix(coord_radial): fill the panel with partial arcs --- plotnine/_mpl/_radial_axes.py | 131 +++++++++++++++++++++++++++++++++- tests/test_coord_radial.py | 28 ++++++++ 2 files changed, 158 insertions(+), 1 deletion(-) diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index 0bdc42e2f8..6970a50369 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -2,9 +2,17 @@ from typing import TYPE_CHECKING, cast +import matplotlib.patches as mpatches +import matplotlib.transforms as mtransforms +import numpy as np from matplotlib import cbook from matplotlib.projections import register_projection -from matplotlib.projections.polar import PolarAxes, RadialAxis, ThetaAxis +from matplotlib.projections.polar import ( + PolarAxes, + RadialAxis, + ThetaAxis, + _WedgeBbox, +) from ._radial_axis import ( p9RadialAxis, @@ -20,6 +28,39 @@ from plotnine.typing import PolarSide +class _TightWedgeBbox(_WedgeBbox): + """ + Wedge bounding box that hugs the sector instead of padding to a square + + matplotlib's `_WedgeBbox` pads the sector's bounding box out to a + square and centres the wedge in it, which — with a square panel — + leaves large empty margins around a partial arc. This subclass + reproduces the tight-box computation but omits that final padding, so + the sector fills the panel the layout engine has already shaped to the + same aspect. + """ + + def get_points(self) -> np.ndarray: + if self._invalid: + points = self._viewLim.get_points().copy() # pyright: ignore[reportAttributeAccessIssue] + points[:, 0] *= 180 / np.pi + if points[0, 0] > points[1, 0]: + points[:, 0] = points[::-1, 0] + points[:, 1] -= self._originLim.y0 # pyright: ignore[reportAttributeAccessIssue] + points[:, 1] *= 0.5 / points[1, 1] + width = min(points[1, 1] - points[0, 1], 0.5) + wedge = mpatches.Wedge( + self._center, # pyright: ignore[reportAttributeAccessIssue] + points[1, 1], + points[0, 0], + points[1, 0], + width=width, + ) + self.update_from_path(wedge.get_path()) + self._invalid = 0 + return self._points # pyright: ignore[reportAttributeAccessIssue] + + class p9RadialAxes(PolarAxes): """ Polar axes that keeps the axis tick labels above the geom layers @@ -58,6 +99,94 @@ def _init_axis(self) -> None: if inner_spine := self.spines.get("inner"): inner_spine.register_axis(self.yaxis) + def apply_aspect(self, position=None) -> None: + """ + Shrink the layout cell to match the tight wedge's aspect, not 1.0 + + The stock `PolarAxes.apply_aspect` always shrinks the cell to a + square (aspect=1.0). That undoes the wedge-shaped cell the layout + engine assigns from `coord_radial.aspect` for partial arcs. This + override uses the tight wedge bbox to derive the actual aspect and + shrinks to that instead, so a full circle still gets a square cell + and a half-disc gets a 2:1 wide cell. + """ + if position is None: + position = self.get_position(original=True) + trans = self.get_figure(root=False).transSubfigure # pyright: ignore[reportOptionalMemberAccess] + bb = mtransforms.Bbox.unit().transformed(trans) + fig_aspect = bb.height / bb.width + pts = self.axesLim.get_points() + w = pts[1, 0] - pts[0, 0] + h = pts[1, 1] - pts[0, 1] + wedge_aspect = (h / w) if w > 0 else 1.0 + pb = position.frozen() + pb1 = pb.shrunk_to_aspect(wedge_aspect, pb, fig_aspect) + anchor = self.get_anchor() + self._set_position( # pyright: ignore[reportAttributeAccessIssue] + pb1.anchored(anchor, pb), # pyright: ignore[reportArgumentType] + "active", + ) + + def _set_lim_and_transforms(self) -> None: + """ + Build the polar transforms, then let the sector fill the panel + + Runs matplotlib's setup, then swaps the square-padding `_WedgeBbox` + for `_TightWedgeBbox` and rebuilds the wedge and data transforms so + a partial arc hugs the panel edges. Rebuilds the theta/r axis + transforms that the base method wired to the old `transData` so they + route through the tight bbox instead. Safe to run at + axes-construction time: `_TightWedgeBbox` reads only the + view/origin limits the base method has already set, not any + plotnine coordinate state. + """ + super()._set_lim_and_transforms() # pyright: ignore[reportAttributeAccessIssue] + self.axesLim = _TightWedgeBbox( + (0.5, 0.5), + self._realViewLim, # pyright: ignore[reportAttributeAccessIssue] + self._originViewLim, # pyright: ignore[reportAttributeAccessIssue] + ) + self.transWedge = mtransforms.BboxTransformFrom(self.axesLim) + self.transData = ( + self.transScale + + self.transShift # pyright: ignore[reportAttributeAccessIssue] + + self.transProjection # pyright: ignore[reportAttributeAccessIssue] + + ( + self.transProjectionAffine # pyright: ignore[reportAttributeAccessIssue] + + self.transWedge + + self.transAxes + ) + ) + # The base method wired _xaxis_transform, _yaxis_transform and + # _yaxis_text_transform against the old transData composite. Rebuild + # them so theta-tick and r-tick positioning routes through the tight + # bbox rather than the stock square-padding one. + self._xaxis_transform = ( + mtransforms.blended_transform_factory( + mtransforms.IdentityTransform(), + mtransforms.BboxTransformTo(self.viewLim), + ) + + self.transData + ) + flipr = ( + mtransforms.Affine2D() + .translate(0.0, -0.5) + .scale(1.0, -1.0) + .translate(0.0, 0.5) + ) + self._xaxis_text_transform = flipr + self._xaxis_transform + self._yaxis_transform = ( + mtransforms.blended_transform_factory( + mtransforms.BboxTransformTo(self.viewLim), + mtransforms.IdentityTransform(), + ) + + self.transData + ) + self._yaxis_text_transform.set( # pyright: ignore[reportAttributeAccessIssue] + self._r_label_position # pyright: ignore[reportAttributeAccessIssue] + + self.transData + ) + @property def axis_at_side(self) -> dict[PolarSide, ThetaAxis | RadialAxis]: """ diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 6ad95e7daf..32f8260aea 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1563,3 +1563,31 @@ def test_coord_radial_aspect_uses_zero_margin_for_half_disc(): # margin 0 (not ggplot2's 0.05) -> exactly 0.5, so the panel matches the # tight wedge and the sector is not stretched. assert coord_radial(start=-pi / 2, end=pi / 2).aspect(None) == 0.5 + + +def _panel_and_ink(p) -> tuple[Any, np.ndarray]: + """Rendered panel bbox and the sector-outline points in display space""" + p.draw_test() + ax = p.axs[0] + ax.figure.draw_without_rendering() + renderer = ax.figure._get_renderer() + panel = ax.get_window_extent(renderer) + theta_lo, theta_hi = ax.get_xlim() + r_lo, r_hi = ax.get_ylim() + thetas = np.linspace(theta_lo, theta_hi, 200) + outline = [(t, r_hi) for t in thetas] + outline += [(theta_lo, r_lo), (theta_hi, r_lo)] + ink = ax.transData.transform(np.array(outline)) + return panel, ink + + +def test_coord_radial_half_disc_fills_panel(): + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + coord_radial(start=-pi / 2, end=pi / 2) + ) + panel, ink = _panel_and_ink(p) + frac_w = (ink[:, 0].max() - ink[:, 0].min()) / panel.width + frac_h = (ink[:, 1].max() - ink[:, 1].min()) / panel.height + assert_allclose([frac_w, frac_h], 1.0, atol=0.02) From c6d724fdffca9b9726bf76810a05f9006111c23a Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 24 Jul 2026 01:28:49 +0300 Subject: [PATCH 089/126] test(coord_radial): assert partial arcs fill panels undistorted --- tests/test_coord_radial.py | 56 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 32f8260aea..b5c42575e2 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1581,6 +1581,13 @@ def _panel_and_ink(p) -> tuple[Any, np.ndarray]: return panel, ink +def _r_length(ax, theta: float) -> float: + """Display-space length of the radius spoke at angle `theta`""" + r_lo, r_hi = ax.get_ylim() + pts = ax.transData.transform([(theta, r_lo), (theta, r_hi)]) + return float(np.hypot(*(pts[1] - pts[0]))) + + def test_coord_radial_half_disc_fills_panel(): p = ( ggplot(mtcars, aes("wt", "mpg")) @@ -1591,3 +1598,52 @@ def test_coord_radial_half_disc_fills_panel(): frac_w = (ink[:, 0].max() - ink[:, 0].min()) / panel.width frac_h = (ink[:, 1].max() - ink[:, 1].min()) / panel.height assert_allclose([frac_w, frac_h], 1.0, atol=0.02) + + +@pytest.mark.parametrize( + "kwargs", + [ + {}, + {"start": -pi / 2, "end": 0.0}, + {"start": -pi / 2, "end": pi / 2}, + {"start": -pi / 2, "end": -pi / 3}, + {"start": 0.0, "end": 0.25}, + {"start": -pi / 2, "end": pi / 2, "inner_radius": 0.3}, + ], +) +def test_coord_radial_sector_fills_panel(kwargs): + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + coord_radial(**kwargs) + ) + panel, ink = _panel_and_ink(p) + frac_w = (ink[:, 0].max() - ink[:, 0].min()) / panel.width + frac_h = (ink[:, 1].max() - ink[:, 1].min()) / panel.height + assert_allclose([frac_w, frac_h], 1.0, atol=0.02) + + +@pytest.mark.parametrize( + "kwargs", + [ + {"start": -pi / 2, "end": pi / 2}, + {"start": 0.0, "end": 0.25}, + {"start": -pi / 2, "end": pi / 2, "inner_radius": 0.3}, + ], +) +def test_coord_radial_sector_is_not_distorted(kwargs): + # The radius must map to the same display length on every spoke; + # otherwise the sector has been stretched to fill a mismatched panel. + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + coord_radial(**kwargs) + ) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] + ax.figure.draw_without_rendering() + theta_lo, theta_hi = ax.get_xlim() + theta_mid = (theta_lo + theta_hi) / 2 + assert_allclose( + _r_length(ax, theta_mid), _r_length(ax, theta_lo), rtol=0.01 + ) From ae844c39dc6532e1575678fddc15d2ef1df0587f Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 24 Jul 2026 01:36:01 +0300 Subject: [PATCH 090/126] test(coord_radial): visual baselines for partial-arc spacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds five visual regression tests covering representative partial arcs (half-disc, thin wedge, sliver, quarter, inner-radius half-disc). Title-gap check (half-disc, axis_line_y): panel bottom/left coincide with ink bottom/left (panel 110/50, ink 110/50 px) — title gap fully collapsed; no dropped-title rerouting needed. --- tests/test_coord_radial.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index b5c42575e2..75f249e020 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1647,3 +1647,33 @@ def test_coord_radial_sector_is_not_distorted(kwargs): assert_allclose( _r_length(ax, theta_mid), _r_length(ax, theta_lo), rtol=0.01 ) + + +_partial_arc_base = ggplot(mtcars, aes("wt", "mpg")) + geom_point() + + +def test_coord_radial_partial_arc_half_disc(): + p = _partial_arc_base + coord_radial(start=-pi / 2, end=pi / 2) + assert p == "coord_radial_partial_arc_half_disc" + + +def test_coord_radial_partial_arc_thin_wedge(): + p = _partial_arc_base + coord_radial(start=-pi / 2, end=-pi / 3) + assert p == "coord_radial_partial_arc_thin_wedge" + + +def test_coord_radial_partial_arc_sliver(): + p = _partial_arc_base + coord_radial(start=0, end=0.25) + assert p == "coord_radial_partial_arc_sliver" + + +def test_coord_radial_partial_arc_quarter(): + p = _partial_arc_base + coord_radial(start=-pi / 2, end=0) + assert p == "coord_radial_partial_arc_quarter" + + +def test_coord_radial_partial_arc_inner_radius_half_disc(): + p = _partial_arc_base + coord_radial( + start=-pi / 2, end=pi / 2, inner_radius=0.3 + ) + assert p == "coord_radial_partial_arc_inner_radius_half_disc" From 929080a23f7f57ea09cce74c7ad02780632d0977 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Mon, 27 Jul 2026 15:12:22 +0300 Subject: [PATCH 091/126] fix(coord_radial): fill narrow donut arcs to the panel edge A narrow arc combined with inner_radius>0 under-filled its panel. Two matching fixes: - polar_bbox placed the inner ring at inner_radius, but inner_radius is a fraction of the outer radius (0.5 in unit-circle space), so the ring belongs at inner_radius * 0.5. This corrects the aspect that sizes the layout cell. - _TightWedgeBbox measured the wedge with update_from_path, whose Bezier control-point hull reaches back to the centre and over-widens the box for a narrow sector. Use the path's true curve extents instead. With both, narrow donut arcs fill 100% x 100% undistorted, matching the non-donut arcs. --- plotnine/_mpl/_radial_axes.py | 8 ++++++-- plotnine/coords/coord_radial.py | 8 ++++++-- tests/test_coord_radial.py | 6 +++++- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index 6970a50369..76164a0f7c 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -56,9 +56,13 @@ def get_points(self) -> np.ndarray: points[1, 0], width=width, ) - self.update_from_path(wedge.get_path()) + # `get_extents` evaluates the true curve extents; a plain + # `update_from_path` would union the wider Bezier control-point + # hull, which reaches back to the centre and over-widens the + # box for a narrow donut sector. + self._points = wedge.get_path().get_extents().get_points().copy() self._invalid = 0 - return self._points # pyright: ignore[reportAttributeAccessIssue] + return self._points class p9RadialAxes(PolarAxes): diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 943bad3a9e..07790aa77a 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -48,8 +48,12 @@ def polar_bbox( ends = np.array([lo, hi]) x_outer = 0.5 * np.sin(ends) + 0.5 y_outer = 0.5 * np.cos(ends) + 0.5 - x_inner = inner_radius * np.sin(ends) + 0.5 - y_inner = inner_radius * np.cos(ends) + 0.5 + # `inner_radius` is a fraction of the outer radius, and the outer + # radius is 0.5 in this unit-circle space, so the inner ring sits at + # `inner_radius * 0.5` from the centre. + r_inner = inner_radius * 0.5 + x_inner = r_inner * np.sin(ends) + 0.5 + y_inner = r_inner * np.cos(ends) + 0.5 def in_arc(theta: float) -> bool: folded = (theta - lo) % (2 * np.pi) + lo diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 75f249e020..f6caf8b352 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1581,7 +1581,7 @@ def _panel_and_ink(p) -> tuple[Any, np.ndarray]: return panel, ink -def _r_length(ax, theta: float) -> float: +def _r_length(ax: Axes, theta: float) -> float: """Display-space length of the radius spoke at angle `theta`""" r_lo, r_hi = ax.get_ylim() pts = ax.transData.transform([(theta, r_lo), (theta, r_hi)]) @@ -1609,6 +1609,10 @@ def test_coord_radial_half_disc_fills_panel(): {"start": -pi / 2, "end": -pi / 3}, {"start": 0.0, "end": 0.25}, {"start": -pi / 2, "end": pi / 2, "inner_radius": 0.3}, + # Narrow arc combined with a donut hole: the inner ring must sit + # at inner_radius * outer_radius, or the panel aspect mismatches + # the wedge and the sector under-fills (regression). + {"start": pi / 4, "end": 3 * pi / 4, "inner_radius": 0.3}, ], ) def test_coord_radial_sector_fills_panel(kwargs): From f1f7ea1509a2c802c08a2924692ae51bb703cfa3 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Mon, 27 Jul 2026 16:32:30 +0300 Subject: [PATCH 092/126] fix(coord_radial): render the wedge background undistorted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `PolarAxes.draw` recomputes the axes-background `Wedge` (grey fill and the clip path the geoms share) on every draw in axes-fraction space, assuming a square box. In the non-square panel a partial arc now gets, that squashed the background — and the theta tick labels riding on it — into an ellipse mismatched with the data and the spine, even though `transData` itself was correct. Return a `_PanelWedge` from `_gen_axes_patch` that ignores those square-fit geometry setters and instead holds a fixed unit wedge routed through `transWedge + transAxes` — the transform the `polar` spine already uses — so the background traces the same arc as the data. `_reshape_panel_wedge`, run after `super().draw`, restores that geometry each draw. Also simplify `_set_lim_and_transforms` to retype `axesLim` in place rather than rebuilding the transform stack, so the tick labels' cached transform copies do not go stale. --- plotnine/_mpl/_radial_axes.py | 139 +++++++++++++++++++++------------- tests/test_coord_radial.py | 30 ++++++++ 2 files changed, 115 insertions(+), 54 deletions(-) diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index 76164a0f7c..3ec1c3fd5d 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -65,6 +65,40 @@ def get_points(self) -> np.ndarray: return self._points +class _PanelWedge(mpatches.Wedge): + """ + Axes-background wedge that fills a wedge-shaped panel undistorted + + `PolarAxes.draw` recomputes the background wedge's centre, radius and + width on every draw in axes-fraction space, a computation that assumes + the axes box is square (it takes the radius from the x-scaling of + `transWedge` alone). In a wedge-shaped, non-square panel that renders the + background — and the clip path the geoms share — as an ellipse mismatched + with the data and the spine. This subclass ignores those geometry setters + and instead holds a fixed unit wedge routed through `transWedge + + transAxes`, the transform the `polar` spine already uses, so the + background traces the same arc as the data. `reshape` sets the real + geometry through the base-class setters, bypassing the no-ops. + """ + + def set_center(self, center) -> None: + pass + + def set_radius(self, radius) -> None: + pass + + def set_width(self, width) -> None: + pass + + def reshape(self, width: float | None) -> None: + """ + Set the unit-wedge geometry, bypassing the ignored setters + """ + mpatches.Wedge.set_center(self, (0.5, 0.5)) + mpatches.Wedge.set_radius(self, 0.5) + mpatches.Wedge.set_width(self, width) + + class p9RadialAxes(PolarAxes): """ Polar axes that keeps the axis tick labels above the geom layers @@ -103,6 +137,40 @@ def _init_axis(self) -> None: if inner_spine := self.spines.get("inner"): inner_spine.register_axis(self.yaxis) + def _gen_axes_patch(self) -> mpatches.Wedge: + """ + Background wedge that fills a wedge-shaped panel undistorted + + Returns a `_PanelWedge` in place of the stock `Wedge` so the axes + background (and the clip path the geoms share) traces the same arc + as the data even when the panel is not square. See `_PanelWedge`. + """ + return _PanelWedge((0.5, 0.5), 0.5, 0.0, 360.0) + + def _reshape_panel_wedge(self) -> None: + """ + Route the background wedge through the same transform as the arc + + `_PanelWedge` ignores the square-fit geometry `PolarAxes.draw` + computes; this sets its true unit-wedge width (the donut hole) and + points it through `transWedge + transAxes`, the transform the + `polar` spine uses, so the background fills the wedge panel exactly. + """ + patch = self.patch + if not isinstance(patch, _PanelWedge): + return + rscale = self.yaxis.get_transform() + rmin, rmax = ( + rscale.transform(self._realViewLim.intervaly) # pyright: ignore[reportAttributeAccessIssue] + - rscale.transform(self.get_rorigin()) + ) * self.get_rsign() + width = min(0.5 * (rmax - rmin) / rmax, 0.5) if rmax else 0.5 + patch.reshape(None if width == 0.5 else width) + patch.set_transform( + self.transWedge # pyright: ignore[reportAttributeAccessIssue] + + self.transAxes + ) + def apply_aspect(self, position=None) -> None: """ Shrink the layout cell to match the tight wedge's aspect, not 1.0 @@ -119,7 +187,7 @@ def apply_aspect(self, position=None) -> None: trans = self.get_figure(root=False).transSubfigure # pyright: ignore[reportOptionalMemberAccess] bb = mtransforms.Bbox.unit().transformed(trans) fig_aspect = bb.height / bb.width - pts = self.axesLim.get_points() + pts = self.axesLim.get_points() # pyright: ignore[reportAttributeAccessIssue] w = pts[1, 0] - pts[0, 0] h = pts[1, 1] - pts[0, 1] wedge_aspect = (h / w) if w > 0 else 1.0 @@ -135,61 +203,19 @@ def _set_lim_and_transforms(self) -> None: """ Build the polar transforms, then let the sector fill the panel - Runs matplotlib's setup, then swaps the square-padding `_WedgeBbox` - for `_TightWedgeBbox` and rebuilds the wedge and data transforms so - a partial arc hugs the panel edges. Rebuilds the theta/r axis - transforms that the base method wired to the old `transData` so they - route through the tight bbox instead. Safe to run at - axes-construction time: `_TightWedgeBbox` reads only the - view/origin limits the base method has already set, not any - plotnine coordinate state. + Runs matplotlib's setup, then retypes the square-padding `axesLim` + bbox in place to `_TightWedgeBbox` so a partial arc hugs the panel + edges. Retyping the existing object -- rather than rebuilding it and + the transform stack -- means every transform the base method already + wired to it (`transData`, the theta/r axis transforms, and the tick + labels' cached copies of them) recomputes through the tight box + without any of them going stale. Safe to run at axes-construction + time: `_TightWedgeBbox` reads only the view/origin limits the base + method has already set, not any plotnine coordinate state. """ super()._set_lim_and_transforms() # pyright: ignore[reportAttributeAccessIssue] - self.axesLim = _TightWedgeBbox( - (0.5, 0.5), - self._realViewLim, # pyright: ignore[reportAttributeAccessIssue] - self._originViewLim, # pyright: ignore[reportAttributeAccessIssue] - ) - self.transWedge = mtransforms.BboxTransformFrom(self.axesLim) - self.transData = ( - self.transScale - + self.transShift # pyright: ignore[reportAttributeAccessIssue] - + self.transProjection # pyright: ignore[reportAttributeAccessIssue] - + ( - self.transProjectionAffine # pyright: ignore[reportAttributeAccessIssue] - + self.transWedge - + self.transAxes - ) - ) - # The base method wired _xaxis_transform, _yaxis_transform and - # _yaxis_text_transform against the old transData composite. Rebuild - # them so theta-tick and r-tick positioning routes through the tight - # bbox rather than the stock square-padding one. - self._xaxis_transform = ( - mtransforms.blended_transform_factory( - mtransforms.IdentityTransform(), - mtransforms.BboxTransformTo(self.viewLim), - ) - + self.transData - ) - flipr = ( - mtransforms.Affine2D() - .translate(0.0, -0.5) - .scale(1.0, -1.0) - .translate(0.0, 0.5) - ) - self._xaxis_text_transform = flipr + self._xaxis_transform - self._yaxis_transform = ( - mtransforms.blended_transform_factory( - mtransforms.BboxTransformTo(self.viewLim), - mtransforms.IdentityTransform(), - ) - + self.transData - ) - self._yaxis_text_transform.set( # pyright: ignore[reportAttributeAccessIssue] - self._r_label_position # pyright: ignore[reportAttributeAccessIssue] - + self.transData - ) + self.axesLim.__class__ = _TightWedgeBbox # pyright: ignore[reportAttributeAccessIssue] + self.axesLim.invalidate() # pyright: ignore[reportAttributeAccessIssue] @property def axis_at_side(self) -> dict[PolarSide, ThetaAxis | RadialAxis]: @@ -224,6 +250,11 @@ def draw(self, renderer: RendererBase) -> None: super().draw(renderer) + # `PolarAxes.draw` (in the super call) re-mutates the background + # wedge each draw assuming a square box; reshape it afterwards so a + # non-square wedge panel is not re-squashed on the next draw. + self._reshape_panel_wedge() + _redraw_raxis(self.raxis, renderer) if self._sec_raxis: _redraw_raxis(self._sec_raxis, renderer) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index f6caf8b352..c4b939d671 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1653,6 +1653,36 @@ def test_coord_radial_sector_is_not_distorted(kwargs): ) +@pytest.mark.parametrize( + "kwargs", + [ + {"start": -pi / 2, "end": pi / 2}, + {"start": 0.0, "end": 0.25}, + {"start": -pi / 2, "end": pi / 2, "inner_radius": 0.3}, + ], +) +def test_coord_radial_background_patch_traces_sector(kwargs): + # The axes-background wedge is drawn through its own transform, not + # transData, so a squashed patch would pass the transData-based + # distortion check while still rendering the panel as an ellipse. + # Assert the patch bbox coincides with the data sector's bbox. + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + coord_radial(**kwargs) + ) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] + ax.figure.draw_without_rendering() + renderer = ax.figure._get_renderer() + patch = ax.patch.get_window_extent(renderer) + _, ink = _panel_and_ink(p) + assert_allclose(patch.x0, ink[:, 0].min(), atol=1.0) + assert_allclose(patch.x1, ink[:, 0].max(), atol=1.0) + assert_allclose(patch.y0, ink[:, 1].min(), atol=1.0) + assert_allclose(patch.y1, ink[:, 1].max(), atol=1.0) + + _partial_arc_base = ggplot(mtcars, aes("wt", "mpg")) + geom_point() From 6bc87056d86f5f42a4361b6b7bfaf3346c1a20ab Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Mon, 27 Jul 2026 17:01:47 +0300 Subject: [PATCH 093/126] fix(coord_radial): clip geoms to the arc on a single render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous distortion fix reshaped the background wedge *after* `super().draw`, but `PolarAxes.draw` (inside that super call) draws the geoms clipped to the patch in the same call. On a single render the geoms were therefore clipped to the patch's initial square geometry — a half-height ellipse — so most of the plot vanished behind an empty background. A second draw hid it, which is why it was missed. The geoms snapshot the patch's transform *object* when they capture their clip, so it must be set once and never replaced. Fix the `_PanelWedge` transform to `transWedge + transAxes` at construction and block `set_transform` (as the geometry setters already are), so `Axes.clear` cannot reset it to `transAxes`. Reshape the wedge width before `super().draw` rather than after, so the clip is correct on the frame that draws the geoms. Add `test_coord_radial_geoms_clip_to_full_sector`, which asserts the geom collection's clip-path bbox coincides with the data sector — the metric the patch-only test missed. --- plotnine/_mpl/_radial_axes.py | 54 +++++++++++++++++++++++------------ tests/test_coord_radial.py | 31 ++++++++++++++++++++ 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index 3ec1c3fd5d..b988c4c100 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -75,10 +75,16 @@ class _PanelWedge(mpatches.Wedge): `transWedge` alone). In a wedge-shaped, non-square panel that renders the background — and the clip path the geoms share — as an ellipse mismatched with the data and the spine. This subclass ignores those geometry setters - and instead holds a fixed unit wedge routed through `transWedge + - transAxes`, the transform the `polar` spine already uses, so the - background traces the same arc as the data. `reshape` sets the real - geometry through the base-class setters, bypassing the no-ops. + and its transform setter, holding a fixed unit wedge whose transform the + axes sets once to `transWedge + transAxes` — the transform the `polar` + spine already uses — so the background traces the same arc as the data. + + Blocking `set_transform` is what keeps the geom clip path correct: the + geoms snapshot the patch's transform *object* when they capture their + clip, so it must be set once and never replaced. `Axes.clear` would + otherwise reset it to `transAxes` (a square wedge, which clips the geoms + to a squashed ellipse). `reshape` sets the real width through the + base-class setter, bypassing the no-op. """ def set_center(self, center) -> None: @@ -90,6 +96,9 @@ def set_radius(self, radius) -> None: def set_width(self, width) -> None: pass + def set_transform(self, t) -> None: + pass + def reshape(self, width: float | None) -> None: """ Set the unit-wedge geometry, bypassing the ignored setters @@ -144,17 +153,26 @@ def _gen_axes_patch(self) -> mpatches.Wedge: Returns a `_PanelWedge` in place of the stock `Wedge` so the axes background (and the clip path the geoms share) traces the same arc as the data even when the panel is not square. See `_PanelWedge`. + Its transform is fixed here to `transWedge + transAxes` (both exist + by the time the patch is generated) and cannot be replaced, so the + geoms clip to the true arc. See `_PanelWedge`. """ - return _PanelWedge((0.5, 0.5), 0.5, 0.0, 360.0) + patch = _PanelWedge((0.5, 0.5), 0.5, 0.0, 360.0) + mpatches.Wedge.set_transform( + patch, + self.transWedge # pyright: ignore[reportAttributeAccessIssue] + + self.transAxes, + ) + return patch def _reshape_panel_wedge(self) -> None: """ - Route the background wedge through the same transform as the arc + Restore the background wedge's unit geometry after the square fit `_PanelWedge` ignores the square-fit geometry `PolarAxes.draw` - computes; this sets its true unit-wedge width (the donut hole) and - points it through `transWedge + transAxes`, the transform the - `polar` spine uses, so the background fills the wedge panel exactly. + computes; this sets its true unit-wedge width (the donut hole) so the + background fills the wedge panel exactly. Its transform is fixed at + construction and never changes. """ patch = self.patch if not isinstance(patch, _PanelWedge): @@ -166,10 +184,6 @@ def _reshape_panel_wedge(self) -> None: ) * self.get_rsign() width = min(0.5 * (rmax - rmin) / rmax, 0.5) if rmax else 0.5 patch.reshape(None if width == 0.5 else width) - patch.set_transform( - self.transWedge # pyright: ignore[reportAttributeAccessIssue] - + self.transAxes - ) def apply_aspect(self, position=None) -> None: """ @@ -248,13 +262,17 @@ def draw(self, renderer: RendererBase) -> None: ): tick.update_position(tick.get_loc()) - super().draw(renderer) - - # `PolarAxes.draw` (in the super call) re-mutates the background - # wedge each draw assuming a square box; reshape it afterwards so a - # non-square wedge panel is not re-squashed on the next draw. + # Reshape the background wedge before `super().draw` clips the geoms + # to it: `PolarAxes.draw` (inside the super call) recomputes the + # patch assuming a square box, then draws the geoms clipped to that + # squashed patch in the same call. `_PanelWedge` ignores those + # geometry setters, so setting the correct unit-wedge geometry and + # transform here survives the super call and clips the geoms to the + # true arc. self._reshape_panel_wedge() + super().draw(renderer) + _redraw_raxis(self.raxis, renderer) if self._sec_raxis: _redraw_raxis(self._sec_raxis, renderer) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index c4b939d671..8241985768 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1683,6 +1683,37 @@ def test_coord_radial_background_patch_traces_sector(kwargs): assert_allclose(patch.y1, ink[:, 1].max(), atol=1.0) +@pytest.mark.parametrize( + "kwargs", + [ + {"start": -pi / 2, "end": pi / 2}, + {"start": -pi / 2, "end": 0.0}, + {"start": 0.0, "end": 0.25}, + {"start": -pi / 2, "end": pi / 2, "inner_radius": 0.3}, + ], +) +def test_coord_radial_geoms_clip_to_full_sector(kwargs): + # The geoms clip to the background wedge, snapshotting its transform + # when they capture the clip. A single render must clip them to the + # true arc, not the square wedge the base draw computes -- otherwise + # the geoms vanish behind a half-height clip (regression). + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + coord_radial(**kwargs) + ) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] + ax.figure.draw_without_rendering() + _, ink = _panel_and_ink(p) + clip = ax.collections[0].get_clip_path().get_fully_transformed_path() + bbox = clip.get_extents() + assert_allclose(bbox.x0, ink[:, 0].min(), atol=1.0) + assert_allclose(bbox.x1, ink[:, 0].max(), atol=1.0) + assert_allclose(bbox.y0, ink[:, 1].min(), atol=1.0) + assert_allclose(bbox.y1, ink[:, 1].max(), atol=1.0) + + _partial_arc_base = ggplot(mtcars, aes("wt", "mpg")) + geom_point() From 646c532a155c698eb3e37856c36995431d32d3e2 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Mon, 27 Jul 2026 17:41:45 +0300 Subject: [PATCH 094/126] fix(coord_radial): stop r-labels reserving spurious panel margin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A partial arc tilted off the axes (e.g. start=-pi/2, end=-pi/3) rendered small and low, with large left/right margins. The layout's protrusion heuristic — meant to reserve room for y tick labels that stick out past a Cartesian axes edge — measured the r-axis labels fanning out along the up-tilted spoke and reserved ~half the figure height above the panel, capping its size and pushing it down. The protrusion concept does not apply to a polar panel: its tick labels are positioned by coord_radial along the arc, inside a panel already sized to the tight sector, and their fixed clearance is already reserved by the polar clearance estimates. Override the four protrusion methods on `PolarPlotLayoutItems` to return zero, alongside the clearance overrides that already handle the "no single side to measure" case. A thin wedge now gets the same panel size as a half-disc of the same aspect, centred in the available space. --- .../_mpl/layout_manager/_plot_layout_items.py | 24 +++++++++++++++---- tests/test_coord_radial.py | 22 +++++++++++++++++ 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/plotnine/_mpl/layout_manager/_plot_layout_items.py b/plotnine/_mpl/layout_manager/_plot_layout_items.py index e601e6e231..8b2c759a11 100644 --- a/plotnine/_mpl/layout_manager/_plot_layout_items.py +++ b/plotnine/_mpl/layout_manager/_plot_layout_items.py @@ -905,13 +905,15 @@ def _position_strip_text(self, st: StripText): class PolarPlotLayoutItems(PlotLayoutItems): """ - `PlotLayoutItems` for a polar panel + The space around a polar panel that its axes need A theta axis wraps the full circle and an r axis runs along one - spoke, so neither has a single side to measure a tick-label - bounding box from (`axis_at` never resolves one for either). These - four methods return a fixed clearance estimate instead, applied to - every side alike. + spoke, so neither has a single side to measure a tick-label bounding + box from (`axis_at` never resolves one for either). The tick and text + clearances are therefore a fixed estimate applied to every side alike, + and the tick-label protrusions are zero: the labels fan out along the + arc within a panel already sized to the tight sector, so their reach + past a Cartesian edge does not call for extra margin. """ def axis_ticks_x_max_height(self, ax: Axes, side: Side) -> float: @@ -926,6 +928,18 @@ def axis_ticks_y_max_width(self, ax: Axes, side: Side) -> float: def axis_text_y_max_width(self, ax: Axes, side: Side) -> float: return _polar_text_clearance_y(self.plot.theme, side) + def axis_text_y_top_protrusion(self, location: AxesLocation) -> float: + return 0.0 + + def axis_text_y_bottom_protrusion(self, location: AxesLocation) -> float: + return 0.0 + + def axis_text_x_left_protrusion(self, location: AxesLocation) -> float: + return 0.0 + + def axis_text_x_right_protrusion(self, location: AxesLocation) -> float: + return 0.0 + def _polar_text_clearance_x(theme: theme, side: Side) -> float: """ diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 8241985768..30a009691c 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1627,6 +1627,28 @@ def test_coord_radial_sector_fills_panel(kwargs): assert_allclose([frac_w, frac_h], 1.0, atol=0.02) +def test_coord_radial_thin_wedge_uses_available_space(): + # A thin wedge and a half-disc share aspect 0.5, so the layout should + # give them the same panel size. The r-axis labels fan out along the + # up-tilted spoke and reach past the sector's top edge; treating that + # as a Cartesian y-label protrusion once reserved ~half the figure + # height above the panel, shrinking it (regression). + thin = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + coord_radial(start=-pi / 2, end=-pi / 3) + ) + half = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + coord_radial(start=-pi / 2, end=pi / 2) + ) + thin_panel, _ = _panel_and_ink(thin) + half_panel, _ = _panel_and_ink(half) + assert_allclose(thin_panel.width, half_panel.width, rtol=0.02) + assert_allclose(thin_panel.height, half_panel.height, rtol=0.02) + + @pytest.mark.parametrize( "kwargs", [ From b31c1841080936530a008f46e5d91e92eb3b7fb6 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 28 Jul 2026 20:32:10 +0300 Subject: [PATCH 095/126] fix(coord_radial): reserve gulley and strip space for polar decorations A polar panel draws its full theta and r axes on every facet, so their ticks and labels reach into the gullies on all sides and sit at the arc apex inside the panel's top edge. Reserve that space in the panel spacing so decorations no longer cross the gulley, and always shift the strip clear of the apex decorations (a polar panel has no geometric "inside" between the panel and its arc-bound axis, so the shift applies at any strip_placement). Zero the polar tick-mark clearance: polar ticks point inward from the arc and never protrude past the panel edge, so their length only pads the label, which the text clearance already covers. Return the label extent bare from the polar text helpers and add the tick-label margin in each caller, matching the Cartesian contract, so the margin is no longer counted twice in the strip shift. --- .../_mpl/layout_manager/_plot_layout_items.py | 91 ++++++++++--------- .../_mpl/layout_manager/_plot_side_space.py | 89 +++++++++++++----- plotnine/themes/themeable.py | 11 ++- tests/test_coord_radial.py | 83 +++++++++++++++++ 4 files changed, 203 insertions(+), 71 deletions(-) diff --git a/plotnine/_mpl/layout_manager/_plot_layout_items.py b/plotnine/_mpl/layout_manager/_plot_layout_items.py index 8b2c759a11..c7f32535fa 100644 --- a/plotnine/_mpl/layout_manager/_plot_layout_items.py +++ b/plotnine/_mpl/layout_manager/_plot_layout_items.py @@ -363,9 +363,19 @@ def strip_shift(self, st: StripText) -> float: panel, so within one facet the strips of axis-bearing panels shift while the others do not. """ - theme = self.plot.theme - if theme.getp("strip_placement") != "outside": + if self.plot.theme.getp("strip_placement") != "outside": return 0 + return self._strip_axis_clearance(st) + + def _strip_axis_clearance(self, st: StripText) -> float: + """ + Extent of the axis a strip must clear to sit beyond it, figure space + + The ticks, tick labels and panel-facing text margin its own panel + draws on the strip's side, plus the strip_switch_pad. Zero when the + panel draws no axis there. + """ + theme = self.plot.theme side, ax = st.position, st.ax W, H = theme.getp("figure_size") if side in ("top", "bottom"): @@ -909,24 +919,29 @@ class PolarPlotLayoutItems(PlotLayoutItems): A theta axis wraps the full circle and an r axis runs along one spoke, so neither has a single side to measure a tick-label bounding - box from (`axis_at` never resolves one for either). The tick and text - clearances are therefore a fixed estimate applied to every side alike, + box from (`axis_at` never resolves one for either). The text + clearance is therefore a fixed estimate applied to every side alike, and the tick-label protrusions are zero: the labels fan out along the arc within a panel already sized to the tight sector, so their reach past a Cartesian edge does not call for extra margin. + + The tick-mark clearance is zero: polar ticks point inward from the + arc, so a mark never reaches past the panel edge whatever its length. + A longer tick only pushes its label farther out, and the text + clearance already covers the label. """ def axis_ticks_x_max_height(self, ax: Axes, side: Side) -> float: - return _polar_ticks_clearance_x(self.plot.theme) + return 0 def axis_text_x_max_height(self, ax: Axes, side: Side) -> float: - return _polar_text_clearance_x(self.plot.theme, side) + return _polar_text_height_x(self.plot.theme, side) def axis_ticks_y_max_width(self, ax: Axes, side: Side) -> float: - return _polar_ticks_clearance_y(self.plot.theme) + return 0 def axis_text_y_max_width(self, ax: Axes, side: Side) -> float: - return _polar_text_clearance_y(self.plot.theme, side) + return _polar_text_width_y(self.plot.theme, side) def axis_text_y_top_protrusion(self, location: AxesLocation) -> float: return 0.0 @@ -940,35 +955,43 @@ def axis_text_x_left_protrusion(self, location: AxesLocation) -> float: def axis_text_x_right_protrusion(self, location: AxesLocation) -> float: return 0.0 + def strip_shift(self, st: StripText) -> float: + """ + Outward shift of a strip past the polar decorations, figure space + + A polar panel draws its theta and r axes on the arc perimeter, + so there is no room between the panel and its axis for a strip to + sit "inside". The strip therefore always clears the decorations, + as it does for `strip_placement="outside"` on a Cartesian panel, + regardless of the theme's `strip_placement`. + """ + return self._strip_axis_clearance(st) + -def _polar_text_clearance_x(theme: theme, side: Side) -> float: +def _polar_text_height_x(theme: theme, side: Side) -> float: """ - Fixed/estimated figure-height clearance for `axis_text_x_` - on a polar panel + Estimated figure-height of an `axis_text_x_` label on a polar panel Not measured from rendered artists: a theta axis wraps the full circle, so `axis_at` never resolves a side to measure a tick-label - bounding box from. Estimated instead from theme metrics — the - `axis_text_x_` font size (standing in for label height) plus - its margin — the same rough idea `_strip_switch_pad` uses to turn a - themed point value into a figure fraction. + bounding box from. The font size stands in for the label height, the + same rough idea `_strip_switch_pad` uses to turn a themed point value + into a figure fraction. Bare like its Cartesian counterpart + `axis_text_x_max_height`: the caller adds the tick-label margin. """ name = f"axis_text_x_{side}" if theme.T.is_blank(name): return 0 _, H = theme.getp("figure_size") size_pt = theme.getp((name, "size")) or 0 - margin = theme.getp((name, "margin")) - margin.setup(theme, name) - return size_pt / 72 / H + margin.fig.t + margin.fig.b + return size_pt / 72 / H -def _polar_text_clearance_y(theme: theme, side: Side) -> float: +def _polar_text_width_y(theme: theme, side: Side) -> float: """ - Fixed/estimated figure-width clearance for `axis_text_y_` on - a polar panel + Estimated figure-width of an `axis_text_y_` label on a polar panel - Mirrors `_polar_text_clearance_x` for the r axis: it runs along one + Mirrors `_polar_text_height_x` for the r axis: it runs along one spoke rather than wrapping the circle, but the same "no side to measure" problem applies. """ @@ -977,29 +1000,7 @@ def _polar_text_clearance_y(theme: theme, side: Side) -> float: return 0 W, _ = theme.getp("figure_size") size_pt = theme.getp((name, "size")) or 0 - margin = theme.getp((name, "margin")) - margin.setup(theme, name) - return size_pt / 72 / W + margin.fig.l + margin.fig.r - - -def _polar_ticks_clearance_x(theme: theme) -> float: - """ - Fixed/estimated figure-height clearance for the major x tick - length on a polar panel - """ - _, H = theme.getp("figure_size") - tick_pt = abs(theme.getp("axis_ticks_length_major_x") or 0) - return tick_pt / 72 / H - - -def _polar_ticks_clearance_y(theme: theme) -> float: - """ - Fixed/estimated figure-width clearance for the major y tick length - on a polar panel - """ - W, _ = theme.getp("figure_size") - tick_pt = abs(theme.getp("axis_ticks_length_major_y") or 0) - return tick_pt / 72 / W + return size_pt / 72 / W def _spine_set_position_outward(spine: Spine, axis: Axis, distance: float): diff --git a/plotnine/_mpl/layout_manager/_plot_side_space.py b/plotnine/_mpl/layout_manager/_plot_side_space.py index 20412e53b6..83bc5f5e83 100644 --- a/plotnine/_mpl/layout_manager/_plot_side_space.py +++ b/plotnine/_mpl/layout_manager/_plot_side_space.py @@ -15,6 +15,7 @@ from functools import cached_property from typing import TYPE_CHECKING, Literal, cast +from plotnine.coords import coord_radial from plotnine.exceptions import PlotnineError from plotnine.facets import facet_grid, facet_null, facet_wrap @@ -910,9 +911,9 @@ def __init__(self, plot: ggplot): self.gridspec = plot._gridspec self.sub_gridspec = plot._sub_gridspec items_cls = ( - PlotLayoutItems - if plot.coordinates._projection is None - else PolarPlotLayoutItems + PolarPlotLayoutItems + if isinstance(plot.coordinates, coord_radial) + else PlotLayoutItems ) self.items = items_cls(plot) @@ -1193,6 +1194,36 @@ def _calculate_panel_spacing(self) -> GridSpecParams: hspace, ) + def _polar_gulley_clearance(self) -> tuple[float, float]: + """ + Extra gulley space a polar panel's decorations need, `(sw, sh)` + + A polar panel draws its full theta and r axes on every panel, + not just the ones on the grid's edge, so their ticks and labels + reach into the gulleys on all four sides. Reserve room for both + the left and right decorations in the horizontal gulley and the + top and bottom decorations in the vertical one. As on the + Cartesian free-scale path, the tick-label extent is bare and the + tick-label margin is added here. + """ + items = self.items + theme = self.plot.theme + sw = 0.0 + for side in ("left", "right"): + text = items.axis_text_y_max_width_at("all", side) + if text: + m = theme.get_margin(f"axis_text_y_{side}").fig + text += m.l + m.r + sw += text + items.axis_ticks_y_max_width_at("all", side) + sh = 0.0 + for side in ("top", "bottom"): + text = items.axis_text_x_max_height_at("all", side) + if text: + m = theme.get_margin(f"axis_text_x_{side}").fig + text += m.t + m.b + sh += text + items.axis_ticks_x_max_height_at("all", side) + return sw, sh + def _calculate_panel_spacing_facet_grid(self) -> tuple[float, float]: """ Calculate spacing parts for facet_grid @@ -1208,6 +1239,11 @@ def _calculate_panel_spacing_facet_grid(self) -> tuple[float, float]: self.sw = theme.getp("panel_spacing_x") self.sh = theme.getp("panel_spacing_y") * self.W / self.H + if isinstance(self.plot.coordinates, coord_radial): + extra_sw, extra_sh = self._polar_gulley_clearance() + self.sw += extra_sw + self.sh += extra_sh + # width and height of axes as fraction of figure width & height self.w = (self.panel_width - self.sw * (ncol - 1)) / ncol self.h = (self.panel_height - self.sh * (nrow - 1)) / nrow @@ -1246,26 +1282,33 @@ def _calculate_panel_spacing_facet_wrap(self) -> tuple[float, float]: else: self.sw += space.strip_text + space.strip_switch_pad - # Per-panel axes claim their ticks, labels and label margins in - # the gullies. - if facet.free["x"]: - for side in ("bottom", "top"): - text = self.items.axis_text_x_max_height_at("all", side) - if text: - m = theme.get_margin(f"axis_text_x_{side}").fig - text += m.t + m.b - self.sh += text + self.items.axis_ticks_x_max_height_at( - "all", side - ) - if facet.free["y"]: - for side in ("left", "right"): - text = self.items.axis_text_y_max_width_at("all", side) - if text: - m = theme.get_margin(f"axis_text_y_{side}").fig - text += m.l + m.r - self.sw += text + self.items.axis_ticks_y_max_width_at( - "all", side - ) + # A polar panel draws its full theta and r axes on every panel, + # so its decorations reach into the gullies regardless of free. + # A cartesian panel only shows them on the grid's edge, so per-panel + # axes claim gulley space only where the scales are free. + if isinstance(self.plot.coordinates, coord_radial): + extra_sw, extra_sh = self._polar_gulley_clearance() + self.sw += extra_sw + self.sh += extra_sh + else: + if facet.free["x"]: + for side in ("bottom", "top"): + text = self.items.axis_text_x_max_height_at("all", side) + if text: + m = theme.get_margin(f"axis_text_x_{side}").fig + text += m.t + m.b + self.sh += text + self.items.axis_ticks_x_max_height_at( + "all", side + ) + if facet.free["y"]: + for side in ("left", "right"): + text = self.items.axis_text_y_max_width_at("all", side) + if text: + m = theme.get_margin(f"axis_text_y_{side}").fig + text += m.l + m.r + self.sw += text + self.items.axis_ticks_y_max_width_at( + "all", side + ) # width and height of axes as fraction of figure width & height self.w = (self.panel_width - self.sw * (ncol - 1)) / ncol diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index 820232c128..01eef1513f 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -3338,7 +3338,9 @@ class strip_switch_pad_x(themeable): theme_element : float Size in points. Only has an effect when `strip_placement="outside"` and an axis shares the strip's - side (top or bottom). + side (top or bottom). Polar panels (`coord_radial`) always draw + their theta axis on the arc, so the pad applies to them at any + `strip_placement`. """ @@ -3351,7 +3353,9 @@ class strip_switch_pad_y(themeable): theme_element : float Size in points. Only has an effect when `strip_placement="outside"` and an axis shares the strip's - side (left or right). + side (left or right). Polar panels (`coord_radial`) always draw + their r axis on the arc, so the pad applies to them at any + `strip_placement`. """ @@ -3364,7 +3368,8 @@ class strip_switch_pad(strip_switch_pad_x, strip_switch_pad_y): theme_element : float Size in points. Only has an effect when `strip_placement="outside"` and an axis shares the strip's - side. + side. Polar panels (`coord_radial`) always draw their axes on the + arc, so the pad applies to them at any `strip_placement`. """ diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 30a009691c..89e6d7220e 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -15,6 +15,8 @@ element_blank, element_line, element_text, + facet_grid, + facet_wrap, geom_col, geom_point, ggplot, @@ -1764,3 +1766,84 @@ def test_coord_radial_partial_arc_inner_radius_half_disc(): start=-pi / 2, end=pi / 2, inner_radius=0.3 ) assert p == "coord_radial_partial_arc_inner_radius_half_disc" + + +@pytest.mark.parametrize( + "facet", + [ + facet_wrap("gear", nrow=2), + facet_grid("am", "gear"), + ], +) +def test_coord_radial_faceted_panels_reserve_gulley_for_decorations(facet): + # Every polar panel draws its full theta and r decorations regardless of + # its grid position, so the gulley between panels must reserve room for + # them. Otherwise the tick labels of one panel spill across the gulley + # into a neighbour's panel region. + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + facet + + coord_radial(start=-1, end=1) + ) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + fig = p.axs[0].figure + fig.draw_without_rendering() # pyright: ignore[reportAttributeAccessIssue] + renderer = fig._get_renderer() # pyright: ignore[reportAttributeAccessIssue] + trans = fig.transFigure.inverted() + + panel_boxes = [ax.get_position() for ax in p.axs] + tight_boxes = [ + ax.get_tightbbox(renderer).transformed(trans) for ax in p.axs + ] + + # No panel's decorations may reach into another panel's box. + for i, tight in enumerate(tight_boxes): + for j, box in enumerate(panel_boxes): + if i == j: + continue + assert not tight.overlaps(box), ( + f"panel {i} decorations overlap panel {j}'s region" + ) + + +def test_coord_radial_faceted_strip_clears_theta_decorations(): + # A polar panel draws its theta axis on the arc perimeter, so its ticks + # and labels sit at the apex inside the panel's top edge. The strip band + # must be pushed clear of them rather than landing on top, as it would if + # it were placed "inside" against the bare panel rectangle. + p = ( + ggplot(mtcars, aes("wt", "mpg")) + + geom_point() + + facet_wrap("gear", nrow=2) + + coord_radial(start=-1, end=1) + ) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + fig = p.axs[0].figure + fig.draw_without_rendering() # pyright: ignore[reportAttributeAccessIssue] + renderer = fig._get_renderer() # pyright: ignore[reportAttributeAccessIssue] + trans = fig.transFigure.inverted() + + strip_texts = p.theme.targets.strip_text_x_top + assert strip_texts, "expected top strips on the faceted polar plot" + + for text in strip_texts: + ax = text.ax + strip_bottom = ( + text.patch.get_window_extent(renderer).transformed(trans).y0 + ) + # Highest point reached by any visible theta tick mark or label. + theta_top = max( + artist.get_window_extent(renderer).transformed(trans).y1 + for tick in ax.thetaaxis.majorTicks + for artist in ( + tick.tick1line, + tick.tick2line, + tick.label1, + tick.label2, + ) + if artist.get_visible() + ) + assert strip_bottom >= theta_top, ( + "strip band overlaps the panel's theta decorations" + ) From edeec46307004958762204fcb801e6d6054989ef Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 29 Jul 2026 18:49:23 +0300 Subject: [PATCH 096/126] fix(coord_radial): stop the donut hole from drawing an axis line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wedge boundary spines were partly matplotlib's to decide: `polar` was always on and `inner` followed the geometry, so `axis_line_x` — which replaces the blank `axis_line_x` a theme uses to hide them — drew a line around the donut hole where no theta axis lives. Every spine is now opt-in, visible only through its own axis_line themeable, and the theta leaves gate on an axis being present at that side like the r leaves do. --- plotnine/_mpl/_radial_axes.py | 3 +++ plotnine/themes/themeable.py | 4 ++++ tests/test_coord_radial.py | 21 +++++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index b988c4c100..45389038bf 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -133,6 +133,9 @@ class p9RadialAxes(PolarAxes): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.set_theta_zero_location("N") # 12 o'clock + # All spines are opt-in + self.set_spine_visible("polar", False) + self.set_spine_visible("inner", False) self.set_spine_visible("start", False) self.set_spine_visible("end", False) diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index 01eef1513f..052e9e6b3b 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -1475,6 +1475,8 @@ class axis_line_theta_inside(themeable): def apply_ax(self, ax: Axes): super().apply_ax(ax) + if axis_at(ax, "theta_inside") is None: + return _style_polar_axis_line(self, ax, "inner") def blank_ax(self, ax: Axes): @@ -1489,6 +1491,8 @@ class axis_line_theta_outside(themeable): def apply_ax(self, ax: Axes): super().apply_ax(ax) + if axis_at(ax, "theta_outside") is None: + return _style_polar_axis_line(self, ax, "polar") def blank_ax(self, ax: Axes): diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 89e6d7220e..0de59062f8 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -658,6 +658,27 @@ def test_coord_radial_axis_line_r_start_shows_on_full_circle(): assert p == "coord_radial_axis_line_r_start_shows_on_full_circle" +@pytest.mark.parametrize( + "themeable", ["axis_line", "axis_line_x", "axis_line_theta"] +) +def test_coord_radial_axis_line_theta_skips_donut_hole(themeable: str): + # No theta axis lives on the inner boundary, so every themeable that + # covers the theta line draws the outer circle only and leaves the donut + # hole undrawn -- including the general ones, which replace the blank + # axis_line_x that would otherwise have hidden it. + p = ( + ggplot(mtcars, aes("factor(cyl)", "mpg")) + + geom_col() + + coord_radial(start=-np.pi / 2, end=np.pi / 2, inner_radius=0.3) + + theme(**{themeable: element_line()}) + ) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + ax = p.axs[0] + ax.figure.draw_without_rendering() # pyright: ignore[reportAttributeAccessIssue] + assert ax.spines["polar"].get_visible() + assert not ax.spines["inner"].get_visible() + + def test_coord_radial_ticks_visible_by_default(): # Ticks were previously invisible on every polar panel regardless of # theme, because activation was skipped entirely; this is the From f4801d8bedc96e9fc7271b4c5f4f3ab14b3bdd29 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 29 Jul 2026 19:20:24 +0300 Subject: [PATCH 097/126] refactor(coord_radial): drop the direction parameter ggplot2 deprecated coord_radial(direction) in favour of reverse, and plotnine's version overlapped the same ground: the sweep is now always clockwise and reverse alone decides angular order. A mirrored partial arc is still expressible, since the arc always sweeps forward from start -- (start, end, direction=-1) is (-end, -start, reverse="theta"), which the reverse docs now spell out. Nothing depended on a variable sweep once the parameter went, so _mpl_direction goes with it and its two readers name the clockwise convention directly. --- plotnine/coords/coord_radial.py | 29 ++++++++------------------- tests/test_coord_radial.py | 35 ++++----------------------------- 2 files changed, 12 insertions(+), 52 deletions(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 07790aa77a..685cfc94a7 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -84,10 +84,6 @@ class coord_radial(coord): Ending angle in radians, measured clockwise from 12 o'clock. Equivalent angles are interpreted as the same endpoint. `None` (default) gives a full circle. - direction : - Angular rotation sense: `1` = clockwise (default), `-1` = - counter-clockwise. Applied regardless of `end`, so it also sets - the sweep direction of a partial arc. expand : Add a small buffer around the data on the radius axis. Default `True`. @@ -118,6 +114,10 @@ class coord_radial(coord): toward the centre. * `"thetar"` — both the angular and radial axes are reversed. + The sweep itself is always clockwise. A full circle runs the other + way with `reverse="theta"`; a partial arc mirrors onto the opposite + side with `start=-end, end=-start, reverse="theta"`. + Notes ----- Theta-axis tick labels are shown by default. Since a polar axes' x-axis @@ -134,7 +134,6 @@ def __init__( theta: str = "x", start: float = 0, end: float | None = None, - direction: Literal[-1, 1] = 1, expand: bool = True, inner_radius: float = 0, rotate_angle: bool = False, @@ -150,7 +149,6 @@ def __init__( self.theta = theta self.start = start self.end = end - self.direction: Literal[-1, 1] = direction self.expand = expand self.inner_radius = inner_radius self.rotate_angle = rotate_angle @@ -306,18 +304,6 @@ def _is_full_circle(self) -> bool: start, end = self._arc_range return bool(np.isclose(end - start, 2 * np.pi)) - @property - def _mpl_direction(self) -> Literal[-1, 1]: - """ - Matplotlib theta direction for this coordinate system - - For matplotlib -1 is clockwise and +1 is counter-clockwise, the - opposite of plotnine's own `direction` convention. `reverse="theta"` - does not enter here: it reverses the data along the arc (in - `_to_radians`), not the physical sweep, so the wedge stays put. - """ - return self.direction * -1 - @cached_property def _r_axis_side(self) -> Literal["r_start", "r_end"]: """ @@ -399,10 +385,10 @@ def transform( if self.rotate_angle and "angle" in data and "x" in data: # Align marks tangentially to their spoke. The PolarAxes places # a data theta t at on-screen angle (deg, CCW from East) - # screen = 90 + mpl_dir * degrees(t), mpl_dir = -1 if cw else 1 + # screen = 90 - degrees(t), since the sweep is clockwise # Tangential text rotation is screen - 90; folding into (-90, 90] # keeps labels upright (a bottom label reads "6", not "9"). - rot = self._mpl_direction * np.degrees(data["x"]) + rot = -np.degrees(data["x"]) rot = (rot + 90.0) % 180.0 - 90.0 data["angle"] = data["angle"] + rot return data @@ -461,7 +447,8 @@ def setup_ax( view = cast("radial_panel_view", panel_params) radial_ax = cast("p9RadialAxes", ax) - radial_ax.set_theta_direction(self._mpl_direction) + # plotnine sweeps clockwise, which is matplotlib's -1. + radial_ax.set_theta_direction(-1) if view.x.sec is not None: warn( diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 0de59062f8..7b4ff7d462 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -83,11 +83,7 @@ def trained_scales( return scale_x, scale_y -def radial_axis_sides( - start: float, - *, - direction: Literal[-1, 1] = 1, -) -> tuple[float, float]: +def radial_axis_sides(start: float) -> tuple[float, float]: """ Signed start-spoke sides occupied by the first r tick and label """ @@ -96,7 +92,6 @@ def radial_axis_sides( + geom_col() + coord_radial( start=start, - direction=direction, inner_radius=0.1, ) + theme(axis_line_y=element_line()) @@ -719,7 +714,6 @@ def test_coord_radial_classifies_full_circle_from_arc_range(): assert coord_radial(start=1, end=1 + 2 * np.pi)._is_full_circle assert coord_radial( end=2 * np.pi, - direction=-1, reverse="theta", )._is_full_circle assert not coord_radial(start=0, end=np.pi)._is_full_circle @@ -779,23 +773,13 @@ def test_coord_radial_full_circle_panel_params_ignore_end_representation(): assert views[0].x.labels == views[1].x.labels -@pytest.mark.parametrize("direction", [1, -1]) -def test_coord_radial_equivalent_endpoints_have_same_sweep( - direction: Literal[-1, 1], -): +def test_coord_radial_equivalent_endpoints_have_same_sweep(): pi = 3.14159 - coords = [ - coord_radial(start=pi, end=end, direction=direction) - for end in (0, 2 * pi) - ] + coords = [coord_radial(start=pi, end=end) for end in (0, 2 * pi)] angles = [coord._to_radians([1, 2, 3], (0.4, 3.6)) for coord in coords] assert_allclose(angles[0], angles[1], rtol=1e-5) assert all(np.all(np.diff(values) > 0) for values in angles) - assert [coord._mpl_direction for coord in coords] == [ - -direction, - -direction, - ] def test_coord_radial_no_longer_has_r_axis_inside(): @@ -1074,14 +1058,13 @@ def test_coord_radial_reverse_theta_keeps_wedge_flips_order(): # reverse="theta" runs the data the other way around the SAME arc; it # must not mirror the wedge onto the opposite side. So the mapped theta # breaks reflect about the arc midpoint (start + end - break), while the - # arc itself and the physical draw direction are unchanged. + # arc itself is unchanged. scale_x, scale_y = trained_scales() base = coord_radial(start=np.pi, end=np.pi / 2, expand=False) rev = coord_radial( start=np.pi, end=np.pi / 2, expand=False, reverse="theta" ) assert base._arc_range == rev._arc_range - assert base._mpl_direction == rev._mpl_direction pv_base = base.setup_panel_params(scale_x, scale_y) pv_rev = rev.setup_panel_params(scale_x, scale_y) @@ -1341,16 +1324,6 @@ def test_coord_radial_r_axis_rotates_with_start(): assert label_side > 0 -def test_coord_radial_r_axis_flips_with_effective_sweep(): - clockwise_tick, clockwise_label = radial_axis_sides(0) - counter_tick, counter_label = radial_axis_sides(0, direction=-1) - - assert clockwise_tick > 0 - assert clockwise_label > 0 - assert counter_tick < 0 - assert counter_label < 0 - - @pytest.mark.parametrize( ("start", "end", "expected_end"), [(0, np.pi, np.pi), (np.pi, 0, 2 * np.pi)], From 2777d66fa5c9467f61928cb03982ee60860a2719 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 29 Jul 2026 21:11:52 +0300 Subject: [PATCH 098/126] docs(coord_radial): correct the polar spine visibility rationale --- plotnine/_mpl/_radial_axes.py | 13 ++++++++----- plotnine/themes/themeable.py | 2 -- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index 45389038bf..ec147d948d 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -331,12 +331,15 @@ def set_spine_visible(self, name: str, visible: bool) -> None: `PolarAxes.draw` derives `inner`/`start`/`end` spine visibility from pure geometry (donut hole present, arc partial) and paints - the spine in that same call, so a plain `set_visible` on a spine - is overwritten and never matters. + the spine in that same call, so a plain `set_visible` on one of + these spines is overwritten and never matters. `polar` is not + subject to that override, but a caller has no way to tell which + spine names need the protection, so all four are routed through + this same method. """ - # This method makes the spine visible and replaces the spine's - # own `set_visible` with a no-op function to block matplotlib's - # internal call before it paints. + # Set the spine to the requested visibility, then replace its + # own `set_visible` with a no-op so matplotlib's internal call + # cannot overwrite it before painting. spine = self.spines[name] spine_cls = type(spine) spine_cls.set_visible(spine, visible) diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index 052e9e6b3b..85ea685f4e 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -1414,8 +1414,6 @@ def _style_polar_axis_line(themeable, ax, spine): rather than the `properties` dict, since that's what makes the choice stick across draws. """ - if spine not in ax.spines: - return properties = themeable._get_properties(omit=("solid_capstyle",)) visible = properties.pop("visible", True) if "zorder" not in properties: From 57a77e8d47f8f8c02692c4edaa3bf2bb2fceee77 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 30 Jul 2026 17:18:47 +0300 Subject: [PATCH 099/126] refactor(guides)!: remove guide_axis_theta It was never a guide: a bare dataclass outside the Registry, with no train/draw and no route into the guides pipeline, since _bind_source matches guides by scale aesthetic and theta is not one. coord_radial read guides.theta directly through cast(Any)/getattr, and six of its seven parameters did nothing. Position guides -- guide_axis and a theta sibling -- will be built comprehensively later, so the placeholder goes now instead of being harmonised twice. Theta labels lose their only rotation control with it, so the rotated-label clearance test goes too. The layout code that test covered stays put: it becomes reachable again when position guides land. --- plotnine/__init__.py | 2 - plotnine/coords/coord_radial.py | 20 ----- plotnine/guides/__init__.py | 2 - plotnine/guides/guide_axis_theta.py | 111 ---------------------------- plotnine/guides/guides.py | 5 -- tests/test_coord_radial.py | 17 ----- 6 files changed, 157 deletions(-) delete mode 100644 plotnine/guides/guide_axis_theta.py diff --git a/plotnine/__init__.py b/plotnine/__init__.py index 4b92a0b8c4..98507e5ece 100644 --- a/plotnine/__init__.py +++ b/plotnine/__init__.py @@ -90,7 +90,6 @@ save_as_pdf_pages, ) from .guides import ( - guide_axis_theta, guide_colorbar, guide_colourbar, guide_legend, @@ -353,7 +352,6 @@ "ggplot", "ggsave", "ggtitle", - "guide_axis_theta", "guide_colorbar", "guide_colourbar", "guide_legend", diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 685cfc94a7..d67db3c124 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -512,31 +512,11 @@ def setup_ax( radial_ax.set_rlabel_position(np.degrees(self.start)) ax.tick_params(axis="x", which="major", direction="out") - if (angle := self._theta_guide_angle()) is not None: - # Use Matplotlib's 'auto' mode so labels orient tangentially - # to the arc, with `angle` as an offset — matching ggplot2's - # guide_axis_theta() semantics where angle=0 means tangential. - # ax.tick_params(labelrotation=...) always sets 'default' mode - # (absolute degrees), so we patch each tick directly instead. - for tick in ax.xaxis.get_major_ticks(): - tick._labelrotation = ( # pyright: ignore[reportAttributeAccessIssue] - "auto", - angle, - ) # Allow geom_text labels to extend past the polar axes bounding box # (e.g. spoke labels placed just beyond the outermost bar tip). for text in ax.texts: text.set_clip_on(False) - def _theta_guide_angle(self) -> float | None: - """ - Return the angle from guides(theta=guide_axis_theta(...)) - """ - if self._owner is None: - return None - guide = cast("Any", self._owner.guides.theta) - return getattr(guide, "angle", None) - def aspect(self, panel_params: panel_view) -> float: left, right, bottom, top = polar_bbox( self._arc_range, self.inner_radius diff --git a/plotnine/guides/__init__.py b/plotnine/guides/__init__.py index 690a8ef0cc..ad64f75fbb 100644 --- a/plotnine/guides/__init__.py +++ b/plotnine/guides/__init__.py @@ -1,10 +1,8 @@ -from .guide_axis_theta import guide_axis_theta from .guide_colorbar import guide_colorbar, guide_colourbar from .guide_legend import guide_legend from .guides import guides __all__ = ( - "guide_axis_theta", "guide_colorbar", "guide_colourbar", "guide_legend", diff --git a/plotnine/guides/guide_axis_theta.py b/plotnine/guides/guide_axis_theta.py deleted file mode 100644 index e1d536423d..0000000000 --- a/plotnine/guides/guide_axis_theta.py +++ /dev/null @@ -1,111 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass, field - -from ..themes.theme import theme as Theme - - -@dataclass -class guide_axis_theta: - """ - Theta-axis guide for radial coordinates. - - A specialized guide used in :class:`~plotnine.coords.coord_radial` to - control how tick labels are rendered on the theta (angular) axis. - Unlike legend-style guides, this guide is not drawn as a box or colorbar; - it is consumed directly by ``coord_radial`` when it positions and rotates - the outer tick labels. - - Parameters - ---------- - title : - Title for the guide. Currently unused; theta-axis labels do not - display a title in Matplotlib's polar axes. - theme : - A :class:`~plotnine.themes.theme.theme` object to style the guide - individually. Currently unused. - angle : - Orientation of the tick labels, in degrees, measured as an offset - from the **tangential direction** at each tick position. - - * ``None`` (default) - labels are not rotated by this guide; the - Matplotlib default (horizontal, i.e. ``0`` absolute) is used. - * ``0`` - labels are oriented tangentially, parallel to the arc. - This matches the ggplot2 heuristic default and is the most common - choice. - * ``90`` - labels point radially outward along the spoke. - * Any other float - a corresponding offset from the tangential - direction. - - .. note:: - This differs from Matplotlib's ``tick_params(labelrotation=N)``, - which treats ``N`` as an **absolute** angle. Here ``angle`` is - always relative to the tangent at each label's position. - - minor_ticks : - Whether to draw minor ticks. Not yet implemented; the value is - stored but has no effect. - cap : - Whether to cap the axis line back to the outermost breaks. Not - yet implemented; the value is stored but has no effect. - order : - Order of this guide among multiple guides. Not yet implemented. - position : - Guide position (``"top"``, ``"bottom"``, ``"left"``, or - ``"right"``). Not yet implemented. - - See Also - -------- - :class:`~plotnine.coords.coord_radial` : The coordinate system that - consumes this guide. - plotnine.guides : Add guides to a plot with ``guides(theta=...)``. - - Examples - -------- - Make theta labels follow the arc (tangential, matching ggplot2's default - heuristic): - - .. code-block:: python - - from plotnine import ggplot, aes, geom_point, coord_radial, guides - from plotnine.guides import guide_axis_theta - from plotnine.data import mtcars - - ( - ggplot(mtcars, aes("disp", "mpg")) - + geom_point() - + coord_radial() - + guides(theta=guide_axis_theta(angle=0)) - ) - - .. note:: - ``coord_radial`` shows theta-axis labels by default. Hide them with - ``theme(axis_text_x=element_blank())`` if a plot needs no theta axis. - """ - - title: str | None = None - """Title of the guide. Currently unused.""" - - theme: Theme = field(default_factory=Theme) - """A theme to style the guide. Currently unused.""" - - angle: float | None = None - """ - Offset from the tangential direction in degrees. - - ``None`` keeps Matplotlib's default (horizontal labels). - ``0`` makes labels tangential (parallel to the arc). - ``90`` makes labels radial (pointing outward). - """ - - minor_ticks: bool | None = None - """Whether to draw minor ticks. Not yet implemented.""" - - cap: bool | None = None - """Whether to cap the axis line. Not yet implemented.""" - - order: int = 0 - """Order of this guide among multiple guides. Not yet implemented.""" - - position: str | None = None - """Guide position. Not yet implemented.""" diff --git a/plotnine/guides/guides.py b/plotnine/guides/guides.py index 106b0bb599..3764adefc4 100644 --- a/plotnine/guides/guides.py +++ b/plotnine/guides/guides.py @@ -29,7 +29,6 @@ from plotnine import ( ggplot, - guide_axis_theta, guide_colorbar, guide_legend, theme, @@ -53,7 +52,6 @@ guide_legend | guide_colorbar | Literal["legend", "colorbar"] ) LegendOnly: TypeAlias = guide_legend | Literal["legend"] - ThetaGuide: TypeAlias = guide_axis_theta class LegendOwner(Protocol): """ @@ -123,9 +121,6 @@ class guides: colour: Optional[LegendOnly | NoGuide] = None """Guide for colour scale.""" - theta: Optional[ThetaGuide | NoGuide] = None - """Guide for theta axis labels in radial coordinates.""" - def __post_init__(self): self.plot: ggplot self.plot_scales: Scales diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 7b4ff7d462..51ddd794a6 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -20,8 +20,6 @@ geom_col, geom_point, ggplot, - guide_axis_theta, - guides, scale_x_reverse, scale_y_reverse, theme, @@ -347,21 +345,6 @@ def test_coord_radial_theta_label_clearance_ignores_blank_tick_length(): ) -def test_coord_radial_rotated_theta_label_clearance(): - p = _theta_margin_plot({}) + guides(theta=guide_axis_theta(angle=35)) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] - - assert_allclose( - [ - _theta_label_clearance(ax, "120"), - _theta_label_clearance(ax, "330"), - ], - 0, - atol=0.01, - ) - - def test_coord_radial_theta_label_clearance_does_not_accumulate(): p = _theta_margin_plot({}) p.draw_test() # pyright: ignore[reportAttributeAccessIssue] From b08e70347f026d972f8ef5c8edf8b10f20d635eb Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 11 Aug 2026 14:22:56 +0300 Subject: [PATCH 100/126] test(coord_radial): drop tests that assert render state The polar tests reached into spines, tick artists, clip paths and text layout to assert what a plot draws. Baseline images state the same things and survive refactoring of the drawing code, so only the claims an image cannot make are kept: arc-range normalisation, the wedge aspect ratio, label swapping, and the two API contracts. The baseline images themselves follow. --- tests/test_coord_radial.py | 1815 ++---------------------------------- 1 file changed, 52 insertions(+), 1763 deletions(-) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 51ddd794a6..8c10e32964 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -1,1826 +1,115 @@ -from dataclasses import replace +import warnings from math import pi -from typing import Any, Literal import numpy as np -import pandas as pd import pytest -from matplotlib.axes import Axes -from matplotlib.backends.backend_agg import FigureCanvasAgg from numpy.testing import assert_allclose from plotnine import ( aes, coord_radial, - element_blank, - element_line, - element_text, - facet_grid, - facet_wrap, - geom_col, geom_point, ggplot, - scale_x_reverse, - scale_y_reverse, - theme, + scale_y_continuous, ) -from plotnine._mpl._radial_axis import p9ThetaTick from plotnine.coords.coord_radial import polar_bbox from plotnine.data import mtcars -from plotnine.iapi import ( - labels_view, - radial_panel_view, - scale_position_view, -) -from plotnine.scales import scale_x_continuous, scale_y_continuous, sec_axis -from plotnine.themes.elements import margin - - -def _dummy_scale_view() -> scale_position_view: - """Minimal position-scale state for radial coordinate unit tests""" - return scale_position_view( - scale=None, # type: ignore[arg-type] - aesthetics=[], - name=None, - limits=(0, 1), - range=(0, 1), - breaks=[], - minor_breaks=np.array([], dtype=float), - labels=[], - position="bottom", - ) - - -def make_panel_view( - theta_range: tuple[float, float], - r_range: tuple[float, float], -) -> radial_panel_view: - """Radial panel state with distinct data and display ranges""" - theta = replace(_dummy_scale_view(), range=theta_range) - r = replace(_dummy_scale_view(), range=r_range) - return radial_panel_view( - x=replace(theta, range=(0, 2 * np.pi)), - y=replace(r), - theta=theta, - r=r, - ) - - -def trained_scales( - x=(0, 10), - y=(0, 10), - x_breaks=(0, 5, 10), - y_breaks=(0, 5, 10), - x_labels=("0", "5", "10"), - y_labels=("0", "5", "10"), -): - scale_x = scale_x_continuous(breaks=x_breaks, labels=x_labels) - scale_y = scale_y_continuous(breaks=y_breaks, labels=y_labels) - scale_x.train(x) - scale_y.train(y) - return scale_x, scale_y - - -def radial_axis_sides(start: float) -> tuple[float, float]: - """ - Signed start-spoke sides occupied by the first r tick and label - """ - p = ( - ggplot(mtcars, aes("factor(cyl)", "mpg", fill="factor(cyl)")) - + geom_col() - + coord_radial( - start=start, - inner_radius=0.1, - ) - + theme(axis_line_y=element_line()) - ) - p.draw_test() - ax = p.axs[0] - ax.figure.draw_without_rendering() - tick = next( - t - for t in ax.yaxis.get_major_ticks() - if t.get_loc() > 0 and t.label1.get_text() - ) - radius = tick.get_loc() - spoke_point = ax.transData.transform((start, radius)) - centre = ax.transData.transform((start, 0)) - label_box = tick.label1.get_window_extent( - renderer=ax.figure._get_renderer() # pyright: ignore - ) - label_point = np.asarray(label_box.get_points()).mean(axis=0) - spoke = spoke_point - centre - label_offset = label_point - spoke_point - label_side = spoke[0] * label_offset[1] - spoke[1] * label_offset[0] - - marker = tick.tick1line._marker # pyright: ignore[reportPrivateUsage] - marker_vertices = ( - marker.get_path().transformed(marker.get_transform()).vertices - ) - tick_offset = marker_vertices[ - np.argmax(np.linalg.norm(marker_vertices, axis=1)) - ] - tick_side = spoke[0] * tick_offset[1] - spoke[1] * tick_offset[0] - return float(tick_side), float(label_side) - - -def _theta_margin_plot( - text_margin: (margin | dict[Literal["t", "r", "b", "l", "unit"], Any]), - *, - inner_radius: float = 0, - labels: tuple[str, str, str] = ("0", "120", "330"), - **text_properties: Any, -) -> ggplot: - data = pd.DataFrame({"x": [0, 120, 330], "y": [1, 2, 3]}) - return ( - ggplot(data, aes("x", "y")) - + geom_point() - + scale_x_continuous( - breaks=[0, 120, 330], - labels=labels, - limits=(0, 360), - expand=(0, 0), - ) - + coord_radial(inner_radius=inner_radius) - + theme( - axis_text_x=element_text( - margin=text_margin, - **text_properties, - ), - axis_text_y=element_blank(), - axis_ticks_y=element_blank(), - axis_title=element_blank(), - ) - ) - - -@pytest.mark.parametrize( - ("text_margin", "expected"), - [ - ({"t": 2, "r": 4, "b": 1, "l": 3}, 4), - ({"t": -2, "r": 0, "b": 0, "l": 0}, 0), - ({"t": -5, "r": -2, "b": -8, "l": -4}, -2), - (margin(t=1 / 72, r=3 / 72, unit="in"), 3), - ], -) -def test_coord_radial_theta_margin_uses_largest_side( - text_margin: (margin | dict[Literal["t", "r", "b", "l", "unit"], Any]), - expected: float, -): - p = _theta_margin_plot(text_margin) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - - assert p.axs[0].xaxis.get_major_ticks()[0].get_pad() == expected - - -def _theta_label_clearance( - ax: Axes, - text: str, - side: Literal["inside", "outside"] = "outside", -) -> float: - canvas = FigureCanvasAgg(ax.figure) - canvas.draw() - renderer = canvas.get_renderer() - label_name = "label1" if side == "inside" else "label2" - tickline_name = "tick1line" if side == "inside" else "tick2line" - tick = next( - tick - for tick in ax.xaxis.get_major_ticks() - if getattr(tick, label_name).get_text() == text - ) - tick.draw(renderer) - - label = getattr(tick, label_name) - tickline = getattr(tick, tickline_name) - boundary = tickline.get_transform().transform(tickline.get_xydata())[0] - centre = ax.transData.transform((tick.get_loc(), ax.get_rorigin())) - outward = boundary - centre - outward /= np.linalg.norm(outward) - if side == "inside": - outward *= -1 +from plotnine.exceptions import PlotnineWarning +from plotnine.iapi import labels_view - tick_length = ( - tick._size # pyright: ignore[reportPrivateUsage] - * ax.figure.dpi - / 72 - if tickline.get_visible() - else 0 - ) - reference = boundary + outward * tick_length - assert isinstance(tick, p9ThetaTick) - corners = tick._label_bounds(label, renderer).corners() - return float(np.min((corners - reference) @ outward)) - - -def _theta_label_bounds( - text: str, -) -> tuple[np.ndarray, np.ndarray, float]: - p = _theta_margin_plot({}) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] - canvas = FigureCanvasAgg(ax.figure) - canvas.draw() - renderer = canvas.get_renderer() - tick = ax.xaxis.get_major_ticks()[0] - assert isinstance(tick, p9ThetaTick) - label = tick.label2 - label.set_text(text) - - anchor = label.get_transform().transform(label.get_position()) - logical, parts, _ = label._get_layout(renderer) - descent = parts[-1][1][2] - logical = logical.translated(*anchor) - corrected = tick._label_bounds(label, renderer) - return logical.get_points(), corrected.get_points(), descent - - -@pytest.mark.parametrize( - "text", - ["300", "-3.0", "+3E0", "3e-2", "\N{MINUS SIGN}300"], -) -def test_coord_radial_numeric_theta_label_bounds_remove_descent(text: str): - logical, corrected, descent = _theta_label_bounds(text) - - assert_allclose(corrected[0], logical[0] + [0, descent]) - assert_allclose(corrected[1], logical[1]) - - -@pytest.mark.parametrize( - "text", - ["3g0", "angle", "3\n0", "$300$", "", "٣٠٠"], -) -def test_coord_radial_other_theta_label_bounds_are_unchanged(text: str): - logical, corrected, _ = _theta_label_bounds(text) - - assert_allclose(corrected, logical) - - -def test_coord_radial_theta_label_clearance_is_uniform(): - p = _theta_margin_plot({}) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] +def test_arc_range_normalizes_end_forward(): + assert coord_radial(start=1, end=4)._arc_range == (1, 4) + assert_allclose(coord_radial(start=pi, end=0)._arc_range, (pi, 2 * pi)) assert_allclose( - [ - _theta_label_clearance(ax, "120"), - _theta_label_clearance(ax, "330"), - ], - 0, - atol=0.01, + coord_radial(start=pi, end=2 * pi)._arc_range, (pi, 2 * pi) ) + assert_allclose(coord_radial(start=0, end=2 * pi)._arc_range, (0, 2 * pi)) + assert coord_radial(start=1, end=1)._arc_range == (1, 1) + assert coord_radial(start=1)._arc_range == (1, 1 + 2 * pi) -def test_coord_radial_theta_label_descent_correction(): - p = _theta_margin_plot({}, labels=("0", "300", "3g0")) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] +def test_classifies_full_circle_from_arc_range(): + assert coord_radial()._is_full_circle + assert coord_radial(start=0, end=2 * pi)._is_full_circle + assert coord_radial(start=1, end=1 + 2 * pi)._is_full_circle + assert coord_radial(end=2 * pi, reverse="theta")._is_full_circle + assert not coord_radial(start=0, end=pi)._is_full_circle + assert not coord_radial(start=1, end=1)._is_full_circle - assert_allclose( - [ - _theta_label_clearance(ax, "300"), - _theta_label_clearance(ax, "3g0"), - ], - 0, - atol=0.01, - ) +def test_polar_bbox_full_circle_is_unit_square(): + assert polar_bbox((0.0, 2 * pi)) == (0.0, 1.0, 0.0, 1.0) -def test_coord_radial_theta_label_ignores_alignment(): - p = _theta_margin_plot({}, ha="left", va="top") - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] - assert_allclose( - [ - _theta_label_clearance(ax, "120"), - _theta_label_clearance(ax, "330"), - ], - 0, - atol=0.01, - ) +def test_aspect_is_square(): + assert coord_radial().aspect(None) == 1 @pytest.mark.parametrize( - ("text_margin", "expected"), + ("start", "end", "expected"), [ - ({"t": 2, "r": 4, "b": 1, "l": 3}, 4), - ({"t": -2, "r": 0, "b": 0, "l": 0}, 0), - ({"t": -5, "r": -2, "b": -8, "l": -4}, -2), + (-pi / 2, pi / 2, 0.5), # right half-disc: wide box + (0.0, 0.25, pytest.approx(4.041, abs=1e-2)), # thin sliver: tall box + (-pi / 2, 0.0, 1.0), # quarter: square + (0.0, 2 * pi, 1.0), # full circle: square ], ) -def test_coord_radial_theta_label_clearance_uses_margin( - text_margin: dict[Literal["t", "r", "b", "l", "unit"], Any], - expected: float, -): - p = _theta_margin_plot(text_margin, size=16) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] - expected_pixels = expected * ax.figure.dpi / 72 - - assert_allclose( - [ - _theta_label_clearance(ax, "120"), - _theta_label_clearance(ax, "330"), - ], - expected_pixels, - atol=0.01, - ) - - -def test_coord_radial_theta_label_clearance_ignores_blank_tick_length(): - p = _theta_margin_plot({}) + theme( - axis_ticks_x=element_blank(), - axis_ticks_length_major_x=20, - ) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] - - assert_allclose( - [ - _theta_label_clearance(ax, "120"), - _theta_label_clearance(ax, "330"), - ], - 0, - atol=0.01, - ) - - -def test_coord_radial_theta_label_clearance_does_not_accumulate(): - p = _theta_margin_plot({}) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] - first = _theta_label_clearance(ax, "330") - - ax.figure.draw_without_rendering() # pyright: ignore[reportAttributeAccessIssue] - second = _theta_label_clearance(ax, "330") - ax.figure.set_size_inches(8, 5) - ax.figure.draw_without_rendering() # pyright: ignore[reportAttributeAccessIssue] - resized = _theta_label_clearance(ax, "330") - - assert_allclose([first, second, resized], 0, atol=0.01) - - -def test_coord_radial_inside_theta_label_clearance(): - p = _theta_margin_plot({}, inner_radius=0.3) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] - ax.tick_params( - axis="x", - bottom=True, - labelbottom=True, - top=False, - labeltop=False, - ) - - assert_allclose( - [ - _theta_label_clearance(ax, "120", "inside"), - _theta_label_clearance(ax, "330", "inside"), - ], - 0, - atol=0.01, - ) - - -def test_coord_radial_theta_label_clearance(): - p = _theta_margin_plot({}, labels=("0", "300", "3g0")) - assert p == "coord_radial_theta_label_clearance" - - -def test_coord_radial_setup_panel_params_theta_x(): - scale_x, scale_y = trained_scales( - x_breaks=(0, 5, 10), - x_labels=("0", "5", "10"), - y_breaks=(0, 2, 5, 10), - y_labels=("0", "2", "5", "10"), - ) - coord = coord_radial(theta="x", start=np.pi / 4, expand=False) - - panel_params = coord.setup_panel_params(scale_x, scale_y) - - assert panel_params.theta.range == (0, 10) - assert panel_params.r.range == (0, 10) - assert panel_params.x.range == (np.pi / 4, np.pi / 4 + 2 * np.pi) - assert panel_params.theta is not panel_params.x - assert panel_params.r is not panel_params.y - assert panel_params.theta.breaks == [0, 5, 10] - # Theta breaks are kept as radian positions (0,5,10 over [0,10] -> - # start + fraction * 2pi), not cleared the way coord_polar used to. - assert_allclose( - panel_params.x.breaks, - [np.pi / 4, np.pi / 4 + np.pi, np.pi / 4 + 2 * np.pi], - ) - assert panel_params.x.labels == ["0", "5", "10"] - assert panel_params.y.breaks == [0, 2, 5, 10] - - -def test_coord_radial_setup_panel_params_theta_y(): - scale_x, scale_y = trained_scales( - x_breaks=(0, 2, 5, 10), - x_labels=("0", "2", "5", "10"), - ) - coord = coord_radial(theta="y", expand=False) - - panel_params = coord.setup_panel_params(scale_x, scale_y) - - assert panel_params.theta.range == (0, 10) - assert panel_params.r.range == (0, 10) - assert panel_params.y.breaks == [0, 2, 5, 10] - - -def test_coord_radial_theta_y_axis_title_positions_stay_valid(): - # theta="y" transposes the x and y panel views. Their `position` - # must stay on the side the layout manager repositions -- x on - # bottom/top, y on left/right -- or the axis titles never move - # from the figure origin. - scale_x, scale_y = trained_scales() - - panel_params = coord_radial(theta="y").setup_panel_params(scale_x, scale_y) - - assert panel_params.x.position in ("bottom", "top") - assert panel_params.y.position in ("left", "right") - - -def test_coord_radial_discrete_theta_spans_full_circle(): - p = ( - ggplot(mtcars, aes("factor(cyl)", "mpg", fill="factor(cyl)")) - + geom_col() - + coord_radial(start=0, end=2 * np.pi, inner_radius=0) - ) - built = p.build_test() - panel_params = built.layout.panel_params[0] - transformed = built.coordinates.transform( - built.layers[0].data.copy(), - panel_params, - ) - - expected = built.coordinates._to_radians( - [1, 2, 3], - panel_params.theta.range, - ) - actual = np.sort(transformed["x"].unique()) - - assert_allclose(actual, expected) - assert np.ptp(actual) > np.pi - - -def test_coord_radial_setup_panel_params_per_panel_independent(): - # one instance reused across panels (faceting) - coord = coord_radial(expand=False) - sx1, sy1 = trained_scales(y=(0, 10)) - pv1 = coord.setup_panel_params(sx1, sy1) - sx2, sy2 = trained_scales(y=(0, 100)) - pv2 = coord.setup_panel_params(sx2, sy2) - assert pv1.y.range == (0, 10) - assert pv2.y.range == (0, 100) - - -def test_coord_radial_transform_uses_per_panel_range_not_last(): - coord = coord_radial() # one instance reused across panels (faceting) - sx1, sy1 = trained_scales(x=(0, 10), y=(0, 10)) - pv1 = coord.setup_panel_params(sx1, sy1) - sx2, sy2 = trained_scales(x=(0, 20), y=(0, 20)) - coord.setup_panel_params(sx2, sy2) # would clobber shared state - # Transform a point against panel 1 AFTER panel 2 was set up. - out = coord.transform(pd.DataFrame({"x": [5], "y": [5]}), pv1) - # x=5 in panel 1's theta range (0,10) -> norm 0.5 -> pi (half turn). - assert_allclose(out.loc[0, "x"], np.pi) - - -def test_coord_radial_transforms_segment_endpoints_theta_x(): - coord = coord_radial(theta="x") - pv = make_panel_view((0, 10), (0, 10)) - data = pd.DataFrame({"x": [0], "y": [1], "xend": [10], "yend": [2]}) - - out = coord.transform(data, pv) - - assert out.loc[0, "x"] == 0 - assert out.loc[0, "y"] == 1 - assert np.isclose(out.loc[0, "xend"], 2 * np.pi) - assert out.loc[0, "yend"] == 2 - - -def test_coord_radial_transforms_segment_endpoints_theta_y(): - coord = coord_radial(theta="y") - pv = make_panel_view((0, 10), (0, 10)) - data = pd.DataFrame({"x": [1], "y": [0], "xend": [2], "yend": [10]}) - - out = coord.transform(data, pv) - - assert out.loc[0, "x"] == 0 - assert out.loc[0, "y"] == 1 - assert np.isclose(out.loc[0, "xend"], 2 * np.pi) - assert out.loc[0, "yend"] == 2 - - -def test_coord_radial_transforms_theta_y_without_endpoints(): - coord = coord_radial(theta="y") - pv = make_panel_view((0, 10), (0, 10)) - data = pd.DataFrame({"x": [1], "y": [5]}) - - out = coord.transform(data, pv) - - assert_allclose(out.loc[0, "x"], np.pi) - assert out.loc[0, "y"] == 1 - - -def test_coord_radial_munches_before_radian_transform(): - coord = coord_radial() - pv = make_panel_view((0, 10), (0, 10)) - data = pd.DataFrame({"x": [0, 10], "y": [1, 2], "group": [1, 1]}) - - out = coord.transform(data, pv, munch=True) - - assert len(out) > len(data) - assert out["x"].between(0, 2 * np.pi).all() +def test_aspect_matches_wedge(start: float, end: float, expected: float): + assert coord_radial(start=start, end=end).aspect(None) == expected -def test_coord_radial_leaves_non_position_data_unchanged(): - coord = coord_radial() - data = pd.DataFrame({"label": ["A"]}) - - assert coord.transform(data, None) is data - - -def test_coord_radial_distance_and_backtransform_theta_x(): - coord = coord_radial() - pv = make_panel_view((0, 10), (0, 20)) - - distance = coord.distance(pd.Series([0, 10]), pd.Series([0, 10]), pv) - - assert_allclose(distance, [np.sqrt(1.25)]) - assert coord.backtransform_range(pv).x == (0, 10) - assert coord.backtransform_range(pv).y == (0, 20) - - -def test_coord_radial_distance_and_backtransform_theta_y(): - coord = coord_radial(theta="y") - pv = make_panel_view((0, 10), (0, 20)) - - distance = coord.distance(pd.Series([0, 10]), pd.Series([0, 10]), pv) - - assert_allclose(distance, [np.sqrt(1.25)]) - assert coord.backtransform_range(pv).x == (0, 20) - assert coord.backtransform_range(pv).y == (0, 10) +def test_aspect_uses_zero_margin_for_half_disc(): + # margin 0 (not ggplot2's 0.05) -> exactly 0.5, so the panel matches the + # tight wedge and the sector is not stretched. + assert coord_radial(start=-pi / 2, end=pi / 2).aspect(None) == 0.5 -def test_coord_radial_aspect_is_square(): - assert coord_radial().aspect(None) == 1 +def test_to_radians_zero_width_range(): + assert_allclose(coord_radial()._to_radians(np.array([1, 2, 3]), (1, 1)), 0) -def test_coord_radial_swaps_labels_when_theta_y(): - coord = coord_radial(theta="y") - out = coord.labels(labels_view(x="xlab", y="ylab")) +def test_swaps_labels_when_theta_y(): + out = coord_radial(theta="y").labels(labels_view(x="xlab", y="ylab")) assert out.x == "ylab" assert out.y == "xlab" -def test_coord_radial_keeps_labels_when_theta_x(): - coord = coord_radial(theta="x") - out = coord.labels(labels_view(x="xlab", y="ylab")) +def test_keeps_labels_when_theta_x(): + out = coord_radial(theta="x").labels(labels_view(x="xlab", y="ylab")) assert out.x == "xlab" assert out.y == "ylab" -def test_coord_radial_default_theme_hides_theta_and_r_boundaries(): - # theme_gray blanks axis_line_x/axis_line_y; axis_line_theta and - # axis_line_r nest under them, so a default plot shows none of the - # wedge/donut boundary lines without any explicit theme() call. - p = ( - ggplot(mtcars, aes("disp", "mpg")) - + geom_point() - + coord_radial(start=-0.4 * np.pi, end=0.4 * np.pi, inner_radius=0.3) - ) - assert p == "coord_radial_default_theme_hides_theta_and_r_boundaries" - - -def test_coord_radial_partial_arc_expands_off_ends(): - # Regression: outermost bars used to sit flush against the arc ends - # (angles 0 and pi). With the default expand=True the theta axis is - # buffered, so the bars sit inside the arc. - p = ( - ggplot(mtcars, aes("cyl", "mpg")) - + geom_col() - + coord_radial(start=0, end=np.pi, inner_radius=0.1) - ) - assert p == "coord_radial_partial_arc_expands_off_ends" - - -def test_coord_radial_axis_line_controls_polar_spine(): - # panel_border no longer owns the outer circle; axis_line does, and - # unlike panel_border it can style it, not just hide it. - data = pd.DataFrame({"x": ["a", "b"], "y": [1, 2]}) - p = ( - ggplot(data, aes("x", "y")) - + geom_col() - + coord_radial() - + theme( - panel_border=element_blank(), - axis_line=element_line(color="red", size=2), - ) - ) - assert p == "coord_radial_axis_line_controls_polar_spine" - - -def test_coord_radial_axis_line_r_start_shows_on_full_circle(): - # coord_radial() is a full circle, so matplotlib's own default hides - # the 'start'/'end' spokes. Explicitly theming axis_line_r_start - # must show it anyway, and the choice must survive the real draw. - data = pd.DataFrame({"x": ["a", "b"], "y": [1, 2]}) - p = ( - ggplot(data, aes("x", "y")) - + geom_col() - + coord_radial() - + theme(axis_line_r_start=element_line(color="blue", size=2)) - ) - assert p == "coord_radial_axis_line_r_start_shows_on_full_circle" - - -@pytest.mark.parametrize( - "themeable", ["axis_line", "axis_line_x", "axis_line_theta"] -) -def test_coord_radial_axis_line_theta_skips_donut_hole(themeable: str): - # No theta axis lives on the inner boundary, so every themeable that - # covers the theta line draws the outer circle only and leaves the donut - # hole undrawn -- including the general ones, which replace the blank - # axis_line_x that would otherwise have hidden it. - p = ( - ggplot(mtcars, aes("factor(cyl)", "mpg")) - + geom_col() - + coord_radial(start=-np.pi / 2, end=np.pi / 2, inner_radius=0.3) - + theme(**{themeable: element_line()}) - ) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] - ax.figure.draw_without_rendering() # pyright: ignore[reportAttributeAccessIssue] - assert ax.spines["polar"].get_visible() - assert not ax.spines["inner"].get_visible() - - -def test_coord_radial_ticks_visible_by_default(): - # Ticks were previously invisible on every polar panel regardless of - # theme, because activation was skipped entirely; this is the - # regression test for that fix. - data = pd.DataFrame({"x": ["a", "b"], "y": [1, 2]}) - p = ggplot(data, aes("x", "y")) + geom_col() + coord_radial() - assert p == "coord_radial_ticks_visible_by_default" - - -def test_coord_radial_default_theme_does_not_crash(): - data = pd.DataFrame({"x": ["a", "b"], "y": [1, 2]}) - p = ggplot(data, aes("x", "y")) + geom_col() + coord_radial() - - p.draw_test() - - -def test_coord_radial_arc_range_normalizes_end_forward(): - assert coord_radial(start=1, end=4)._arc_range == (1, 4) - assert_allclose( - coord_radial(start=np.pi, end=0)._arc_range, - (np.pi, 2 * np.pi), - ) - assert_allclose( - coord_radial(start=np.pi, end=2 * np.pi)._arc_range, - (np.pi, 2 * np.pi), - ) - assert_allclose( - coord_radial(start=0, end=2 * np.pi)._arc_range, - (0, 2 * np.pi), - ) - assert coord_radial(start=1, end=1)._arc_range == (1, 1) - assert coord_radial(start=1)._arc_range == (1, 1 + 2 * np.pi) - - -def test_coord_radial_classifies_full_circle_from_arc_range(): - assert coord_radial()._is_full_circle - assert coord_radial(start=0, end=2 * np.pi)._is_full_circle - assert coord_radial(start=1, end=1 + 2 * np.pi)._is_full_circle - assert coord_radial( - end=2 * np.pi, - reverse="theta", - )._is_full_circle - assert not coord_radial(start=0, end=np.pi)._is_full_circle - assert not coord_radial(start=1, end=1)._is_full_circle - - -@pytest.mark.parametrize("end", [None, 2 * np.pi]) -def test_coord_radial_full_circle_r_axis_uses_start( - end: float | None, -): - p = ( - ggplot(mtcars, aes("cyl", "mpg")) - + geom_col() - + coord_radial(start=0, end=end, inner_radius=0.1) - + theme(axis_line_y=element_line()) - ) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] - ax.figure.draw_without_rendering() # pyright: ignore[reportAttributeAccessIssue] - tick = next( - tick - for tick in ax.yaxis.get_major_ticks() - if tick.get_loc() > 0 and tick.label1.get_text() - ) - renderer = ax.figure._get_renderer() # pyright: ignore[reportAttributeAccessIssue] - radius = tick.get_loc() - tick_point = tick.tick1line.get_transform().transform((0, radius)) - label_point = np.asarray( - tick.label1.get_window_extent(renderer=renderer).get_points() - ).mean(axis=0) - start_point = ax.transData.transform((0, radius)) - - assert_allclose(ax.get_xlim(), (0, 2 * np.pi)) - assert_allclose((ax.get_thetamin(), ax.get_thetamax()), (0, 360)) - assert ax.get_rlabel_position() == 0 - assert_allclose(tick_point, start_point) - assert_allclose(label_point[1], start_point[1]) - - -def test_coord_radial_full_circle_panel_params_ignore_end_representation(): - views = [] - for end in (None, 2 * np.pi): - scale_x, scale_y = trained_scales( - x_breaks=(0, 5, 10), - x_labels=("0", "5", "10"), - ) - views.append( - coord_radial(start=0, end=end, expand=False).setup_panel_params( - scale_x, - scale_y, - ) - ) - - assert_allclose(views[0].x.range, views[1].x.range) - assert_allclose(views[0].x.breaks, views[1].x.breaks) - assert_allclose(views[0].x.minor_breaks, views[1].x.minor_breaks) - assert views[0].x.labels == views[1].x.labels - - -def test_coord_radial_equivalent_endpoints_have_same_sweep(): - pi = 3.14159 - coords = [coord_radial(start=pi, end=end) for end in (0, 2 * pi)] - angles = [coord._to_radians([1, 2, 3], (0.4, 3.6)) for coord in coords] - - assert_allclose(angles[0], angles[1], rtol=1e-5) - assert all(np.all(np.diff(values) > 0) for values in angles) - - -def test_coord_radial_no_longer_has_r_axis_inside(): - import pytest - +def test_no_longer_has_r_axis_inside(): with pytest.raises(TypeError, match="r_axis_inside"): coord_radial(r_axis_inside=True) # type: ignore[call-arg] -def test_coord_radial_full_circle_position_right_no_warning(): - import warnings - - from plotnine.exceptions import PlotnineWarning - - # position="right" now moves only the r title, so a full circle no - # longer warns that it has no visible effect. - p = ( - ggplot(mtcars, aes("disp", "mpg")) - + geom_point() - + coord_radial() - + scale_y_continuous(position="right") - ) - with warnings.catch_warnings(): - warnings.simplefilter("error", PlotnineWarning) - p.draw_test() - - -def test_coord_radial_setup_panel_params_for_partial_arc(): - scale_x, scale_y = trained_scales( - y_breaks=(0, 2, 4, 8, 10), - y_labels=("0", "2", "4", "8", "10"), - ) - coord = coord_radial( - start=0, - end=np.pi, - thetalim=(0, 10), - rlim=(2, 8), - expand=False, - ) - - panel_params = coord.setup_panel_params(scale_x, scale_y) - - assert panel_params.theta.range == (0, 10) - assert panel_params.r.range == (2, 8) - assert_allclose(panel_params.x.breaks, [0, np.pi / 2, np.pi]) - assert panel_params.x.labels == ["0", "5", "10"] - assert panel_params.x.range == (0, np.pi) - assert panel_params.y.range == (2, 8) - assert panel_params.y.breaks == [2, 4, 8] - assert panel_params.y.labels == ["2", "4", "8"] - - -def test_coord_radial_rlim_recomputes_breaks(): - # Auto breaks (breaks=True), so the scale recomputes nice breaks over the - # zoomed range. Full-range breaks would be [0, 5, 10]; the naive filter to - # (2, 8) would leave only [5]. - scale_x = scale_x_continuous() - scale_y = scale_y_continuous() - scale_x.train((0, 10)) - scale_y.train((0, 10)) - coord = coord_radial(rlim=(2, 8), expand=False) - pv = coord.setup_panel_params(scale_x, scale_y) - breaks = list(pv.y.breaks) - # Recomputed nice breaks over (2, 8) — NOT the naive filter, which would - # leave only [5]. Must be within the zoom and contain more than one break. - assert len(breaks) > 1 - assert all(2 <= b <= 8 for b in breaks) - assert len(pv.y.labels) == len(breaks) - - -def test_coord_radial_reverse_r_builds_reversed_display_view(): - scale_x, scale_y = trained_scales( - y=(0, 10), - y_breaks=(0, 5, 10), - y_labels=("0", "5", "10"), - ) - panel_params = coord_radial( - reverse="r", - expand=False, - ).setup_panel_params(scale_x, scale_y) - - assert panel_params.r.range == (0, 10) - assert panel_params.r.limits == (0, 10) - assert panel_params.y.range == (-10, 0) - assert panel_params.y.limits == (-10, 0) - assert_allclose(panel_params.y.breaks, [0, -5, -10]) - assert_allclose( - panel_params.y.minor_breaks, - -np.asarray(panel_params.r.minor_breaks), - ) - assert panel_params.y.labels == ["0", "5", "10"] - - -@pytest.mark.parametrize( - ("theta", "data", "expected_y", "expected_yend"), - [ - ( - "x", - pd.DataFrame( - {"x": [2.0], "y": [3.0], "xend": [4.0], "yend": [5.0]} - ), - [-3.0], - [-5.0], - ), - ( - "y", - pd.DataFrame( - {"x": [3.0], "y": [2.0], "xend": [5.0], "yend": [4.0]} - ), - [-3.0], - [-5.0], - ), - ], -) -def test_coord_radial_reverse_r_negates_display_coordinates( - theta: Literal["x", "y"], - data: pd.DataFrame, - expected_y: list[float], - expected_yend: list[float], -): - panel_params = make_panel_view((0, 10), (0, 10)) - result = coord_radial( - theta=theta, - reverse="r", - expand=False, - ).transform(data, panel_params) - - assert_allclose(result["y"], expected_y) - assert_allclose(result["yend"], expected_yend) - - -def test_coord_radial_reverse_r_matches_reversed_scale_geometry(): - base = ggplot(mtcars, aes("factor(cyl)", "mpg")) + geom_col() - plots = [ - base - + coord_radial( - start=np.pi, - end=np.pi / 2, - inner_radius=0.1, - reverse="r", - ), - base - + scale_y_reverse() - + coord_radial( - start=np.pi, - end=np.pi / 2, - inner_radius=0.1, - ), - ] - - for plot in plots: - plot.draw_test() # pyright: ignore[reportAttributeAccessIssue] - - actual, expected = (plot.axs[0] for plot in plots) - assert_allclose(actual.get_ylim(), expected.get_ylim()) - assert_allclose(actual.get_rorigin(), expected.get_rorigin()) - assert len(actual.collections) == len(expected.collections) - for actual_collection, expected_collection in zip( - actual.collections, - expected.collections, - strict=True, - ): - actual_paths = actual_collection.get_paths() - expected_paths = expected_collection.get_paths() - assert len(actual_paths) == len(expected_paths) - for actual_path, expected_path in zip( - actual_paths, - expected_paths, - strict=True, - ): - distances = np.linalg.norm( - actual_path.vertices[:, None, :] - - expected_path.vertices[None, :, :], - axis=2, - ) - assert_allclose(distances.min(axis=0), 0, atol=1e-12) - assert_allclose(distances.min(axis=1), 0, atol=1e-12) - - theta = actual.collections[0].get_paths()[0].vertices[0, 0] - origin = np.asarray( - actual.transData.transform((theta, actual.get_rorigin())) - ) - baseline = np.asarray(actual.transData.transform((theta, 0))) - inward = np.asarray(actual.transData.transform((theta, -22.8))) - assert np.linalg.norm(baseline - origin) > np.linalg.norm(inward - origin) - - -@pytest.mark.parametrize( - ("theta", "reverse", "inner_radius"), - [ - ("x", "r", 0), - ("x", "r", 0.3), - ("x", "thetar", 0.1), - ("y", "r", 0.1), - ], -) -def test_coord_radial_reverse_r_matches_reversed_scale_cases( - theta: Literal["x", "y"], - reverse: Literal["r", "thetar"], - inner_radius: float, -): - data = pd.DataFrame({"x": [1, 2, 3], "y": [2, 5, 8]}) - mapping = aes("x", "y") if theta == "x" else aes("y", "x") - scale = scale_y_reverse() if theta == "x" else scale_x_reverse() - plots = [ - ggplot(data, mapping) - + geom_point() - + coord_radial( - theta=theta, - start=0, - end=np.pi if reverse == "thetar" else None, - inner_radius=inner_radius, - reverse=reverse, - ), - ggplot(data, mapping) - + geom_point() - + scale - + coord_radial( - theta=theta, - start=0, - end=np.pi if reverse == "thetar" else None, - inner_radius=inner_radius, - reverse="theta" if reverse == "thetar" else "none", - ), - ] - - for plot in plots: - plot.draw_test() # pyright: ignore[reportAttributeAccessIssue] - - actual, expected = (plot.axs[0] for plot in plots) - assert_allclose(actual.get_ylim(), expected.get_ylim()) - assert_allclose(actual.get_rorigin(), expected.get_rorigin()) - assert_allclose( - actual.collections[0].get_offsets(), - expected.collections[0].get_offsets(), - ) - - -def test_coord_radial_reverse_r_respects_rlim(): - scale_x, scale_y = trained_scales(y=(0, 10)) - panel_params = coord_radial( - reverse="r", - rlim=(2, 8), - expand=False, - ).setup_panel_params(scale_x, scale_y) - - assert panel_params.r.limits == (2, 8) - assert panel_params.r.range == (2, 8) - assert panel_params.y.limits == (-8, -2) - assert panel_params.y.range == (-8, -2) - - -def test_coord_radial_reverse_r_cancels_reversed_scale_geometry(): - data = pd.DataFrame({"x": [1, 2], "y": [2, 8]}) - plots = [ - ggplot(data, aes("x", "y")) + geom_point() + coord_radial(), - ggplot(data, aes("x", "y")) - + geom_point() - + scale_y_reverse() - + coord_radial(reverse="r"), - ] - - for plot in plots: - plot.draw_test() # pyright: ignore[reportAttributeAccessIssue] - - ordinary, double_reversed = (plot.axs[0] for plot in plots) - assert_allclose(ordinary.get_ylim(), double_reversed.get_ylim()) - assert_allclose(ordinary.get_rorigin(), double_reversed.get_rorigin()) - assert_allclose( - ordinary.collections[0].get_offsets(), - double_reversed.collections[0].get_offsets(), - ) - - -def test_coord_radial_reverse_theta_keeps_wedge_flips_order(): - # reverse="theta" runs the data the other way around the SAME arc; it - # must not mirror the wedge onto the opposite side. So the mapped theta - # breaks reflect about the arc midpoint (start + end - break), while the - # arc itself is unchanged. - scale_x, scale_y = trained_scales() - base = coord_radial(start=np.pi, end=np.pi / 2, expand=False) - rev = coord_radial( - start=np.pi, end=np.pi / 2, expand=False, reverse="theta" - ) - assert base._arc_range == rev._arc_range - - pv_base = base.setup_panel_params(scale_x, scale_y) - pv_rev = rev.setup_panel_params(scale_x, scale_y) - arc_lo, arc_hi = base._arc_range - assert_allclose( - list(pv_rev.x.breaks), - [arc_lo + arc_hi - b for b in pv_base.x.breaks], - ) - - -def test_coord_radial_shows_theta_labels_by_default(): - scale_x, scale_y = trained_scales() - coord = coord_radial(expand=False) # no theta_labels arg - pv = coord.setup_panel_params(scale_x, scale_y) - assert list(pv.x.breaks) # theta breaks present by default - assert list(pv.x.labels) - - -def test_coord_radial_restores_theta_minor_breaks(): - # coord_polar clears theta minor breaks (data-space ticks are meaningless - # on the arc); coord_radial must restore them as radian positions, the - # same way it restores the major breaks, so the theta minor grid renders. - scale_x, scale_y = trained_scales() - coord = coord_radial(expand=False) - pv = coord.setup_panel_params(scale_x, scale_y) - minor = list(pv.x.minor_breaks) - assert minor # not empty - assert all(0 <= b <= 2 * np.pi for b in minor) - - -def test_coord_radial_clips_theta_minor_breaks_to_partial_arc(): - scale_x, scale_y = trained_scales() - coord = coord_radial(start=0, end=np.pi, expand=False) - pv = coord.setup_panel_params(scale_x, scale_y) - minor = list(pv.x.minor_breaks) - assert minor - assert all(0 <= b <= np.pi for b in minor) - - -def test_coord_radial_discrete_theta_has_no_minor_breaks(): - # A discrete theta scale has no minor breaks and no get_minor_breaks - # method, so setting one up must not crash and leaves the theta minor - # grid empty. - df = pd.DataFrame({"cat": ["a", "b", "c"], "y": [2, 3, 5]}) - p = ggplot(df, aes("cat", "y")) + geom_col() + coord_radial(theta="x") - pv = p.build_test().layout.panel_params[0] - assert list(pv.x.minor_breaks) == [] - - -def test_coord_radial_to_radians_zero_width_range(): - coord = coord_radial() - - assert_allclose(coord._to_radians(np.array([1, 2, 3]), (1, 1)), [0, 0, 0]) - - -def test_coord_radial_transform_rotates_angle(): - coord = coord_radial(rotate_angle=True) - pv = make_panel_view((0, 10), (0, 10)) - data = pd.DataFrame({"x": [0, 5], "y": [1, 1], "angle": [10, 20]}) - - out = coord.transform(data, pv) - - # x=0 -> top spoke (rotation 0); x=5 -> bottom spoke (180deg) which - # folds back to a horizontal, readable label (rotation 0). - assert_allclose(out["x"], [0, np.pi]) - assert_allclose(out["angle"], [10, 20]) - - -def test_coord_radial_rotate_angle_aligns_upright(): - coord = coord_radial(rotate_angle=True) - pv = make_panel_view((0, 3), (0, 10)) - # theta data 0 -> top, 1 -> 120deg clockwise, 2 -> 240deg clockwise. - data = pd.DataFrame( - {"x": [0.0, 1.0, 2.0], "y": [1, 1, 1], "angle": [0.0, 0.0, 0.0]} - ) - - out = coord.transform(data, pv) - - # Labels align tangentially to the arc and are folded into (-90, 90] - # so they stay upright (a bottom label reads "6", not "9"). - assert all(-90 <= a <= 90 for a in out["angle"]) - assert_allclose(out["angle"], [0.0, 60.0, -60.0], atol=1e-6) - - -def test_coord_radial_axis_clearance(): - data = pd.DataFrame( - {"x": ["a", "a", "a"], "y": [2, 3, 5], "group": ["a", "b", "c"]} - ) - p = ( - ggplot(data, aes("x", "y", fill="group")) - + geom_col() - + coord_radial("y") - ) - assert p == "coord_radial_axis_clearance" - - -def test_coord_radial_axis_text_theming(): - # axis_text used to be a no-op on a polar panel's tick labels; - # axis_text_theta_* / axis_text_r_* now nest under axis_text_x/y - # so a plain axis_text= theme reaches both the theta and r labels. - p = ( - ggplot(mtcars, aes("disp", "mpg")) - + geom_point() - + coord_radial(start=0.5 * np.pi, end=-0.5 * np.pi, inner_radius=0.3) - + theme(axis_text=element_text(color="red", size=5)) - ) - assert p == "coord_radial_axis_text_theming" - - -def test_coord_radial_axis_ticks_major_theming(): - # axis_ticks used to be a no-op on a polar panel's theta tick - # marks; axis_ticks_major_theta_* / axis_ticks_major_r_* now nest - # under axis_ticks_major_x/y so a plain axis_ticks= theme reaches - # both the theta and r tick marks. - p = ( - ggplot(mtcars, aes("disp", "mpg")) - + geom_point() - + coord_radial(start=0.5 * np.pi, end=-0.5 * np.pi, inner_radius=0.3) - + theme(axis_ticks=element_line(color="blue", size=2)) - ) - assert p == "coord_radial_axis_ticks_major_theming" - - -def test_coord_radial_axis_ticks_minor_theming(): - # theme_gray blanks axis_ticks_minor globally, so minor ticks stay - # invisible everywhere until a user overrides axis_ticks_minor - # explicitly. axis_ticks_minor_theta_* / axis_ticks_minor_r_* now - # nest under axis_ticks_minor_x/y so a plain axis_ticks_minor= - # theme reaches minor ticks on both axes. This plot only exercises - # the r-axis half: coord_radial doesn't currently populate theta - # minor breaks, so there are no theta minor ticks to render here. - p = ( - ggplot(mtcars, aes("disp", "mpg")) - + geom_point() - + coord_radial(start=0.5 * np.pi, end=-0.5 * np.pi, inner_radius=0.3) - + theme( - axis_ticks_minor=element_line(color="green", size=1), - ) - ) - assert p == "coord_radial_axis_ticks_minor_theming" - - -def test_coord_radial_ticks_length_themed_nonzero(): - # axis_ticks_length_major_x/_y used to zero the tick length on a - # polar panel because the first major tick's tick1line/tick2line - # visibility check never held before Task 5 activated polar tick - # visibility. axis_ticks_length_major_theta/_r are the leaves that - # now apply length on a polar panel, so a themed nonzero length - # actually renders long tick marks. - p = ( - ggplot(mtcars, aes("disp", "mpg")) - + geom_point() - + coord_radial(start=0.5 * np.pi, end=-0.5 * np.pi, inner_radius=0.3) - + theme(axis_ticks_length=10) - ) - assert p == "coord_radial_ticks_length_themed_nonzero" - - -def test_coord_radial_ticks_length_themed_zero_stays_invisible(): - # Counterpart to the nonzero case above: a themed length of 0 - # should still render with no visible tick marks. - p = ( - ggplot(mtcars, aes("disp", "mpg")) - + geom_point() - + coord_radial(start=0.5 * np.pi, end=-0.5 * np.pi, inner_radius=0.3) - + theme(axis_ticks_length=0) - ) - assert p == "coord_radial_ticks_length_themed_zero_stays_invisible" - - -def test_coord_radial_scale_x_position_top_keeps_theta_outside(): - # On a polar panel scale.position moves only the axis title; the - # primary theta axis stays on the outside regardless of position. - from plotnine._mpl.axes import axis_at - - p = ( - ggplot(mtcars, aes("disp", "mpg")) - + geom_point() - + coord_radial(start=-1.0, end=1.0, inner_radius=0.3) - + scale_x_continuous(position="top") - ) - p.draw_test() - ax = p.axs[0] - assert axis_at(ax, "theta_outside") is ax.xaxis - assert axis_at(ax, "theta_inside") is None - - -def test_coord_radial_scale_y_position_right_keeps_r_start(): - # On a polar panel scale.position moves only the axis title; the - # primary r axis stays on r_start regardless of position. - from plotnine._mpl.axes import axis_at - - p = ( - ggplot(mtcars, aes("disp", "mpg")) - + geom_point() - + coord_radial(start=-1.0, end=1.0, inner_radius=0.3) - + scale_y_continuous(position="right") - ) - p.draw_test() - ax = p.axs[0] - assert axis_at(ax, "r_start") is ax.yaxis - assert axis_at(ax, "r_end") is None - - -def test_coord_radial_reverse_theta_moves_r_axis_to_end(): - # reverse="theta" runs a partial arc from end back to start, so the - # radial axis follows the data to the end spoke; a full circle has a - # single shared spoke and keeps the axis at the start. - from plotnine._mpl.axes import axis_at - - def r_side(**kwargs) -> ggplot: - p = ( - ggplot(mtcars, aes("factor(cyl)", "mpg")) - + geom_col() - + coord_radial(inner_radius=0.1, **kwargs) - + theme(axis_line_y=element_line()) - ) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - return p.axs[0] - - partial = r_side(start=np.pi, end=np.pi / 2, reverse="theta") - assert axis_at(partial, "r_end") is partial.yaxis - assert axis_at(partial, "r_start") is None - - full = r_side(reverse="theta") - assert axis_at(full, "r_start") is full.yaxis - assert axis_at(full, "r_end") is None - - -def test_p9_radial_axes_build_plotnine_ticks(): - from plotnine._mpl._radial_axis import ( - p9RadialAxis, - p9RadialTick, - p9ThetaAxis, - p9ThetaTick, - ) - - assert p9ThetaAxis._tick_class is p9ThetaTick - assert p9RadialAxis._tick_class is p9RadialTick - - p = ggplot(mtcars, aes("disp", "mpg")) + geom_point() + coord_radial() - p.draw_test() - assert isinstance(p.axs[0].yaxis, p9RadialAxis) - - -def test_coord_radial_r_axis_uses_pre_sweep_side(): - tick_side, label_side = radial_axis_sides(0) - - assert tick_side > 0 - assert label_side > 0 - - -def test_coord_radial_r_axis_rotates_with_start(): - tick_side, label_side = radial_axis_sides(np.pi / 2) - - assert tick_side > 0 - assert label_side > 0 - - -@pytest.mark.parametrize( - ("start", "end", "expected_end"), - [(0, np.pi, np.pi), (np.pi, 0, 2 * np.pi)], -) -def test_coord_radial_partial_r_axis_uses_start( - start: float, - end: float, - expected_end: float, -): - p = ( - ggplot(mtcars, aes("factor(cyl)", "mpg")) - + geom_col() - + coord_radial(start=start, end=end, inner_radius=0.1) - + theme(axis_line_y=element_line()) - ) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] - ax.figure.draw_without_rendering() # pyright: ignore[reportAttributeAccessIssue] - tick = next( - tick - for tick in ax.yaxis.get_major_ticks() - if tick.get_loc() > 0 and tick.label1.get_text() - ) - radius = tick.get_loc() - tick_point = tick.tick1line.get_transform().transform((0, radius)) - expected = ax.transData.transform((start, radius)) - - assert_allclose(ax.get_xlim(), (start, expected_end)) - assert_allclose(tick_point, expected) - assert tick.tick1line.get_visible() - assert tick.label1.get_visible() - assert not tick.tick2line.get_visible() - assert not tick.label2.get_visible() - assert ax.spines["start"].get_visible() - assert not ax.spines["end"].get_visible() - - def test_coord_polar_is_alias_of_coord_radial(): # coord_polar is superseded; it is exported only as an alias so old # code keeps working. It IS coord_radial with the same signature. from plotnine import coord_polar assert issubclass(coord_polar, coord_radial) - c = coord_polar(theta="y", inner_radius=0.4, end=3.14) - assert isinstance(c, coord_radial) + assert isinstance( + coord_polar(theta="y", inner_radius=0.4, end=3.14), coord_radial + ) assert coord_polar.__doc__ == ( "alias of [coord_radial](`plotnine.coords.coord_radial.coord_radial`)" ) -def test_secondary_r_axis(): - p = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + scale_y_continuous( - sec_axis=sec_axis(lambda x: x * 0.354006, name="km/L") - ) - + coord_radial(start=-pi / 2, end=pi / 2, inner_radius=0.1) - + theme(axis_line_y=element_line()) - ) - assert p == "secondary_r_axis" - - -def test_secondary_r_axis_themed(): - p = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + scale_y_continuous( - sec_axis=sec_axis(lambda x: x * 0.354006, name="km/L") - ) - + coord_radial(start=-pi / 2, end=pi / 2, inner_radius=0.1) - + theme( - axis_line_y=element_line(), - axis_text_y=element_text(color="blue"), - ) - ) - assert p == "secondary_r_axis_themed" - - -def test_secondary_r_axis_reverse_theta_swaps_spokes(): - # reverse="theta" moves the primary r-axis to the end spoke, so the - # secondary must follow to the opposite (start) spoke rather than stay - # pinned to r_end. Each side must resolve to its own axis so per-side - # theming (axis_line_r_start/r_end, ticks, text) routes correctly. - from plotnine._mpl.axes import axis_at - - p = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + scale_y_continuous(sec_axis=sec_axis(lambda x: x * 2)) - + coord_radial(start=-pi / 2, end=pi / 2, reverse="theta") - ) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] # pyright: ignore[reportAttributeAccessIssue] - - assert axis_at(ax, "r_end") is ax.raxis - assert axis_at(ax, "r_start") is ax._sec_raxis - - -def test_secondary_r_axis_reverse_r_builds(): - # reverse="r" negates the r view interval, so setting the secondary - # axis breaks changes its view interval and mpl fires a - # `sec_rlim_changed` callback -- the panel must register that signal. - p = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + scale_y_continuous( - sec_axis=sec_axis(lambda x: x, breaks=[10, 20, 30]) - ) - + coord_radial(reverse="r") - ) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - - -def test_secondary_r_axis_reverse_r_keeps_radial_range(): - # The secondary breaks must be negated into the same reversed display - # frame as the primary ones; otherwise their positive positions drag - # `rmax` outward and squeeze the data into the inner half of the disc. - def r_range(with_sec): - sec = ( - {"sec_axis": sec_axis(lambda x: x, breaks=[10, 20, 30])} - if with_sec - else {} - ) - p = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + scale_y_continuous(**sec) - + coord_radial(reverse="r") - ) - p.draw(show=False) - ax = p.axs[0] # pyright: ignore[reportAttributeAccessIssue] - return ax.get_rmin(), ax.get_rmax() - - assert r_range(True) == r_range(False) - - -def test_secondary_r_axis_full_circle_labels(): - # A full circle shares one labelling spoke between the primary and - # secondary r-axes. Matplotlib forces the second (`label2`) tick pair - # invisible for a full circle, so the secondary axis must render on the - # same spoke as the primary, offset outward, rather than on its own. - p = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + scale_y_continuous( - sec_axis=sec_axis(lambda x: x * 2, breaks=[10, 20, 30]) - ) - + coord_radial() - ) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] # pyright: ignore[reportAttributeAccessIssue] - labels = [t.get_text() for t in ax._sec_raxis.get_ticklabels()] - assert labels == ["20", "30"] - - -def test_secondary_r_axis_full_circle_marks(): - # On a full circle the primary and secondary r-axes share one spoke. - # The secondary tick marks must anchor on that spoke (not fan off it as - # the radius grows), point to the opposite side of it from the primary, - # and stay above the gridlines they cross. +def test_full_circle_position_right_no_warning(): + # position="right" moves only the r title, so a full circle no longer + # warns that it has no visible effect. This is the one test that draws; + # it inspects nothing afterwards. p = ( - ggplot(mtcars, aes("wt", "mpg")) + ggplot(mtcars, aes("disp", "mpg")) + geom_point() - + scale_y_continuous(sec_axis=sec_axis(lambda x: x * 2)) + coord_radial() - + theme( - axis_line_r=element_line(), - axis_ticks_y=element_line(color="red", size=2), - axis_ticks_length_major=20, - ) - ) - assert p == "secondary_r_axis_full_circle_marks" - - -def test_secondary_r_axis_theta_y(): - p = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + scale_x_continuous( - sec_axis=sec_axis(lambda x: x * 0.354006, name="scaled") - ) - + coord_radial(theta="y", start=-pi / 2, end=pi / 2) - ) - assert p == "secondary_r_axis_theta_y" - - -def test_secondary_r_axis_title_sides(): - p = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + scale_y_continuous( - sec_axis=sec_axis(lambda x: x * 0.354006, name="km/L") - ) - + coord_radial(start=-pi / 2, end=pi / 2, inner_radius=0.1) - ) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - fig = p.axs[0].figure # pyright: ignore[reportAttributeAccessIssue] - fig.draw_without_rendering() # pyright: ignore[reportAttributeAccessIssue] - targets = p.theme.targets - renderer = fig._get_renderer() # pyright: ignore[reportAttributeAccessIssue] - left = targets.axis_title_y_left - right = targets.axis_title_y_right - assert left is not None, "primary r-axis title not found" - assert right is not None, "secondary r-axis title not found" - left_box = left.get_window_extent(renderer=renderer) - right_box = right.get_window_extent(renderer=renderer) - assert right_box.x0 > left_box.x1, ( - f"secondary title (x0={right_box.x0:.1f}) expected right of " - f"primary title (x1={left_box.x1:.1f})" - ) - - -def test_polar_bbox_full_circle_is_unit_square(): - assert polar_bbox((0.0, 2 * pi)) == (0.0, 1.0, 0.0, 1.0) - - -@pytest.mark.parametrize( - "start,end,expected", - [ - (-pi / 2, pi / 2, 0.5), # right half-disc: wide box - (0.0, 0.25, pytest.approx(4.041, abs=1e-2)), # thin sliver: tall box - (-pi / 2, 0.0, 1.0), # quarter: square - (0.0, 2 * pi, 1.0), # full circle: square - ], -) -def test_coord_radial_aspect_matches_wedge(start, end, expected): - c = coord_radial(start=start, end=end) - assert c.aspect(None) == expected - - -def test_coord_radial_aspect_uses_zero_margin_for_half_disc(): - # margin 0 (not ggplot2's 0.05) -> exactly 0.5, so the panel matches the - # tight wedge and the sector is not stretched. - assert coord_radial(start=-pi / 2, end=pi / 2).aspect(None) == 0.5 - - -def _panel_and_ink(p) -> tuple[Any, np.ndarray]: - """Rendered panel bbox and the sector-outline points in display space""" - p.draw_test() - ax = p.axs[0] - ax.figure.draw_without_rendering() - renderer = ax.figure._get_renderer() - panel = ax.get_window_extent(renderer) - theta_lo, theta_hi = ax.get_xlim() - r_lo, r_hi = ax.get_ylim() - thetas = np.linspace(theta_lo, theta_hi, 200) - outline = [(t, r_hi) for t in thetas] - outline += [(theta_lo, r_lo), (theta_hi, r_lo)] - ink = ax.transData.transform(np.array(outline)) - return panel, ink - - -def _r_length(ax: Axes, theta: float) -> float: - """Display-space length of the radius spoke at angle `theta`""" - r_lo, r_hi = ax.get_ylim() - pts = ax.transData.transform([(theta, r_lo), (theta, r_hi)]) - return float(np.hypot(*(pts[1] - pts[0]))) - - -def test_coord_radial_half_disc_fills_panel(): - p = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + coord_radial(start=-pi / 2, end=pi / 2) - ) - panel, ink = _panel_and_ink(p) - frac_w = (ink[:, 0].max() - ink[:, 0].min()) / panel.width - frac_h = (ink[:, 1].max() - ink[:, 1].min()) / panel.height - assert_allclose([frac_w, frac_h], 1.0, atol=0.02) - - -@pytest.mark.parametrize( - "kwargs", - [ - {}, - {"start": -pi / 2, "end": 0.0}, - {"start": -pi / 2, "end": pi / 2}, - {"start": -pi / 2, "end": -pi / 3}, - {"start": 0.0, "end": 0.25}, - {"start": -pi / 2, "end": pi / 2, "inner_radius": 0.3}, - # Narrow arc combined with a donut hole: the inner ring must sit - # at inner_radius * outer_radius, or the panel aspect mismatches - # the wedge and the sector under-fills (regression). - {"start": pi / 4, "end": 3 * pi / 4, "inner_radius": 0.3}, - ], -) -def test_coord_radial_sector_fills_panel(kwargs): - p = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + coord_radial(**kwargs) - ) - panel, ink = _panel_and_ink(p) - frac_w = (ink[:, 0].max() - ink[:, 0].min()) / panel.width - frac_h = (ink[:, 1].max() - ink[:, 1].min()) / panel.height - assert_allclose([frac_w, frac_h], 1.0, atol=0.02) - - -def test_coord_radial_thin_wedge_uses_available_space(): - # A thin wedge and a half-disc share aspect 0.5, so the layout should - # give them the same panel size. The r-axis labels fan out along the - # up-tilted spoke and reach past the sector's top edge; treating that - # as a Cartesian y-label protrusion once reserved ~half the figure - # height above the panel, shrinking it (regression). - thin = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + coord_radial(start=-pi / 2, end=-pi / 3) - ) - half = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + coord_radial(start=-pi / 2, end=pi / 2) - ) - thin_panel, _ = _panel_and_ink(thin) - half_panel, _ = _panel_and_ink(half) - assert_allclose(thin_panel.width, half_panel.width, rtol=0.02) - assert_allclose(thin_panel.height, half_panel.height, rtol=0.02) - - -@pytest.mark.parametrize( - "kwargs", - [ - {"start": -pi / 2, "end": pi / 2}, - {"start": 0.0, "end": 0.25}, - {"start": -pi / 2, "end": pi / 2, "inner_radius": 0.3}, - ], -) -def test_coord_radial_sector_is_not_distorted(kwargs): - # The radius must map to the same display length on every spoke; - # otherwise the sector has been stretched to fill a mismatched panel. - p = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + coord_radial(**kwargs) - ) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] - ax.figure.draw_without_rendering() - theta_lo, theta_hi = ax.get_xlim() - theta_mid = (theta_lo + theta_hi) / 2 - assert_allclose( - _r_length(ax, theta_mid), _r_length(ax, theta_lo), rtol=0.01 - ) - - -@pytest.mark.parametrize( - "kwargs", - [ - {"start": -pi / 2, "end": pi / 2}, - {"start": 0.0, "end": 0.25}, - {"start": -pi / 2, "end": pi / 2, "inner_radius": 0.3}, - ], -) -def test_coord_radial_background_patch_traces_sector(kwargs): - # The axes-background wedge is drawn through its own transform, not - # transData, so a squashed patch would pass the transData-based - # distortion check while still rendering the panel as an ellipse. - # Assert the patch bbox coincides with the data sector's bbox. - p = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + coord_radial(**kwargs) - ) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] - ax.figure.draw_without_rendering() - renderer = ax.figure._get_renderer() - patch = ax.patch.get_window_extent(renderer) - _, ink = _panel_and_ink(p) - assert_allclose(patch.x0, ink[:, 0].min(), atol=1.0) - assert_allclose(patch.x1, ink[:, 0].max(), atol=1.0) - assert_allclose(patch.y0, ink[:, 1].min(), atol=1.0) - assert_allclose(patch.y1, ink[:, 1].max(), atol=1.0) - - -@pytest.mark.parametrize( - "kwargs", - [ - {"start": -pi / 2, "end": pi / 2}, - {"start": -pi / 2, "end": 0.0}, - {"start": 0.0, "end": 0.25}, - {"start": -pi / 2, "end": pi / 2, "inner_radius": 0.3}, - ], -) -def test_coord_radial_geoms_clip_to_full_sector(kwargs): - # The geoms clip to the background wedge, snapshotting its transform - # when they capture the clip. A single render must clip them to the - # true arc, not the square wedge the base draw computes -- otherwise - # the geoms vanish behind a half-height clip (regression). - p = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + coord_radial(**kwargs) - ) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - ax = p.axs[0] - ax.figure.draw_without_rendering() - _, ink = _panel_and_ink(p) - clip = ax.collections[0].get_clip_path().get_fully_transformed_path() - bbox = clip.get_extents() - assert_allclose(bbox.x0, ink[:, 0].min(), atol=1.0) - assert_allclose(bbox.x1, ink[:, 0].max(), atol=1.0) - assert_allclose(bbox.y0, ink[:, 1].min(), atol=1.0) - assert_allclose(bbox.y1, ink[:, 1].max(), atol=1.0) - - -_partial_arc_base = ggplot(mtcars, aes("wt", "mpg")) + geom_point() - - -def test_coord_radial_partial_arc_half_disc(): - p = _partial_arc_base + coord_radial(start=-pi / 2, end=pi / 2) - assert p == "coord_radial_partial_arc_half_disc" - - -def test_coord_radial_partial_arc_thin_wedge(): - p = _partial_arc_base + coord_radial(start=-pi / 2, end=-pi / 3) - assert p == "coord_radial_partial_arc_thin_wedge" - - -def test_coord_radial_partial_arc_sliver(): - p = _partial_arc_base + coord_radial(start=0, end=0.25) - assert p == "coord_radial_partial_arc_sliver" - - -def test_coord_radial_partial_arc_quarter(): - p = _partial_arc_base + coord_radial(start=-pi / 2, end=0) - assert p == "coord_radial_partial_arc_quarter" - - -def test_coord_radial_partial_arc_inner_radius_half_disc(): - p = _partial_arc_base + coord_radial( - start=-pi / 2, end=pi / 2, inner_radius=0.3 - ) - assert p == "coord_radial_partial_arc_inner_radius_half_disc" - - -@pytest.mark.parametrize( - "facet", - [ - facet_wrap("gear", nrow=2), - facet_grid("am", "gear"), - ], -) -def test_coord_radial_faceted_panels_reserve_gulley_for_decorations(facet): - # Every polar panel draws its full theta and r decorations regardless of - # its grid position, so the gulley between panels must reserve room for - # them. Otherwise the tick labels of one panel spill across the gulley - # into a neighbour's panel region. - p = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + facet - + coord_radial(start=-1, end=1) - ) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - fig = p.axs[0].figure - fig.draw_without_rendering() # pyright: ignore[reportAttributeAccessIssue] - renderer = fig._get_renderer() # pyright: ignore[reportAttributeAccessIssue] - trans = fig.transFigure.inverted() - - panel_boxes = [ax.get_position() for ax in p.axs] - tight_boxes = [ - ax.get_tightbbox(renderer).transformed(trans) for ax in p.axs - ] - - # No panel's decorations may reach into another panel's box. - for i, tight in enumerate(tight_boxes): - for j, box in enumerate(panel_boxes): - if i == j: - continue - assert not tight.overlaps(box), ( - f"panel {i} decorations overlap panel {j}'s region" - ) - - -def test_coord_radial_faceted_strip_clears_theta_decorations(): - # A polar panel draws its theta axis on the arc perimeter, so its ticks - # and labels sit at the apex inside the panel's top edge. The strip band - # must be pushed clear of them rather than landing on top, as it would if - # it were placed "inside" against the bare panel rectangle. - p = ( - ggplot(mtcars, aes("wt", "mpg")) - + geom_point() - + facet_wrap("gear", nrow=2) - + coord_radial(start=-1, end=1) + + scale_y_continuous(position="right") ) - p.draw_test() # pyright: ignore[reportAttributeAccessIssue] - fig = p.axs[0].figure - fig.draw_without_rendering() # pyright: ignore[reportAttributeAccessIssue] - renderer = fig._get_renderer() # pyright: ignore[reportAttributeAccessIssue] - trans = fig.transFigure.inverted() - - strip_texts = p.theme.targets.strip_text_x_top - assert strip_texts, "expected top strips on the faceted polar plot" - - for text in strip_texts: - ax = text.ax - strip_bottom = ( - text.patch.get_window_extent(renderer).transformed(trans).y0 - ) - # Highest point reached by any visible theta tick mark or label. - theta_top = max( - artist.get_window_extent(renderer).transformed(trans).y1 - for tick in ax.thetaaxis.majorTicks - for artist in ( - tick.tick1line, - tick.tick2line, - tick.label1, - tick.label2, - ) - if artist.get_visible() - ) - assert strip_bottom >= theta_top, ( - "strip band overlaps the panel's theta decorations" - ) + with warnings.catch_warnings(): + warnings.simplefilter("error", PlotnineWarning) + p.draw_test() # pyright: ignore[reportAttributeAccessIssue] From b8d9272ba84c6378c1204eae47e447f00152df7f Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 11 Aug 2026 14:25:53 +0300 Subject: [PATCH 101/126] test(coord_radial): add arc and donut baseline images Cover the full circle, half disc, quarter, thin wedge and sliver, an arc whose end wraps past 12 o'clock, a rotated full circle, and three donut shapes including the narrow arc whose inner ring is a fraction of a reduced outer radius. --- tests/test_coord_radial.py | 57 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 8c10e32964..bac395b129 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -17,6 +17,8 @@ from plotnine.exceptions import PlotnineWarning from plotnine.iapi import labels_view +p_point = ggplot(mtcars, aes("wt", "mpg")) + geom_point() + def test_arc_range_normalizes_end_forward(): assert coord_radial(start=1, end=4)._arc_range == (1, 4) @@ -113,3 +115,58 @@ def test_full_circle_position_right_no_warning(): with warnings.catch_warnings(): warnings.simplefilter("error", PlotnineWarning) p.draw_test() # pyright: ignore[reportAttributeAccessIssue] + + +def test_full_circle(): + p = p_point + coord_radial() + assert p == "full_circle" + + +def test_half_disc(): + p = p_point + coord_radial(start=-pi / 2, end=pi / 2) + assert p == "half_disc" + + +def test_quarter(): + p = p_point + coord_radial(start=-pi / 2, end=0) + assert p == "quarter" + + +def test_thin_wedge(): + p = p_point + coord_radial(start=-pi / 2, end=-pi / 3) + assert p == "thin_wedge" + + +def test_sliver(): + p = p_point + coord_radial(start=0, end=0.25) + assert p == "sliver" + + +def test_arc_wraps_past_twelve(): + # end < start, so the end normalises forward and the arc sweeps through + # 12 o'clock rather than backwards to meet the start. + p = p_point + coord_radial(start=pi, end=pi / 2) + assert p == "arc_wraps_past_twelve" + + +def test_rotated_full_circle(): + # The theta labels and the labelling r spoke both follow `start`. + p = p_point + coord_radial(start=pi / 4) + assert p == "rotated_full_circle" + + +def test_donut_full_circle(): + p = p_point + coord_radial(inner_radius=0.3) + assert p == "donut_full_circle" + + +def test_donut_half_disc(): + p = p_point + coord_radial(start=-pi / 2, end=pi / 2, inner_radius=0.3) + assert p == "donut_half_disc" + + +def test_donut_narrow_arc(): + # The inner ring must sit at inner_radius * outer_radius. When it did + # not, the panel aspect mismatched the wedge and the sector under-filled. + p = p_point + coord_radial(start=pi / 4, end=3 * pi / 4, inner_radius=0.3) + assert p == "donut_narrow_arc" From e3d938bf6dce835b74f11c0de0ed7dc63e80c9ec Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 11 Aug 2026 14:27:29 +0300 Subject: [PATCH 102/126] test(coord_radial): add theta-axis, expansion and zoom images Cover mapping the radius to x, a discrete theta scale closing the circle, the theta buffer at the ends of a partial arc, and zooming each axis. --- tests/test_coord_radial.py | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index bac395b129..0f2ee75895 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -8,6 +8,7 @@ from plotnine import ( aes, coord_radial, + geom_col, geom_point, ggplot, scale_y_continuous, @@ -18,6 +19,9 @@ from plotnine.iapi import labels_view p_point = ggplot(mtcars, aes("wt", "mpg")) + geom_point() +p_col = ( + ggplot(mtcars, aes("factor(cyl)", "mpg", fill="factor(cyl)")) + geom_col() +) def test_arc_range_normalizes_end_forward(): @@ -170,3 +174,41 @@ def test_donut_narrow_arc(): # not, the panel aspect mismatched the wedge and the sector under-filled. p = p_point + coord_radial(start=pi / 4, end=3 * pi / 4, inner_radius=0.3) assert p == "donut_narrow_arc" + + +def test_theta_y(): + # theta="y" puts mpg on the arc and wt on the radius, and swaps which + # axis title the layout treats as the theta title. + p = p_point + coord_radial("y") + assert p == "theta_y" + + +def test_discrete_theta(): + # A discrete theta scale spans the whole circle rather than leaving the + # last category short of the first. + p = p_col + coord_radial() + assert p == "discrete_theta" + + +def test_expand_partial_arc(): + # With the default expand=True the theta axis is buffered, so the + # outermost bars sit inside the arc ends instead of flush against them. + p = p_col + coord_radial(start=0, end=pi, inner_radius=0.1) + assert p == "expand_partial_arc" + + +def test_expand_false_partial_arc(): + p = p_col + coord_radial(start=0, end=pi, inner_radius=0.1, expand=False) + assert p == "expand_false_partial_arc" + + +def test_rlim(): + # Zooming the radius recomputes nice breaks over (10, 25) rather than + # filtering the full-range breaks, which would leave almost none. + p = p_point + coord_radial(rlim=(10, 25)) + assert p == "rlim" + + +def test_thetalim(): + p = p_point + coord_radial(thetalim=(2, 4)) + assert p == "thetalim" From f16a519bf14e59e3839231921817601abb3fe0eb Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 11 Aug 2026 14:28:42 +0300 Subject: [PATCH 103/126] test(coord_radial): add reverse-axis baseline images Cover running the data the other way around an arc, inverting the radius with and without a donut hole, and both together. --- tests/test_coord_radial.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 0f2ee75895..74a1fe6760 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -212,3 +212,35 @@ def test_rlim(): def test_thetalim(): p = p_point + coord_radial(thetalim=(2, 4)) assert p == "thetalim" + + +def test_reverse_theta_full_circle(): + # The sweep is still clockwise; the data runs the other way along it. + p = p_col + coord_radial(reverse="theta") + assert p == "reverse_theta_full_circle" + + +def test_reverse_theta_partial_arc(): + # On a partial arc the data runs from end back to start, so the + # labelling r spoke follows it to the end spoke. + p = p_col + coord_radial( + start=pi, end=pi / 2, inner_radius=0.1, reverse="theta" + ) + assert p == "reverse_theta_partial_arc" + + +def test_reverse_r(): + p = p_point + coord_radial(reverse="r") + assert p == "reverse_r" + + +def test_reverse_r_donut(): + p = p_point + coord_radial(reverse="r", inner_radius=0.3) + assert p == "reverse_r_donut" + + +def test_reverse_thetar(): + p = p_col + coord_radial( + start=pi, end=pi / 2, inner_radius=0.1, reverse="thetar" + ) + assert p == "reverse_thetar" From 6749ceee4885e47d3613ddad4f447d03bfc266d0 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 11 Aug 2026 14:33:16 +0300 Subject: [PATCH 104/126] test(coord_radial): add geom-interaction baseline images Cover paths and ribbons bending along the arc, a polygon closing across the seam at 12 o'clock, tangential upright text, and the pie and donut idioms built from stacked bars. The pie images record today's behaviour, which leaves a gap at 12 o'clock because the continuous angular scale is expanded. See kata issue 0xfh. --- tests/test_coord_radial.py | 68 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 74a1fe6760..b8a704b14d 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -2,6 +2,7 @@ from math import pi import numpy as np +import pandas as pd import pytest from numpy.testing import assert_allclose @@ -9,7 +10,11 @@ aes, coord_radial, geom_col, + geom_path, geom_point, + geom_polygon, + geom_ribbon, + geom_text, ggplot, scale_y_continuous, ) @@ -23,6 +28,13 @@ ggplot(mtcars, aes("factor(cyl)", "mpg", fill="factor(cyl)")) + geom_col() ) +# Points spread evenly around theta, so a chord and an arc differ visibly +path_data = pd.DataFrame({"x": range(6), "y": [3, 8, 5, 9, 4, 7]}) + +pie_data = pd.DataFrame( + {"one": ["a"] * 4, "value": [3, 5, 2, 6], "slice": list("wxyz")} +) + def test_arc_range_normalizes_end_forward(): assert coord_radial(start=1, end=4)._arc_range == (1, 4) @@ -244,3 +256,59 @@ def test_reverse_thetar(): start=pi, end=pi / 2, inner_radius=0.1, reverse="thetar" ) assert p == "reverse_thetar" + + +def test_path_munched_into_arc(): + # Each segment is subdivided before the radian transform, so it bends + # along the arc instead of cutting a straight chord across it. + p = ggplot(path_data, aes("x", "y")) + geom_path(size=1) + coord_radial() + assert p == "path_munched_into_arc" + + +def test_polygon_closes_across_seam(): + # The closing edge from the last vertex back to the first crosses the + # seam at 12 o'clock and must be munched like every other edge. + p = ( + ggplot(path_data, aes("x", "y")) + + geom_polygon(fill="none", color="black", size=1) + + coord_radial() + ) + assert p == "polygon_closes_across_seam" + + +def test_ribbon(): + p = ( + ggplot(path_data, aes("x", ymin="y - 2", ymax="y + 2")) + + geom_ribbon(alpha=0.5) + + coord_radial() + ) + assert p == "ribbon" + + +def test_rotate_angle_text(): + # The labels align tangentially to the arc and fold into (-90, 90], so + # the one at the bottom of the circle stays readable. + p = ( + ggplot(path_data, aes("x", "y", label="y")) + + geom_text(angle=0, size=12) + + coord_radial(rotate_angle=True) + ) + assert p == "rotate_angle_text" + + +def test_pie(): + p = ( + ggplot(pie_data, aes("one", "value", fill="slice")) + + geom_col() + + coord_radial("y") + ) + assert p == "pie" + + +def test_donut_chart(): + p = ( + ggplot(pie_data, aes("one", "value", fill="slice")) + + geom_col() + + coord_radial("y", inner_radius=0.4) + ) + assert p == "donut_chart" From f9c1bb6d15d56797cb13f203eeb3c8468e0b6102 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 11 Aug 2026 14:37:16 +0300 Subject: [PATCH 105/126] test(coord_radial): add axis-line baseline images Cover the default theme leaving every polar boundary unlined, axis_line styling the outer circle that panel_border used to hide, the theta line skipping the donut hole, and each radial spoke themed on its own side. A spoke is themeable only where a radial axis sits. A plain partial arc has one, at the start; the end spoke gains one when reversing the angular axis moves it there, or when a secondary axis adds a second. --- tests/test_coord_radial.py | 74 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index b8a704b14d..3eb2529d4d 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -9,6 +9,8 @@ from plotnine import ( aes, coord_radial, + element_blank, + element_line, geom_col, geom_path, geom_point, @@ -17,11 +19,13 @@ geom_text, ggplot, scale_y_continuous, + theme, ) from plotnine.coords.coord_radial import polar_bbox from plotnine.data import mtcars from plotnine.exceptions import PlotnineWarning from plotnine.iapi import labels_view +from plotnine.scales import sec_axis p_point = ggplot(mtcars, aes("wt", "mpg")) + geom_point() p_col = ( @@ -312,3 +316,73 @@ def test_donut_chart(): + coord_radial("y", inner_radius=0.4) ) assert p == "donut_chart" + + +def test_default_theme_hides_boundaries(): + # theme_gray blanks axis_line_x and axis_line_y, and axis_line_theta and + # axis_line_r nest under them, so a default plot outlines neither the + # wedge nor the hole without any theme() call. + p = p_point + coord_radial(start=-0.4 * pi, end=0.4 * pi, inner_radius=0.3) + assert p == "default_theme_hides_boundaries" + + +def test_axis_line_styles_polar_spine(): + # panel_border no longer owns the outer circle. axis_line does, and + # unlike panel_border it can style it rather than only hide it. + p = ( + p_col + + coord_radial() + + theme( + panel_border=element_blank(), + axis_line=element_line(color="red", size=2), + ) + ) + assert p == "axis_line_styles_polar_spine" + + +def test_axis_line_theta_skips_donut_hole(): + # No theta axis lives on the inner boundary, so the themeable that + # covers the theta line draws the outer arc only. + p = ( + p_col + + coord_radial(start=-pi / 2, end=pi / 2, inner_radius=0.3) + + theme(axis_line_theta=element_line(color="red", size=2)) + ) + assert p == "axis_line_theta_skips_donut_hole" + + +def test_axis_line_r_start_on_full_circle(): + # A full circle's start and end spokes coincide, so matplotlib hides + # them by default. Theming axis_line_r_start must show it anyway. + p = ( + p_col + + coord_radial() + + theme(axis_line_r_start=element_line(color="blue", size=2)) + ) + assert p == "axis_line_r_start_on_full_circle" + + +def test_axis_line_r_end_on_reversed_arc(): + # A spoke is themeable only where a radial axis lives, and reverse= + # "theta" moves the only one to the end spoke. So axis_line_r_end + # styles that spoke and the start spoke stays bare. + p = ( + p_col + + coord_radial( + start=-pi / 2, end=pi / 2, inner_radius=0.1, reverse="theta" + ) + + theme(axis_line_r_end=element_line(color="blue", size=2)) + ) + assert p == "axis_line_r_end_on_reversed_arc" + + +def test_axis_line_r_reaches_both_spokes(): + # A secondary axis puts a radial axis on each spoke, so the parent + # themeable styles both leaves at once. + p = ( + p_col + + scale_y_continuous(sec_axis=sec_axis(lambda x: x * 2)) + + coord_radial(start=-pi / 2, end=pi / 2, inner_radius=0.1) + + theme(axis_line_r=element_line(color="blue", size=2)) + ) + assert p == "axis_line_r_reaches_both_spokes" From 2b77cb8f43459f26f411ffe20880882f800b395c Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 11 Aug 2026 14:40:52 +0300 Subject: [PATCH 106/126] test(coord_radial): add tick, label and grid theming images A plain axis_text, axis_ticks or axis_ticks_minor now reaches a polar panel's angular and radial decorations, tick length applies on both axes, and a blanked tick leaves the label gap alone. --- tests/test_coord_radial.py | 64 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 3eb2529d4d..7a377617f8 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -11,6 +11,7 @@ coord_radial, element_blank, element_line, + element_text, geom_col, geom_path, geom_point, @@ -26,6 +27,7 @@ from plotnine.exceptions import PlotnineWarning from plotnine.iapi import labels_view from plotnine.scales import sec_axis +from plotnine.themes.elements import margin p_point = ggplot(mtcars, aes("wt", "mpg")) + geom_point() p_col = ( @@ -39,6 +41,11 @@ {"one": ["a"] * 4, "value": [3, 5, 2, 6], "slice": list("wxyz")} ) +# A wedge with a hole shows both sets of tick marks and labels clearly +p_wedge = p_point + coord_radial( + start=0.5 * pi, end=-0.5 * pi, inner_radius=0.3 +) + def test_arc_range_normalizes_end_forward(): assert coord_radial(start=1, end=4)._arc_range == (1, 4) @@ -386,3 +393,60 @@ def test_axis_line_r_reaches_both_spokes(): + theme(axis_line_r=element_line(color="blue", size=2)) ) assert p == "axis_line_r_reaches_both_spokes" + + +def test_axis_text_theming(): + # axis_text used to be a no-op on a polar panel. axis_text_theta_* and + # axis_text_r_* nest under axis_text_x/y, so a plain axis_text= reaches + # both the theta and the r labels. + p = p_wedge + theme(axis_text=element_text(color="red", size=13)) + assert p == "axis_text_theming" + + +def test_axis_text_theta_blank(): + p = p_wedge + theme(axis_text_theta=element_blank()) + assert p == "axis_text_theta_blank" + + +def test_axis_text_theta_margin(): + # The largest margin side sets the gap between a theta label and the + # arc, whichever side it is. + p = p_wedge + theme(axis_text_theta=element_text(margin=margin(r=12))) + assert p == "axis_text_theta_margin" + + +def test_axis_ticks_theming(): + # As with axis_text, a plain axis_ticks= reaches both sets of marks, and + # axis_ticks_minor= overrides it on the minor ones. theme_gray blanks + # minor marks globally, so they appear only once overridden. Colouring + # the two differently shows both reaching the arc and the spoke. + p = p_wedge + theme( + axis_ticks=element_line(color="blue", size=2), + axis_ticks_minor=element_line(color="green", size=1), + ) + assert p == "axis_ticks_theming" + + +def test_axis_ticks_length_per_axis(): + # Length is themed per axis, so the arc grows long marks while the spoke + # is left with none. A length of 0 renders no mark rather than failing. + p = p_wedge + theme( + axis_ticks_length_major_theta=10, + axis_ticks_length_major_r=0, + ) + assert p == "axis_ticks_length_per_axis" + + +def test_axis_ticks_theta_blank_keeps_label_gap(): + # A blank tick contributes no length, so the label keeps its plain gap + # to the arc rather than being pushed out by the themed length. + p = p_wedge + theme( + axis_ticks_major_theta=element_blank(), + axis_ticks_length_major_theta=20, + ) + assert p == "axis_ticks_theta_blank_keeps_label_gap" + + +def test_panel_grid_theming(): + p = p_wedge + theme(panel_grid=element_line(color="white", size=1.5)) + assert p == "panel_grid_theming" From 231d049b03f71d7c949fe143bc1aa79d8513a0a0 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 11 Aug 2026 14:43:29 +0300 Subject: [PATCH 107/126] test(coord_radial): add axis-title and secondary-axis images A scale's position moves a polar axis title without moving the axis, and a secondary radial axis renders on the opposite spoke of a partial arc or on the other side of a full circle's single spoke. --- tests/test_coord_radial.py | 79 +++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 7a377617f8..9e6056116e 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -26,7 +26,7 @@ from plotnine.data import mtcars from plotnine.exceptions import PlotnineWarning from plotnine.iapi import labels_view -from plotnine.scales import sec_axis +from plotnine.scales import scale_x_continuous, sec_axis from plotnine.themes.elements import margin p_point = ggplot(mtcars, aes("wt", "mpg")) + geom_point() @@ -450,3 +450,80 @@ def test_axis_ticks_theta_blank_keeps_label_gap(): def test_panel_grid_theming(): p = p_wedge + theme(panel_grid=element_line(color="white", size=1.5)) assert p == "panel_grid_theming" + + +def test_scale_x_position_top(): + # On a polar panel scale.position moves only the axis title. The theta + # axis itself stays outside the arc. + p = ( + p_point + + coord_radial(start=-1.0, end=1.0, inner_radius=0.3) + + scale_x_continuous(position="top") + ) + assert p == "scale_x_position_top" + + +def test_scale_y_position_right(): + # Likewise the r axis stays on the start spoke; only its title moves. + p = ( + p_point + + coord_radial(start=-1.0, end=1.0, inner_radius=0.3) + + scale_y_continuous(position="right") + ) + assert p == "scale_y_position_right" + + +def test_secondary_r_axis_partial_arc(): + p = ( + p_point + + scale_y_continuous( + sec_axis=sec_axis(lambda x: x * 0.354006, name="km/L") + ) + + coord_radial(start=-pi / 2, end=pi / 2, inner_radius=0.1) + + theme(axis_line_r=element_line()) + ) + assert p == "secondary_r_axis_partial_arc" + + +def test_secondary_r_axis_full_circle(): + # A full circle's start and end spokes coincide, so the secondary axis + # shares the primary's spoke. Its labels and marks go to the other side + # of that spoke rather than onto a spoke of their own. + p = ( + p_point + + scale_y_continuous( + sec_axis=sec_axis(lambda x: x * 2, breaks=[20, 40, 60]) + ) + + coord_radial() + + theme( + axis_line_r=element_line(), + axis_ticks_length_major=20, + ) + ) + assert p == "secondary_r_axis_full_circle" + + +def test_secondary_r_axis_themed(): + p = ( + p_point + + scale_y_continuous( + sec_axis=sec_axis(lambda x: x * 0.354006, name="km/L") + ) + + coord_radial(start=-pi / 2, end=pi / 2, inner_radius=0.1) + + theme( + axis_line_r=element_line(), + axis_text_r=element_text(color="blue"), + ) + ) + assert p == "secondary_r_axis_themed" + + +def test_secondary_r_axis_theta_y(): + p = ( + p_point + + scale_x_continuous( + sec_axis=sec_axis(lambda x: x * 0.354006, name="scaled") + ) + + coord_radial("y", start=-pi / 2, end=pi / 2) + ) + assert p == "secondary_r_axis_theta_y" From c4e6df5a6ed113847f7eae0408bdb8081762ea98 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 11 Aug 2026 14:48:50 +0300 Subject: [PATCH 108/126] test(coord_radial): add facet, label-gap and legend images Faceted polar panels keep their decorations out of the gulley and below the strips, angular labels sit on one gap regardless of descenders, and a colourbar sits clear of a half-disc panel. --- tests/test_coord_radial.py | 46 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 9e6056116e..4ccda8d56b 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -12,6 +12,8 @@ element_blank, element_line, element_text, + facet_grid, + facet_wrap, geom_col, geom_path, geom_point, @@ -527,3 +529,47 @@ def test_secondary_r_axis_theta_y(): + coord_radial("y", start=-pi / 2, end=pi / 2) ) assert p == "secondary_r_axis_theta_y" + + +def test_facet_wrap(): + # Every polar panel draws its full theta and radial decorations, so the + # gulley must hold them and the strip band must clear the arc apex. + p = p_point + facet_wrap("gear", nrow=2) + coord_radial(start=-1, end=1) + assert p == "facet_wrap" + + +def test_facet_grid(): + p = p_point + facet_grid("am", "gear") + coord_radial(start=-1, end=1) + assert p == "facet_grid" + + +def test_theta_label_descent(): + # "300" has no descender and "3g0" does. Both labels sit on the same gap + # from the arc, because the gap is measured to the glyphs rather than to + # the font's logical box. + data = pd.DataFrame({"x": [0, 120, 330], "y": [1, 2, 3]}) + p = ( + ggplot(data, aes("x", "y")) + + geom_point() + + scale_x_continuous( + breaks=[0, 120, 330], + labels=["0", "300", "3g0"], + limits=(0, 360), + expand=(0, 0), + ) + + coord_radial() + + theme( + axis_text_y=element_blank(), + axis_ticks_major_r=element_blank(), + ) + ) + assert p == "theta_label_descent" + + +def test_legend_beside_polar_panel(): + p = ( + ggplot(mtcars, aes("wt", "mpg", color="hp")) + + geom_point() + + coord_radial(start=-pi / 2, end=pi / 2) + ) + assert p == "legend_beside_polar_panel" From 0402cc3b48f536161a688e125d3abc604c862ad0 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 11 Aug 2026 15:22:13 +0300 Subject: [PATCH 109/126] test(coord_radial): review and trim the image matrix Every image was reviewed against the question of what it claims that its neighbours do not, leaving twenty-nine. Where a group swept a parameter, only the members carrying a named behaviour stayed: two arc shapes rather than five, one donut alongside the narrow arc that regressed, one reverse mode per axis, and one facet layout. Where two claims could share a channel that already tells them apart, they merged into one image: the general themeables now show their reach across labels, marks, grid and a secondary axis together, and both axes zoom on one coordinate system. Some images went because their claim already holds implicitly in the rest of the suite: the default theme leaving every polar boundary unlined, a legend beside a polar panel, and a plain full circle. Expansion kept only the case where it is switched off, the default being visible everywhere. The angular-label descent image went for a different reason. Its two labels sat on opposite sides of the circle, where the bounding boxes face different ways, so the gaps it compared were never comparable. The extension-themeable test in the theme suite asserted a panel colour by reading the axes back. It now compares an image of the red panel, which removes the only pyplot use in that file. --- tests/test_coord_radial.py | 248 +++++++------------------------------ tests/test_theme.py | 11 +- 2 files changed, 51 insertions(+), 208 deletions(-) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 4ccda8d56b..68ee234cd6 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -12,12 +12,10 @@ element_blank, element_line, element_text, - facet_grid, facet_wrap, geom_col, geom_path, geom_point, - geom_polygon, geom_ribbon, geom_text, ggplot, @@ -146,31 +144,16 @@ def test_full_circle_position_right_no_warning(): p.draw_test() # pyright: ignore[reportAttributeAccessIssue] -def test_full_circle(): - p = p_point + coord_radial() - assert p == "full_circle" - - def test_half_disc(): p = p_point + coord_radial(start=-pi / 2, end=pi / 2) assert p == "half_disc" -def test_quarter(): - p = p_point + coord_radial(start=-pi / 2, end=0) - assert p == "quarter" - - def test_thin_wedge(): p = p_point + coord_radial(start=-pi / 2, end=-pi / 3) assert p == "thin_wedge" -def test_sliver(): - p = p_point + coord_radial(start=0, end=0.25) - assert p == "sliver" - - def test_arc_wraps_past_twelve(): # end < start, so the end normalises forward and the arc sweeps through # 12 o'clock rather than backwards to meet the start. @@ -189,11 +172,6 @@ def test_donut_full_circle(): assert p == "donut_full_circle" -def test_donut_half_disc(): - p = p_point + coord_radial(start=-pi / 2, end=pi / 2, inner_radius=0.3) - assert p == "donut_half_disc" - - def test_donut_narrow_arc(): # The inner ring must sit at inner_radius * outer_radius. When it did # not, the panel aspect mismatched the wedge and the sector under-filled. @@ -215,28 +193,21 @@ def test_discrete_theta(): assert p == "discrete_theta" -def test_expand_partial_arc(): - # With the default expand=True the theta axis is buffered, so the - # outermost bars sit inside the arc ends instead of flush against them. - p = p_col + coord_radial(start=0, end=pi, inner_radius=0.1) - assert p == "expand_partial_arc" - - -def test_expand_false_partial_arc(): +def test_expand_false(): + # Every other image in this file uses the default buffer, which holds the + # data clear of the arc ends and the outer radius. Turning it off is what + # needs an image of its own: the bars then run flush to both ends of the + # arc and out to the rim. p = p_col + coord_radial(start=0, end=pi, inner_radius=0.1, expand=False) - assert p == "expand_false_partial_arc" - - -def test_rlim(): - # Zooming the radius recomputes nice breaks over (10, 25) rather than - # filtering the full-range breaks, which would leave almost none. - p = p_point + coord_radial(rlim=(10, 25)) - assert p == "rlim" + assert p == "expand_false" -def test_thetalim(): - p = p_point + coord_radial(thetalim=(2, 4)) - assert p == "thetalim" +def test_zoom(): + # Both axes zoom on one coordinate system. The radius recomputes nice + # breaks over (10, 25) rather than filtering the full-range breaks, which + # would leave almost none, and (2, 4) of wt spans the whole circle. + p = p_point + coord_radial(rlim=(10, 25), thetalim=(2, 4)) + assert p == "zoom" def test_reverse_theta_full_circle(): @@ -264,13 +235,6 @@ def test_reverse_r_donut(): assert p == "reverse_r_donut" -def test_reverse_thetar(): - p = p_col + coord_radial( - start=pi, end=pi / 2, inner_radius=0.1, reverse="thetar" - ) - assert p == "reverse_thetar" - - def test_path_munched_into_arc(): # Each segment is subdivided before the radian transform, so it bends # along the arc instead of cutting a straight chord across it. @@ -278,17 +242,6 @@ def test_path_munched_into_arc(): assert p == "path_munched_into_arc" -def test_polygon_closes_across_seam(): - # The closing edge from the last vertex back to the first crosses the - # seam at 12 o'clock and must be munched like every other edge. - p = ( - ggplot(path_data, aes("x", "y")) - + geom_polygon(fill="none", color="black", size=1) - + coord_radial() - ) - assert p == "polygon_closes_across_seam" - - def test_ribbon(): p = ( ggplot(path_data, aes("x", ymin="y - 2", ymax="y + 2")) @@ -303,7 +256,7 @@ def test_rotate_angle_text(): # the one at the bottom of the circle stays readable. p = ( ggplot(path_data, aes("x", "y", label="y")) - + geom_text(angle=0, size=12) + + geom_text(angle=0, size=18, color="green") + coord_radial(rotate_angle=True) ) assert p == "rotate_angle_text" @@ -318,46 +271,21 @@ def test_pie(): assert p == "pie" -def test_donut_chart(): - p = ( - ggplot(pie_data, aes("one", "value", fill="slice")) - + geom_col() - + coord_radial("y", inner_radius=0.4) - ) - assert p == "donut_chart" - - -def test_default_theme_hides_boundaries(): - # theme_gray blanks axis_line_x and axis_line_y, and axis_line_theta and - # axis_line_r nest under them, so a default plot outlines neither the - # wedge nor the hole without any theme() call. - p = p_point + coord_radial(start=-0.4 * pi, end=0.4 * pi, inner_radius=0.3) - assert p == "default_theme_hides_boundaries" - - -def test_axis_line_styles_polar_spine(): +def test_axis_line_styles_polar_boundaries(): # panel_border no longer owns the outer circle. axis_line does, and - # unlike panel_border it can style it rather than only hide it. + # unlike panel_border it can style it rather than only hide it. On a + # donut arc the same themeable shows which boundaries it owns: the outer + # arc and the spoke holding the radial axis are drawn, while the hole, + # which no angular axis sits on, is left bare. p = ( p_col - + coord_radial() + + coord_radial(start=-pi / 2, end=pi / 2, inner_radius=0.3) + theme( panel_border=element_blank(), axis_line=element_line(color="red", size=2), ) ) - assert p == "axis_line_styles_polar_spine" - - -def test_axis_line_theta_skips_donut_hole(): - # No theta axis lives on the inner boundary, so the themeable that - # covers the theta line draws the outer arc only. - p = ( - p_col - + coord_radial(start=-pi / 2, end=pi / 2, inner_radius=0.3) - + theme(axis_line_theta=element_line(color="red", size=2)) - ) - assert p == "axis_line_theta_skips_donut_hole" + assert p == "axis_line_styles_polar_boundaries" def test_axis_line_r_start_on_full_circle(): @@ -397,46 +325,28 @@ def test_axis_line_r_reaches_both_spokes(): assert p == "axis_line_r_reaches_both_spokes" -def test_axis_text_theming(): - # axis_text used to be a no-op on a polar panel. axis_text_theta_* and - # axis_text_r_* nest under axis_text_x/y, so a plain axis_text= reaches - # both the theta and the r labels. - p = p_wedge + theme(axis_text=element_text(color="red", size=13)) - assert p == "axis_text_theming" - - -def test_axis_text_theta_blank(): - p = p_wedge + theme(axis_text_theta=element_blank()) - assert p == "axis_text_theta_blank" - - -def test_axis_text_theta_margin(): - # The largest margin side sets the gap between a theta label and the - # arc, whichever side it is. - p = p_wedge + theme(axis_text_theta=element_text(margin=margin(r=12))) - assert p == "axis_text_theta_margin" - - -def test_axis_ticks_theming(): - # As with axis_text, a plain axis_ticks= reaches both sets of marks, and - # axis_ticks_minor= overrides it on the minor ones. theme_gray blanks - # minor marks globally, so they appear only once overridden. Colouring - # the two differently shows both reaching the arc and the spoke. - p = p_wedge + theme( - axis_ticks=element_line(color="blue", size=2), - axis_ticks_minor=element_line(color="green", size=1), - ) - assert p == "axis_ticks_theming" - - -def test_axis_ticks_length_per_axis(): - # Length is themed per axis, so the arc grows long marks while the spoke - # is left with none. A length of 0 renders no mark rather than failing. - p = p_wedge + theme( - axis_ticks_length_major_theta=10, - axis_ticks_length_major_r=0, +def test_theming_reaches_all_decorations(): + # The general themeables reach every polar decoration and the polar + # leaves refine them. Both label sets are red at size 13 from axis_text, + # and the angular labels sit further out because their own leaf adds a + # margin while inheriting that colour and size. Major marks are blue and + # minor ones green, on the arc and on the spokes alike. The angular marks + # are the longer pair, each axis taking its own themed length. The grid + # thickens on the rings and the spokes together. A secondary radial axis + # gives axis_text a second set of radial labels to reach. + p = ( + p_wedge + + scale_y_continuous(sec_axis=sec_axis(lambda x: x * 2)) + + theme( + axis_text=element_text(color="red", size=13), + axis_text_theta=element_text(margin=margin(r=12)), + axis_ticks=element_line(color="blue", size=2), + axis_ticks_minor=element_line(color="green", size=1), + axis_ticks_length_major_theta=10, + panel_grid=element_line(color="white", size=1.5), + ) ) - assert p == "axis_ticks_length_per_axis" + assert p == "theming_reaches_all_decorations" def test_axis_ticks_theta_blank_keeps_label_gap(): @@ -449,30 +359,18 @@ def test_axis_ticks_theta_blank_keeps_label_gap(): assert p == "axis_ticks_theta_blank_keeps_label_gap" -def test_panel_grid_theming(): - p = p_wedge + theme(panel_grid=element_line(color="white", size=1.5)) - assert p == "panel_grid_theming" - - -def test_scale_x_position_top(): - # On a polar panel scale.position moves only the axis title. The theta - # axis itself stays outside the arc. +def test_scale_positions_move_only_titles(): + # On a polar panel a scale's position moves its axis title and nothing + # else. Moving both at once shows each title on its new side while the + # angular axis stays outside the arc and the radial axis stays on the + # start spoke. p = ( p_point + coord_radial(start=-1.0, end=1.0, inner_radius=0.3) + scale_x_continuous(position="top") - ) - assert p == "scale_x_position_top" - - -def test_scale_y_position_right(): - # Likewise the r axis stays on the start spoke; only its title moves. - p = ( - p_point - + coord_radial(start=-1.0, end=1.0, inner_radius=0.3) + scale_y_continuous(position="right") ) - assert p == "scale_y_position_right" + assert p == "scale_positions_move_only_titles" def test_secondary_r_axis_partial_arc(): @@ -505,21 +403,6 @@ def test_secondary_r_axis_full_circle(): assert p == "secondary_r_axis_full_circle" -def test_secondary_r_axis_themed(): - p = ( - p_point - + scale_y_continuous( - sec_axis=sec_axis(lambda x: x * 0.354006, name="km/L") - ) - + coord_radial(start=-pi / 2, end=pi / 2, inner_radius=0.1) - + theme( - axis_line_r=element_line(), - axis_text_r=element_text(color="blue"), - ) - ) - assert p == "secondary_r_axis_themed" - - def test_secondary_r_axis_theta_y(): p = ( p_point @@ -536,40 +419,3 @@ def test_facet_wrap(): # gulley must hold them and the strip band must clear the arc apex. p = p_point + facet_wrap("gear", nrow=2) + coord_radial(start=-1, end=1) assert p == "facet_wrap" - - -def test_facet_grid(): - p = p_point + facet_grid("am", "gear") + coord_radial(start=-1, end=1) - assert p == "facet_grid" - - -def test_theta_label_descent(): - # "300" has no descender and "3g0" does. Both labels sit on the same gap - # from the arc, because the gap is measured to the glyphs rather than to - # the font's logical box. - data = pd.DataFrame({"x": [0, 120, 330], "y": [1, 2, 3]}) - p = ( - ggplot(data, aes("x", "y")) - + geom_point() - + scale_x_continuous( - breaks=[0, 120, 330], - labels=["0", "300", "3g0"], - limits=(0, 360), - expand=(0, 0), - ) - + coord_radial() - + theme( - axis_text_y=element_blank(), - axis_ticks_major_r=element_blank(), - ) - ) - assert p == "theta_label_descent" - - -def test_legend_beside_polar_panel(): - p = ( - ggplot(mtcars, aes("wt", "mpg", color="hp")) - + geom_point() - + coord_radial(start=-pi / 2, end=pi / 2) - ) - assert p == "legend_beside_polar_panel" diff --git a/tests/test_theme.py b/tests/test_theme.py index 2bb0631706..69d3071e5d 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -2,7 +2,6 @@ import matplotlib as mpl import pytest -from matplotlib import pyplot as plt from packaging import version from plotnine import ( @@ -114,6 +113,9 @@ def test_add_element_blank(): def test_extension_themeable_applies_from_theme_kwargs(): + # A themeable defined outside plotnine is reachable by its own name as a + # theme() keyword, and its apply_ax runs during the draw. The red panel + # is the evidence that it ran. class test_extension_panel_facecolor(themeable): def apply_ax(self, ax): super().apply_ax(ax) @@ -124,12 +126,7 @@ def apply_ax(self, ax): + geom_point() + theme(test_extension_panel_facecolor="red") ) - - fig = p.draw() - try: - assert fig.axes[0].get_facecolor() == (1.0, 0.0, 0.0, 1.0) - finally: - plt.close(fig) + assert p == "extension_themeable_applies_from_theme_kwargs" def test_element_line_dashed_capstyle(): From 45c7624f24d957a703f9d37f3840abf26fcff25f Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 12 Aug 2026 17:39:11 +0300 Subject: [PATCH 110/126] docs(coord_radial): correct what expand buffers The buffer applies to both axes, holding the data clear of the arc ends as well as the outer radius. The description mentioned only the radius. --- plotnine/coords/coord_radial.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index d67db3c124..183fc70774 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -85,8 +85,9 @@ class coord_radial(coord): Equivalent angles are interpreted as the same endpoint. `None` (default) gives a full circle. expand : - Add a small buffer around the data on the radius axis. - Default `True`. + If `True` (the default), add a small buffer around the data on both + axes, so the data stops short of the arc ends and of the outer + radius. If `False`, use the limits from the data. inner_radius : Size of the inner hole as a fraction of the outer radius, in `[0, 1)`. `0` (default) means no hole; `0.3` creates a 30 % From d90bf2aa6401724dbc57c69bda7a9d759c2965a0 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 12 Aug 2026 19:23:13 +0300 Subject: [PATCH 111/126] test(themes): add the extension-themeable baseline image --- ...nsion_themeable_applies_from_theme_kwargs.png | Bin 0 -> 7401 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/baseline_images/test_theme/extension_themeable_applies_from_theme_kwargs.png diff --git a/tests/baseline_images/test_theme/extension_themeable_applies_from_theme_kwargs.png b/tests/baseline_images/test_theme/extension_themeable_applies_from_theme_kwargs.png new file mode 100644 index 0000000000000000000000000000000000000000..13d38e85767be3dd58cda2af10fb0c6d57b95a95 GIT binary patch literal 7401 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 Date: Wed, 12 Aug 2026 23:27:32 +0300 Subject: [PATCH 112/126] feat(coord_radial): place the secondary theta breaks on the arc A secondary theta axis now reaches the panel with its breaks as angles, trimmed to a partial arc like the primary ones. Without an inner hole there is no rim to draw it on, so it is dropped with a warning. --- plotnine/coords/coord_radial.py | 63 ++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 183fc70774..53004bdb28 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -14,11 +14,19 @@ from .coord import _activate_axis, _set_fixed_ticks, coord, dist_euclidean if TYPE_CHECKING: + from collections.abc import Sequence + import numpy.typing as npt import pandas as pd from matplotlib.axes import Axes - from plotnine.iapi import labels_view, layout_details, panel_view + from plotnine.iapi import ( + labels_view, + layout_details, + panel_view, + scale_position_view, + sec_axis_view, + ) from plotnine.scales.scale import scale from plotnine.typing import FloatArrayLike, FloatSeries, Side @@ -200,14 +208,35 @@ def setup_panel_params( self._to_radians(theta.minor_breaks, theta.range) ), labels=list(theta.labels), + sec=self._theta_sec_view(theta), ) y = replace(r) # Partial arcs keep only breaks inside their visible bounds. if not self._is_full_circle: arc_lo, arc_hi = sorted(arc_range) + + def on_arc(values: Sequence[float]) -> list[bool]: + return [arc_lo <= value <= arc_hi for value in values] + x_breaks = cast("list[float]", x.breaks) - keep = [arc_lo <= value <= arc_hi for value in x_breaks] + keep = on_arc(x_breaks) + sec = x.sec + if sec is not None: + sec_keep = on_arc(sec.breaks) + sec = replace( + sec, + breaks=[ + value + for value, include in zip(sec.breaks, sec_keep) + if include + ], + labels=[ + label + for label, include in zip(sec.labels, sec_keep) + if include + ], + ) x = replace( x, breaks=[ @@ -221,6 +250,7 @@ def setup_panel_params( labels=[ label for label, include in zip(x.labels, keep) if include ], + sec=sec, ) # Keep r in trained scale space. For radial reversal, make y match @@ -259,8 +289,9 @@ def setup_panel_params( y = replace(y, position=_FLIP_POSITION[y.position]) for sv in (x, y): if sv.sec: - # A secondary theta axis is rejected in setup_ax, so a - # secondary position here is always cartesian. + # A secondary axis takes the side opposite its primary, + # which is always a cartesian side: on a polar panel the + # position moves only the title. pos = cast("Side", sv.sec.position) sv.sec.position = _FLIP_POSITION[pos] @@ -347,6 +378,30 @@ def _to_radians( norm = 1 - norm return list(arc_start + norm * (arc_end - arc_start)) + def _theta_sec_view( + self, theta: scale_position_view + ) -> sec_axis_view | None: + """ + Secondary theta axis of a panel, with its breaks in radians + + The axis is drawn on the rim of the inner hole, so a panel without a + hole has nowhere to put it and drops it. Dropping it here also drops + its title, which is built from this view. + """ + if (sec := theta.sec) is None: + return None + + if not self.inner_radius: + warn( + f"{self.__class__.__name__}() draws a secondary theta axis " + "on the rim of the inner hole, so it needs inner_radius > 0. " + "Ignoring it.", + PlotnineWarning, + ) + return None + + return replace(sec, breaks=self._to_radians(sec.breaks, theta.range)) + def transform( self, data: pd.DataFrame, From f065d88928295293d8cb6f88cdd5e30cc0097c43 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 12 Aug 2026 23:29:41 +0300 Subject: [PATCH 113/126] feat(coord_radial): draw the secondary theta axis on the inner rim MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Draw a theta scale’s secondary ticks and labels along the inner rim, and expose the axis to the inner theta themeables. Continue to warn and omit it when the panel has no hole. --- doc/changelog.qmd | 4 ++++ plotnine/_mpl/_radial_axes.py | 29 +++++++++++++++++++++++++++++ plotnine/coords/coord_radial.py | 31 ++++++++++++++++++------------- tests/test_coord_radial.py | 13 +++++++++++++ 4 files changed, 64 insertions(+), 13 deletions(-) diff --git a/doc/changelog.qmd b/doc/changelog.qmd index 83fd02d505..09182ed61a 100644 --- a/doc/changelog.qmd +++ b/doc/changelog.qmd @@ -11,6 +11,10 @@ title: Changelog transformation of the primary axis with [](:class:`~plotnine.sec_axis`), or as a mirror of the primary axis with [](:func:`~plotnine.dup_axis`). +- [](:class:`~plotnine.coord_radial`) can now draw an angular scale's secondary + axis around the inner rim. Add a [](:class:`~plotnine.sec_axis`) to the scale + and set `inner_radius > 0`. + - Added [](:class:`~plotnine.composition.plot_layout`) with which you can customise the layout of plots in composition. diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index ec147d948d..96cc04a096 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -118,6 +118,7 @@ class p9RadialAxes(PolarAxes): _shared_axes = { **PolarAxes._shared_axes, # pyright: ignore[reportAttributeAccessIssue] "sec_r": cbook.Grouper(), + "sec_theta": cbook.Grouper(), } _axis_at_side: dict[PolarSide, ThetaAxis | RadialAxis] | None = None @@ -125,6 +126,9 @@ class p9RadialAxes(PolarAxes): # Secondary r-axis drawn at the end spoke; None until first requested. _sec_raxis: p9RadialAxis | None = None + # Secondary theta axis around the inner rim, or `None` before creation. + _sec_thetaaxis: p9ThetaAxis | None = None + # Whether the r-axis ticks already keep their themed styling across # matplotlib's tick resets. Guards `lock_raxis_tick_style` so repeated # calls do not stack wrappers on top of each other. @@ -325,6 +329,31 @@ def add_sec_raxis(self) -> p9RadialAxis: self._sec_raxis = axis return axis + def add_sec_thetaaxis(self) -> p9ThetaAxis: + """ + Return the secondary theta axis, creating it when needed + + A new axis has no ticks, labels, or grid lines. + + Returns + ------- + : + The secondary theta axis. + """ + if self._sec_thetaaxis: + return self._sec_thetaaxis + + axis = p9ThetaAxis(self, clear=True) + # Matplotlib resolves the axis name through `_axis_map`; the signal + # keeps its view limits synchronised with the polar axes. + self._axis_map["sec_theta"] = axis + register_lim_changed_signal(self, "sec_theta") + self.add_artist(axis) + axis.set_clip_on(False) + axis.grid(visible=False) + self._sec_thetaaxis = axis + return axis + def set_spine_visible(self, name: str, visible: bool) -> None: """ Set a spine's visibility and block matplotlib's from overwriting it diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 53004bdb28..30b10c5fcd 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -133,6 +133,10 @@ class coord_radial(coord): *is* the theta axis, they are styled through the theme: hide them with `theme(axis_text_x=element_blank())` and adjust the gap to the outer circle through the `axis_text_x` margin. + + A secondary axis on the theta scale is drawn around the inner rim, with + its labels inside the hole. It requires `inner_radius > 0`; otherwise, + `coord_radial` warns and ignores it. """ is_linear = False @@ -493,12 +497,11 @@ def setup_ax( inner radius, and radial-axis placement using `panel_params` so faceted panels with free scales each get their own radial range. - The primary theta axis always renders on the outside. The primary r - axis sits on the spoke where the data begins (`_r_axis_side`): the - start spoke normally, the end spoke when `reverse="theta"` runs a - partial arc the other way. Neither follows `scale.position`, which - moves only the axis title. The fixed choice is recorded on - `p9RadialAxes.axis_at_side` so theming can find it. + The primary theta axis uses the outer rim and its secondary axis uses + the inner rim. The primary r axis sits on the spoke where the data + begins: normally the start spoke, or the end spoke when + `reverse="theta"` reverses a partial arc. `scale.position` moves only + an axis title, not the axis itself. """ view = cast("radial_panel_view", panel_params) radial_ax = cast("p9RadialAxes", ax) @@ -506,13 +509,6 @@ def setup_ax( # plotnine sweeps clockwise, which is matplotlib's -1. radial_ax.set_theta_direction(-1) - if view.x.sec is not None: - warn( - f"{self.__class__.__name__}() does not support a secondary " - "theta axis.", - PlotnineWarning, - ) - self._setup_ticks_labels(ax, view) # The radial axis is the yaxis; its start spoke is matplotlib's @@ -524,6 +520,15 @@ def setup_ax( radial_ax.axis_at_side["theta_outside"] = radial_ax.thetaaxis radial_ax.axis_at_side[self._r_axis_side] = radial_ax.raxis + if (sec := view.x.sec) is not None: + sec_thetaaxis = radial_ax.add_sec_thetaaxis() + _set_fixed_ticks(sec_thetaaxis, sec.breaks, sec.labels) + # Matplotlib places the `bottom` tick and label pair inside the + # circle. + _activate_axis(sec_thetaaxis, "bottom", True) + radial_ax.set_spine_visible("inner", True) + radial_ax.axis_at_side["theta_inside"] = sec_thetaaxis + if (sec := view.y.sec) is not None: sec_raxis = radial_ax.add_sec_raxis() _set_fixed_ticks(sec_raxis, sec.breaks, sec.labels) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 68ee234cd6..398d236e45 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -414,6 +414,19 @@ def test_secondary_r_axis_theta_y(): assert p == "secondary_r_axis_theta_y" +def test_secondary_theta_axis_donut(): + # The ticks and labels move to the inner rim, while the title retains the + # secondary scale's position above the panel. + p = ( + p_point + + scale_x_continuous( + sec_axis=sec_axis(lambda x: x * 1000, name="grams") + ) + + coord_radial(inner_radius=0.45) + ) + assert p == "secondary_theta_axis_donut" + + def test_facet_wrap(): # Every polar panel draws its full theta and radial decorations, so the # gulley must hold them and the strip band must clear the arc apex. From a74684b073955e48e6157532850d83c149283cad Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Wed, 12 Aug 2026 23:32:13 +0300 Subject: [PATCH 114/126] test(coord_radial): cover secondary theta axis variants Verify secondary theta axes on partial arcs, with `theta="y"`, and with reversed angular direction. Cover general axis themeables and their inner-theta overrides. --- tests/test_coord_radial.py | 54 +++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 398d236e45..87fd71ec49 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -26,7 +26,7 @@ from plotnine.data import mtcars from plotnine.exceptions import PlotnineWarning from plotnine.iapi import labels_view -from plotnine.scales import scale_x_continuous, sec_axis +from plotnine.scales import dup_axis, scale_x_continuous, sec_axis from plotnine.themes.elements import margin p_point = ggplot(mtcars, aes("wt", "mpg")) + geom_point() @@ -427,6 +427,58 @@ def test_secondary_theta_axis_donut(): assert p == "secondary_theta_axis_donut" +def test_secondary_theta_axis_partial_arc(): + # Both theta axes omit breaks outside the visible arc. + p = ( + p_point + + scale_x_continuous(sec_axis=sec_axis(lambda x: x * 2)) + + coord_radial(start=-pi / 2, end=pi / 2, inner_radius=0.4) + ) + assert p == "secondary_theta_axis_partial_arc" + + +def test_secondary_theta_axis_theta_y(): + # With `theta="y"`, the y scale supplies both angular axes. + p = ( + p_point + + scale_y_continuous( + sec_axis=sec_axis(lambda x: x * 0.354006, name="km/L") + ) + + coord_radial("y", start=-pi / 2, end=pi / 2, inner_radius=0.4) + ) + assert p == "secondary_theta_axis_theta_y" + + +def test_secondary_theta_axis_reverse_theta(): + # Reversing theta maps both angular axes in the opposite direction. + p = ( + p_point + + scale_x_continuous(sec_axis=dup_axis()) + + coord_radial(inner_radius=0.4, reverse="theta") + ) + assert p == "secondary_theta_axis_reverse_theta" + + +def test_theming_reaches_the_inside_theta_axis(): + # General axis themeables style every axis, while inside variants override + # the secondary theta axis. The shared theta tick length makes both sets + # cross their spines. + p = ( + p_point + + scale_x_continuous(sec_axis=dup_axis()) + + coord_radial(inner_radius=0.45) + + theme( + axis_text=element_text(color="red", size=10), + axis_text_theta_inside=element_text(size=14, margin=margin(b=10)), + axis_ticks=element_line(color="blue", size=2), + axis_ticks_major_theta_inside=element_line(color="green"), + axis_ticks_length_major_theta=12, + axis_line_theta_inside=element_line(color="purple", size=2), + ) + ) + assert p == "theming_reaches_the_inside_theta_axis" + + def test_facet_wrap(): # Every polar panel draws its full theta and radial decorations, so the # gulley must hold them and the strip band must clear the arc apex. From 0365c1b7b57451d693561437955c81d827f75c61 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 13 Aug 2026 00:06:35 +0300 Subject: [PATCH 115/126] refactor(coord_flip): narrow the secondary axis side before flipping it A secondary axis records a polar side as readily as a cartesian one, but flipping is a cartesian operation and only cartesian sides have an opposite. Saying so leaves the type checker with nothing to report. --- plotnine/coords/coord_flip.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plotnine/coords/coord_flip.py b/plotnine/coords/coord_flip.py index 4d7ad9997e..db4969a0b7 100644 --- a/plotnine/coords/coord_flip.py +++ b/plotnine/coords/coord_flip.py @@ -1,13 +1,13 @@ from __future__ import annotations -import typing +from typing import TYPE_CHECKING, cast import pandas as pd from ..iapi import labels_view, panel_ranges, panel_view from .coord_cartesian import coord_cartesian -if typing.TYPE_CHECKING: +if TYPE_CHECKING: from typing import Sequence, TypeVar from plotnine.scales.scale import scale @@ -63,7 +63,7 @@ def setup_panel_params(self, scale_x: scale, scale_y: scale) -> panel_view: panel_params.y.position = _FLIP_POSITION[panel_params.y.position] for sv in (panel_params.x, panel_params.y): if sv.sec is not None: - sv.sec.position = _FLIP_POSITION[sv.sec.position] + sv.sec.position = _FLIP_POSITION[cast("Side", sv.sec.position)] return panel_params def setup_layout(self, layout: pd.DataFrame) -> pd.DataFrame: From e4f5b91d1d5706901dee679f03c53d61abc5c610 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 13 Aug 2026 00:57:18 +0300 Subject: [PATCH 116/126] fix(themes): hide the radial tick labels when axis_text_r is blank A polar panel reports its radial axis as sitting on the left of a Cartesian box, so the y-axis tick themeables reached the radial ticks and labels. Every text element carries a visible property, so styling the left of a panel re-showed the labels a polar themeable had just blanked, and blanking axis_text_r or axis_text_r_start had no effect. Cartesian sides now resolve no axis on a polar panel, so only the theta and r themeables reach its decorations. --- plotnine/_mpl/axes.py | 8 ++++++ .../_mpl/layout_manager/_plot_layout_items.py | 26 ++++++++----------- plotnine/themes/themeable.py | 9 ++++--- tests/test_coord_radial.py | 12 +++++++++ 4 files changed, 36 insertions(+), 19 deletions(-) diff --git a/plotnine/_mpl/axes.py b/plotnine/_mpl/axes.py index 8f4ebab75b..8426671e5c 100644 --- a/plotnine/_mpl/axes.py +++ b/plotnine/_mpl/axes.py @@ -6,6 +6,7 @@ from matplotlib.axes import Axes from matplotlib.axis import XAxis, YAxis from matplotlib.projections import register_projection +from matplotlib.projections.polar import PolarAxes if TYPE_CHECKING: from plotnine.typing import PolarSide, Side @@ -154,6 +155,11 @@ def axis_at(ax: Axes, side: Side | PolarSide) -> XAxis | YAxis | None: occupy through `get_tick_params()`, so plotnine tracks it explicitly there. + Cartesian sides never resolve on polar axes. Polar tick flags describe + theta and r placement rather than box edges. For example, a full-circle + r axis reports `left=True` even though it does not occupy the left edge + of a Cartesian panel. + Parameters ---------- ax : @@ -169,6 +175,8 @@ def axis_at(ax: Axes, side: Side | PolarSide) -> XAxis | YAxis | None: side nothing occupies). """ if side in ("top", "bottom", "left", "right"): + if isinstance(ax, PolarAxes): + return None candidates = ( (ax.xaxis, getattr(ax, "sec_xaxis", None)) if side in ("top", "bottom") diff --git a/plotnine/_mpl/layout_manager/_plot_layout_items.py b/plotnine/_mpl/layout_manager/_plot_layout_items.py index c7f32535fa..10676608ee 100644 --- a/plotnine/_mpl/layout_manager/_plot_layout_items.py +++ b/plotnine/_mpl/layout_manager/_plot_layout_items.py @@ -917,13 +917,11 @@ class PolarPlotLayoutItems(PlotLayoutItems): """ The space around a polar panel that its axes need - A theta axis wraps the full circle and an r axis runs along one - spoke, so neither has a single side to measure a tick-label bounding - box from (`axis_at` never resolves one for either). The text - clearance is therefore a fixed estimate applied to every side alike, - and the tick-label protrusions are zero: the labels fan out along the - arc within a panel already sized to the tight sector, so their reach - past a Cartesian edge does not call for extra margin. + A theta axis follows an arc and an r axis follows a spoke, so neither + occupies a Cartesian side with a measurable tick-label bounding box. + Tick-label protrusions are therefore zero, and every side receives the + same fixed text clearance. The panel already fits the tight sector. Its + labels fan around the arc instead of extending from one box edge. The tick-mark clearance is zero: polar ticks point inward from the arc, so a mark never reaches past the panel edge whatever its length. @@ -970,14 +968,12 @@ def strip_shift(self, st: StripText) -> float: def _polar_text_height_x(theme: theme, side: Side) -> float: """ - Estimated figure-height of an `axis_text_x_` label on a polar panel - - Not measured from rendered artists: a theta axis wraps the full - circle, so `axis_at` never resolves a side to measure a tick-label - bounding box from. The font size stands in for the label height, the - same rough idea `_strip_switch_pad` uses to turn a themed point value - into a figure fraction. Bare like its Cartesian counterpart - `axis_text_x_max_height`: the caller adds the tick-label margin. + Estimate an `axis_text_x_` label's height in figure space + + A theta axis does not occupy a Cartesian side, so it has no side-specific + tick-label bounding box to measure. Use the font size as the label height, + following the estimate used for strip padding. As with + `axis_text_x_max_height`, the caller adds the tick-label margin. """ name = f"axis_text_x_{side}" if theme.T.is_blank(name): diff --git a/plotnine/themes/themeable.py b/plotnine/themes/themeable.py index 85ea685f4e..e19e5e305f 100644 --- a/plotnine/themes/themeable.py +++ b/plotnine/themes/themeable.py @@ -16,6 +16,7 @@ from warnings import warn import numpy as np +from matplotlib.projections.polar import PolarAxes from .._mpl.axes import axis_at from .._utils import MARGIN_SIDE, has_alpha_channel, side_artists, to_rgba @@ -2487,7 +2488,7 @@ class axis_ticks_length_major_x(themeable): def apply_ax(self, ax: Axes): super().apply_ax(ax) - if getattr(ax, "axis_at_side", None) is not None: + if isinstance(ax, PolarAxes): return for axis in (ax.xaxis, getattr(ax, "sec_xaxis", None)): if axis is None: @@ -2530,7 +2531,7 @@ class axis_ticks_length_major_y(themeable): def apply_ax(self, ax: Axes): super().apply_ax(ax) - if getattr(ax, "axis_at_side", None) is not None: + if isinstance(ax, PolarAxes): return for axis in (ax.yaxis, getattr(ax, "sec_yaxis", None)): if axis is None: @@ -2651,7 +2652,7 @@ class axis_ticks_length_minor_x(themeable): def apply_ax(self, ax: Axes): super().apply_ax(ax) - if getattr(ax, "axis_at_side", None) is not None: + if isinstance(ax, PolarAxes): return value: float | complex = self.properties["value"] @@ -2682,7 +2683,7 @@ class axis_ticks_length_minor_y(themeable): def apply_ax(self, ax: Axes): super().apply_ax(ax) - if getattr(ax, "axis_at_side", None) is not None: + if isinstance(ax, PolarAxes): return value: float | complex = self.properties["value"] diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 87fd71ec49..90f2554cce 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -359,6 +359,18 @@ def test_axis_ticks_theta_blank_keeps_label_gap(): assert p == "axis_ticks_theta_blank_keeps_label_gap" +def test_axis_text_r_blank_hides_radial_labels(): + # `axis_text_r` hides both radial axes without hiding the theta axis. On a + # full circle, the primary and secondary radial labels share one spoke. + p = ( + p_point + + scale_y_continuous(sec_axis=sec_axis(lambda x: x * 2)) + + coord_radial() + + theme(axis_text_r=element_blank()) + ) + assert p == "axis_text_r_blank_hides_radial_labels" + + def test_scale_positions_move_only_titles(): # On a polar panel a scale's position moves its axis title and nothing # else. Moving both at once shows each title on its new side while the From 326b559dcfab2b105e605fb407527faae15371cf Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 13 Aug 2026 14:36:42 +0300 Subject: [PATCH 117/126] fix(coord_radial): reject zero-width arcs Reject `start == end` before computing panel geometry. Direct callers to use `end=None` for a full circle or choose a different end angle. --- plotnine/coords/coord_radial.py | 24 +++++++++++++----------- tests/test_coord_radial.py | 9 ++++++--- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 30b10c5fcd..7573cc5b1b 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -9,7 +9,7 @@ from .._mpl._radial_axes import p9RadialAxes # noqa: TCH001 from .._utils.registry import alias -from ..exceptions import PlotnineWarning +from ..exceptions import PlotnineError, PlotnineWarning from ..iapi import panel_ranges, radial_panel_view from .coord import _activate_axis, _set_fixed_ticks, coord, dist_euclidean @@ -90,8 +90,10 @@ class coord_radial(coord): Default 0. end : Ending angle in radians, measured clockwise from 12 o'clock. - Equivalent angles are interpreted as the same endpoint. `None` - (default) gives a full circle. + `None` (default) draws a full circle. Angles separated by whole turns + are equivalent, except that `end == start` raises a + [](:class:`~plotnine.exceptions.PlotnineError`) because it defines a + zero-width arc. expand : If `True` (the default), add a small buffer around the data on both axes, so the data stops short of the arc ends and of the outer @@ -159,6 +161,11 @@ def __init__( "reverse must be one of 'none', 'theta', 'r', 'thetar'; " f"got {reverse!r}." ) + if end is not None and end == start: + raise PlotnineError( + f"start={start!r} and end={end!r} define a zero-width arc. " + "Set end=None for a full circle or choose a different angle." + ) self.theta = theta self.start = start self.end = end @@ -317,19 +324,14 @@ def _arc_range(self) -> tuple[float, float]: """ Forward angular limits of the displayed arc - Equivalent endpoints select the same clockwise arc. An explicit - non-zero whole turn remains a full circle, while equal endpoints - remain a zero-width arc. + Equivalent end angles select the same clockwise arc. A non-zero + whole turn selects a full circle. """ turn = 2 * np.pi if self.end is None: return (self.start, self.start + turn) - delta = self.end - self.start - if delta == 0: - return (self.start, self.start) - - span = delta % turn + span = (self.end - self.start) % turn if span == 0: span = turn return (self.start, self.start + span) diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 90f2554cce..e174d3c94b 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -24,7 +24,7 @@ ) from plotnine.coords.coord_radial import polar_bbox from plotnine.data import mtcars -from plotnine.exceptions import PlotnineWarning +from plotnine.exceptions import PlotnineError, PlotnineWarning from plotnine.iapi import labels_view from plotnine.scales import dup_axis, scale_x_continuous, sec_axis from plotnine.themes.elements import margin @@ -54,17 +54,20 @@ def test_arc_range_normalizes_end_forward(): coord_radial(start=pi, end=2 * pi)._arc_range, (pi, 2 * pi) ) assert_allclose(coord_radial(start=0, end=2 * pi)._arc_range, (0, 2 * pi)) - assert coord_radial(start=1, end=1)._arc_range == (1, 1) assert coord_radial(start=1)._arc_range == (1, 1 + 2 * pi) +def test_rejects_zero_width_arc(): + with pytest.raises(PlotnineError, match="zero-width arc"): + coord_radial(start=1, end=1) + + def test_classifies_full_circle_from_arc_range(): assert coord_radial()._is_full_circle assert coord_radial(start=0, end=2 * pi)._is_full_circle assert coord_radial(start=1, end=1 + 2 * pi)._is_full_circle assert coord_radial(end=2 * pi, reverse="theta")._is_full_circle assert not coord_radial(start=0, end=pi)._is_full_circle - assert not coord_radial(start=1, end=1)._is_full_circle def test_polar_bbox_full_circle_is_unit_square(): From 55f402fd240028f685a2254c016933cbf3679749 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 13 Aug 2026 14:42:28 +0300 Subject: [PATCH 118/126] fix(coord_radial): transform all position aesthetics Transform bounds, endpoints, and intercepts with `x` and `y` so radial reversal and `theta="y"` preserve their dimensions. Let geoms select transformed dimensions through the coordinate-system contract. --- plotnine/coords/coord.py | 10 ++++++++++ plotnine/coords/coord_flip.py | 4 ++++ plotnine/coords/coord_radial.py | 35 +++++++++++++++++---------------- plotnine/geoms/geom_ribbon.py | 17 ++++++++-------- plotnine/mapping/aes.py | 7 +++++++ tests/test_coord_radial.py | 22 +++++++++++++++++++++ 6 files changed, 69 insertions(+), 26 deletions(-) diff --git a/plotnine/coords/coord.py b/plotnine/coords/coord.py index 474b2104eb..75a93d5386 100644 --- a/plotnine/coords/coord.py +++ b/plotnine/coords/coord.py @@ -135,6 +135,16 @@ class coord: # than the final plot that's actually drawn. _owner: ggplot | None = None + @property + def preserves_dimensions(self) -> bool: + """ + Whether transformed position aesthetics retain their dimensions + + When this is `False`, geoms must read each position and its bounds + from the opposite dimension. + """ + return True + def __radd__(self, other: ggplot) -> ggplot: """ Add coordinates to ggplot object diff --git a/plotnine/coords/coord_flip.py b/plotnine/coords/coord_flip.py index db4969a0b7..bf8b62f2d4 100644 --- a/plotnine/coords/coord_flip.py +++ b/plotnine/coords/coord_flip.py @@ -45,6 +45,10 @@ class coord_flip(coord_cartesian): use the limits from the data. """ + @property + def preserves_dimensions(self) -> bool: + return False + def labels(self, cur_labels: labels_view) -> labels_view: return flip_labels(super().labels(cur_labels)) diff --git a/plotnine/coords/coord_radial.py b/plotnine/coords/coord_radial.py index 7573cc5b1b..0cf469435a 100644 --- a/plotnine/coords/coord_radial.py +++ b/plotnine/coords/coord_radial.py @@ -11,6 +11,11 @@ from .._utils.registry import alias from ..exceptions import PlotnineError, PlotnineWarning from ..iapi import panel_ranges, radial_panel_view +from ..mapping.aes import ( + TRANSPOSED_POSITION_AESTHETICS, + X_AESTHETICS, + Y_AESTHETICS, +) from .coord import _activate_axis, _set_fixed_ticks, coord, dist_euclidean if TYPE_CHECKING: @@ -177,11 +182,11 @@ def __init__( self.reverse = reverse @property - def is_default_axes(self) -> bool: + def preserves_dimensions(self) -> bool: return self.theta == "x" def _flip(self, p1: T1, p2: T2) -> tuple[T1, T2] | tuple[T2, T1]: - return (p1, p2) if self.is_default_axes else (p2, p1) + return (p1, p2) if self.preserves_dimensions else (p2, p1) def setup_panel_params( self, scale_x: scale, scale_y: scale @@ -293,7 +298,7 @@ def on_arc(values: Sequence[float]) -> list[bool]: # rotates with them (matching coord_flip): the angular title lands # on x's bottom/top and the radial title on y's left/right, the # sides the layout manager repositions. theta="x" keeps them as is. - if not self.is_default_axes: + if not self.preserves_dimensions: from .coord_flip import _FLIP_POSITION x = replace(x, position=_FLIP_POSITION[x.position]) @@ -422,26 +427,22 @@ def transform( if "x" not in data or "y" not in data: return data - t = self._flip("x", "y")[0] - tend = self._flip("xend", "yend")[0] view = cast("radial_panel_view", panel_params) + theta_aesthetics = self._flip(X_AESTHETICS, Y_AESTHETICS)[0] data = data.copy() - data[t] = self._to_radians(data[t], view.theta.range) - if tend in data.columns: - data[tend] = self._to_radians(data[tend], view.theta.range) + for aes in theta_aesthetics & set(data.columns): + data[aes] = self._to_radians(data[aes], view.theta.range) - # PolarAxes always expects x = theta (radians) and y = r. - # When theta = "y" we need to swap the columns. - if not self.is_default_axes: - data["x"], data["y"] = data["y"], data["x"] - if "xend" in data and "yend" in data: - data["xend"], data["yend"] = data["yend"], data["xend"] + # Matplotlib requires theta in `x` and radius in `y`. Transpose every + # position aesthetic so bounds, endpoints, and intercepts stay with + # their dimension when `theta="y"`. + if not self.preserves_dimensions: + data = data.rename(columns=TRANSPOSED_POSITION_AESTHETICS) if self.reverse in ("r", "thetar"): - data["y"] = -data["y"] - if "yend" in data: - data["yend"] = -data["yend"] + for aes in Y_AESTHETICS & set(data.columns): + data[aes] = -data[aes] # After the swap, data["x"] is always theta in radians. if self.rotate_angle and "angle" in data and "x" in data: diff --git a/plotnine/geoms/geom_ribbon.py b/plotnine/geoms/geom_ribbon.py index 0469e038b2..42d5a9424c 100644 --- a/plotnine/geoms/geom_ribbon.py +++ b/plotnine/geoms/geom_ribbon.py @@ -3,7 +3,6 @@ import typing from .._utils import SIZE_FACTOR, to_rgba -from ..coords import coord_flip from ..doctools import document from ..exceptions import PlotnineError from .geom import geom @@ -90,7 +89,7 @@ def draw_group( ax: Axes, params: dict[str, Any], ): - _x = "y" if isinstance(coord, coord_flip) else "x" + _x = "x" if coord.preserves_dimensions else "y" data = coord.transform(data, panel_params, munch=True) data = data.sort_values(by=["group", _x], kind="mergesort") units = ["alpha", "color", "fill", "linetype", "size"] @@ -122,12 +121,12 @@ def draw_unit( if fill is None: fill = "none" - if isinstance(coord, coord_flip): - fill_between = ax.fill_betweenx - _x, _min, _max = data["y"], data["xmin"], data["xmax"] - else: + if coord.preserves_dimensions: fill_between = ax.fill_between _x, _min, _max = data["x"], data["ymin"], data["ymax"] + else: + fill_between = ax.fill_betweenx + _x, _min, _max = data["y"], data["xmin"], data["xmax"] # We only change this defaults for fill_between when necessary where = data.get("where", None) @@ -167,9 +166,9 @@ def _draw_outline( if outline_type == "full": return - # The data is already in panel coordinates. After `coord_flip`, - # the ribbon bounds are `xmin` and `xmax`. - bounds = "x" if isinstance(coord, coord_flip) else "y" + # The data is already in panel coordinates. Where the coord swaps + # the dimensions, the ribbon bounds are `xmin` and `xmax`. + bounds = "y" if coord.preserves_dimensions else "x" # Each call receives one ribbon group, so an outline forms one path # with constant aesthetics. diff --git a/plotnine/mapping/aes.py b/plotnine/mapping/aes.py index de1bc0ac5d..eb841b4b8b 100644 --- a/plotnine/mapping/aes.py +++ b/plotnine/mapping/aes.py @@ -32,6 +32,13 @@ class ColorOrColour(Protocol): X_AESTHETICS = {"x", "xmin", "xmax", "xend", "xintercept"} Y_AESTHETICS = {"y", "ymin", "ymax", "yend", "yintercept"} +# Counterparts for transposing every position aesthetic between dimensions. +# Bounds, endpoints, and intercepts must move with `x` and `y`. +TRANSPOSED_POSITION_AESTHETICS = { + **{aes: f"y{aes[1:]}" for aes in X_AESTHETICS}, + **{aes: f"x{aes[1:]}" for aes in Y_AESTHETICS}, +} + ALL_AESTHETICS = { "alpha", "angle", diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index e174d3c94b..767832ac3d 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -254,6 +254,28 @@ def test_ribbon(): assert p == "ribbon" +def test_ribbon_reverse_r(): + # Radial reversal must negate the ribbon bounds with its centreline; + # otherwise, the band falls outside the visible range. + p = ( + ggplot(path_data, aes("x", ymin="y - 2", ymax="y + 2")) + + geom_ribbon(alpha=0.5) + + coord_radial(reverse="r") + ) + assert p == "ribbon_reverse_r" + + +def test_ribbon_theta_y(): + # With `theta="y"`, `ymin` and `ymax` become angular bounds at each `x` + # radius. + p = ( + ggplot(path_data, aes("x", ymin="y - 2", ymax="y + 2")) + + geom_ribbon(alpha=0.5) + + coord_radial(theta="y") + ) + assert p == "ribbon_theta_y" + + def test_rotate_angle_text(): # The labels align tangentially to the arc and fold into (-90, 90], so # the one at the bottom of the circle stays readable. From 1faae45dc580e8662d19a51e62404c3b078e8634 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 13 Aug 2026 14:52:51 +0300 Subject: [PATCH 119/126] fix(layout): keep polar tick labels inside the figure Measure the largest visible polar tick label in each dimension and add the larger configured label margin or non-blank tick length. Reserve that band on every panel edge because polar axes follow arcs and spokes rather than Cartesian sides. This slightly shrinks every polar panel, so each baseline image changes. --- .../_mpl/layout_manager/_plot_layout_items.py | 135 ++++++++++++------ tests/test_coord_radial.py | 14 ++ 2 files changed, 102 insertions(+), 47 deletions(-) diff --git a/plotnine/_mpl/layout_manager/_plot_layout_items.py b/plotnine/_mpl/layout_manager/_plot_layout_items.py index 10676608ee..f2f3f561f0 100644 --- a/plotnine/_mpl/layout_manager/_plot_layout_items.py +++ b/plotnine/_mpl/layout_manager/_plot_layout_items.py @@ -45,6 +45,7 @@ from plotnine.themes.theme import theme from plotnine.typing import ( HorizontalJustification, + PolarSide, Side, StripPosition, VerticalJustification, @@ -73,6 +74,32 @@ ) +# Polar boundaries that may carry a theta or r axis. +POLAR_SIDES: tuple[PolarSide, ...] = ( + "theta_outside", + "theta_inside", + "r_start", + "r_end", +) + +# Tick-label themeables for every polar boundary. +POLAR_TEXT_THEMEABLES = ( + "axis_text_theta_outside", + "axis_text_theta_inside", + "axis_text_r_start", + "axis_text_r_end", +) + +# Map each polar tick-length themeable to the marks it sizes. Blank marks +# contribute no clearance. +POLAR_TICK_LENGTHS = { + "axis_ticks_length_major_theta": "axis_ticks_major_theta", + "axis_ticks_length_major_r": "axis_ticks_major_r", + "axis_ticks_length_minor_theta": "axis_ticks_minor_theta", + "axis_ticks_length_minor_r": "axis_ticks_minor_r", +} + + @dataclass class StripSpec: """ @@ -915,31 +942,78 @@ def _position_strip_text(self, st: StripText): class PolarPlotLayoutItems(PlotLayoutItems): """ - The space around a polar panel that its axes need - - A theta axis follows an arc and an r axis follows a spoke, so neither - occupies a Cartesian side with a measurable tick-label bounding box. - Tick-label protrusions are therefore zero, and every side receives the - same fixed text clearance. The panel already fits the tight sector. Its - labels fan around the arc instead of extending from one box edge. - - The tick-mark clearance is zero: polar ticks point inward from the - arc, so a mark never reaches past the panel edge whatever its length. - A longer tick only pushes its label farther out, and the text - clearance already covers the label. + Space around a polar panel for its axis decorations + + Theta axes follow arcs and r axes follow spokes, so neither belongs to + one Cartesian edge. Every panel edge therefore reserves the same band: + the largest visible label extent in that dimension, plus the greater of + the configured label margins and tick lengths whose marks are not blank. + + Label size is stable before final positioning, so it can be measured from + the artists. Margins and tick lengths come from the theme because their + final offsets are applied during drawing. + + The complete band is reported as text clearance. Tick clearance and + per-edge label protrusions remain zero to avoid counting it twice. """ + def _axis_clearance(self, ax: Axes, side: Side) -> float: + """ + Return polar-axis clearance beyond one panel edge in figure space + """ + dim: Literal["width", "height"] = ( + "width" if side in ("left", "right") else "height" + ) + W, H = self.plot.theme.getp("figure_size") + dpi = self.plot.figure.dpi + band = self._label_extent(ax, dim) + self._label_offset() * dpi / 72 + return band / ((W if dim == "width" else H) * dpi) + + def _label_extent( + self, ax: Axes, dim: Literal["width", "height"] + ) -> float: + """ + Return the largest visible polar tick-label extent in display space + """ + extents = [ + getattr(label.get_window_extent(self.geometry.renderer), dim) + for polar_side in POLAR_SIDES + if (axis := axis_at(ax, polar_side)) is not None + for tick in chain(axis.get_major_ticks(), axis.get_minor_ticks()) + for label in (tick.label1, tick.label2) + if _text_is_visible(label) + ] + return max([*extents, 0]) + + def _label_offset(self) -> float: + """ + Return the largest configured margin or non-blank tick length in points + """ + theme = self.plot.theme + margins = [ + max(m.t, m.b, m.l, m.r) + for name in POLAR_TEXT_THEMEABLES + if not theme.T.is_blank(name) + for m in [theme.get_margin(name).pt] + ] + lengths = [ + theme.getp(name) or 0 + for name, marks in POLAR_TICK_LENGTHS.items() + if not theme.T.is_blank(marks) + ] + return max([*margins, *lengths, 0]) + def axis_ticks_x_max_height(self, ax: Axes, side: Side) -> float: return 0 def axis_text_x_max_height(self, ax: Axes, side: Side) -> float: - return _polar_text_height_x(self.plot.theme, side) + return self._axis_clearance(ax, side) def axis_ticks_y_max_width(self, ax: Axes, side: Side) -> float: return 0 def axis_text_y_max_width(self, ax: Axes, side: Side) -> float: - return _polar_text_width_y(self.plot.theme, side) + return self._axis_clearance(ax, side) def axis_text_y_top_protrusion(self, location: AxesLocation) -> float: return 0.0 @@ -966,39 +1040,6 @@ def strip_shift(self, st: StripText) -> float: return self._strip_axis_clearance(st) -def _polar_text_height_x(theme: theme, side: Side) -> float: - """ - Estimate an `axis_text_x_` label's height in figure space - - A theta axis does not occupy a Cartesian side, so it has no side-specific - tick-label bounding box to measure. Use the font size as the label height, - following the estimate used for strip padding. As with - `axis_text_x_max_height`, the caller adds the tick-label margin. - """ - name = f"axis_text_x_{side}" - if theme.T.is_blank(name): - return 0 - _, H = theme.getp("figure_size") - size_pt = theme.getp((name, "size")) or 0 - return size_pt / 72 / H - - -def _polar_text_width_y(theme: theme, side: Side) -> float: - """ - Estimated figure-width of an `axis_text_y_` label on a polar panel - - Mirrors `_polar_text_height_x` for the r axis: it runs along one - spoke rather than wrapping the circle, but the same "no side to - measure" problem applies. - """ - name = f"axis_text_y_{side}" - if theme.T.is_blank(name): - return 0 - W, _ = theme.getp("figure_size") - size_pt = theme.getp((name, "size")) or 0 - return size_pt / 72 / W - - def _spine_set_position_outward(spine: Spine, axis: Axis, distance: float): """ Move a spine and its tick marks outward, keeping the theme's tick styling diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 767832ac3d..7fdbfcf658 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -384,6 +384,20 @@ def test_axis_ticks_theta_blank_keeps_label_gap(): assert p == "axis_ticks_theta_blank_keeps_label_gap" +def test_large_theta_text_fits_in_the_figure(): + # Larger theta labels must shrink the panel enough to remain inside the + # figure. + p = p_point + coord_radial() + theme(axis_text_theta=element_text(size=28)) + assert p == "large_theta_text_fits_in_the_figure" + + +def test_long_theta_ticks_fit_in_the_figure(): + # Longer visible theta ticks move their labels outward, so the panel must + # shrink by the themed tick length. + p = p_point + coord_radial() + theme(axis_ticks_length_major_theta=60) + assert p == "long_theta_ticks_fit_in_the_figure" + + def test_axis_text_r_blank_hides_radial_labels(): # `axis_text_r` hides both radial axes without hiding the theta axis. On a # full circle, the primary and secondary radial labels share one spoke. From 16722b30a67577625a36223252b089efdf2f1bab Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 13 Aug 2026 16:18:30 +0300 Subject: [PATCH 120/126] fix(layout): reserve polar space only where a label reaches A polar panel reserved its largest tick label on all four sides. One long label therefore cost a third of the panel and bought space on three sides that had nothing long on them. Each side now reserves the labels that reach past it, from where a label attaches to the panel, its own size, and the gap that holds it off its boundary. How much of that reach falls inside the panel depends on the panel the layout is still solving for, so it is taken from the previous render. A first render over-reserves by that difference rather than clipping. Polar baseline images shift again: the panels are larger. --- .../_mpl/layout_manager/_plot_layout_items.py | 182 +++++++++++------- tests/test_coord_radial.py | 14 ++ 2 files changed, 131 insertions(+), 65 deletions(-) diff --git a/plotnine/_mpl/layout_manager/_plot_layout_items.py b/plotnine/_mpl/layout_manager/_plot_layout_items.py index f2f3f561f0..0563e229d1 100644 --- a/plotnine/_mpl/layout_manager/_plot_layout_items.py +++ b/plotnine/_mpl/layout_manager/_plot_layout_items.py @@ -2,8 +2,9 @@ from dataclasses import dataclass from itertools import chain -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast +import numpy as np from matplotlib.text import Text from plotnine._utils import ha_as_float, side_artists, va_as_float @@ -31,9 +32,11 @@ from matplotlib.axes import Axes from matplotlib.axis import Axis, Tick + from matplotlib.backend_bases import RendererBase from matplotlib.figure import Figure from matplotlib.lines import Line2D from matplotlib.patches import Rectangle + from matplotlib.projections.polar import PolarAxes from matplotlib.spines import Spine from matplotlib.transforms import Bbox, Transform @@ -82,22 +85,80 @@ "r_end", ) -# Tick-label themeables for every polar boundary. -POLAR_TEXT_THEMEABLES = ( - "axis_text_theta_outside", - "axis_text_theta_inside", - "axis_text_r_start", - "axis_text_r_end", -) -# Map each polar tick-length themeable to the marks it sizes. Blank marks -# contribute no clearance. -POLAR_TICK_LENGTHS = { - "axis_ticks_length_major_theta": "axis_ticks_major_theta", - "axis_ticks_length_major_r": "axis_ticks_major_r", - "axis_ticks_length_minor_theta": "axis_ticks_minor_theta", - "axis_ticks_length_minor_r": "axis_ticks_minor_r", -} +@dataclass +class PolarLabel: + """ + A tick label of a polar panel, and where it attaches to the panel + + Sizes are in display space. `anchor` is in panel fractions, which hold + whatever size the panel settles at. + """ + + anchor: tuple[float, float] + """Point on the panel the label is placed against""" + + width: float + """Width of the label""" + + height: float + """Height of the label""" + + gap: float + """Distance the label is held off its anchor""" + + @classmethod + def make( + cls, + ax: PolarAxes, + side: PolarSide, + loc: float, + tick: Tick, + label: Text, + renderer: RendererBase, + ) -> PolarLabel: + """ + The label at one tick of a polar axis + + A theta tick anchors on its own boundary at the angle of its break; + an r tick anchors along its spoke at the radius of its break. The + gap covers the tick mark the label clears and the pad beyond it. + """ + r_inner, r_outer = ax.get_ylim() + if side == "theta_outside": + point = (loc, r_outer) + elif side == "theta_inside": + point = (loc, r_inner) + elif side == "r_start": + point = (np.deg2rad(ax.get_thetamin()), loc) + else: + point = (np.deg2rad(ax.get_thetamax()), loc) + + box = label.get_window_extent(renderer) + anchor = ax.transAxes.inverted().transform( + ax.transData.transform(point) + ) + gap_pt = (tick.get_pad() or 0) + tick.get_tick_padding() + return cls( + anchor=(anchor[0], anchor[1]), + width=box.width, + height=box.height, + gap=gap_pt * ax.figure.dpi / 72, + ) + + def reach_past(self, side: Side, panel: Bbox) -> float: + """ + How far the label reaches past one edge of the panel, display space + + Negative when the label stops short of the edge, which is what a + label facing away from the side does. + """ + if side in ("left", "right"): + along, extent, span = self.anchor[0], self.width, panel.width + else: + along, extent, span = self.anchor[1], self.height, panel.height + inset = along if side in ("left", "bottom") else 1 - along + return extent + self.gap - inset * span @dataclass @@ -944,64 +1005,55 @@ class PolarPlotLayoutItems(PlotLayoutItems): """ Space around a polar panel for its axis decorations - Theta axes follow arcs and r axes follow spokes, so neither belongs to - one Cartesian edge. Every panel edge therefore reserves the same band: - the largest visible label extent in that dimension, plus the greater of - the configured label margins and tick lengths whose marks are not blank. - - Label size is stable before final positioning, so it can be measured from - the artists. Margins and tick lengths come from the theme because their - final offsets are applied during drawing. - - The complete band is reported as text clearance. Tick clearance and - per-edge label protrusions remain zero to avoid counting it twice. + A theta axis follows an arc and an r axis follows a spoke, so a tick + label attaches to the panel wherever its break falls rather than along + one Cartesian side. Each side reserves the labels that reach past it: + a label's size and the gap that holds it off its boundary, less however + much of that reach falls inside the panel. + + The size and the gap are physical, so they hold wherever the panel ends + up. The inset is a fraction of the panel, and the panel is what the + layout is solving for, so it comes from the previous render. A first + render therefore over-reserves, which costs a little space and clips + nothing. + + The band is reported as text clearance, so tick clearance stays zero + rather than counting the marks twice. Per-side label protrusions stay + zero too: a label reaching past a second edge is counted on that edge. """ def _axis_clearance(self, ax: Axes, side: Side) -> float: """ - Return polar-axis clearance beyond one panel edge in figure space - """ - dim: Literal["width", "height"] = ( - "width" if side in ("left", "right") else "height" - ) - W, H = self.plot.theme.getp("figure_size") - dpi = self.plot.figure.dpi - band = self._label_extent(ax, dim) + self._label_offset() * dpi / 72 - return band / ((W if dim == "width" else H) * dpi) - - def _label_extent( - self, ax: Axes, dim: Literal["width", "height"] - ) -> float: - """ - Return the largest visible polar tick-label extent in display space + Space the panel's axes need past one edge, figure space """ - extents = [ - getattr(label.get_window_extent(self.geometry.renderer), dim) - for polar_side in POLAR_SIDES - if (axis := axis_at(ax, polar_side)) is not None - for tick in chain(axis.get_major_ticks(), axis.get_minor_ticks()) - for label in (tick.label1, tick.label2) - if _text_is_visible(label) + panel = ax.get_window_extent(self.geometry.renderer) + reaches = [ + label.reach_past(side, panel) for label in self._outward_labels(ax) ] - return max([*extents, 0]) + figure = self.plot.figure.bbox + span = figure.width if side in ("left", "right") else figure.height + return max([*reaches, 0]) / span - def _label_offset(self) -> float: + def _outward_labels(self, ax: Axes) -> Iterator[PolarLabel]: """ - Return the largest configured margin or non-blank tick length in points + Every tick label the panel will show, with where it attaches """ - theme = self.plot.theme - margins = [ - max(m.t, m.b, m.l, m.r) - for name in POLAR_TEXT_THEMEABLES - if not theme.T.is_blank(name) - for m in [theme.get_margin(name).pt] - ] - lengths = [ - theme.getp(name) or 0 - for name, marks in POLAR_TICK_LENGTHS.items() - if not theme.T.is_blank(marks) - ] - return max([*margins, *lengths, 0]) + renderer = self.geometry.renderer + panel = cast("PolarAxes", ax) + for polar_side in POLAR_SIDES: + axis = axis_at(ax, polar_side) + if axis is None: + continue + for locs, ticks in ( + (axis.get_majorticklocs(), axis.get_major_ticks()), + (axis.get_minorticklocs(), axis.get_minor_ticks()), + ): + for loc, tick in zip(locs, ticks): + for label in (tick.label1, tick.label2): + if _text_is_visible(label): + yield PolarLabel.make( + panel, polar_side, loc, tick, label, renderer + ) def axis_ticks_x_max_height(self, ax: Axes, side: Side) -> float: return 0 diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 7fdbfcf658..7a63b46087 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -398,6 +398,20 @@ def test_long_theta_ticks_fit_in_the_figure(): assert p == "long_theta_ticks_fit_in_the_figure" +def test_one_long_theta_label(): + # A label reserves space only on the sides it reaches, so one long + # label costs the panel nothing on the other three. + p = ( + p_point + + scale_x_continuous( + breaks=[2, 3, 4, 5], + labels=["2", "3", "Compact and subcompact", "5"], + ) + + coord_radial() + ) + assert p == "one_long_theta_label" + + def test_axis_text_r_blank_hides_radial_labels(): # `axis_text_r` hides both radial axes without hiding the theta axis. On a # full circle, the primary and secondary radial labels share one spoke. From 6afc200d49f4fa63e5fa237a1efbca46e851622e Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 13 Aug 2026 18:35:28 +0300 Subject: [PATCH 121/126] refactor(coord_radial): centralise polar label geometry Move label-bound measurement out of the theta tick class and define the full-circle and outward-direction calculations beside it. This gives polar label placement one coordinate convention without changing theta-label behaviour. --- plotnine/_mpl/_radial_axis.py | 131 +++++++++++++++++++++++++--------- 1 file changed, 97 insertions(+), 34 deletions(-) diff --git a/plotnine/_mpl/_radial_axis.py b/plotnine/_mpl/_radial_axis.py index 9901a8a21c..f6bbdfb21f 100644 --- a/plotnine/_mpl/_radial_axis.py +++ b/plotnine/_mpl/_radial_axis.py @@ -18,11 +18,107 @@ from matplotlib.backend_bases import RendererBase from matplotlib.text import Text from matplotlib.transforms import Transform + from numpy.typing import NDArray + + from plotnine.typing import PolarSide _NON_DESCENDING_NUMERIC_CHARS = frozenset("0123456789.+-\N{MINUS SIGN}eE") +def label_bounds(label: Text, renderer: RendererBase) -> Bbox: + """ + Return label bounds with unused numeric descent removed + + Matplotlib reserves the font's full descent even when a numeric label + has no descenders. Excluding that space keeps the visible gap between + labels and ticks consistent around the panel. + """ + bbox, parts, _ = label._get_layout(renderer) # pyright: ignore[reportAttributeAccessIssue] + descent = parts[-1][1][2] + x, y = label.get_unitless_position() + x, y = label.get_transform().transform((x, y)) + bbox = bbox.translated(x, y) + + text = label.get_text() + _, ismath = label._preprocess_math(text) # pyright: ignore[reportAttributeAccessIssue] + # Exclude the unused font descent from simple numeric labels without + # measuring rendered glyphs. + if ( + text + and "\n" not in text + and ismath is False + and set(text) <= _NON_DESCENDING_NUMERIC_CHARS + ): + bbox = Bbox.from_extents(bbox.x0, bbox.y0 + descent, bbox.x1, bbox.y1) + + return bbox + + +def facing_point(box: Bbox, unit: NDArray[np.float64]) -> NDArray[np.float64]: + """ + Return the point on a label's bounds that faces the panel + + `unit` points away from the panel. The opposite point is an edge midpoint + for an axis-aligned vector and moves towards a corner between axes. + """ + ux, uy = unit + return np.array( + [ + box.x0 + (0.5 - ux / 2) * box.width, + box.y0 + (0.5 - uy / 2) * box.height, + ] + ) + + +def _is_full_circle(ax: PolarAxes) -> bool: + """ + Whether the panel spans a full circle + """ + span = abs(ax.get_thetamax() - ax.get_thetamin()) + return abs(span - 360.0) < 1e-12 + + +def outward_unit( + ax: PolarAxes, side: PolarSide, loc: float +) -> NDArray[np.float64]: + """ + Return the outward unit vector at a polar tick + + For a theta boundary, `loc` is the break angle and the vector follows its + radius. For an r boundary, the vector is perpendicular to the boundary's + spoke, so `loc` does not affect the result. Start and end boundaries point + in opposite directions. + + Parameters + ---------- + ax : + Polar panel axes. + side : + Polar boundary containing the tick. + loc : + Break location in the axis's data coordinates. + """ + direction = ax.get_theta_direction() + offset = ax.get_theta_offset() + sign = 1 if side in ("theta_outside", "r_end") else -1 + + if side in ("theta_outside", "theta_inside"): + angle = loc * direction + offset + else: + if _is_full_circle(ax): + spoke_deg = ax.get_rlabel_position() + elif side == "r_start": + spoke_deg = ax.get_thetamin() + else: + spoke_deg = ax.get_thetamax() + spoke = np.deg2rad((spoke_deg * direction + np.rad2deg(offset)) % 360) + angle = spoke + sign * direction * np.pi / 2 + return np.array([np.cos(angle), np.sin(angle)]) + + return sign * np.array([np.cos(angle), np.sin(angle)]) + + class p9ThetaTick(ThetaTick): """ Theta tick whose labels sit on the rim of their matching tick marks @@ -48,39 +144,6 @@ def _update_padding(self, pad: float, angle: float) -> None: self._text2_translate._t = (0, 0) # pyright: ignore[reportAttributeAccessIssue] self._text2_translate.invalidate() # pyright: ignore[reportAttributeAccessIssue] - def _label_bounds(self, label: Text, renderer: RendererBase) -> Bbox: - """ - Return label bounds without unused numeric descent - - Matplotlib reserves the font's full descent even when a numeric label - has no descenders. Removing that unused space prevents the visible - gap between theta labels and ticks from varying around the circle. - """ - bbox, parts, _ = label._get_layout(renderer) # pyright: ignore[reportAttributeAccessIssue] - descent = parts[-1][1][2] - x, y = label.get_unitless_position() - x, y = label.get_transform().transform((x, y)) - bbox = bbox.translated(x, y) - - text = label.get_text() - _, ismath = label._preprocess_math(text) # pyright: ignore[reportAttributeAccessIssue] - # Common numeric ticks have no descenders, so exclude Matplotlib's - # unused descent without measuring rendered ink. - if ( - text - and "\n" not in text - and ismath is False - and set(text) <= _NON_DESCENDING_NUMERIC_CHARS - ): - bbox = Bbox.from_extents( - bbox.x0, - bbox.y0 + descent, - bbox.x1, - bbox.y1, - ) - - return bbox - def _position_labels(self, renderer: RendererBase) -> None: # Matplotlib pads theta labels from their centres by a fixed amount, # which leaves angle-dependent gaps at the edges. We override to place @@ -124,7 +187,7 @@ def _position_labels(self, renderer: RendererBase) -> None: translate.invalidate() anchor = label.get_transform().transform(label.get_position()) - corners = self._label_bounds(label, renderer).corners() + corners = label_bounds(label, renderer).corners() # Offset the label by its radial extent so the margin starts at # the nearest text edge, not at the label centre. edge_offset = np.min((corners - anchor) @ unit) From a7140e4d5bb0c0380f4a09cd20ba0d976a3e41d1 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 13 Aug 2026 18:38:35 +0300 Subject: [PATCH 122/126] fix(coord_radial): align theta labels with their ticks Theta labels were anchored from the nearest bounding-box corner. Between the cardinal directions, this moved the label edge away from its tick and made the gap depend on label width. Centre each label on its tick, then offset the point facing the panel by the themed padding. --- plotnine/_mpl/_radial_axis.py | 44 ++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/plotnine/_mpl/_radial_axis.py b/plotnine/_mpl/_radial_axis.py index f6bbdfb21f..24600ead78 100644 --- a/plotnine/_mpl/_radial_axis.py +++ b/plotnine/_mpl/_radial_axis.py @@ -16,6 +16,7 @@ if TYPE_CHECKING: from matplotlib.backend_bases import RendererBase + from matplotlib.lines import Line2D from matplotlib.text import Text from matplotlib.transforms import Transform from numpy.typing import NDArray @@ -145,33 +146,34 @@ def _update_padding(self, pad: float, angle: float) -> None: self._text2_translate.invalidate() # pyright: ignore[reportAttributeAccessIssue] def _position_labels(self, renderer: RendererBase) -> None: - # Matplotlib pads theta labels from their centres by a fixed amount, - # which leaves angle-dependent gaps at the edges. We override to place - # each nearest edge at the themed margin beyond its tick or the axis - # boundary. + # Matplotlib pads theta labels from their centres, so the visible gap + # varies with the label's angle. Place the point facing the panel at + # the themed distance beyond its own tick. axes = cast("PolarAxes", self.axes) - radial = np.array( - [np.cos(self._radial_angle), np.sin(self._radial_angle)] - ) + loc = self.get_loc() - for label, tickline, translate, direction in ( + pairs: tuple[ + tuple[Text, Line2D, ScaledTranslation, PolarSide], ... + ] = ( ( self.label1, self.tick1line, self._text1_translate, # pyright: ignore[reportAttributeAccessIssue] - -1, + "theta_inside", ), ( self.label2, self.tick2line, self._text2_translate, # pyright: ignore[reportAttributeAccessIssue] - 1, + "theta_outside", ), - ): + ) + + for label, tickline, translate, side in pairs: if not label.get_visible() or not label.get_text(): continue - label.set_verticalalignment("center") + label.set_horizontalalignment("center") label.set_verticalalignment("center") tick_length = ( self._size # pyright: ignore[reportAttributeAccessIssue] @@ -182,17 +184,17 @@ def _position_labels(self, renderer: RendererBase) -> None: self._base_pad # pyright: ignore[reportAttributeAccessIssue] + tick_length ) - unit = radial * direction - translate._t = tuple(unit * base_pad / 72) + unit = outward_unit(axes, side, loc) + + # Apply the padding at the label's centre, then correct the offset + # so the point facing the panel reaches the same target. + padded = unit * base_pad / 72 + translate._t = tuple(padded) translate.invalidate() - anchor = label.get_transform().transform(label.get_position()) - corners = label_bounds(label, renderer).corners() - # Offset the label by its radial extent so the margin starts at - # the nearest text edge, not at the label centre. - edge_offset = np.min((corners - anchor) @ unit) - corrected = base_pad - edge_offset * 72 / axes.figure.dpi - translate._t = tuple(unit * corrected / 72) + target = label.get_transform().transform(label.get_position()) + point = facing_point(label_bounds(label, renderer), unit) + translate._t = tuple(padded + (target - point) / axes.figure.dpi) translate.invalidate() @martist.allow_rasterization From cebc7051689f73f04b6816b01c1634bab0b5494f Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 13 Aug 2026 18:46:28 +0300 Subject: [PATCH 123/126] fix(coord_radial): align r labels with their spokes Matplotlib's discrete alignments made the gap between an r label and its spoke depend on the spoke angle and label dimensions. Centre each label on its spoke, then offset the edge facing the panel by the themed padding. Apply this placement to full circles and both boundaries of an arc. Recompute the placement during each draw because it depends on rendered label dimensions. Recompute it before the final redraw so r labels retain the measured position above geoms. --- plotnine/_mpl/_radial_axes.py | 7 ++ plotnine/_mpl/_radial_axis.py | 210 ++++++++++++++++++++-------------- tests/test_coord_radial.py | 27 +++++ 3 files changed, 161 insertions(+), 83 deletions(-) diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index 96cc04a096..889ae10e9d 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -16,6 +16,7 @@ from ._radial_axis import ( p9RadialAxis, + p9RadialTick, p9SecondaryRadialAxis, p9ThetaAxis, p9ThetaTick, @@ -423,8 +424,14 @@ def _redraw_raxis(axis: p9RadialAxis, renderer: RendererBase) -> None: primary r-axis moves to the end spoke under `reverse="theta"`, swapping with the secondary. Each mark draws before its label so a label never sits under its own mark. + + Reposition each label before redrawing it because placement depends on + its rendered bounds. This preserves the measured position when the redraw + lifts the label above the geoms. """ for tick in (*axis.majorTicks, *axis.minorTicks): + if isinstance(tick, p9RadialTick): + tick._position_labels(renderer) for mark, label in ( (tick.tick1line, tick.label1), (tick.tick2line, tick.label2), diff --git a/plotnine/_mpl/_radial_axis.py b/plotnine/_mpl/_radial_axis.py index 24600ead78..bc88d3e2c0 100644 --- a/plotnine/_mpl/_radial_axis.py +++ b/plotnine/_mpl/_radial_axis.py @@ -224,98 +224,142 @@ class p9RadialTick(RadialTick): # negative side the start pair. _tick_side: int = -1 + def __init__(self, axes, *args, **kwargs): + # `RadialTick` has no label translations. Add one to each label + # transform so `draw` can position the pair from its measured bounds. + figure = axes.get_figure(root=False) + self._text1_translate = ScaledTranslation(0, 0, figure.dpi_scale_trans) + self._text2_translate = ScaledTranslation(0, 0, figure.dpi_scale_trans) + super().__init__(axes, *args, **kwargs) + + def _placements( + self, + ) -> list[tuple[PolarSide, Text, Line2D, ScaledTranslation]]: + """ + Return the label placement for each active r boundary + + An arc places one pair on each end spoke. A full circle places both + pairs on one spoke, where this axis draws the pair selected by + `_tick_side`. + """ + start: tuple[PolarSide, Text, Line2D, ScaledTranslation] = ( + "r_start", + self.label1, + self.tick1line, + self._text1_translate, + ) + end: tuple[PolarSide, Text, Line2D, ScaledTranslation] = ( + "r_end", + self.label2, + self.tick2line, + self._text2_translate, + ) + if not _is_full_circle(cast("PolarAxes", self.axes)): + return [start, end] + return [end] if self._tick_side > 0 else [start] + + def _pad_offset(self, unit: NDArray[np.float64]) -> NDArray[np.float64]: + """ + Return the pre-measurement translation from the spoke + """ + # `self._pad` includes the tick length and the gap beyond the mark. + return unit * self._pad / 72 # pyright: ignore[reportAttributeAccessIssue] + def update_position(self, loc: float) -> None: - # Matplotlib already handles the two radial axes at the ends of a - # partial arc. For a full circle it places the labels at - # `rlabel_position`, but leaves the tick marker unrotated. Convert - # that position to its rendered screen angle, then turn the marker - # perpendicular to the spoke, opposite the effective theta sweep. - - # On a full circle the two tick/label pairs would land on the same - # spoke, so the `super().update_position` below hides the second - # one. But the secondary r-axis draws that pair, on the opposite - # side of the spoke — so save its visibility now and restore it, - # after the super call, in the `_tick_side > 0` branch. + # Matplotlib's discrete text alignments make the gap depend on the + # spoke angle. Centre each label on its spoke and pad it in display + # space. `draw` then aligns its measured edge with that padded point. + + # On a full circle, Matplotlib hides the second tick and label pair + # because both pairs occupy one spoke. The secondary r-axis uses that + # pair on the opposite side, so preserve its visibility across the + # base position update. want_label = self.label2.get_visible() want_tick = self.tick2line.get_visible() super().update_position(loc) axes = cast("PolarAxes", self.axes) - thetamin = axes.get_thetamin() - thetamax = axes.get_thetamax() - if abs(abs(thetamax - thetamin) - 360.0) >= 1e-12: - return - - if self._tick_side > 0: - label, tickline = self.label2, self.tick2line - text1_transform = axes.get_yaxis_text2_transform(0)[0] - label.set_visible(want_label) - tickline.set_visible(want_tick) - else: - label, tickline = self.label1, self.tick1line - text1_transform = axes.get_yaxis_text1_transform(0)[0] - - # The end pair's tick line anchors at theta = thetamax (x = 1), which - # for a full circle fans away from the shared start spoke as the - # radius grows. Anchor it on the spoke (x = 0), where the start pair - # already sits, so the mirrored marker below stays on the spoke. - tickline.set_xdata([0]) - - direction = axes.get_theta_direction() - offset = np.rad2deg(axes.get_theta_offset()) - spoke_angle = (axes.get_rlabel_position() * direction + offset) % 360 - # `_tick_side` flips the perpendicular so a secondary axis mirrors - # the primary across the shared spoke. - marker_angle = np.deg2rad( - spoke_angle + self._tick_side * direction * 90 - ) + full_circle = _is_full_circle(axes) - # Replace the marker's base transform so its one-sided tick points - # along `marker_angle`. `MarkerStyle.transformed()` would compose - # with the existing TICKLEFT/TICKRIGHT transform instead. - # - # `marker_angle` already carries the full direction (the primary and - # secondary sides differ by 180°), and TICKLEFT/TICKRIGHT share the - # same `[[0, 0], [1, 0]]` path, so both rotate by it directly. Adding - # TICKRIGHT's own `scale(-1, 1)` here would undo the `_tick_side` - # mirror and send the secondary mark back onto the primary's side. - marker = tickline.get_marker() - tick_marker = cast("mmarkers.MarkerStyle", tickline._marker) # pyright: ignore[reportAttributeAccessIssue] - if marker in (mmarkers.TICKLEFT, mmarkers.TICKRIGHT): - transform = Affine2D().rotate(marker_angle) - elif marker == "_": - transform = Affine2D().rotate(marker_angle + np.pi / 2) - else: - transform = tick_marker._transform # pyright: ignore[reportAttributeAccessIssue] - tick_marker._transform = transform # pyright: ignore[reportAttributeAccessIssue] - - # A constant theta offset fans labels farther from the spoke as the - # radius grows. Keep every label on the spoke and apply its padding - # in display space so their edges remain aligned. - mode, _ = self._labelrotation # pyright: ignore[reportAttributeAccessIssue] - angle = spoke_angle - 90 - start = (self._tick_side * direction) < 0 - ha, va = self._determine_anchor(mode, angle, start) # pyright: ignore[reportAttributeAccessIssue] - # self._pad includes the tick length and the gap between - # the tick and text. - shift = self._pad # pyright: ignore[reportAttributeAccessIssue] - text_transform = ( - # Place the text flush with the spoke. For a full circle, - # matplotlib ignores the pad passed to this method. - text1_transform - # Move the text by a fixed physical distance independent of the - # data radius. - + ScaledTranslation( - np.cos(marker_angle) * shift / 72, - np.sin(marker_angle) * shift / 72, - axes.figure.dpi_scale_trans, + for side, label, tickline, translate in self._placements(): + unit = outward_unit(axes, side, loc) + + if full_circle: + if side == "r_end": + label.set_visible(want_label) + tickline.set_visible(want_tick) + + # On a full circle, the end tick anchors at `thetamax` + # (`x = 1`) and diverges from the shared start spoke as the + # radius grows. Move it to `x = 0`, where the start tick + # already anchors. + tickline.set_xdata([0]) + + # Replace the marker's base transform so a one-sided tick + # points outwards. `MarkerStyle.transformed()` would compose + # with the existing `TICKLEFT` or `TICKRIGHT` transform. + # + # The outward angle distinguishes primary and secondary ticks + # by 180°. `TICKLEFT` and `TICKRIGHT` share the same path, so + # rotate both directly. Applying `TICKRIGHT`'s horizontal flip + # would place the secondary mark on the primary side. + marker_angle = np.arctan2(unit[1], unit[0]) + marker = tickline.get_marker() + tick_marker = cast("mmarkers.MarkerStyle", tickline._marker) # pyright: ignore[reportAttributeAccessIssue] + if marker in (mmarkers.TICKLEFT, mmarkers.TICKRIGHT): + transform = Affine2D().rotate(marker_angle) + elif marker == "_": + transform = Affine2D().rotate(marker_angle + np.pi / 2) + else: + transform = tick_marker._transform # pyright: ignore[reportAttributeAccessIssue] + tick_marker._transform = transform # pyright: ignore[reportAttributeAccessIssue] + + # For a full circle, the base transform interprets x as an + # angle. Set x to the shared spoke. For an arc, it interprets + # x as a sweep fraction and already places each pair on an + # end spoke. + label.set_x(0) + + # Request the base transform without padding, then add a fixed + # display-space offset. Otherwise Matplotlib derives an arc + # label's padding direction from the view limits, which can point + # away from its spoke after the sweep reverses. + base = ( + axes.get_yaxis_text2_transform(0)[0] + if side == "r_end" + else axes.get_yaxis_text1_transform(0)[0] ) - ) - label.set_x(0) - label.set_transform(text_transform) - label.set_horizontalalignment(ha) - label.set_verticalalignment(va) + label.set_transform(base + translate) + label.set_horizontalalignment("center") + label.set_verticalalignment("center") + translate._t = tuple(self._pad_offset(unit)) # pyright: ignore[reportAttributeAccessIssue] + translate.invalidate() + + def _position_labels(self, renderer: RendererBase) -> None: + # Recompute from the initial pad offset on every draw so translations + # do not accumulate across repeated draws. + axes = cast("PolarAxes", self.axes) + loc = self.get_loc() + + for side, label, _, translate in self._placements(): + if not label.get_visible() or not label.get_text(): + continue + + unit = outward_unit(axes, side, loc) + padded = self._pad_offset(unit) + translate._t = tuple(padded) # pyright: ignore[reportAttributeAccessIssue] + translate.invalidate() + + target = label.get_transform().transform(label.get_position()) + point = facing_point(label_bounds(label, renderer), unit) + translate._t = tuple(padded + (target - point) / axes.figure.dpi) # pyright: ignore[reportAttributeAccessIssue] + translate.invalidate() + + @martist.allow_rasterization + def draw(self, renderer: RendererBase) -> None: + self._position_labels(renderer) + super().draw(renderer) class p9SecondaryRadialTick(p9RadialTick): diff --git a/tests/test_coord_radial.py b/tests/test_coord_radial.py index 7a63b46087..07edd92815 100644 --- a/tests/test_coord_radial.py +++ b/tests/test_coord_radial.py @@ -412,6 +412,33 @@ def test_one_long_theta_label(): assert p == "one_long_theta_label" +def test_long_theta_labels_all_quadrants(): + # Each long label must meet the rim beside its own tick in every quadrant. + p = ( + p_point + + scale_x_continuous( + breaks=[1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5], + labels=[f"long label {i}" for i in range(1, 9)], + ) + + coord_radial() + ) + assert p == "long_theta_labels_all_quadrants" + + +def test_long_r_labels_on_both_spokes(): + # Labels on both collinear spokes must face away from the filled half. + p = p_wedge + scale_y_continuous( + breaks=[10, 20, 30], + labels=["ten mpg", "twenty mpg", "thirty mpg"], + sec_axis=sec_axis( + lambda x: x * 2, + breaks=[20, 40, 60], + labels=["twenty kph", "forty kph", "sixty kph"], + ), + ) + assert p == "long_r_labels_on_both_spokes" + + def test_axis_text_r_blank_hides_radial_labels(): # `axis_text_r` hides both radial axes without hiding the theta axis. On a # full circle, the primary and secondary radial labels share one spoke. From dbceebf4d7e2319315ef20e3e8ec0a951d9b7099 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 13 Aug 2026 18:50:46 +0300 Subject: [PATCH 124/126] fix(layout): reserve space by polar label direction Polar layout reserved each label's full width or height for every panel edge. Long labels therefore shrank the panel along edges they did not extend towards. Calculate clearance from each label's anchor and outward direction, and measure only the label pair assigned to each polar boundary. For full circles, anchor r-label clearance to the configured label spoke instead of the start of the theta range. --- plotnine/_mpl/_radial_axis.py | 32 ++++++++++------ .../_mpl/layout_manager/_plot_layout_items.py | 37 ++++++++++++------- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/plotnine/_mpl/_radial_axis.py b/plotnine/_mpl/_radial_axis.py index bc88d3e2c0..8c1b2b08c2 100644 --- a/plotnine/_mpl/_radial_axis.py +++ b/plotnine/_mpl/_radial_axis.py @@ -80,6 +80,21 @@ def _is_full_circle(ax: PolarAxes) -> bool: return abs(span - 360.0) < 1e-12 +def spoke_angle(ax: PolarAxes, side: PolarSide) -> float: + """ + Return an r boundary's spoke angle in data coordinates + + For a full circle, both r boundaries use the configured label spoke. For + an arc, `r_start` uses `thetamin` and `r_end` uses `thetamax`. + """ + if _is_full_circle(ax): + return np.deg2rad(ax.get_rlabel_position()) + elif side == "r_start": + return np.deg2rad(ax.get_thetamin()) + else: + return np.deg2rad(ax.get_thetamax()) + + def outward_unit( ax: PolarAxes, side: PolarSide, loc: float ) -> NDArray[np.float64]: @@ -106,18 +121,11 @@ def outward_unit( if side in ("theta_outside", "theta_inside"): angle = loc * direction + offset - else: - if _is_full_circle(ax): - spoke_deg = ax.get_rlabel_position() - elif side == "r_start": - spoke_deg = ax.get_thetamin() - else: - spoke_deg = ax.get_thetamax() - spoke = np.deg2rad((spoke_deg * direction + np.rad2deg(offset)) % 360) - angle = spoke + sign * direction * np.pi / 2 - return np.array([np.cos(angle), np.sin(angle)]) - - return sign * np.array([np.cos(angle), np.sin(angle)]) + return sign * np.array([np.cos(angle), np.sin(angle)]) + + spoke = spoke_angle(ax, side) * direction + offset + angle = spoke + sign * direction * np.pi / 2 + return np.array([np.cos(angle), np.sin(angle)]) class p9ThetaTick(ThetaTick): diff --git a/plotnine/_mpl/layout_manager/_plot_layout_items.py b/plotnine/_mpl/layout_manager/_plot_layout_items.py index 0563e229d1..221c87f23c 100644 --- a/plotnine/_mpl/layout_manager/_plot_layout_items.py +++ b/plotnine/_mpl/layout_manager/_plot_layout_items.py @@ -4,13 +4,13 @@ from itertools import chain from typing import TYPE_CHECKING, cast -import numpy as np from matplotlib.text import Text from plotnine._utils import ha_as_float, side_artists, va_as_float from plotnine.composition._compose import Compose from plotnine.exceptions import PlotnineError +from .._radial_axis import outward_unit, spoke_angle from ..axes import axis_at from ..utils import ( ArtistGeometry, @@ -98,6 +98,9 @@ class PolarLabel: anchor: tuple[float, float] """Point on the panel the label is placed against""" + unit: tuple[float, float] + """Outward unit vector at the label's anchor""" + width: float """Width of the label""" @@ -122,25 +125,26 @@ def make( A theta tick anchors on its own boundary at the angle of its break; an r tick anchors along its spoke at the radius of its break. The - gap covers the tick mark the label clears and the pad beyond it. + gap covers the tick mark the label clears and the pad beyond it. The + unit vector points from the panel towards the label. """ r_inner, r_outer = ax.get_ylim() if side == "theta_outside": point = (loc, r_outer) elif side == "theta_inside": point = (loc, r_inner) - elif side == "r_start": - point = (np.deg2rad(ax.get_thetamin()), loc) else: - point = (np.deg2rad(ax.get_thetamax()), loc) + point = (spoke_angle(ax, side), loc) box = label.get_window_extent(renderer) anchor = ax.transAxes.inverted().transform( ax.transData.transform(point) ) + unit = outward_unit(ax, side, loc) gap_pt = (tick.get_pad() or 0) + tick.get_tick_padding() return cls( anchor=(anchor[0], anchor[1]), + unit=(unit[0], unit[1]), width=box.width, height=box.height, gap=gap_pt * ax.figure.dpi / 72, @@ -150,15 +154,19 @@ def reach_past(self, side: Side, panel: Bbox) -> float: """ How far the label reaches past one edge of the panel, display space - Negative when the label stops short of the edge, which is what a - label facing away from the side does. + The direction component towards the edge determines how much of the + label and gap extend past it. A label parallel to the edge contributes + half its extent and none of its gap. A negative result means the label + stops inside the edge. """ if side in ("left", "right"): along, extent, span = self.anchor[0], self.width, panel.width + facing = self.unit[0] if side == "right" else -self.unit[0] else: along, extent, span = self.anchor[1], self.height, panel.height + facing = self.unit[1] if side == "top" else -self.unit[1] inset = along if side in ("left", "bottom") else 1 - along - return extent + self.gap - inset * span + return extent * (1 + facing) / 2 + self.gap * facing - inset * span @dataclass @@ -1037,6 +1045,8 @@ def _axis_clearance(self, ax: Axes, side: Side) -> float: def _outward_labels(self, ax: Axes) -> Iterator[PolarLabel]: """ Every tick label the panel will show, with where it attaches + + Each boundary contributes only the label pair assigned to that side. """ renderer = self.geometry.renderer panel = cast("PolarAxes", ax) @@ -1044,16 +1054,17 @@ def _outward_labels(self, ax: Axes) -> Iterator[PolarLabel]: axis = axis_at(ax, polar_side) if axis is None: continue + _, label_attr = side_artists(polar_side) for locs, ticks in ( (axis.get_majorticklocs(), axis.get_major_ticks()), (axis.get_minorticklocs(), axis.get_minor_ticks()), ): for loc, tick in zip(locs, ticks): - for label in (tick.label1, tick.label2): - if _text_is_visible(label): - yield PolarLabel.make( - panel, polar_side, loc, tick, label, renderer - ) + label = getattr(tick, label_attr) + if _text_is_visible(label): + yield PolarLabel.make( + panel, polar_side, loc, tick, label, renderer + ) def axis_ticks_x_max_height(self, ax: Axes, side: Side) -> float: return 0 From 5b1b510b8f8c3e5ccd890343f91229a61434688f Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 13 Aug 2026 19:18:33 +0300 Subject: [PATCH 125/126] refactor(coord_radial): move polar geometry to panel axes Make the polar axes responsible for identifying full-circle panels, locating r-axis spokes, and returning outward directions. Tick placement and layout now obtain this geometry from the panel instead of duplicating it or depending on Matplotlib's `PolarAxes` type. --- plotnine/_mpl/_radial_axes.py | 56 ++++++++++++++ plotnine/_mpl/_radial_axis.py | 76 +++---------------- .../_mpl/layout_manager/_plot_layout_items.py | 11 ++- 3 files changed, 71 insertions(+), 72 deletions(-) diff --git a/plotnine/_mpl/_radial_axes.py b/plotnine/_mpl/_radial_axes.py index 889ae10e9d..3677cc52da 100644 --- a/plotnine/_mpl/_radial_axes.py +++ b/plotnine/_mpl/_radial_axes.py @@ -25,6 +25,7 @@ if TYPE_CHECKING: from matplotlib.backend_bases import RendererBase + from numpy.typing import NDArray from plotnine.typing import PolarSide @@ -248,6 +249,61 @@ def axis_at_side(self) -> dict[PolarSide, ThetaAxis | RadialAxis]: self._axis_at_side = {} return self._axis_at_side + @property + def is_full_circle(self) -> bool: + """ + Whether the panel spans a full circle + """ + span = abs(self.get_thetamax() - self.get_thetamin()) + return abs(span - 360.0) < 1e-12 + + def spoke_angle(self, side: PolarSide) -> float: + """ + Return an r boundary's spoke angle in data coordinates + + For a full circle, both r boundaries use the configured label spoke. + For an arc, `r_start` uses `thetamin` and `r_end` uses `thetamax`. + + Parameters + ---------- + side : + One of the panel's two r boundaries. + """ + if self.is_full_circle: + return np.deg2rad(self.get_rlabel_position()) + elif side == "r_start": + return np.deg2rad(self.get_thetamin()) + else: + return np.deg2rad(self.get_thetamax()) + + def outward_unit(self, side: PolarSide, loc: float) -> NDArray[np.float64]: + """ + Return the outward unit vector at a polar tick + + For a theta boundary, `loc` is the break angle and the vector follows + its radius. For an r boundary, the vector is perpendicular to the + boundary's spoke, so `loc` does not affect the result. Start and end + boundaries point in opposite directions. + + Parameters + ---------- + side : + Polar boundary containing the tick. + loc : + Break location in the axis's data coordinates. + """ + direction = self.get_theta_direction() + offset = self.get_theta_offset() + sign = 1 if side in ("theta_outside", "r_end") else -1 + + if side in ("theta_outside", "theta_inside"): + angle = loc * direction + offset + return sign * np.array([np.cos(angle), np.sin(angle)]) + + spoke = self.spoke_angle(side) * direction + offset + angle = spoke + sign * direction * np.pi / 2 + return np.array([np.cos(angle), np.sin(angle)]) + def draw(self, renderer: RendererBase) -> None: """ Draw the axes, then lift the r-axis ticks above the geoms diff --git a/plotnine/_mpl/_radial_axis.py b/plotnine/_mpl/_radial_axis.py index 8c1b2b08c2..5947d86a77 100644 --- a/plotnine/_mpl/_radial_axis.py +++ b/plotnine/_mpl/_radial_axis.py @@ -6,7 +6,6 @@ from matplotlib import artist as martist from matplotlib import markers as mmarkers from matplotlib.projections.polar import ( - PolarAxes, RadialAxis, RadialTick, ThetaAxis, @@ -23,6 +22,8 @@ from plotnine.typing import PolarSide + from ._radial_axes import p9RadialAxes + _NON_DESCENDING_NUMERIC_CHARS = frozenset("0123456789.+-\N{MINUS SIGN}eE") @@ -72,62 +73,6 @@ def facing_point(box: Bbox, unit: NDArray[np.float64]) -> NDArray[np.float64]: ) -def _is_full_circle(ax: PolarAxes) -> bool: - """ - Whether the panel spans a full circle - """ - span = abs(ax.get_thetamax() - ax.get_thetamin()) - return abs(span - 360.0) < 1e-12 - - -def spoke_angle(ax: PolarAxes, side: PolarSide) -> float: - """ - Return an r boundary's spoke angle in data coordinates - - For a full circle, both r boundaries use the configured label spoke. For - an arc, `r_start` uses `thetamin` and `r_end` uses `thetamax`. - """ - if _is_full_circle(ax): - return np.deg2rad(ax.get_rlabel_position()) - elif side == "r_start": - return np.deg2rad(ax.get_thetamin()) - else: - return np.deg2rad(ax.get_thetamax()) - - -def outward_unit( - ax: PolarAxes, side: PolarSide, loc: float -) -> NDArray[np.float64]: - """ - Return the outward unit vector at a polar tick - - For a theta boundary, `loc` is the break angle and the vector follows its - radius. For an r boundary, the vector is perpendicular to the boundary's - spoke, so `loc` does not affect the result. Start and end boundaries point - in opposite directions. - - Parameters - ---------- - ax : - Polar panel axes. - side : - Polar boundary containing the tick. - loc : - Break location in the axis's data coordinates. - """ - direction = ax.get_theta_direction() - offset = ax.get_theta_offset() - sign = 1 if side in ("theta_outside", "r_end") else -1 - - if side in ("theta_outside", "theta_inside"): - angle = loc * direction + offset - return sign * np.array([np.cos(angle), np.sin(angle)]) - - spoke = spoke_angle(ax, side) * direction + offset - angle = spoke + sign * direction * np.pi / 2 - return np.array([np.cos(angle), np.sin(angle)]) - - class p9ThetaTick(ThetaTick): """ Theta tick whose labels sit on the rim of their matching tick marks @@ -157,7 +102,7 @@ def _position_labels(self, renderer: RendererBase) -> None: # Matplotlib pads theta labels from their centres, so the visible gap # varies with the label's angle. Place the point facing the panel at # the themed distance beyond its own tick. - axes = cast("PolarAxes", self.axes) + axes = cast("p9RadialAxes", self.axes) loc = self.get_loc() pairs: tuple[ @@ -192,7 +137,7 @@ def _position_labels(self, renderer: RendererBase) -> None: self._base_pad # pyright: ignore[reportAttributeAccessIssue] + tick_length ) - unit = outward_unit(axes, side, loc) + unit = axes.outward_unit(side, loc) # Apply the padding at the label's centre, then correct the offset # so the point facing the panel reaches the same target. @@ -262,7 +207,7 @@ def _placements( self.tick2line, self._text2_translate, ) - if not _is_full_circle(cast("PolarAxes", self.axes)): + if not cast("p9RadialAxes", self.axes).is_full_circle: return [start, end] return [end] if self._tick_side > 0 else [start] @@ -287,13 +232,12 @@ def update_position(self, loc: float) -> None: super().update_position(loc) - axes = cast("PolarAxes", self.axes) - full_circle = _is_full_circle(axes) + axes = cast("p9RadialAxes", self.axes) for side, label, tickline, translate in self._placements(): - unit = outward_unit(axes, side, loc) + unit = axes.outward_unit(side, loc) - if full_circle: + if axes.is_full_circle: if side == "r_end": label.set_visible(want_label) tickline.set_visible(want_tick) @@ -347,14 +291,14 @@ def update_position(self, loc: float) -> None: def _position_labels(self, renderer: RendererBase) -> None: # Recompute from the initial pad offset on every draw so translations # do not accumulate across repeated draws. - axes = cast("PolarAxes", self.axes) + axes = cast("p9RadialAxes", self.axes) loc = self.get_loc() for side, label, _, translate in self._placements(): if not label.get_visible() or not label.get_text(): continue - unit = outward_unit(axes, side, loc) + unit = axes.outward_unit(side, loc) padded = self._pad_offset(unit) translate._t = tuple(padded) # pyright: ignore[reportAttributeAccessIssue] translate.invalidate() diff --git a/plotnine/_mpl/layout_manager/_plot_layout_items.py b/plotnine/_mpl/layout_manager/_plot_layout_items.py index 221c87f23c..5677b4ed56 100644 --- a/plotnine/_mpl/layout_manager/_plot_layout_items.py +++ b/plotnine/_mpl/layout_manager/_plot_layout_items.py @@ -10,7 +10,6 @@ from plotnine.composition._compose import Compose from plotnine.exceptions import PlotnineError -from .._radial_axis import outward_unit, spoke_angle from ..axes import axis_at from ..utils import ( ArtistGeometry, @@ -36,11 +35,11 @@ from matplotlib.figure import Figure from matplotlib.lines import Line2D from matplotlib.patches import Rectangle - from matplotlib.projections.polar import PolarAxes from matplotlib.spines import Spine from matplotlib.transforms import Bbox, Transform from plotnine import ggplot + from plotnine._mpl._radial_axes import p9RadialAxes from plotnine._mpl.offsetbox import FlexibleAnchoredOffsetbox from plotnine._mpl.text import StripText from plotnine.iapi import legend_artists @@ -113,7 +112,7 @@ class PolarLabel: @classmethod def make( cls, - ax: PolarAxes, + ax: p9RadialAxes, side: PolarSide, loc: float, tick: Tick, @@ -134,13 +133,13 @@ def make( elif side == "theta_inside": point = (loc, r_inner) else: - point = (spoke_angle(ax, side), loc) + point = (ax.spoke_angle(side), loc) box = label.get_window_extent(renderer) anchor = ax.transAxes.inverted().transform( ax.transData.transform(point) ) - unit = outward_unit(ax, side, loc) + unit = ax.outward_unit(side, loc) gap_pt = (tick.get_pad() or 0) + tick.get_tick_padding() return cls( anchor=(anchor[0], anchor[1]), @@ -1049,7 +1048,7 @@ def _outward_labels(self, ax: Axes) -> Iterator[PolarLabel]: Each boundary contributes only the label pair assigned to that side. """ renderer = self.geometry.renderer - panel = cast("PolarAxes", ax) + panel = cast("p9RadialAxes", ax) for polar_side in POLAR_SIDES: axis = axis_at(ax, polar_side) if axis is None: From 596006f0c9f724bf98357fdb3b2f51c7eff27400 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 14 Aug 2026 21:59:55 +0300 Subject: [PATCH 126/126] test(coord_radial): add the baseline images The 42 images cover the polar suite: arcs and donuts, reversed theta and r, secondary theta and radial axes, per-side axis theming, and the panel spacing that long tick labels and large theta text demand. --- .../test_coord_radial/arc_wraps_past_twelve.png | Bin 0 -> 16523 bytes .../axis_line_r_end_on_reversed_arc.png | Bin 0 -> 28273 bytes .../axis_line_r_reaches_both_spokes.png | Bin 0 -> 27904 bytes .../axis_line_r_start_on_full_circle.png | Bin 0 -> 37984 bytes .../axis_line_styles_polar_boundaries.png | Bin 0 -> 35614 bytes .../axis_text_r_blank_hides_radial_labels.png | Bin 0 -> 19333 bytes .../axis_ticks_theta_blank_keeps_label_gap.png | Bin 0 -> 17038 bytes .../test_coord_radial/discrete_theta.png | Bin 0 -> 37910 bytes .../test_coord_radial/donut_full_circle.png | Bin 0 -> 21456 bytes .../test_coord_radial/donut_narrow_arc.png | Bin 0 -> 13089 bytes .../test_coord_radial/expand_false.png | Bin 0 -> 24258 bytes .../test_coord_radial/facet_wrap.png | Bin 0 -> 22584 bytes .../test_coord_radial/half_disc.png | Bin 0 -> 15909 bytes .../large_theta_text_fits_in_the_figure.png | Bin 0 -> 19879 bytes .../long_r_labels_on_both_spokes.png | Bin 0 -> 14418 bytes .../long_theta_labels_all_quadrants.png | Bin 0 -> 23459 bytes .../long_theta_ticks_fit_in_the_figure.png | Bin 0 -> 20123 bytes .../test_coord_radial/one_long_theta_label.png | Bin 0 -> 19854 bytes .../test_coord_radial/path_munched_into_arc.png | Bin 0 -> 19723 bytes tests/baseline_images/test_coord_radial/pie.png | Bin 0 -> 19273 bytes .../test_coord_radial/reverse_r.png | Bin 0 -> 20319 bytes .../test_coord_radial/reverse_r_donut.png | Bin 0 -> 22108 bytes .../reverse_theta_full_circle.png | Bin 0 -> 37909 bytes .../reverse_theta_partial_arc.png | Bin 0 -> 34688 bytes .../test_coord_radial/ribbon.png | Bin 0 -> 19191 bytes .../test_coord_radial/ribbon_reverse_r.png | Bin 0 -> 19117 bytes .../test_coord_radial/ribbon_theta_y.png | Bin 0 -> 24173 bytes .../test_coord_radial/rotate_angle_text.png | Bin 0 -> 24468 bytes .../test_coord_radial/rotated_full_circle.png | Bin 0 -> 18455 bytes .../scale_positions_move_only_titles.png | Bin 0 -> 16476 bytes .../secondary_r_axis_full_circle.png | Bin 0 -> 20185 bytes .../secondary_r_axis_partial_arc.png | Bin 0 -> 16471 bytes .../secondary_r_axis_theta_y.png | Bin 0 -> 16076 bytes .../secondary_theta_axis_donut.png | Bin 0 -> 21690 bytes .../secondary_theta_axis_partial_arc.png | Bin 0 -> 17344 bytes .../secondary_theta_axis_reverse_theta.png | Bin 0 -> 21368 bytes .../secondary_theta_axis_theta_y.png | Bin 0 -> 16760 bytes .../theming_reaches_all_decorations.png | Bin 0 -> 28338 bytes .../theming_reaches_the_inside_theta_axis.png | Bin 0 -> 37867 bytes .../test_coord_radial/theta_y.png | Bin 0 -> 19207 bytes .../test_coord_radial/thin_wedge.png | Bin 0 -> 12075 bytes .../baseline_images/test_coord_radial/zoom.png | Bin 0 -> 16323 bytes 42 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/baseline_images/test_coord_radial/arc_wraps_past_twelve.png create mode 100644 tests/baseline_images/test_coord_radial/axis_line_r_end_on_reversed_arc.png create mode 100644 tests/baseline_images/test_coord_radial/axis_line_r_reaches_both_spokes.png create mode 100644 tests/baseline_images/test_coord_radial/axis_line_r_start_on_full_circle.png create mode 100644 tests/baseline_images/test_coord_radial/axis_line_styles_polar_boundaries.png create mode 100644 tests/baseline_images/test_coord_radial/axis_text_r_blank_hides_radial_labels.png create mode 100644 tests/baseline_images/test_coord_radial/axis_ticks_theta_blank_keeps_label_gap.png create mode 100644 tests/baseline_images/test_coord_radial/discrete_theta.png create mode 100644 tests/baseline_images/test_coord_radial/donut_full_circle.png create mode 100644 tests/baseline_images/test_coord_radial/donut_narrow_arc.png create mode 100644 tests/baseline_images/test_coord_radial/expand_false.png create mode 100644 tests/baseline_images/test_coord_radial/facet_wrap.png create mode 100644 tests/baseline_images/test_coord_radial/half_disc.png create mode 100644 tests/baseline_images/test_coord_radial/large_theta_text_fits_in_the_figure.png create mode 100644 tests/baseline_images/test_coord_radial/long_r_labels_on_both_spokes.png create mode 100644 tests/baseline_images/test_coord_radial/long_theta_labels_all_quadrants.png create mode 100644 tests/baseline_images/test_coord_radial/long_theta_ticks_fit_in_the_figure.png create mode 100644 tests/baseline_images/test_coord_radial/one_long_theta_label.png create mode 100644 tests/baseline_images/test_coord_radial/path_munched_into_arc.png create mode 100644 tests/baseline_images/test_coord_radial/pie.png create mode 100644 tests/baseline_images/test_coord_radial/reverse_r.png create mode 100644 tests/baseline_images/test_coord_radial/reverse_r_donut.png create mode 100644 tests/baseline_images/test_coord_radial/reverse_theta_full_circle.png create mode 100644 tests/baseline_images/test_coord_radial/reverse_theta_partial_arc.png create mode 100644 tests/baseline_images/test_coord_radial/ribbon.png create mode 100644 tests/baseline_images/test_coord_radial/ribbon_reverse_r.png create mode 100644 tests/baseline_images/test_coord_radial/ribbon_theta_y.png create mode 100644 tests/baseline_images/test_coord_radial/rotate_angle_text.png create mode 100644 tests/baseline_images/test_coord_radial/rotated_full_circle.png create mode 100644 tests/baseline_images/test_coord_radial/scale_positions_move_only_titles.png create mode 100644 tests/baseline_images/test_coord_radial/secondary_r_axis_full_circle.png create mode 100644 tests/baseline_images/test_coord_radial/secondary_r_axis_partial_arc.png create mode 100644 tests/baseline_images/test_coord_radial/secondary_r_axis_theta_y.png create mode 100644 tests/baseline_images/test_coord_radial/secondary_theta_axis_donut.png create mode 100644 tests/baseline_images/test_coord_radial/secondary_theta_axis_partial_arc.png create mode 100644 tests/baseline_images/test_coord_radial/secondary_theta_axis_reverse_theta.png create mode 100644 tests/baseline_images/test_coord_radial/secondary_theta_axis_theta_y.png create mode 100644 tests/baseline_images/test_coord_radial/theming_reaches_all_decorations.png create mode 100644 tests/baseline_images/test_coord_radial/theming_reaches_the_inside_theta_axis.png create mode 100644 tests/baseline_images/test_coord_radial/theta_y.png create mode 100644 tests/baseline_images/test_coord_radial/thin_wedge.png create mode 100644 tests/baseline_images/test_coord_radial/zoom.png diff --git a/tests/baseline_images/test_coord_radial/arc_wraps_past_twelve.png b/tests/baseline_images/test_coord_radial/arc_wraps_past_twelve.png new file mode 100644 index 0000000000000000000000000000000000000000..ad1218fc0af061a5d1fc53f73a298cfe15329b94 GIT binary patch literal 16523 zcmZ8|1x#E|wD&IV?i6=-cXxMR97^$0+}#}(cXw@}P+W>bvEoqN-R157OB z03pmzS3p7Z>N}=VxbUr>CbUCnv|p$45s;hlhs;2M7E6`+IwPySuyp{{7q8+1cLS z-rCyQ+}zyQ*jQg*Ut3#SU0q#SS^4|-@AC5U($do6;^M->!u3SRCnqN+CdS9d$HvBf|NcEXIyy2kGCVvyG&D3gI5;pc(BI$R*VotE+uPIA^Xu2I z?(XicuCAXye|C0uc64<7`0?ZW_wViP?QLyst*xysEiKK>%}q^Bjg5^B4Gs16^>uZ1 zwY9Z1H8s`M)m2qhm6eqh6&2;>-_Os_*Vos_$H&{-+sn(# z)6?_Yw{IRE9$&wHb$54nb8~Zbb#-xZadviga&mHXbo}z=i-Uv1=g*(*?d|RC>}+jq zZES3;t*x!BtSl`pEi5d|&CNj|keQj8si~=niHWhXv5}FHp`oFHfq}lhzMh_*uCA_* zj*hmrww9KbrlzL4y1JU0nu>~wva+(GqN0L=g1o%EoSdAjtgMWTjI^}0l$4Z&goL=b zxR{uju&}U@kdUCDAU{7pA0HntFE2MYHy0Nd2L}f`J3AX28!Ia-3kwS~Gcywt6C)!d z0|Ns+Jv|*A9W5;_4Gj%7H8m9#6$J$aIXO8Q85t=lDG3S5r%#`ViHV7bhzJP@2?z-A z@$vES@NjW)ad2?3v9YnRurM((F)%RD(b3V+&`?oPQBY8jk&%&*kPs0O5fBjI;o;%n z;9y~4VPIgOp`oFmpnyOi#Oq~+31|QS5gi3-32m>H({-VmG6O^SOP{5;fH(Lta)Rh! zXvu*scntGib!Z`iiAj|&mL;k<;e_n2>gvPG4YspCYo@9Meb?{LzAhN_psUaSed*b> z?LN7#^;~Ut{Qo~jx4s{Dv2PS#8V+7Qp8oe@UoGIxStw}rwmFkO;V7Dr}DgmDwnu-yI zM#WRxo%YigvtPc62xaNy@YrL|+A}lp#f6bGhF2s5j(u!BnTE)c;fPf`f;`?V2$+UU z?KRiD(qW4!T-KIDXY+?RblOBR4{}{?7-cj?bB4h`=_Rq6+zL%cxrhf ze8;bxi=#IS#x?|;VmKv=mu7^m#@Sl+{?3!Cyfte()4#7*R0xkTUKHnXJ&hQCN)PkL37N6+{LayUaTg=EK(9$ndK zVpTn)tm`FyR?r>{TJUQ844kgQ^zA-B$#gk zs|jK-moZmo7Ks)tb29_km^`ij64`Z#s2gawyao@x2kGq}oCKIpK4&){NL|9Si? z$?_i!Rw{PdTNzy^muhQ8RR*N@JWxuEmr*EqiDajOuY1@jw#@X4Uae74@T0nqT63Nx z?{kPHFjKh=;qBlh|3<2N)vCI)X+`$`SYnBDHwA8g71FbxKHmfnAJ8{PdYC&tN86ok%!mP8+*+IxACj{AP;~ zE?h36cuj|ET|^%Y*7y_SbZvK(Df+%qiH>{$)Ey)jYZ&OiO70E2U}K+~@-50_DtIb{ z?XgF2Q(-PE9Om#r8&Vf(ALFhmzBj7$pRlIFo1yPMpWxGZ(;N~)=)bYsV^58d`fq5* z5w=&rkg2&CaHM@5gl)zSlUL$$B8Q)hYOI>YEUz()#80ik$TuFryrnx$(-iCd9*~Gh7qp%>LhdcF-DE#M`)!Q;VGO;H`;2q%!ve8 zodB_%)ijlS0GhZ=Rt4;VNOI!Af4+{0P5~YgpT)dMp

$<5N_52DKMDcB9$0^C4~& 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 B5}^P9 literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/axis_line_r_end_on_reversed_arc.png b/tests/baseline_images/test_coord_radial/axis_line_r_end_on_reversed_arc.png new file mode 100644 index 0000000000000000000000000000000000000000..83666e47a34a5f711296484938a5f99376423389 GIT binary patch literal 28273 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 literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/axis_line_r_reaches_both_spokes.png b/tests/baseline_images/test_coord_radial/axis_line_r_reaches_both_spokes.png new file mode 100644 index 0000000000000000000000000000000000000000..a44f965bfd8ac8035f7c08c2586d5308758ffc43 GIT binary patch literal 27904 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 JHR8rW{~!5p>hk~q literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/axis_line_r_start_on_full_circle.png b/tests/baseline_images/test_coord_radial/axis_line_r_start_on_full_circle.png new file mode 100644 index 0000000000000000000000000000000000000000..10086ab4775bbb74fb829def71b93fe87bcf78c2 GIT binary patch literal 37984 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 IrGyOmKZpinQ2+n{ literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/axis_line_styles_polar_boundaries.png b/tests/baseline_images/test_coord_radial/axis_line_styles_polar_boundaries.png new file mode 100644 index 0000000000000000000000000000000000000000..e58ca00be1534ab3568f4d21cea1c8d54b491d4f GIT binary patch literal 35614 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 NT%0bPe&9$*{S%;OliUCR literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/axis_text_r_blank_hides_radial_labels.png b/tests/baseline_images/test_coord_radial/axis_text_r_blank_hides_radial_labels.png new file mode 100644 index 0000000000000000000000000000000000000000..cef4f829f84fb174d9eb0a4a39399f9ffae070d4 GIT binary patch literal 19333 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 z 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 Q{kzopr0IVE8r2qf` literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/discrete_theta.png b/tests/baseline_images/test_coord_radial/discrete_theta.png new file mode 100644 index 0000000000000000000000000000000000000000..cef81738c9c804ddb8db8146a60548809814a889 GIT binary patch literal 37910 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 literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/donut_full_circle.png b/tests/baseline_images/test_coord_radial/donut_full_circle.png new file mode 100644 index 0000000000000000000000000000000000000000..73cf8890a66c664a668f4947ba46442157f3e5e9 GIT binary patch literal 21456 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 IWBK*}08jkw*Z=?k literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/expand_false.png b/tests/baseline_images/test_coord_radial/expand_false.png new file mode 100644 index 0000000000000000000000000000000000000000..af7c9ca98cc36fed0ec9883c2983462dd8b847e7 GIT binary patch literal 24258 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 literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/half_disc.png b/tests/baseline_images/test_coord_radial/half_disc.png new file mode 100644 index 0000000000000000000000000000000000000000..044befee50d808b71ef31352f2d241b3b682f103 GIT binary patch literal 15909 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 P_YKHODoIp|83p_=L?62) literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/large_theta_text_fits_in_the_figure.png b/tests/baseline_images/test_coord_radial/large_theta_text_fits_in_the_figure.png new file mode 100644 index 0000000000000000000000000000000000000000..abcd66d1210a36831a26ca6cd76488fdef7df132 GIT binary patch literal 19879 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 FV1nfX$PoYl literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/path_munched_into_arc.png b/tests/baseline_images/test_coord_radial/path_munched_into_arc.png new file mode 100644 index 0000000000000000000000000000000000000000..2bf0f3477bb793d599db1441b3ba9f04e5f03366 GIT binary patch literal 19723 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 e00000h|GT}c(0w~o8LPC0000XNO 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 P00000NkvXXu0mjf=aZPx literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/reverse_r.png b/tests/baseline_images/test_coord_radial/reverse_r.png new file mode 100644 index 0000000000000000000000000000000000000000..96bfe77d55e5c74c172f61f4d79a0fd1a45fc07a GIT binary patch literal 20319 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 Cu#zkQ literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/reverse_r_donut.png b/tests/baseline_images/test_coord_radial/reverse_r_donut.png new file mode 100644 index 0000000000000000000000000000000000000000..a7a1faec7f45974e93206b03411a0ec2e5902582 GIT binary patch literal 22108 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�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 XzvJ2)!D!0x00000NkvXXu0mjfbNU-D literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/reverse_theta_full_circle.png b/tests/baseline_images/test_coord_radial/reverse_theta_full_circle.png new file mode 100644 index 0000000000000000000000000000000000000000..bab8e5166521f6b83a6be0857ff0f48339b9d1ec GIT binary patch literal 37909 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 z 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%0DwGS;nuCa{R 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 IM6N<$f;nV--2eap literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/rotate_angle_text.png b/tests/baseline_images/test_coord_radial/rotate_angle_text.png new file mode 100644 index 0000000000000000000000000000000000000000..7628b9a90e49d1e2a8485a502b93d282f5bdedb3 GIT binary patch literal 24468 zcmXVW1yCGK*Yzw5i@O93?vlkJ5ZnSmg1d&`?(P=c-GT*z2KU7YE(z`gcX#-C-tXU< zt?8P&x6irf-tMmMnFu8XX>=4~6aWCwKgvib0|1;q0D#tz;9j4!)XLSq-jMBOw449{ z75BeC2*}7H1ONl6j}oG)?hA)Yqq5W=0oa*GcX|*fHUY%UknLOeclYx=_cL^O1Or$m z1Cl4MV8nPHg{KVd)$(7pWikzGS7+&&(X`$#2`?vq=!L(byd0M)_%tB`z{xRK{1p)d z0sH~*D;Ko?wE_57#Q)kzP~f#qXr!h#?{Q8w7xH|2;lFZVtryeQ0TG`yvu* z?C19~ocb4&QB;7 zuKx3{ZDX=>KjDsUim69VNz%|S?;RO(a`N%f(I?$VeSQ6}+JXNC_3z&oS6A0OJHnzQ zF{H8P+z6W(CQ6BHGb@`EQ}+I@kh}A9$FE&T&d#c-`8?fU9UeygFM`hH z<>m2QVMRs7m)kROt7SO{aike-MpGx_KOcGezi7?K#m2_O6ql5cL_=abDPRD@@uJsZ zdo25TyZ)<|PZ74VzrCuGaKt7S*u1POMlZ4m!SIB)ie6q`GYhGTEfQ`M>=%U?y; zv`SX?c|>jP+_R|H9&7&2kk�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 literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/rotated_full_circle.png b/tests/baseline_images/test_coord_radial/rotated_full_circle.png new file mode 100644 index 0000000000000000000000000000000000000000..0a62a991322994f43904a2c605056816aaa8f4e2 GIT binary patch literal 18455 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 KMNUMnLSTZVF$l5% literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/scale_positions_move_only_titles.png b/tests/baseline_images/test_coord_radial/scale_positions_move_only_titles.png new file mode 100644 index 0000000000000000000000000000000000000000..3db3985ea980ae29806bc25078646f8c6432a5fc GIT binary patch literal 16476 zcmcJ1bB`{}59s1q+qP|-wQbwB&id>=YumPM+c;-!+qUof%l!oRkK1HANt-s)X4*8( zbRv`#BoSb7VL?DZ5TvEVR6sz$fFK~CyU^hOQNWPsm;c*foTRi}K|tWp{#@etv#@e7wKEzrDS^zP`S^ygWZYKRrD?K0ZD?Jlx;k-`(Ba z-rnBa++1H@UtL{YUS3{YT%4bupPikZo}QkZoE#q?9~~VX9v&VX9PID!@9pjF?(XjF z>}+puZ*6UDZfc(&&|!v&d$!v%>4WJ z@9*Eg)6>&aQ&W?ZlM@pYogEz=?d|PtZEdZstt~Ar&CShCO-+rBjSUSA_4W02b#=A1wKX+0)z#Hi zRaKRhl@%2g<>lpNWo4zMr6naL#l^)%MMZ^$g#`r#`T6;Id3m|Hxj8vG+1c4ySy`Ex znHd=w>FMcdX=$mcsVONb$;ruyiHQjb3GwmqadC06v9U2RG11Y{QBhHmk&zJ*5#izC zVPRpRp`jrmA;H1HK|w)*fq?-50YD(o-{0TQ&+pHlKfb=cK0ZF)-rinbUY?$w9v&X< z?(V;T|8{e8b9HrfadB~Wc6M@da&&ZbaB#49*Voh2)7910(b3V?*4EO}($v({(9lplq!;raFJ7dJOI7Z(>NCnpC72Rl1E8yg!dD=P~N3lkF)BO@aN z0|PxhJslk#EiEk#4GlFlH5C;VB_$;V1qC@dIT;xlDJdxl2?;SVF%c0F0RaI%K0Y2E z9xg5}4h{}BHZ~R(7A7Vp1_lN?IyxE}8Y(I(3JMA`GBOep5+WiZ0s;a&JUkp694ss> z3=9l3G&B?x6eJ`h1Ox;C000LE2Ll5G1qJ;t4j|C>{=@v}(qh7Dp6i!Of{yaWAY*qY zpB*pR3){=~j`s6bt`zX0q>vF}#X-U*`CTw9RWisy65%Ck!7!*3A#Qi?!<1tB2U-IP)aR#Wa> zbLc7sl~pNgdEpFBFuf>f1?A%9!5aUVK^w2kZT&9|{0Bx^8y+VEN1$vI&+Cn2x03DUHkSQwJy zGx+lQo#klDk2VUKVC3p4-INHLD-w;0j}1?1_V;h-3KEi_-*%gM>=|Qj%Zb!5xHnGX zcT>j-T!}DPprvt}@?KV)eMQi_i%-+!!^Uh(lm!L&W?M9YwrPhcl6~sQ!LzF$2v1~^ zg(9*Mr?@+$G`Z2n@oV(a8wK!0z?#K+_9xl<4rK39EY`!m>Y#xhl)h_ImIKt{ff%v7 zp5uaL>zD`AjHI5AzLFFJGMxcj7h1&7al>pZfE*xY>svKotG$p?eSSEd#}EqaBpzBSLJAV!(l>aF;Wr<^|lW>9Y-%LS5&|tOAe|ipafVKczrfQT;v6 z?f<=q+ui$)KdXV2&|L0355{=|O;&{AfM9@N)-ayrAk(@G(zcygx`WUPxA%K7jG9+G zz2qFEV__^&>u}jXFk4}y6eurqN(3xxfARU1c%f`fq-IA-`f*gnf%w12=6R7>`&M~*SPW$Eh958ebE!_Ls&i=e#cg<9;w|wNfe&iUs5j|dbTvz-U_|&MYU-;|3RS5x2 zc#Jtd+6wizc_E4?`a^ZGyCZkUAmeJHulK<&k!Jji+MajK6D*A46O7t4+!R>2ZFdD^ zhTx)qlay}efGsBmOzDcd(4;S`-r7>ZRu_^B!u{hWGGS~tQV^7l9q4*<)XI}lgq1== zUua|Y1SOYpV0AdslG+0WS%eu+jr~#d@Y8`$vuIZ*k0bb0>qj)>ro$G64n;PKLuMPY z2sAmcPdc27CvK;anq7~KeeSq58L+@43PmmCf2iXQ!K2oIZr3;W2gHIiW4LeKFb#mD z$2-$O>ZbpU1R^o(Y)ar~XtZ(NNlrO0%AI_96Rc(ZBIM5nIZ3zuj4d;7vUp$4{f7Q^ zo*+D+zdHa5lx+uxY)iWS6FprYWU}g66(LBPuAzFqBF|3jHrfMnX$(6|IZb1Pl#1k( zwEOLZ9lRm!ONri$>w#e;H~@@rgOxf64%U_KW&#U-T$OJG>6NQFm4%LHRKOOyq?B@S z_!#~m;lLYo*x&e3W8ZZ!)8GTjuS%<3E~l(TfTrU%MDa!3pyM!73l+k(he0ol@BYU) zYL5Dc&9jIKqBjb$u2%$dg3inf1crNFOzI(*l=Vh!*Y*lE7uLxD`L;eD33;iVl8jEK zuqMgVfLESCsml%nE#PBVWT|Y>$fnCc4Rkse|GKEroe^nn(8&c_KAy8Q>n^NAN%Yu8 z!T^HPkoqVNQA6&2KRLP5^*vazqRoq~(n-P=;tkiJ5WMvPEf#M@T>m*8DltJy6GRr* zzgXGG9i0UPd|}2caz`)w%ns~6EaYE!#sX2JhM1>9_!Y|jITCpg)pfAlU~`?VM{#2K1@kE$ zC}y0E+2eJ*rxPXSOnhg`9-z)V)v)vg zgA&1Yv_!nzirp4rX18h*#E>Bb8QaGOa+yYb(M_#NIqy1wDz{<$ovxv9MJf@q>-o{5 z7rPv=>Q>~#FxW1Go|wyUX6<)J(UivLitkTeM#2oib}Q*Ux3v+sbVRpQ>3YPrPd{un z^99NC*vMvtR5>`A9IC<2K(412C`V53-zOY@9V~TG8PYIHV%cN1&6NYSrXQ|sU5d7B zQ)Rtz&W?)|U-Q25 zK*{5iVLpd-7b(PvMIA(Z{w4h3BTpAdE%B82Vt{RgrXZ1hs(<&TAU?!6Gek({NZP(& z7XoQ;ZeoUSW+UgO(%Q3c3MN7_Un_eF!N#y=MEac2pPHJWe|EHmv)6BDT9MYJ!MnB> zV*V6;uSwQFj3PT*s2jYN&I%=jJ0{ef+=Er%qWegvie+l*H%xVxWAeBJJsV4vd&Xk~ zHIfbnVM`;^e@6lDPl)Ff&zs5{Etf9VT*yQ(bR=*Y#_qkTunda_&9sGs!!f?OXe`A& zM+o*cerO}^*&PjX7PD(daUq3F95m!T$y}aR`7$QAu)*e_f3uH)XE&;D)}0Ju4Y#%r zS*o{|dQCc7>X%}E{Ce-Hb05AWMv(f_4SfuO9}cm290*o?bBa+`YFL1v#n(YD-A!NC z+-$Y_?s+R;m{7V}dAqk50wJ|>V9+2#)@`1;Ph!gPUQ#ZO&Ag)WuV(X+6uOc%U=sg> zWfh6;WI+qVX+xe|wH|Xu;9J)th>k{nCQUx6aRw*fz&+{fZv<}R$I0(VlilMyE&%I>j%D@zC+0ADiIKPT7 zH8-Y_>lCngGo$$gP^SnEEoo&HA^}R@y}v{7wyWyN&`Bzb3*)*X@gj|ITe^Q{QUH{;m41vE-UW*kNYgi5SbPlwAm}WL6 z=ENg$BHWhwOUQn{ogAO9FvkI6>R{83Z&G3^JQ%+cQG2{<3xz7EyPh^iN)^JZ?J@%E zTuq~BHOj{K&Tgi^j#v5~OJ@9>&iF`#T^r#as9(D;gi=36Jf|`sn?Ybm-n%c#2R~<% zPUZ!695G)I!bhkC?jrVX-K6Szo^^chCQ|67LNQ`S=ZfMv>Ir4gC&dZtSAXb)ds|jnv8vf8Vd3Y5kQ+ z<0`n7KOu#EJ@=cwV+ZrQirc$geBH~l0Hu4~YU_P++}W^#sc?6O0x-ZP&Vt#M4NnNu zM>;r)@Sad$1Wz{q$~C|b-Mf9;;pJm#$D}ub*y-)^dGUz1xb6b5dm;HRerveF=Ug`w zjVkO%)yf5w=1^wROFE~&O)wGWVh-iY7~#-Fejjgb`F}xbm}JPzn6IAU^0gKEK&*!0 z4*)KoDcKp~%qrmni|`SdUYognzh96PVgG`n#0Uj~9AV}C`N$ItQRfyY_EJ-hd!=md zsRz-{7`k&$G=aq{p9Uc0wTAk?)%CrLd+J!$m0mxT-_nng>{kii!Y=QIL=%iDMS??{ z@2Jj^TsNcOLkp6_@;k*ndG_OeqVX4uz{kCUSv%!y&P&Fs1RpiGDj=0iGtmhZ@qWc0 zpRh>b)Mx6gxXaHl+4p`Q?@5zLXdh!BwuiE5$>X*UsWuAZdRuGvmV<-|`Obz;7@xky z*W;Sn`pnn)A3P1IicFwblFHgLQ~+Dh|6VR%2(4x7E)JL?3H&YtHm{M&w{oXgnU3ld zGHQWZ_B}Q~mUWD%W>XK>Dc6M>;V){sn#gm79wuBDjVG=5(6H!36bW*s}I)Z`~0S4h7YRJ^{< z9HS}HSRh+gj%T9oxP*CefuXoFELB9-!NM%j%qvJlNeU-*fT@u0-PU%-`ZJ8v3wr%l ztmHvps4O=qY;AnU&n}gb;@|~eo%ns@Ds-XrBD(r17or50fdUXjN@N2d87HgK@&%FF zVj3vEnv_>(7;rhgh8oYkhoE6HUdG)iaIpIg`lRN0?!i!MAINd{oZo;*WHQUGT6`Q< z6&lyLOKX#@cGM87#U`$vBvHyk5I|UDRhQT@59s#w;HRqjKmqfE_WbAoifR+CQv4 zO#Z0k1&1%rMcFdLTO{c+`@~JKq{IB8Mi_lZR<%|HwO;0*^zCO;n<1m)VZvj6nNuOG zO%}Exhn@!4NPt4wuse$i==R5_ozSfTbXj6|XUnUpz;ENuGF=8{U^hQCqnfWW2lBNc z$jP~kYe4|WJ(Psu!>Yt!gc1`p6{AON+}eDMf7QY&Qp-pkXFK8Cy~M3fvd(|BZ>*eM zo_)|(XCvbyX^xLGBnn|;%#*X2RgZ-^yoe+S*pZLNn#@{|;EMn+MEDv(_0}D+W`)VY zc1N*f0NK=4aOds4qX$rkG?5BXLji8{B51`!3KBjhC6&=LK8mM*2dMp$*Cj|H>|Q9{ zDp*TKVUO|k!D{z1dU?KB47JV8uP5fXOYAAS<5nZ^g8n#A+KUcI zR#9^t6dW^-l;_SLA{v3x1ZJpiuyI>#?1%&T(d*vsNczq-f39o%UvuTbiV3P(n6|j? zNx9z{y+1T<`ADVsB)b_Q2G8!&1V5Mieiqjhx(<%A?bU1|{Sv(4=JwI-m?JRs?k#V& zdp^>10a8iTCL(wdf5@*uVb%>9x8-b`6-=B;?rtn|x3>PcdJRDD-#hIlj&pEohr31O z1JdDXHFf5G{_8-!C=!5?ONA0qfp=Z?W7p?Sl1yBu#}l)*|NFer{CT*(&bi^)*~d4s zKUv}8{~T7LUT<6!Xa6qv z<|ppwaq%T zRF~U@C7}k7wYH@F0&_EJ9erdV2UCE?jt@{00mGbrq z*LG%=O!+b2S*d+q>)-VxX%3Bh2vs$wV_5moQHC)vNv+z+W>*QJe(Np6Fc%5^2Hu8R zQbJ&kD|t;vx&f4D^@%a66enI}E?fekAZIIni+XBhZc3QFtpKxLQ}K*Gv@n3gc+|P$ ztkmx2+kY3#Ku%G)rv^;cRQ`r$qb?5TDe@B75v`M=q>h5#y z9(Q=S*YZrSCt}J3h=cjqecj2O)e(o73N$tsHHf!9pnrt}%;?@%cKkl*>fE56#=`N4 zZYdWZn`3annjSFPYX*wLC3ADkwJ4F?SDrxenU@cf`|wr4lcknB>3|-yV5AJZ1!5@{MnO zfS~E|Gya^Ts}uiP5bZm=VSYzpfcKzgK#&&n6@pi)v^#v67hsiO{hG{?K)ViMs5$NP zyjbh=dx_YfsHTP|Y=&zkQKd18<0?SnUcdtcNZ!lAJjp!xa`U130~r82Z< zg9BpB+msXPJ};SkC1ZA&s&Vx5p70o}r?Ql|ruB=QB|}y#gvdrrIY2PJlws)MqOOSc z2}BM`oS?hpWa#D?LnPsx^usyPNg=97;Pz1d*rj(#upw>|yN9MYo5isKRoq^Fgbt7P z`YD!8_=RB=*{LdZ=Db)NH`|c30&@teD$wJ<<%1*wOC3=TlTn{2H13sUjCVUU{iARK zB~WJ=rSaRT5?wQgQ#E?KGzR8Cfm2(i0sLtwFO4BtJSgI^&0ux;=*#r(*L{27Y=9+C zN}qy>NZmqyVP!tx(HV-nNFumX2lFL}#Zb4OZaXuF5o8EJTUD`Mz;_sa^XXg>plKwW_Bx?;KT&-XYco(?pvGr%`Bd1*r z@L#TVcc>y=ug4mXrdJTQN%SBjB9%LcC1aQlYr(UL8oT1M#u7Oo%>g=&6(|!)?f)LR zSFNfkwjQM93uS>C9p4W>+Tc76S9{KdEC2}*Lm%4rvD~e%o4!kWp}JcD5_0Bs;mqs1a-?_}$4Ma0m}(vvT$n6I z|Ms5Oyf*~f1}v1&Wp?2?X$wC{!JgWq7YsBfLUZz0Bzs_SU7ItAZ-w-O>;C#N@OajP6Q957&--u zh&(o08hMGtW}rEys*bYk6hI6O-dq?!hC1ld2FlKSVwm1RW_f*8H{+`CLjT*#Pe~lZ z*UFfxi*S zsTbF~;%G-&sEP`G7HbuDAt*>#@wS7V$hq!2Y3%JO%I7sgQK>bMmBaEOHwyalCH^Hs z?6x@@=<>IZ$t!+MPB0u$om)U)-kvitlnOBg3hVsZJ zzyPsuQvp{fr6m@}!$#b6`@T?oloB$*S`DGdM$f(MW!h={|TwasQzfWNN) zJNaHN6J+Si02ImSYD&c^0(GnR>dzExu@oNRp(L8xagNoQ_&g@oyELw@z2M-u`2LAv z2j`EhA~PFg#RKkYjhkaD9J)7*CLQ~Kvl4B6{2W!?>~@4>2N(;;n~L(BPJ#Uyfo=zb?WV{^@I>tCAZOY<(VR6O)@%ou?c=*iN_Jz%UB#_u;)tR@1^S zx7m$Ot&IX90uO8Y1Kn^GyY<{l)v+fueiK_DL)5ky;c?@)OSfgPX?i7^7PE%ITBS57 zhXC5ZN#X=_)&ZwH3I9MUi+%N?WmSrzQG7k(V+48pd!`(7h2#pI`#Hjtw}giXfW;#A zzIV;yf9r8vmn`u++uzUWLv`V|DI3cy^BD=$WRbOO;&2(j4Fle-2OF*OO4yf>$A8SA z#Rcuc#ZhE?oYb^!Juil1Ivt!@76Dcd?Eqm$8yCvw-K;FfRGkVbJ{@^V{};Bw8)w>B!kIb%5rfI~2;Ayss<{18VQyK3$7RmE{@_Ec08Y(cKO z&4oQK&Yoa)ExOBv2G*J3<><67M zbT1WE|5*6!fQXfQ#p&i4w(PK=Ea4z~i2J~6x+m*E?eV{-YFbHN3l(RQgfv5+S0Al8 zgxuieP{CWmVDO#%PGL>?C}1vG+s|8yU-L7r5^M<^R+Ls5Jb$diTL&mR-y>8MQG(+c zmWu|2U9&2=QE7D26Li}X{~>@+#8g37f@5Yp!gQepT{^OdDv0w|4uC58(lh!dJbR$J=m}%9w<3= zz1DAkyW$!ST^^i{j@rW{+BVm?zXO)>#{zKLod2W zEW9B9V~M43Cq-!bas$olX5fL%q}_<5*JUtpyyzOY-RJrghNRtfsg6+F9!V>{V|O*P zy`u{rvMZiq(K1`)akK6;37uGFV1%YRf{m~XZSTkq_;!a<962jsj@wiQGCbY>RRxovlIgK zJav0~uTqag-MQ-&k(C<{<$98!H>o@@#5O+MQ%n8v9V6kAGb5PfWdTIPZMWaw>}sEL zyr;@4OeyvHwNP6PtNQB<@5TSu&Yv-avT2{_+)$O$F5=Jc{lTAHc!b-xml|K=hweB> z>(n{Uf1smilI@zSu2cbI{g){M8W{JWAdS=a0+kh{@rqxsOv<4jE4klBH?6fvezIB(&jTwnXyR@!f|y%ei$xnjN*Iv1DC+4#a9TkZ%YmtzK3G8} z{nf~;Kyq-EbmDJORss3`->Zn+`&8MTUhPu(QabfhscC|&T9Kpe6FMK-RzTh~_bun_ zwxu$!0Cf5EXX-TJdbt5g*J1qjb8ch6&un6LA*f}l8RM!eQ@>mimNhzY85mYXe37kBzt%1X{8M%k4F zY`agQN;svv6c#eH)fq*3X9H%#+8Xb<1eT7jG2CvT2a|ETUk)>Bh-aO%#F_BD!Db5! z`HLUE3%{e_cK8O#iXxqc@3MH!fSy-~7v46IV93$5InyGw3y|4tCxj-pC+mT@&du=8 zMi*`3Zp(7bf~p$39%J?G3Bqp^@_k<0C`sgIt#&_ECt|op6#Gvs6$7;1%z;&KY$Yrq z1aKsinoU{awV<{}!hhCF7*28`^f?eYz3oQqE2$<5CYF0bH}Pi^-uk+B7;|wffl?-u zw+3(O4@TP@dGLaN8z2;96b|CH;T>SFrZdeJnoE_Bm`M4NRad@2TmF+ zT_GrgK@tVZ>mgOrOWt2iFhXL*`pZB_ zVWmlDOi|p_cS*;`l$=AZ(zFFeJ%54~)Q&QmRB`T9Z8aVfkiaT0Qhy{~$IU67>v=bV zRd+mH5^K80?NBvwh;8u8SI8EDH*`irora@1YPcG%zf1G-i+3Mh$=rE8xCCs=j=ow2 z^)_(qj*m0r%)icGS~^V{+u$Qt@col5G3xsr!I9d38zF*5dT)=PM$#2}avEOk&#e5Y z9vu;JD|*ix_|94b1op1jJwt8^p9oXQ$U-q}qAWWpEs8N%uoj>E;9Y1NC6@m0(56+r zk!sSY&5JDbtbP)yC{NH|qAE5*_!tF87Xa^42D19Xp`o+>T|O$ecL?06*4e;0PLY97Y_7lT5d(|V z6EAC8UE8ClJilyZ-bOU@D&V0(hi1p2`7^(-h}jS(_3lzwb6T$-aLNw`-LLD7_9Wk{=9EVF5v(Q7D*l36R{xCw5@+}BFje*L~r z&qYQCik;747aHFT3K`3_)9szdZ4cydDw5tEw^79HCUVtP$gDuF##b$SNi~-vY6(F& z8psCmOByui=OD?>$tdC0XG@>_&0nwTtaT+~N%dFfobzUH2Gy5qu8m=$Q{nZvaAh}3 z72A6&v;R2kn1E0eyl^cew~d$az~Ak`CZF49xW*hn&wxNfGoNRh4K9eB?5MK>@Ahlc z?`XeR=2os94F(SPC>5Ni$39P*;UjBABdoaY!HlLjm7H+vwI7^gQBJ_Q>pf))h~~S$ zV8_Da!lLdvS^&AknOksD|)<(#x;^Bg!`id-d~v`Fay-GSGfl*5X5EVg8M^* zif!Te#C>)V>J#J6-DKF#ldN&q(~B|!hFeTkjm4YWPDU&z280FZy*FRi57ZQom`}hE z#7&mi6~)M@ZH;NXJ3&h={7X!X13l(GNHMsLiwG=8h|*x;jx*)~B_|Q-2%nXS@33Ii z;!r39#x#6utPiCVnMgW$A|+OKz&l*>NM*tbu46Ordgm55TDT4qa0Ef7G(PM8cRV;6uZer<+wdXAxriZlCdK$7+H4j{Ta}~s0VjE z?T>r}Eq5sA&%C0M@<;KMxX71)#&()HO){H6sIpQ#BbqLO^=&+07T0`QP$$_ud1?rL z*`VC(KBK6`#DL@tQjV-4j|A|`+0MUrPl#S&SID%Pjm|3{ftf>;{FT)TYir>3*F`0+ z9S%fVU-;cLC|3rk>I@W|?S-8mY(^qD;=`*tLrSIdJ-v+3o)MIU2!hjla#f++$P$AW zEl81BIRx&{*~c-^w(+?}3K2)8x9=CWBU@8}(J)1>s8X$93y*D)UC98A)e7bvWH`fn zh@<~Ha*rn3Qirmaxsxhd9dRRR`bXVKc?|FK`22$#`s5SDKyUYdcE$oJaKyd}?$_Or z6|AKaDcxz`#}&7}o(}Rs_mOXNWV^6taOi(WazKR%HDV%b?@=&!kBleNegC}!82@KM z6Fq?l$q>1xW)T)T?S4m$P!jZJkE`t5P3MllqvDn?pWheS0c$J_mz=cx6#7CL{8@y5)J!-HqzZ(9Gh(PEN_2Q_aon~{J6f|w29achPSh(a913ls@Gub ze%nG4G*0WEy)hFXP#CG>Z<_`A8po`_RkK3fH%G7tE^z$iCaijJ*K4lO4I}%x45f=Z z&2`FaiBgQN05h>4ObtU?WzJrNxm}vigJmOv&>XoxWZr)zpiIV=ciQZA?H94&yXoXv zVL4v$DYxeBCfYRj(BJ+&^F6?D@?ssur!EAoDj~;&iJ@@r$wq>)Tzrj>=zNBwN?nZl zI1hzsA@wUow6k_|P1LEG%QIfp%F)!%-egVA&_QpD%NVC32C@*0s25fw z&x%u3K+7rppK78^gpYrRM&PQ{YCKB(gUAr5yVQuT9CSk^loi z8>NRu^#ioRtQH>nVfIqljhfio(QEJqtn^dFQ~z1K>Gs_plTKM!zmwQr{Uoc)uT{41 zh1IkOLXr@dYmA=Gu~&d3+025;Q~sHDbWK7|pbUc=Gw_?^(4?sQ946BHh&Is1jb`IS zC+{D4mSj7M#Sd?b&DqJSNio!40^RQN-#+I^46oB`75s1^gD9%%9HiRv`Pru^5*0$N zkt|{jY~f><3enBmMga}R3&VY6n`n`wIJvx(mn9P{iWr5dCkdDb2dkTRKE`o_(yDQT zRVft!wORk2)w=WeX1eKUH*#0UO(qLRx)_dJpMY{%rZ7)>;#3Zv0PRgZjt-?d6Nav9 za7V=?C6Gt{x!z)amal-CR1R(zKCn+0Pyw%uHmZ5v$!Osl8k6L;ZuzT6OeD&U9I^0&~}=AoWXQx8C-%@(aMMtPiI;UkFD zv`)QSmcy5@`ZckTh#|kp=xrDbU(!HoPo-lt(dpp%2n1G~h(y1IlnG@_1X1g`i@G~@gQy?VC^ zDuRma+V^nrY`7{p(R(YK$P)0~ruvs9$Y4)B-ev_Zg3(WMdPWw|hvyz3e>JS8N?-xp zX?Xr_dquM$yP&TzKV|g(ksnn3BTriZL3%BSbi}mqUxa!(Pe0Qc>QYWj?YwNbKCc*8 z)YR(h0$g5A5QSGH2f47^0~!_JYJEzq%zz#`wjS?Y+Erwb<~q`BBL3OV^&5M&kWB0$ zoBl&ZP~`cKzOTwQf+5l4J@iS10buR0r-2=0(Mbkug)*`Z?@Ok3z#GHu9?w*+ZX3d{ zL{^3n(5_Q(`dz0QWY?bnM%MxUckmi$YJMWWX>Np!Zf3EOoEhdhA9c}I9ljgJ6C4TE zV4U63v~Jg|zCx>IoX+TfR(fK48RxS`+|WIHJnDpBpp^4wWcf-!P3w3uS=DKh@bZlh z@Omv@`Gsa=3vx~Nn6nCQS1Q<-nV|GFIN0_d6BGQ5juW2Gl9MxXLW7aG&{52MX$KHC z&7ammMkRC_^;{KpkPBk!oQGQTkp9u=7MjX+GByl-^Z213-Yg0{C@(x#&(erk+Wy55 z(7^2;0Ai<*xQb#{>g^n4_^%oBltj)-kqIv#tAlooVu&?c^Fh(QnW(V@{HKrsAw{-l}I4M@oA^!MaK}%+%L`D5fUnKsiojvvqjQy0f8N5ie!jp6rF#HiF46sXSKL9W^xYIpFF1Kjczw#h*fo$=AZ zuLa|_JEw)tyi7?PSwiGQ%bd~|mVb*U1jbl8I?B=LOgIp>+QlsRsc-fWk_cszrF*Ah zD;`xA0x#W6jU`-S&_UVfj8?&+i}%c_L{)m1uUW)m5Rk(XuotB_GZ2g9@%Y-|6oS0n zYULFyCrqKo47YYk+$aYIiC0O4$4Q3llt)vJbkGdZ^(uPA6}&&Sv|560T|wOd@~xjl z;TYjFN)x{j(TI-B2><7b{dC#3ym)e^zWqq9&PMihg;Iq4zXAAxzitMf^re(&*4d*~ zD3?B=PVpMnKX$k%`2gfBk-Yl)#@B+I$*N+uJTH|}gmmqPzw16lvM#$1H`(;Gy}xl= zgLaMfz@4Gbn_)ED6A^L+Cr-1EDFYqy`eEgo8NUFg(N{g>dKQ zj?PCP4&)doP8fIHVC4DSe=mscEkgfS1f?O*Z-|EY(uO2{`=CGSDT2x>(EMNiHfiC_ zzzDcS=JT0M{`D>koj&~*80!qOc+DF~rgLA9Ui(VGVDM(AkJ9Vs`TB8Hl9^g7Gfr+p z12pZo@hF?)^t?}%2RPI5EQ6DGMADEhP8YKn8y{dh(dcalj~h;2duWDw2s$|AUJ3Ml zKYry6yGIj)n@-h(C=ga{B}8Ho10u_aLp0cli8+1Zg$#02!%F8&in@_EWR17%TRNV6 z?c000x_laZp6+5+91Ow8&9d>@gZzIfPu4M`gC%$z6Mq4cLEp(j-yx2{i+;y-{{>!U zi*Nbgbv$*G!h=eU!;i_bPY+RR3->YrBKIM)BS$|YAbEyS@p+|eAP}qKZ3P$hPXi7% zy$#UhQV7`^j^NmfCxRv5pk^#L)XsRO5jJ<-i(;^M8NDTN`J6C=X%8JL-w1v+=~6t` z4o1=tMFIbcRIi4M8Z|a}WK%fJa2q$X><|PXVMdFKAXpu^1i>}ut^}Ke9bST9`qqUu zV<%mRgWTAIdEpZq%}An!ShUDxP=PjC00a24@5f7Q`0KEIXf+xzP#78zSjc{iKn9i@ z#*+dENXo*<7 zo8)jw>!7WXK~+R{fbFv|qaieIauxu3H3jT=!#nI#t)u)aYzL9?aPggKu)g6M1%8Fx ztWebpaTXci^Q&x(gY4tFhjb+Ytg@?7zF(p$|10dlv^7c~(X@$0p{=APINA?^?E7!2 zIg_N?xuR8RCA0{Q(S{_M^iV0YyyPAPJ7hv>Ug!9psSeG1CX zQ91}ZR9QIDq=0aHtEbbjJbnXq3od~HBiH?)wa z=Tt3xWx5Q}<{m&i1fDz62r2viJZ`4wezg5zxgljtI8_oB8sfYfqgK_SCI~O=VCtHy z%&)NG!q?wxxgzFKf$PxoR*`t*8`sR^d!i+YyI$Ao6x`P|c|bf1g>6t44#k9FpjAXC zF!;(O>Q0^$YsUK>m^@Dyu+bf&_aL*ON$O2_IPxLzflu-xIM;S1TakMfTtLuE@4#g> zUhbl!RzTfppIKTvRcbA|RoT*n6pc{cP2=JyYIL+wswOMP@3@~}IbN1Wbx4;Mj?wVV zM%?YP4)&=-;{}jn^=oMMxmCfIpDKOr|`>Osw+EIEoZcSKV1h^IF>o7{^yVL$NvT`dxAH+To>N0 z&org2=E2TA zu*k`>?KGKWoH~W&GeFQKKyAhwqpTid%1CR!ZC~9K6)f3dLm*K z$+Ou>L()?=#}?)Mn-~5EnLn?*{hY?&P~D1iwe3xKHl+wBIUAey@t(v}y5j_Tvmfy^ zlSh$=qcJT>*~+q{=q}kx8esGR)IM@5}9t{>nk07q)Gevl=qRz5A4}hOSM;&LAu(+w2U@!0Ab9IM* zXf^cK)Ye>3>Tn>%x`Cesb#{k?;Zf*^CL?y?2}5MND#<}~1?vHYN{+er^nppk zIs0wvKa_SS8Y)d$XOKySHhfvCdG27`&4NtTYxb0wRcWeyp88B>wwFtlvd5Bv)g4Un z3!j{n`irt|^q3KqdtN`5|JtjFYfxC=?L|=VYg6u9-)c*Sg%l}wXwF-sORxMbBQ9`s z3}JPY3)Q0$4+(haQU(7dopaVzl2t;)MD_`>XEift2gsD(_K#S%fQ=n&1X#6Nrn&w9 zLc+W-CSguYrG%8ri?jI8JORJbxIyb-Embbn%IEq0k>je?x-v6=wP?I~DRI%1eSv=h zqE((A0lyUrzvJH%BJUu*hxZEA$!#Hxl$Nr6>OZecS_@IUBUz$DTYnuo@(A?1nsm-&IUh+oLbSA*||*sUZ-n_i=HCx1kv? zO`UJks-W;JSf*8#)gh+iXD8~XceClHd?2^RnW>+DBBj^by4IfXU>+v#c)XMO8FukF zWW?ld=BWrO(|BeXz}WvTGs9HX$eN4F(6eYWI#FDIlv@XMH1a#CzedZ6DH*eEZ1r-{kDbeL`{G=haNx)6z>SLg7h6_V(IZM-aJ(peL2{ z;X016mG*|m7lxUOi~mX$!VmVdojhuaLkN7`G86p71i(B;(E6ub@xMfpTUyQj-w6`r z9^RW8Qx>M$t~Q(8AM?c?`IME{R124KTIj~bj&f|$bXFzjhyWBQqZtTrU*G*PquO2u1;u7uQCA%N z8q{FO#*3XdW}F}LYI`4m*4ja*UJ7>f_AS=OpWmG7^Q%|$We(S|ghuCHlqTjpm0HwI zWf9}U;!pPts2$Oq> zy>q?j?di-=S@BpLr*i{IymQ;yXP>d!->4Du{-E&B^a$_m!m62RkQbidl?WU61&XV( z(lv`e`VO&E$9Hr5%E*A9d+;@n`}rmm8OJj8euiHY#mUG4Tbr`OILX^4Ib2L(^XLoZ z0w29yAQ0(&BNH)rMc;rhNoMgRZ*_V)Jt{QU6n@bB;M@$vEQ?(XvP^6l;I{r&yy?Ck67 z>-F{Z^z`)e^Yi)n`TP6(_xJbu`uh0z_%kyzCMG5w9UbK4&Ucnb>)o12@!z`&WA znWv|xot>Suw6t7YT!e&#T3T9&h=^cdV7>|92^`qH8pQ8&9btxtE;Q3s;YK&c87BeUFciV`F2crlx#+e8t7Ze}8{e zR8-vD+{eeqqobo=Utgl4qT1TpL_|cInwp-To-i;lGBPqtOG^R*0yZ`_6ciK}7Z(;5 z784T_dU|?;gM)N*bRZxgJUl!hAt8y0iFA|2L}f%EG#ZAE-^7NEiEku1_lxm63@@i5D*Z|%*^TO>C4N@LqkKe zv$N{z>P<~e-QC?qMMcWW%GlW0b#---k&y}t3IhWJ1Ox;+Iyyo^LP<$UK|w(`H#Zd( z6%i2;7#J8E8yg=VA2>KTG&D3XFE1k_BPb{+IXO8R8X5rs0VyddJ3BikCnqH(B_19g z85tQ$N=gL<1#E0=XlQ6rQBesA34VTladC0T$jH09yZ-+EJ?MBT002vrNklQEcwgyf=9IJ>T1Xs_OQe zuUo(6o4N1x?f&;Uba|xU$CzpA zeO0w}O*WX4b?UmUD(|VD z{IN^l<6i#A4IlptSj-f9FoL_fp|hs8r1n{cnP6rOvBb}s+VQX7^3cAs;hNvs^$9=( zIIP2&Czdo>g_!{D)m}G53JXC@AqXK!qkWxKm>CiuX*RS6>o}7lKwO0l$s)`w7GLEU z9H(F@0>oji$|B5c#&_b|gp%O`OcG&De7^VD^i{U*#J2?-D8NKmRq5JR*$}QUx+RUS zxdDI^FF?V;@LD&d%_-}Zgw}*oQb5NE6kr$R6ogVz)BHSE3tbc4H~~YHY7_zD`WCLy zq|SD5Wtg?rd#3<_729h8VH3)EZ`-^cTm>3ZX(9l}xbX!zkmV6XSER|$=gfwZR@ViD zVbcVd{>zJ6w$p<#f(d0}>To#HH#r_T zjiJ^z1t5gEV8wX3P-@dfvMf1Mh_nOYjkh^7czU8Otr#Gn)e6Jpy6j{jOt=d+qLAxS zl*}N~#$gq;*$SS1XyPjY%V3ymtt|_cz6Ml9X{{}pnOAlMqVm}hpLU@~vyAgK>%wt` zv7-bbrf};*Hhmr#r)ZiSj+|EVYdwcVl-$rBtP>|dT!m6~0*r(EoRm3jXlp7{#n*`> zS%CQOS0%(FY2D^$dm7LX?H~bjTznm+!3CHg^0rLNi|c%h>6AwkO$B&Yaj|{EdziPEI?=auK>r`?1#?`?<*Yc15;gajH(OkME1j{J2cY!n$k)G zC{k>P1zZq-mnO~+=c6O1nECUyfpCh@2QK(10>tZZri=oHLNb&X=!#?~```eDd-IN0SQ9`IPfw!EvNJcJIIQ z^@H~S-1+^8#7>8Cp|Mh_ZFa*aEBX+j57R&*FJF51l{@zX+!AV z%o@=A?yd(fxcd9Ae)gBYreF*1Q-A-$|9%g`P&V*OYLeaX3560bAOKDTB%-*j^w%Hy z{Dtp?$8Wsp+}JeCzC8!-f`7aCLP>RHx$5e0|2dt$_BwTEy21dMa@j<*DO^i+TZ&^I_`jyp=>b59{}OeTw+WswYU%*TSeYt5PrBI+c9cqp z;l!_t(86!L@Zt{+Lk!Xyz;U$B$~aNWzXEW|gwWUUx)Wc#0C5r0Sh(q2UfKDGci^iHW$k$zYL3h_B3xUJM$0)~o< zeNEZu!B4pH1jzX2MS^Zh0}(Ky07L2=ifkVI_;2X7*f0<6-e*_Tlm7wxjf!l$u8#A8yGmbVg<`f$)!1UiF8|n-^ z_2P8;A7iWpO>Qi`4bZj%1nwXZMRYA75G6wenC{F)t%>@_e>m%75P;)=P_?--JTe}y z{@Mb<5OrJ+4?CU!`!P2BohU3`8O{m8QE3{B)(R8^!CLJMz)29GDv?!zZds@W$Fypj zyfqMarC#9uGt`;qI3ujjbuoI0Cd`qiNVUZn&gb zfure9V(!;K0HUQ55@5x60>qKD6%c^I+_20F98G^?M8B%m4pNxkle&2-(sa}GB(x3} zhSRkQi-xlTm*Tel`~Q3d{W%Ue#$u5#Zn^|_b{)5QSl4vZpsu(7nG2*2mwvJMn|IOQ z{O!)Hz%X`HW7t0;0H@SMK{AB9W5O~aF4%~-|GS!(aYUlB1RO!&=lFvcr+@duYy-pC zarvu9merpkCM5P~1f-$X#ndtrzXmi+Zc(9e5QAt!q7g$BY+>JP>-D^>z#yLAyYC7t z|K}wZ3!<3Rxi!pLD+RJAb&BX{U4!;t*_6V6KVu18b8ZXk}ptR{)0aO~T=j zW5eP)-14X5ipnM&1!=qhjKaYXw#s$k#XNq;LkItAGJtMtWZ)2BSS=G~CaH)F-ZXqm zgDT_aBS{_7&JBjB+q^esF-Kkz$jnO^f+*blwI+6M2sZ$bC_uapXL#&f5YysDP!Sj+ z&)FR0+)wxDk2J#zBr~%46V7tzNOZw?8H8IWQjB2w&1mv2{Q(Xg{32Pr|Jf{+ei6P> zW6cdI*zy8Qp9`WYF9Omr{g}K*dI<8xgNG(_SgMAXJqRww{1r%mpd>E9xH_DQnhPS4 zDk8QZR@7#l4e;sj&beWt#bA;IJ)&KyG3SQZWd-QWQgcC6gC55pTw4lFvdm)KaH|=T zco?LhE@Ezo0oZl{+GXd0SXuz2C#9IqB9aswMPSXgB`Y8dfYP zKs(B48mEgT6j8ax@STYm-%`)uKzJ#ObeY%r>+xe{#>~V4V+N&uv9# z+E=wLIaBj4STSDOy7GEPE6Wv9hg+&0xoPTD1q!Spa`CTBRv7B#u~?!t$ZegYs^ZNH z&bJTzDuDXbD-Z9wu62b9TLY@XwOi&R+0omPS1oizy{d9}8Tg)CR=Wjn4aZn8iOVX) zCSY)DAeO84@8ACo`pIXn{Q{2&Lb~}#4Ur0rgD4C#wjZ(g$YQx3%1593te>nf#BT~H zc+7SqPXNJ?0un|yv_Cr?;t&D$s!;n@LL>$Lt`M&vEnVQrWFh`b=T0tt>xH{-vZ_>C z^scl^Hn=Kxqnp2?rrt8VnmY~Yh(t*-x)F3#q!lcMt|kk3?!X)8Oja7&$e-KN*J-AZ z(2vj(eV$PQFtXK6se+0bQ$VqxT%%iQ#x;Uc3+P7{QQ5%ieP4cRw(2t&+PS<63rPSn z&4V8%POn&OssNQdE^4O$A?BswTBM}|+qq@ntp~3@GFfp5HGL^pLzdE?<58{iJ9jyb z06SA!5M9Mwi$fL2l9DlolF4T5?dQKao2)o~Q3+W18cV=1to)&ffc~ z6#UW)&61Ka1@)7Tdw=nliaZ3QCa{RMQQ*W9`r~$_xgn)V6=2U?kPs}#6eyBHVbYc4 z*#93roVUSD2!+KsJ@gQS=I9d)!j#zD5LO@p4F8;LXlWn_ZkJdGat?j`%n8rlJpZ!1 zO>o@-Gw7kzE?IwusNlIF43%h_09{Y3{@_b9=?W|v;)IX9>)?Y~eVqedtf8i_!G@v| z@85`HNYJ{>rU}sY&Z#42j9$=^DkKo;b;7f!zWa&XKd^Jay|<8f0Shp`M54&(ez@Zc z&^Y`LFBip^-lPN~l-zgmfxTIKhy#}Fcwugi#v;!xz9abf0^}^PZ+iNhWE<#(N$A1O zE_x<6Zo?d~DEqg-A;Y_fh?JfxHGtJddqLjO}Y2R``5WeI3%G1@-x}5Yn6F!e0|VHpwOkuvB3S0RtTgOA6y< zzrbZ*-^4Z_(|n6YtH4aY@O4)JQP2;wfWfr|H~|8*5QN1N>UjGr7I;pMb!}sn&MovO zv)VxqWMN@$9W_{h<0WqXJcDQ)bQn=cS(c@nmSrhrAxyjgdt{Lo))!QTECUUp9FF61 z95N7rl%lsg%3uPJ%T)7+XH*nFHX+WHoGE3DfvGV@Dc1d5VfDLoBPjV`IuNj20zk24 z_DGIHh9VH*dRq^*HKxDu%^xUES}TPacELtSYrEq}4#HuyHOnZlZZRipYFa=?jzb0` z5YnTArnSv7vun5}5Xpw9q*&-rr$ZkA3o#JNmr-Ejpbyg5?Yy-)0zn+`9PNyy+dtD% z22m6nQd$QU;JgZJh=S4Dq$|)+KH!8^J=?W~H z-kD!iKu3;4jxlRxv;!vX@xt633~L_f6$D}ynJ!ku3a~Sz1_DtYYXQihmzF?J<*nDH z$aZy2VC`Ws%0UmDnyxSxH#|f&Eg&Eh6igW>KqawR5WU9`hOh!(2F_RBoPx#pl{JC7 z;js=_Y1Z4MOcB(#1z}vTR&1OAONFR9aiu^Mz6_kNyxIPRRF=+EZoWe=~7+ruJGRzYT zYE#=^RTg?GZ{3G-HhLXf#qJwI`iAwZq6`vMXS3p&})T6AczCr+{%)b zNb?vB<6Pv)I9!0nVSjI_)-O&BIpMj=+h81`w4baRwvLTJDDMkzjRmB62SSNB#Uuf8 zG3@Zmr4+dL-Its<;Dk%>N1u$PJ?qM@{Du%6CE|4@qX9zo71wAO& zUpio#;pT6Mv(JH-uYesp;pwly1~yQDe|q=jZ^EVThP&&9I- zz#SI>+<7Q{C5RhwGQZ)mHQf!1h|P;euhcw-$Q*QQ(N@Uwdq{*XQ1gefbH{hC+7EF5 zfu2!aXD#^Izr%yOAAaZtSbGX!_hCRNU0@SR{DztOc&fm*uo$P1=?yEutm1M0p`Zcz zSf`7~=79oG&e{I~IC~A?(T9$W{b=ZH>rA-m@l%?Pw9Skj8!U3*>AeRYg-gB(aP(oo z5O@o~uzWP9n|>25;#N7lnyq{{HdV$(Ajo=n`54`*`066f`bFTQyV}p;J99{zo zNg7St4}JX7qjw*=eeH?sb^rp>^#E%FVO7s@7w#iV?Uby}Nsu0)#YZ4yVPS5Aij2@a z@Yc0Gdv?Ljn=ZMG5-9+{ltTjSR7myHuK;{)_Z=_62rU3fg)RfUQ@*$RotG`yBBAP z#(qujHUG|hC|K=g+ll1@dsyNKG7=;VIijO;8jsB}&+;GZ-ViDlfVM9sk9cj(XP|Dw zDx&pZu^BTI?4${mnPKyxP}$xJ?$&W{ z;UV>~K|G}c9-Z3;9XH%qU_K>wvW3zB(D^XbeFdv$BYv#b)k)X}q!U~`Tiqrg4jfW-&$6X9vM7VOqkq%W{7&>5{K^iGz6j?T53XJHQK zFLwiZM1pfNaOfEz&>l*dl{e4JX%uL?PhIs0l9Ht%=;++3voHtiZ@WeI2+2Nxn{7va zYcHs!Y--@NgEs*3VS22HI+ZXB3miaw zRq0ipi7<^J*V9saH`Z9vtSxNF2rZ#MfKthDh4IX@$ocs*d{RvSU9qQM+Fw!Cx7-%w95!MAe62d^XPAYi) z5FaL78@ zFGMQx#^d8YLxduw8LhUnMc|0`@UhQ#ov6SZEM8P!WMWiQIjPGzAgN0w#nSU@|vH3Y}a{|7XbobF<9%&auT^JA!GXX0cD0fYe?;BJ4DONdgpiwAZo z(pDN!m^uJZ-(NeCsh|9PphUp}pBCbvTxcKyM*(%ZgkS?Ohx+>Jc+hl74E5G?dN4S;MUXn?pB<}k6uRo1r;7|#!B1rG7-Fo;M<6WU5l z4KxK6N5K672B_4ti2;vsK3yu-$FBmki6M!TgCQY!plZW-gF^y$6{!$tk7{lv&Hxjn z2h4Ap_7!|KYQX9}Z_}!bG5Jfb#~4?R<AuE5-eh5Yu7*B@^s`tED5 zeIGVJT>3>)w~V}rb)Mh1{#45E?)R?B>7*$j-m}sY6i*n$H859SNnNc8dHBQtU*`er z0?xy!v=)SQKM{xh@f5V5zEsMef!^vFJAs3152`+3yg|$X8p)4irr9B$8sK-n{k4ym z5CkG$h_HD$&08P^Y50rbJ27wm>w`z%*nakcZ@;Z>I7JGWsI^|hd4q~oZ@U8KIQy#x z_`(a_t1mLEy8ai!jT|T8?Cca#3siCn#Jsi6^ZW3xA3bw#ffADho;L+#G`(ToATx3^ z2j*BwmKuFO_vLCpalGmKWVF8x!Csp*$(u2Xwn>o8NygMlcqh+~QC&Z85-6tj8%2Sc z;X+CfZ5p@ERVhoSI>rEh^mg~)!PCFZ5c;U8i}M_i^dQ4vS}f2l4W|fp!yBJ}D5zhs z>|fIvX5&O^zfbCN4<$$h6hqaM!A!m6On&^<8$bC=_p4vN^~HBS-5Zkq-CwQT`6+#_ zdqI$yUw`R!RfI+K{1BessM??G6pzKlQtc^VO#4|2J|l|1ew@h1O}};d#m|2>^GN0T z6n&KSY8&ORa!6a6*xB#r7{u6~1gguy1~P%?R0GU4h&RYbII*dYpvV4|N9E-&U2{^) z0uJZ;dJPRZogYWLxtW#{LIg4vfY>8bk)N~%p&Mv^23rVuVZ}Sv)vD}8y6*p_lz(3z zp)CIDj$iap)e4u9w@U_NFkr4Xgj6AiuR*LsQa~gEISbAXXI+6gb{W=#m&&`po*5u> ziaD&s^qO+o{^FamTI+xu1WE#(J-lFkOzjuG{8`G#>|Df@@;aL zUXuYNWdy_zQIdBbvr+^U=&`RL!aI(BkDvcAZwUEk+)1hgkH4RE34YwZ| zXxIUx4oDJ6AsoI2K^{`z=A*npn`gk>OiY2z3CikGqqHl-m=oQ@OthPzBoL@VE(fnc zylXEjCKs`So4awAAvN(TmEF{MEv~F@hU+Z!o zyuoa*fw}sYv|W<{^5a!@w)?9Neb|iY`6b4ywI_jYU_N{7oi)M%TtEwrj_e;brZ@xU zs{Q${*#Hx}<1%+0gSk-mzU~3cgaS3K-K&gDqSRIjf~zBZ%fqxt9N3`THP%pZxp-z$%RPm zC5y=I&ukbEE6vL@_6FBp(oGI=6Kb^uGw;4g(=?h{>R;P4zjNd*Tciw0)ZU1p++Kwk zduXX%F|;H!cnu8+JW71CY?58}{BFvHhC2t!b* zh>0hNDM}55;A$ExlK@&y)T-x~_E#3b4NiwfgO_RQ5vb@AgP9=BGKe>*ByBD*G}>!r ziqgJe9*(3UAx$%JN{zemjRd&XBZP6;n zm?qj(L6w3K+>lzX4zEH0UWgrUPS=IW0;xfeJRRZ%;Un$nC*vko#vXXh{{ATR8Km>wlM9&$PN1#Dz#wV;lptvbg_8 z{H8w!4k9o&@rGc>iYJxAVpwVz{R9D&K0(z9&X6|)D6fOpmv=V0C?A-RB}98NBike8-~bPy+6jsC zP+6rclbQW$MY_B`_On;H5$N7fE+0k2ih^T=%uBF zr5<%b3tHbn-c%2C!zt{oLbTaQicaxNY(V}=PK076I9A15NtLpUwrerK&c-^=TlIq&vm{tyvUcQ*xpvDX+A<&l!AsTm=Hs zbPP6@eI7SQ_j_v)VOT^!GWF!YHX%YM4;hCxQOE7fltg~CO}?nGjBXxgbG-~#{*$i%cFOB z`_?H$&G7Pa5uL?Yn1}76e10b4djLO(PbtEz6gIorIY_!B3^6f>Q3dm0%)H^dFpI!Y zJK;RGWBc~!Q!(-w#eqD0CWF&Y0YPF9j4Lcabo6s^65q2GVxd9b8ZjA-sTgx&F8!(Z zY+wT9;=`PILukRAilwxb@fvu)n+(QgLgGV?lZ)s!xd40|Mj8~UTM8M50 zJ$<}u5%{>U#tx;+7I-OZp<`wNi4JgfIgL3n=eTe0_U+rkwu9?W%$c`_29T}tSfZcO z{%X4S9fQ-PEioV*a$J1)S0iXIUAOkG?_;^4&?vOE1R8;3gNwkT%@7uNmflBChZmctJ6VuYxb9YTkO$$JeQ&Jp5li(0w zBRWF(;R!Hr18_XUwJpVoVeo_6Iv^1j<}V9d3`60rbUy4&X}a{STfx^f#6)vPG@(t* zH5~#az@taN2@vxn!Oh$a8HE@7xSdkb+XIyMSBbAz`6z6>AeO_dR0k58TH3Y@gCB&x zXl_}(hW(s4Y*QxjS)S6ul9i3ZzO^trmU}`ZXK`>xMxjm2=6y*^LQ9Z z7%!nPt@3eE*zFTy!IA*$>bbdd`*2*dMQsrf0fdy5MFY} z4=~qbm6$9tXd9wuvr*LQro6D^U?O1=ItP{nC=tiat)YWH)gcJ%aPY7*Z)fBI4ZC2> zyt!rO@O0_|OF(&IT*JalVMmLfnbKL4VRS+p)27Yx2p%Kx36PH_g(Zs+1#Nk78!%`K zcfBq~LBb@!0W6NT^8yniFUEo7lhTfG^Y-g;XJwMwT_D_AqqJD#BP#*s=bdF38esbC zS1(ss4xR+a%uoI`bv3nVokv)X>DBI%z&vpXG%?4s9l+kz^F|E>QLnvU-rHR}HVxDW zg7^a-hmdmkxA7AcPbk3DBfNAOxbL0#RLweRjM# z9AoeKeB7Qgg%!#2DN)|Nc{}rF99?8L_lxiQu(b4GZGP$E)x09Dq1HwXz)S<;Z(mm) zUpJ#OcYeXL#GV@&0EGzv)UN}DGH%TAPTYCpVcjaoxC8LeX4mq6AGAz}o4qp3kpUd} z5;QLt>(_x4lhGjL9qEdK^D5J+f{aes0Xcvt|3I${Gi{j=v#;*lJ^G{FUegAk&;ej0 za#6Qy1KdtP%eo8A3%lk4c==^ihWY-iecyyu+(r`X<^wQqDq$2mh)BVK%cF<@7+s}f z7nI?~5`ba=03O~*V%ah)%um4#Lm;K#NeD@hgDFeTE7S(Si+*;x@jZkmFa?_!t&@>+ z;=KZ(IfoGFrxYi6hB)MK$aa49HMI$$9@u&fG44G^Ae)#&h)`p3yM-=Rj8s-`(8kkzEAIKMWlY>!JhNQ`f)Se$aw?8=V~VR`Drxb8 z*Uu2&wmsV?qGUfGb<(WdfQ_fwY@IP6m|rkP1HOBzG++aEtbAUj7@~yYLZB#aFjEPt zINZa0(Sz}ngJV(&D`aj43Fmw&z;$gr`Y)@gZ^wGQYK{iHpfq4JhEztOL)>i?m#Mhg zrV=j76QshQ`a1$qb*B;sDl0c*<59teXGpzfkOth$$ZuGd{FLI9igO@tM$4mWi*cFH z`|nU2=?%V9M^(_B^-2$y{u*4{(|P$x=kJWUQIy*+)Z-Byh_16pzG8e^Gki{iT5 zD2@(o9S@Low|G!lfIsyq#Qy6^K`SE#BQMFyg;lA&Go8-Noc4mi;f!UfR^0NW45l+G zh@v9~ zz$U;5A%XFR1PBb6VKLwV@xUU1!H69MJYY=sZ3`p|8#Bf|ERq>kBLM=B00R;OCLd{6 z&2-3>88_loX7#kP?(cCXyDBs5f6s{%an3&~;h}DhrPGf7c79>UfK1YaYcQ4E&B`D8 z5g+}TkAFSkBNA%slegyRw>iWSh9GLH3Wr(=B367a3kh{(g?Yc$?1?i+`HYOHgm*RFg0fPn519#5Kxwt<57k9D-m0Lo+diaAZD+{mc zMXB0hAqjWG;$BM}mlH6YZnw!#!){g%AtwL8Qitxhn-hRguM(Fs^gm_&wHOtO9;w|_ z3FOlN0G?q#YQ#Pg!6`1P6her?K7hru=r*SSsa`$y)zUEqj?F2+D32V?pY8B#!s}@=Gx2&)jdh}|Kh1%oJz+U3C#m_>Y@lAh ztGfEi7g=vyAp{etu#Y$t^f!5CtJ`PN4ITxCi2A~nzIkK?>vLAlD1GP=(rE}VOay0w z_KxHbC{+j{_4)lFaOwa=Q`Q(p{#&g0PgBBUy<>%25EglKf}E2bZ!pGX8Oc(V{X zl8J{}^xL27_Zbb{xd5153Nn^Sunzm_5^(5!5y>v4)tr@uCtU+z%?V%EIuW+sDHq?m zgD+nY0(;@Iq(1=BlpiA#0bmT+hvcBV3+FG_LXo?@#;9y%`>JvYTv$cu5ah+*CPUD9v8S;^8nG82LAfuYU93LtW@RA6_7!Me(>tiPAuy!8@fvh?RmcQnkE5b8nAmx+#azJ0w(qEqeVHJqmGf#fNptUo;V{-%>l{VC(PCJvkqH z^Tz*2##lnZ99tLJ+OQ#yq+~|6ISo4AzOajdQiYImFf9baoGCA6{Q*DpF+cfus{kRK z-RYIyg`xaz&#z;;@dJd9R(-%jEM<*s-|#H1+nfe`d+7OwPwiKVBZ82pAj{h5vi?d0 zlu14Ev`7IX7d!Xg`oUG__t!q!C<>yYw<6>=oAX_m(_lO5tEnuUZ-4aBDXNBsE^>>@ z3NRaXbOY_*=g08F{^iD8vBT>E`!H^?TJszUI!Mew+9Fi+KHZ_Nbsb- zL9GZ`)mXymkuF$coeBeCFqd)&k5Lc~L%j3Yi^@Oc^KZ^KzC7y>NKRmZh-Jbyp@>i* zz0v3irC7khUA)ugROq({cTNRLPz$BNQjJ9aI?wYH$od0>ODN(K7D;aC$O#y6poCi% zk5Gf0fW$EXR|)Ar1{ zZhvRG2Vw4YyEz$HqD&(a{?uHPLSR583ID(w{`Jj|0#IDzK5@EuZ`7fB?{$fah z61p4pGrv@pqAux8`b1lWv>$XTNjM2U=_-W26)i?elP3a00anmoUd&klnZKCl`EBKs zWIMRD+=x(qCjpe^*-?<%W(YONu-RP8Dg<0pCMPT<2m4|Ov;p@KPU9lq^*NvWop+J> zm^<^07Z#2tV42Hu0b-9Gf+dd5Ay7^jg&-{fOa1t#UWG&)JVQAoQlDR(P(YZhKZzaw z_{sU^?@SW+dC3od^4&>tB#B+V=Y22xDol~oH<$*>q$k5ZVzCA}^(sV2 zicvVlZ3O{s18*547W9Yr&-47Xsqdp+Uh3=E_sJLATeI4C`TE=Q{NB49qz^QzgO`9( zlxmQZuR=U!5omNFVN+g>)S*PMW&PpJH|IOA1tjB~N#OCX_@!Tb^@U<5|KDHcJEkj&7NTzLqEMPRfL*c+5!7Lfb(hr8eX#<$`O9s2F~ zQSX_5;h#R_#zVu#Z69@|z4Z*su#fNmrKjHhdm}9XvOp+UBO^E|(gkNq^TPk7U}k6YIdNh0&^zy7NaAd^k>m(1IK>&?HZG)Uun z&eD2v2_`9gb5cyAeGw>oiljP23xTmik;w@(@kMv$n=iF;QsyIX%r}1*7kJ&RdH$8x z?6kLL{T1jUEdZL1BG+EFF9F50IW1Tqq*Z4NFQ)YQ8BI>Y(oGhP3Vi8jKK=)v7=VAC z=lTEO0u%rqb>Q3HGe7-iT;RjsIY0HjM=mx8nu`9c(;$5m?QpQ&76PcgM5n(9RNXQ9{FG=- z!dZVnkZ9voADq{CJs$nyx4rZcT;wfZ`PHx6{rM6ypiCEueOp7MY=U-*yaYV`MWCcO zLm|-m{1{r3aMmA?44U}uTlWO`eITHCO2{l(B7NA;-}>{Pu6+~G*_?90FAmEvu3X<8G9sbLCoF_mM<8?;k17l@o(VJ&(2igp=o1Qb0m%Hwo%zORRmyoa;_!uvRwup$%uJN|jmc12 zl2lI?NXPc*FJr>71sF^=?8p@F+WeO1zr1>;qbPlYW+Hr}YTBG0fgL`?>mw}$+QREE zG%8!3Z~@EYF$E}@#~HHVsMLEDh_+a>5F|zGT3}&wdQ6<=&q(=;3r@7Y0IZV*{Sks- zB!s_$)Uqsz)*)OF9txsECqKmq=nB&_iMGW>GoS~nm2Ji?MDp_sr6rvwCt-xqXZ;mWOuX)$^L*poZ}G1bF*TT92_ZCJ1B- zCi;G4c!pn{=lQ?eMwb}7!oEQ>Y)%t49ih+9mj~YHN!UB?QRfSOrgucOvZBCQXfzszk+vh?UKuyAaWUTj%m28wX`+Ql0<-OJqQIFxwXS}n^R>=>+_ouEd)yEQD9F&&<#05F=XEIwwJvGRjgDg zj^Q5hm9J7sNed#$ag8!MmMbSo3xH{$UDLVUoGgLV=O;OFV$3};VYEKKUeJz15T>R@{j~cs z-B149i0$Lv_yM#L@Ll&3_EUFWbQdK2)az{6(ONX+1WXYWL{Ca19ow8ToYv=O%2p}l zB#b2Y=`RzU0*!!g`QS0}Z-a(%ASKZL+cylo0Eo>`uu2aYDSZja4vH>7cK0l|6o$B-?j9|0=Nr{e`Op^jtU4U=;v-#=&LWUe#W8b#^!!UtZ zfuKvI$K6cZ&B?>4w&H@^=GUj9{yc(46+Q6;plK-XPyhPXe?Ed5iI*3b!_cI^)RRZ& z1l97KZB8G@HEAPNjiA;c)``*#NUH&x>|C(Ol!xNp^*8Um^#xeO2G_~z#nylf%k4A7 z3Z#6Nne0*tvX&u@Wo@+0f2=$O2OudlV40f)GNpdH7dy1z-fk(l6!Iz#u&=_13a^6L=CPFa$%I<6|Z~@dyt`bgF+(% z_gElwASYnYCFv9neArwMwxgisl7ZE;PsM*A@(YR!0!?Hs+2)}**sR(%MCby_DDLSA zm<^SGK^#Up{iZAkMICsPrp@v;@m={OG?BBZ>w>biTZyd-oy&Quk zX5QYh&2_7AQU+?MoL-}FFPTjALWkhn7?60%Au1ewp|_F%aJeRO0b8ivW< zZLSSrA_lbH(33C>w?+MB6H2lQBb!FPb$!jUSSRtQV(X;bG}cv*qaK|^- zh%l6vTI4!Js3isZg8pbhkoL5Y!(TqcChAWKas`qsP`#e@^y!QCVxC%uAX*fOy*FHA zffTP^fRB8{yYFHzF(I>SqF=Euv=P(_#24-D`pvb1G2(Y>MwDPE6LPjMV41#R0Y3V9 zw{JhOPl1}*%6VHhFQR?$-VmQw#x~cBvFxu?ze$bKL+rRkg%qz=fZz1NQRCN@!(Z90 z-11m4FCtz^>m#oBDO`qyQjQ?sB?+0z`qQzlK+Dus3h*0m&-0Bx#RU|zwL@&n)1}Od zw6ny<)xb8_jm~*uqp9fu(B^1`(?H^uV&!~%0pd$<-2Cxf^XbYVc1?f6&=asur2yD% zHfsoc1_p8xM$r59Cjr24jVg-iD-$68=&>F9{rVxc=B8p^L`}e8SOZ`^1WH3s!eG9r zKR`J}xX$CK0;FF+;<;*g)1HBzfQ?*Nfc3k(fZ*v#*t4?!0L=p$NWovd0B!vAH~u85 z_qK-Ru+2O@0sDa3EZn*mxKyeET|jdwEVKRqKTc@C$7s<^1RWdV3vbMKeg>7^mud8< z6u{)pt?1W1z=~^rtq$>ApoEMU^d|shhTft9pMpReK7on|$>aCtd43a?2xC8Bi6~J* za9^oFDz5nJiEv&llWz5cNDKM{l9OmcFyqmHH6Y>5xwhZ%AM*>Ik0s=`!xBlLge`wc zhV>Nq0-+IJQ1kjECF3{m5Z$Nv4_Ui3UHkeuohDxb5!7|D%;UD#Y# zTra3~2)fV^8MIT_#r6!XsE{m{(F(2D6ck!vy5(#Fjl@U+A8Vg8)CS)v%x%?i>Q@}E!vH+w#(qq_MU)-q5X@>qK13_o~?IUo3cKqU< zGeS1rZsXC8;-Tb&P&ZHTT#(bY8r|j^GjIW74xRCklW-WZs6fpXiby_qP>?YgOSxYs zV@-$>6#T$tc7mbm4`U!FV9$rmb!IyRauUYGP$npDums}*G;9hXHV?(++ia2r4Z#^Q zV0Xzo0z{GJfEqFj(4XUgXK0(>fo* zty?jLr2tkmj!h0l@FY3x8A}WP@WbM(zUwImnU|vMXCV3QdtwU%@xcFsT zo`5Jo41)B5Boo6JLMB!QlW5^@zlpwxB=e$7@M{H9hH(@Sij;102VGJi2SGXk$3t*d z-PTj-lIBH9tQAP54xt~p4^vrxfH7zYg7#q%pdGlVdJ-9h48;}J2CNkbsSe4oid|H3 zf=t8~%Vy@E69GPY_d+LG$&@;q=-1X2tV0oowE|ftbhNp{thVxhkpUI*U;e=g@R85C z`8Tg>d=9c<$8u#Adyf*)Vm$<2#&w21c@57^L4WNQfOc@{K@0GAo}TCT<5^PeRtUi@ z;F(6IP^?fF(xzK0kY|{j1$`N4xhhQ15-b39$IXKk;Ez5v&$sR?r_-nqN7&_uNpTDM z0?@i?RbD*_gDs|~g8nELfO71`gB0NJzvJoKUkCsY{U}jq=t9-sw+O?!1$bBz))7U7 z(>TYE3>l&Ypz25Q-~@O#uHs2f4Kv3n4=DK+Y^J@a8R>Jf`1Zg|fvsf#dxk;f3Yd}U#j%j4t-*^|u`ipu^ zz!Y2;35)4WH*#Bd!EMKMZ65bp0aTy;j`T)iVcWfuf9u6tTodp*DdJ%Y}rIuQD^-D!YQ-`L1NUJ%Ar?jG$7NIqbVi))n@XQ z6D0Gj%*56vtOBk}w6ApP?uP>dauBTJ&CqZAA<4@KP^PngY&@u_;A=g9W;y6HzwW?X zhR$h}ux!sMX@i?`MlcJ|k!i0AjV1gO3bvT8bJuglcX;80O4qGAD-*bH-`5{Hs_#aM z$WT={o?V7?fA}N$eWN1h*C>t5Xk3z*>T@5g_gElkl<<7rfw}ZDP{v*T5iI~MH4pDz z2lwo~Tq9#4gcw5zj6HjM*KmH3+~rZh0;rgSRm=mUTo-_shL5T7Y@Lk&=oWy2X%wJI zu}dk&7($FOrGo6J=vuLydJmh;1IgVRY2>TMmg%@e5u=dx=Yf(1Al-w@YoJEl&3i_j10Qwj`@K8);(%)6T6<9(nx<;&Hm<@y;RRxB^DoFQ? z5Nn=u0n+0#<6K=y{E4yII#pWv${BtirAA>~`(Wz{Z zNREH1?^k8DGA*T<$ORU$W9OR}{`R>QplvRQd^g%`kVw{_UVBiFbRrVDPXYzVPP{$^ z`P>OmhUC$P;GyC~BoeTmfEZY_YRxM3>juhIT0xrBC9i1xt=(8fye ztA3Q!BU3fcnufIiCc`II7z1vcIk)3zX&Etj%F9uN{)_0JBR`*Y^DH*4C%~7WA7fd6 zfM)c_Q4dVUgD@0F#~W&>O)gb0klDHU+%iuM9(8mIQKJE^Kt{)q zGzw5Zf#<5t2h8;A4ALd(xIjj6eX&d5H^h$sdXNeNHKYIy5rXj`5?rddVpYEewRXT< zp211v0}hRgFo_y=;tFJB3{ikM6jw&X5GBYcKG~`-rDRw_t{pJPP+-;{-T6iIodqr@ zLS-vMaaF9!;-g`^nsH(102NpLT_w!vYz0_w@2L`9z~#^;6jW$zllFqqxE#Sn)aY2{ z3Dj<#0X~r6Gyvgbv5VbsK509_y{mpnZgdDEv9r5$C*bmDTXdw zASQF1$t1|GEnbTN2yR#XT{U5AP=MqGoyuvAPQVpmC*JsR3;@J&)!$X~uGcz=40$1# ziH_NaTY#3ihz9c2A;W;Z$aVd9EdBlAyI4Vi)HDnq+@Ns`BCY^EwBFq$@&nMvRex8k zzs-g@QMYq!!bdmBhcg1o$wiM$qvhsa6MeDj?}+-l$G{Di(aqU4@lHH^0u&uj&3Rf^ zZmKcsMXX2E-x*yWK}y4w^Lg`d3DD%u3rhOMeiQxJujju<)ZaPd$OyiAHB{$Mz5G4o3`v z@TTX{KDQCjM&26_LinrRUE2+7|MOAxmvLc|(rGcw>hgfMmB!=0Mfn$S##ilVJ&zWq7V$%I(TQCV zsz?1kjWc-svM~Pw+%{rC>&P=J8_LRIhvEWdMvb)DpsULRqG@pZI|N^4j2*qlc4rn= z01h7ulGnbAglRI5e7??m1c$UP51bu60)2Y(?}NKt-chYOQ0w4Ptb{EwGf5g3G7{vA zrR;W^C?qkqcI-F=>dUzg+XZCC*#+-{&i4*G%JR6FrF_A=qpdS+gIntb4C=>=SAM{7 zt~b$Ik@dzkTyDq7RsBh+<6GX|8MX~we*+fv;N;n}AFZEeo#4dtuB9ZY2BQ>tn%JV& ziniFCh)dF3l+|D)T?=>4i8B_c9^8jUy}12e_eH+4j{7{PJ`@-K7gNh8m+wA&bPp1Z zg)zSDyb!!t6#{YJn2?`h=TN5axlI&^jKd4~FXxMNs z(uz=8R1=)a3<%wQQlRwCvmN>1I%eV~R|G>Y$Rq^}xgm3xzrgIqbNILL1dCwE1({3> zj?WDV<{gO;KaoHaF8LdA`nH@i#$j_boULsz5W+k@G|QkK#mlPLB>mXk=f~4Xs zif`Y4^V-*tVL3FaW!Z9j#L+NAalyS1xD-#Rq-t0!Sqc%BhYRcGdsw(zyLtd@XQT+BboUHW86kBlSrA6^O2smXVrPC5HC216% zx*QZ{Mm1ceQxPy#yio?N_n_SsH=L5?p8*rg8{G9C&^kyVG-hl8LpTnh_8nYij;wS# z9d9TukZLEba!q9hbWZ~2ULgkWE=T?nHj%(LF|5lSD}NkWy04h|*rzUHqW zAJ|hu5tNjjf<(ksXZT99VT!rkAh&+@qU`M2CNc*6xmSV%@nXLXjBRYXu%pv*IE zos@>)9+(U1EGg?|GURl%2^iA4)Q_iinaywZ zj^nfk23OH|OGIh|+Q4o=;cM#+v;zPD000000I;w5>=RrAFg}_w?-|ql&M!4qxPZ~Q zaGAYxj9oso_S4UQoWkV*WApMUbN;;f?hd>84K4;4pLfr#-F)fwJKsYSOS+WGIsG2W2c|_@dsQD0000000000000000APyz Y2Q6SBn-b$lCIA2c07*qoM6N<$f^C8@!2kdN literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/secondary_r_axis_partial_arc.png b/tests/baseline_images/test_coord_radial/secondary_r_axis_partial_arc.png new file mode 100644 index 0000000000000000000000000000000000000000..af057274d5d2f7bd9daee659a4f36d6e302e4933 GIT binary patch literal 16471 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 literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/secondary_theta_axis_donut.png b/tests/baseline_images/test_coord_radial/secondary_theta_axis_donut.png new file mode 100644 index 0000000000000000000000000000000000000000..d39bd27d7e12bc3661c1fa9f31502021ac1bb132 GIT binary patch literal 21690 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 VLA=49)-C`5002ovPDHLkV1iKXoHhUe literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/secondary_theta_axis_partial_arc.png b/tests/baseline_images/test_coord_radial/secondary_theta_axis_partial_arc.png new file mode 100644 index 0000000000000000000000000000000000000000..dc8ef0df60f74a080de01be44f9b0b33b05a4128 GIT binary patch literal 17344 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�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 literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/theming_reaches_the_inside_theta_axis.png b/tests/baseline_images/test_coord_radial/theming_reaches_the_inside_theta_axis.png new file mode 100644 index 0000000000000000000000000000000000000000..495d28fac799330be9f265e017e88634ef6e978e GIT binary patch literal 37867 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 PFOasT{^KeQM8y99c6{=W literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_coord_radial/theta_y.png b/tests/baseline_images/test_coord_radial/theta_y.png new file mode 100644 index 0000000000000000000000000000000000000000..a9a58bea9e362fc10f7050e598f21bacd9dea596 GIT binary patch literal 19207 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

*++TR5$7nYzR?#+WQwUq+@0q0a9-1NeI46 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 FV1f?b_I3aO literal 0 HcmV?d00001