From c940b10fc2677fcaffaaebaf00eefc9028ada9db Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 13 Aug 2026 23:47:18 +0300 Subject: [PATCH 1/6] refactor(geom_path): separate drawing from coordinate transformation Draw paths from data that is already in panel coordinates. Pass the constant-aesthetics decision explicitly so callers can draw a path without applying the coordinate transformation again. --- plotnine/geoms/geom_path.py | 53 +++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/plotnine/geoms/geom_path.py b/plotnine/geoms/geom_path.py index 7f0fa12b8..981a51b8c 100644 --- a/plotnine/geoms/geom_path.py +++ b/plotnine/geoms/geom_path.py @@ -154,17 +154,7 @@ def draw_group( params: dict[str, Any], ): data = coord.transform(data, panel_params, munch=True) - data["linewidth"] = data["size"] * SIZE_FACTOR - - if "constant" in params: - constant: bool = params.pop("constant") - else: - constant = len(np.unique(data["group"].to_numpy())) == 1 - - if not constant: - _draw_segments(data, ax, params) - else: - _draw_lines(data, ax, params) + constant = stroke_paths(data, ax, params, params.get("constant")) if "arrow" in params and params["arrow"]: params["arrow"].draw( @@ -451,6 +441,47 @@ def get_paths( return paths +def stroke_paths( + data: pd.DataFrame, + ax: Axes, + params: dict[str, Any], + constant: bool | None = None, +) -> bool: + """ + Draw paths from panel-coordinate data + + Parameters + ---------- + data : + Path data in panel coordinates. Must include a `size` column. + ax : + Axes on which to draw the paths. + params : + Geom and stat parameters used to style the paths. + constant : + Whether aesthetics remain constant along each path. If `False`, + draw each pair of adjacent points as a separate segment. If + `None`, infer `True` when the data contains one group. + + Returns + ------- + : + Whether the paths were drawn with constant aesthetics. Callers + use this value to draw matching arrowheads. + """ + data["linewidth"] = data["size"] * SIZE_FACTOR + + if constant is None: + constant = len(np.unique(data["group"].to_numpy())) == 1 + + if constant: + _draw_lines(data, ax, params) + else: + _draw_segments(data, ax, params) + + return constant + + def _draw_segments(data: pd.DataFrame, ax: Axes, params: dict[str, Any]): """ Draw independent line segments between all the From febb6c5b5b68d603a03a46d39c968d9e5cc64887 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 13 Aug 2026 23:51:48 +0300 Subject: [PATCH 2/6] fix(geom_ribbon): avoid transforming outlines twice Ribbon data is already in panel coordinates when its upper and lower outlines are drawn. Draw those outlines directly so non-linear coordinate systems keep each line on the corresponding edge of the ribbon. --- plotnine/geoms/geom_ribbon.py | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/plotnine/geoms/geom_ribbon.py b/plotnine/geoms/geom_ribbon.py index 0d305b852..530f80803 100644 --- a/plotnine/geoms/geom_ribbon.py +++ b/plotnine/geoms/geom_ribbon.py @@ -7,7 +7,7 @@ from ..doctools import document from ..exceptions import PlotnineError from .geom import geom -from .geom_path import geom_path +from .geom_path import stroke_paths from .geom_polygon import geom_polygon if typing.TYPE_CHECKING: @@ -154,12 +154,11 @@ def draw_unit( # Alpha does not affect the outlines data["alpha"] = 1 - geom_ribbon._draw_outline(data, panel_params, coord, ax, params) + geom_ribbon._draw_outline(data, coord, ax, params) @staticmethod def _draw_outline( data: pd.DataFrame, - panel_params: panel_view, coord: coord, ax: Axes, params: dict[str, Any], @@ -169,25 +168,15 @@ def _draw_outline( if outline_type == "full": return - x, y = "x", "y" - if isinstance(coord, coord_flip): - x, y = y, x - data[x], data[y] = data[y], data[x] + # 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 outline uses one colour, size, and linetype throughout. if outline_type in ("lower", "both"): - geom_path.draw_group( - data.assign(y=data[f"{y}min"]), - panel_params, - coord, - ax, - params, - ) + lower = data.assign(**{bounds: data[f"{bounds}min"]}) + stroke_paths(lower, ax, params, constant=True) if outline_type in ("upper", "both"): - geom_path.draw_group( - data.assign(y=data[f"{y}max"]), - panel_params, - coord, - ax, - params, - ) + upper = data.assign(**{bounds: data[f"{bounds}max"]}) + stroke_paths(upper, ax, params, constant=True) From 80e71587852fdce69a50bcead462177fff417866 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 14 Aug 2026 00:03:13 +0300 Subject: [PATCH 3/6] fix(geom_ribbon): supply positions for every outline type Coordinate munching requires both `x` and `y`. Derive either missing position from the corresponding upper bound even when `outline_type="full"`, so ribbons with full outlines render under non-linear coordinates. --- plotnine/geoms/geom_ribbon.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/plotnine/geoms/geom_ribbon.py b/plotnine/geoms/geom_ribbon.py index 530f80803..e89633118 100644 --- a/plotnine/geoms/geom_ribbon.py +++ b/plotnine/geoms/geom_ribbon.py @@ -75,12 +75,11 @@ def handle_na(self, data: pd.DataFrame) -> pd.DataFrame: return data def setup_data(self, data: pd.DataFrame) -> pd.DataFrame: - # The outlines need x and y coordinates - if self.params["outline_type"] in ("upper", "lower", "both"): - if "xmax" in data and "x" not in data: - data["x"] = data["xmax"] - if "ymax" in data and "y" not in data: - data["y"] = data["ymax"] + # Coordinate munching requires `x` and `y` for every outline type. + if "xmax" in data and "x" not in data: + data["x"] = data["xmax"] + if "ymax" in data and "y" not in data: + data["y"] = data["ymax"] return data @staticmethod From 8eef4637b5fe74921e2f6fdfeb2abe53ce384313 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 14 Aug 2026 00:12:51 +0300 Subject: [PATCH 4/6] docs(geoms): clarify path and ribbon drawing Document that path drawing stores the computed line width on its input so arrowheads use the same width. Explain that each ribbon outline is one path because each draw receives one ribbon group. --- plotnine/geoms/geom_path.py | 2 ++ plotnine/geoms/geom_ribbon.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/plotnine/geoms/geom_path.py b/plotnine/geoms/geom_path.py index 981a51b8c..42bf268f7 100644 --- a/plotnine/geoms/geom_path.py +++ b/plotnine/geoms/geom_path.py @@ -454,6 +454,8 @@ def stroke_paths( ---------- data : Path data in panel coordinates. Must include a `size` column. + The function adds a `linewidth` column in place so subsequent + arrowheads use the same width. ax : Axes on which to draw the paths. params : diff --git a/plotnine/geoms/geom_ribbon.py b/plotnine/geoms/geom_ribbon.py index e89633118..0469e038b 100644 --- a/plotnine/geoms/geom_ribbon.py +++ b/plotnine/geoms/geom_ribbon.py @@ -171,7 +171,8 @@ def _draw_outline( # the ribbon bounds are `xmin` and `xmax`. bounds = "x" if isinstance(coord, coord_flip) else "y" - # The outline uses one colour, size, and linetype throughout. + # Each call receives one ribbon group, so an outline forms one path + # with constant aesthetics. if outline_type in ("lower", "both"): lower = data.assign(**{bounds: data[f"{bounds}min"]}) stroke_paths(lower, ax, params, constant=True) From 65542ca27b981d7f02de533c8915a9e6e33d3a7c Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 14 Aug 2026 00:17:38 +0300 Subject: [PATCH 5/6] test(geom_ribbon): cover outlines under transformed coordinates Render all four outline types with a square-root y transformation. The baseline verifies that each outline follows the corresponding ribbon edge after the coordinate transformation. --- .../ribbon_outline_type_coord_trans.png | Bin 0 -> 17836 bytes tests/test_geom_ribbon_area.py | 6 ++++++ 2 files changed, 6 insertions(+) create mode 100644 tests/baseline_images/test_geom_ribbon_area/ribbon_outline_type_coord_trans.png diff --git a/tests/baseline_images/test_geom_ribbon_area/ribbon_outline_type_coord_trans.png b/tests/baseline_images/test_geom_ribbon_area/ribbon_outline_type_coord_trans.png new file mode 100644 index 0000000000000000000000000000000000000000..47c54b818e967c3af2e08136323708bfecc5ad4e GIT binary patch literal 17836 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 Date: Fri, 14 Aug 2026 01:04:59 +0300 Subject: [PATCH 6/6] docs(changelog): document transformed ribbon outlines Record that ribbon and area outlines now follow their transformed boundaries for every outline type. --- doc/changelog.qmd | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/changelog.qmd b/doc/changelog.qmd index 7d4e65785..2e70d2759 100644 --- a/doc/changelog.qmd +++ b/doc/changelog.qmd @@ -134,6 +134,11 @@ title: Changelog value of the point its segment started from, so the band was drawn as a staircase. +- [](:class:`~plotnine.geom_ribbon`) and [](:class:`~plotnine.geom_area`) now + draw every outline type correctly in non-linear coordinate systems. The + `upper`, `lower` and `both` outlines follow the transformed band edges, and + `full` outlines no longer raise an error. + - The space between facet panels now accounts for the margins of the axis text, so with free scales large margins no longer push the tick labels into the neighbouring panel.