From 423b0935f64d6a29b6d83fde208fb08bc2ac662f Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 14 Aug 2026 15:19:20 +0300 Subject: [PATCH 1/6] refactor(geom_rug): render transformed rug marks directly Move coordinate transformation and side resolution to the geom boundary. The drawing function now receives panel-coordinate positions and resolved panel sides. --- plotnine/geoms/geom_rug.py | 132 ++++++++++++++++++++++--------------- 1 file changed, 79 insertions(+), 53 deletions(-) diff --git a/plotnine/geoms/geom_rug.py b/plotnine/geoms/geom_rug.py index 33711c9953..747b4ac887 100644 --- a/plotnine/geoms/geom_rug.py +++ b/plotnine/geoms/geom_rug.py @@ -57,62 +57,88 @@ def draw_group( ax: Axes, params: dict[str, Any], ): - from matplotlib.collections import LineCollection - data = coord.transform(data, panel_params) sides = params["sides"] # coord_flip does not flip the side(s) on which the rugs # are plotted. We do the flipping here if isinstance(coord, coord_flip): - t = str.maketrans("tblr", "rlbt") - sides = sides.translate(t) - - linewidth = data["size"] * SIZE_FACTOR - - has_x = "x" in data.columns - has_y = "y" in data.columns - - if has_x or has_y: - n = len(data) - else: - return - - rugs = [] - xmin, xmax = panel_params.x.range - ymin, ymax = panel_params.y.range - xheight = (xmax - xmin) * params["length"] - yheight = (ymax - ymin) * params["length"] - - if has_x: - x = cast("FloatArray", np.repeat(data["x"].to_numpy(), 2)) - - if "b" in sides: - y = np.tile([ymin, ymin + yheight], n) - rugs.extend(make_line_segments(x, y, ispath=False)) - - if "t" in sides: - y = np.tile([ymax - yheight, ymax], n) - rugs.extend(make_line_segments(x, y, ispath=False)) - - if has_y: - y = cast("FloatArray", np.repeat(data["y"].to_numpy(), 2)) - - if "l" in sides: - x = np.tile([xmin, xmin + xheight], n) - rugs.extend(make_line_segments(x, y, ispath=False)) - - if "r" in sides: - x = np.tile([xmax - xheight, xmax], n) - rugs.extend(make_line_segments(x, y, ispath=False)) - - color = to_rgba(data["color"], data["alpha"]) - coll = LineCollection( - rugs, - edgecolor=color, - linewidth=linewidth, - linestyle=data["linetype"], - zorder=params["zorder"], - rasterized=params["raster"], - ) - ax.add_collection(coll) + sides = sides.translate(str.maketrans("tblr", "rlbt")) + + stroke_rugs(data, panel_params, ax, params, sides) + + +def stroke_rugs( + data: pd.DataFrame, + panel_params: panel_view, + ax: Axes, + params: dict[str, Any], + sides: str, +) -> None: + """ + Draw rug marks in panel coordinates + + Parameters + ---------- + data : + Rug-mark aesthetics. Include `x`, `y`, or both; position values + must use panel coordinates. + panel_params : + Panel ranges used to determine the mark endpoints. + ax : + Axes to draw on. + params : + Geom and stat parameters that control line appearance. + sides : + Panel sides to mark, using any combination of `b`, `t`, `l`, and + `r`. Resolve any axis flip before calling. + """ + from matplotlib.collections import LineCollection + + linewidth = data["size"] * SIZE_FACTOR + + has_x = "x" in data.columns + has_y = "y" in data.columns + + if not (has_x or has_y): + return + + n = len(data) + rugs = [] + xmin, xmax = panel_params.x.range + ymin, ymax = panel_params.y.range + xheight = (xmax - xmin) * params["length"] + yheight = (ymax - ymin) * params["length"] + + if has_x: + x = cast("FloatArray", np.repeat(data["x"].to_numpy(), 2)) + + if "b" in sides: + y = np.tile([ymin, ymin + yheight], n) + rugs.extend(make_line_segments(x, y, ispath=False)) + + if "t" in sides: + y = np.tile([ymax - yheight, ymax], n) + rugs.extend(make_line_segments(x, y, ispath=False)) + + if has_y: + y = cast("FloatArray", np.repeat(data["y"].to_numpy(), 2)) + + if "l" in sides: + x = np.tile([xmin, xmin + xheight], n) + rugs.extend(make_line_segments(x, y, ispath=False)) + + if "r" in sides: + x = np.tile([xmax - xheight, xmax], n) + rugs.extend(make_line_segments(x, y, ispath=False)) + + color = to_rgba(data["color"], data["alpha"]) + coll = LineCollection( + rugs, + edgecolor=color, + linewidth=linewidth, + linestyle=data["linetype"], + zorder=params["zorder"], + rasterized=params["raster"], + ) + ax.add_collection(coll) From 472f8763da64837286a6de60a05805722dcd2bff Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 14 Aug 2026 15:22:39 +0300 Subject: [PATCH 2/6] refactor(geom_rect): render transformed rectangles directly Move coordinate transformation to the geom boundary. The drawing function now receives rectangle bounds in panel coordinates. --- plotnine/geoms/geom_rect.py | 61 +++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/plotnine/geoms/geom_rect.py b/plotnine/geoms/geom_rect.py index 9b52ecf8ea..7d53bca6b9 100644 --- a/plotnine/geoms/geom_rect.py +++ b/plotnine/geoms/geom_rect.py @@ -75,32 +75,53 @@ def draw_group( ax: Axes, params: dict[str, Any], ): - from matplotlib.collections import PolyCollection - data = coord.transform(data, panel_params, munch=True) - linewidth = data["size"] * SIZE_FACTOR + fill_rects(data, ax, params) + + +def fill_rects( + data: pd.DataFrame, + ax: Axes, + params: dict[str, Any], +) -> None: + """ + Draw rectangles whose bounds use panel coordinates + + Parameters + ---------- + data : + Rectangle aesthetics with panel-coordinate `xmin`, `xmax`, + `ymin`, and `ymax` bounds. + ax : + Axes to draw on. + params : + Geom and stat parameters that control rectangle appearance. + """ + from matplotlib.collections import PolyCollection - limits = zip(data["xmin"], data["xmax"], data["ymin"], data["ymax"]) + linewidth = data["size"] * SIZE_FACTOR - verts = [[(l, b), (l, t), (r, t), (r, b)] for (l, r, b, t) in limits] + limits = zip(data["xmin"], data["xmax"], data["ymin"], data["ymax"]) - fill = to_rgba(data["fill"], data["alpha"]) - color = data["color"] + verts = [[(l, b), (l, t), (r, t), (r, b)] for (l, r, b, t) in limits] - # prevent unnecessary borders - if all(color.isna()): - color = "none" + fill = to_rgba(data["fill"], data["alpha"]) + color = data["color"] - col = PolyCollection( - verts, - facecolors=fill, - edgecolors=color, - linestyles=data["linetype"], - linewidths=linewidth, - zorder=params["zorder"], - rasterized=params["raster"], - ) - ax.add_collection(col) + # prevent unnecessary borders + if all(color.isna()): + color = "none" + + col = PolyCollection( + verts, + facecolors=fill, + edgecolors=color, + linestyles=data["linetype"], + linewidths=linewidth, + zorder=params["zorder"], + rasterized=params["raster"], + ) + ax.add_collection(col) def _rectangles_to_polygons(df: pd.DataFrame) -> pd.DataFrame: From 37927a63a29156ecf920b98faac1f344784b2c03 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 14 Aug 2026 15:25:59 +0300 Subject: [PATCH 3/6] fix(annotation_logticks): avoid transforming panel positions twice Log tick positions come from panel ranges, so they already use panel coordinates. Drawing them through the rug geom transformed those positions again, which bunched or dropped ticks under non-linear coordinates. Render them directly instead. --- plotnine/geoms/annotation_logticks.py | 42 +++++++++------------------ 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/plotnine/geoms/annotation_logticks.py b/plotnine/geoms/annotation_logticks.py index 57494b23c8..f148e82722 100644 --- a/plotnine/geoms/annotation_logticks.py +++ b/plotnine/geoms/annotation_logticks.py @@ -12,7 +12,7 @@ from ..scales.scale_continuous import scale_continuous as ScaleContinuous from .annotate import annotate from .geom_path import geom_path -from .geom_rug import geom_rug +from .geom_rug import geom_rug, stroke_rugs if typing.TYPE_CHECKING: from typing import Any, Literal, Optional, Sequence @@ -21,7 +21,6 @@ from plotnine.coords.coord import coord from plotnine.facets.layout import Layout - from plotnine.geoms.geom import geom from plotnine.iapi import panel_view from plotnine.typing import AnyArray @@ -58,7 +57,6 @@ def _check_log_scale( base: Optional[float], sides: str, panel_params: panel_view, - coord: coord, ) -> tuple[float, float]: """ Check the log transforms @@ -70,14 +68,10 @@ def _check_log_scale( calculated. If `None`, the base of the log transform the scale will be used. sides : str, default="bl" - Sides onto which to draw the marks. Any combination - chosen from the characters `btlr`, for *bottom*, *top*, - *left* or *right* side marks. If `coord_flip()` is used, - these are the sides *before* the flip. + Panel sides to mark, using any combination of `b`, `t`, `l`, + and `r`. Resolve any axis flip before calling. panel_params : panel_view `x` and `y` view scale values. - coord : coord - Coordinate (e.g. coord_cartesian) system of the geom. Returns ------- @@ -111,10 +105,6 @@ def get_base(sc, ubase: Optional[float]) -> float: x_scale = panel_params.x.scale y_scale = panel_params.y.scale - if isinstance(coord, coord_flip): - x_scale, y_scale = y_scale, x_scale - base_x, base_y = base_y, base_x - if "t" in sides or "b" in sides: base_x = get_base(x_scale, base) @@ -191,35 +181,31 @@ def draw_panel( "linetype": params["linetype"], } + # `sides` names edges before `coord_flip`. Convert it to the + # displayed panel edges used below. + if isinstance(coord, coord_flip): + sides = sides.translate(str.maketrans("tblr", "rlbt")) + def _draw( - geom: geom, axis: Literal["x", "y"], tick_positions: tuple[AnyArray, AnyArray, AnyArray], ): for position, length in zip(tick_positions, lengths): data = pd.DataFrame({axis: position, **_aesthetics}) params["length"] = length - geom.draw_group(data, panel_params, coord, ax, params) - - if isinstance(coord, coord_flip): - tick_range_x = panel_params.y.range - tick_range_y = panel_params.x.range - else: - tick_range_x = panel_params.x.range - tick_range_y = panel_params.y.range + stroke_rugs(data, panel_params, ax, params, sides) - # these are already flipped iff coord_flip base_x, base_y = self._check_log_scale( - params["base"], sides, panel_params, coord + params["base"], sides, panel_params ) if "b" in sides or "t" in sides: - tick_positions = self._calc_ticks(tick_range_x, base_x) - _draw(self, "x", tick_positions) + tick_positions = self._calc_ticks(panel_params.x.range, base_x) + _draw("x", tick_positions) if "l" in sides or "r" in sides: - tick_positions = self._calc_ticks(tick_range_y, base_y) - _draw(self, "y", tick_positions) + tick_positions = self._calc_ticks(panel_params.y.range, base_y) + _draw("y", tick_positions) class annotation_logticks(annotate): From 013c434575cb9a92195c4d8f36929279b9506f4f Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 14 Aug 2026 15:32:03 +0300 Subject: [PATCH 4/6] fix(annotation_stripes): avoid transforming panel bounds twice Stripe bounds come from panel breaks and ranges, so they already use panel coordinates. Drawing them through the rectangle geom transformed the bounds again and raised an error under non-linear coordinates. Render them directly instead. --- plotnine/geoms/annotation_stripes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plotnine/geoms/annotation_stripes.py b/plotnine/geoms/annotation_stripes.py index e4c8254c76..d20807eb19 100644 --- a/plotnine/geoms/annotation_stripes.py +++ b/plotnine/geoms/annotation_stripes.py @@ -11,7 +11,7 @@ from .annotate import annotate from .geom import geom from .geom_polygon import geom_polygon -from .geom_rect import geom_rect +from .geom_rect import fill_rects if typing.TYPE_CHECKING: from typing import Any, Literal, Sequence @@ -173,7 +173,7 @@ def draw_group( fill[0] = fill[1] fill[-1] = fill[-2] - if direction != "vertical": + if axis != "x": xmin, xmax, ymin, ymax = ymin, ymax, xmin, xmax data = pd.DataFrame( @@ -190,4 +190,4 @@ def draw_group( } ) - return geom_rect.draw_group(data, panel_params, coord, ax, params) + fill_rects(data, ax, params) From efa66bc66ce8af2ad41179e2e4f88fc2af330ae8 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 14 Aug 2026 15:50:44 +0300 Subject: [PATCH 5/6] test(annotations): cover non-linear coordinate rendering Add visual regressions for log ticks and background stripes under `coord_trans`. The tests detect any second transformation of positions derived from panel ranges. --- .../annotation_logticks_coord_trans.png | Bin 0 -> 3125 bytes .../annotation_stripes_coord_trans.png | Bin 0 -> 2722 bytes tests/test_annotation_logticks.py | 15 +++++++++++++++ tests/test_annotation_stripes.py | 13 +++++++++++++ 4 files changed, 28 insertions(+) create mode 100644 tests/baseline_images/test_annotation_logticks/annotation_logticks_coord_trans.png create mode 100644 tests/baseline_images/test_annotation_stripes/annotation_stripes_coord_trans.png diff --git a/tests/baseline_images/test_annotation_logticks/annotation_logticks_coord_trans.png b/tests/baseline_images/test_annotation_logticks/annotation_logticks_coord_trans.png new file mode 100644 index 0000000000000000000000000000000000000000..2a6ef67256b92bed358a1a5dc6873547f3bb0595 GIT binary patch literal 3125 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 literal 0 HcmV?d00001 diff --git a/tests/baseline_images/test_annotation_stripes/annotation_stripes_coord_trans.png b/tests/baseline_images/test_annotation_stripes/annotation_stripes_coord_trans.png new file mode 100644 index 0000000000000000000000000000000000000000..e5d77df8f5ca818790eb718bd25dc6ff305748b2 GIT binary patch literal 2722 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 BMkD|L literal 0 HcmV?d00001 diff --git a/tests/test_annotation_logticks.py b/tests/test_annotation_logticks.py index 632ea8977d..f36ba45f26 100644 --- a/tests/test_annotation_logticks.py +++ b/tests/test_annotation_logticks.py @@ -7,6 +7,7 @@ aes, annotation_logticks, coord_flip, + coord_trans, element_line, facet_wrap, geom_point, @@ -37,6 +38,20 @@ def test_annotation_logticks(): assert p == "annotation_logticks" +def test_annotation_logticks_coord_trans(): + # Major grid lines and long log ticks must coincide. + p = ( + ggplot(data, aes("x", "x")) + + annotation_logticks(sides="b", size=0.75) + + geom_point() + + scale_x_continuous(breaks=[1, 10, 100, 1000]) + + coord_trans(x="log10") + + theme(panel_grid_major=element_line(color="red")) + ) + with pytest.warns(PlotnineWarning): + assert p == "annotation_logticks_coord_trans" + + def test_annotation_logticks_faceting(): n = len(data) data2 = pd.DataFrame( diff --git a/tests/test_annotation_stripes.py b/tests/test_annotation_stripes.py index c3afae15bd..ccdb510083 100644 --- a/tests/test_annotation_stripes.py +++ b/tests/test_annotation_stripes.py @@ -6,6 +6,7 @@ aes, annotation_stripes, coord_flip, + coord_trans, facet_wrap, geom_point, geom_vline, @@ -122,3 +123,15 @@ def test_annotation_stripes_single_stripe(): ) assert p == "annotation_stripes_single_stripe" + + +def test_annotation_stripes_coord_trans(): + data2 = data.assign(y=10.0 ** (data["y"] % 3)) + p = ( + ggplot(data2) + + annotation_stripes(fill_range="no") + + geom_point(aes("factor(x)", "y")) + + coord_trans(y="log10") + ) + + assert p == "annotation_stripes_coord_trans" From 6b5494bb93d7ca52acc3372579ddf94c2e5bd96a Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 14 Aug 2026 16:01:26 +0300 Subject: [PATCH 6/6] refactor(annotations): pass rug-mark lengths directly Pass each rug-mark length directly to the renderer instead of storing it in the geom parameters between draws. Document that log-tick sides are interpreted before `coord_flip`, and record the fixes for log ticks and stripes under non-linear coordinates. --- doc/changelog.qmd | 6 ++++++ plotnine/geoms/annotation_logticks.py | 13 ++++++------- plotnine/geoms/geom_rug.py | 9 ++++++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/doc/changelog.qmd b/doc/changelog.qmd index 2e70d27599..b17fb867d1 100644 --- a/doc/changelog.qmd +++ b/doc/changelog.qmd @@ -139,6 +139,12 @@ title: Changelog `upper`, `lower` and `both` outlines follow the transformed band edges, and `full` outlines no longer raise an error. +- [](:class:`~plotnine.annotation_logticks`) and + [](:class:`~plotnine.annotation_stripes`) now render correctly in non-linear + coordinate systems such as [](:class:`~plotnine.coord_trans`). Previously, + log tick positions were transformed twice, which misplaced or removed ticks, + and stripes raised 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. diff --git a/plotnine/geoms/annotation_logticks.py b/plotnine/geoms/annotation_logticks.py index f148e82722..4d68bd8f83 100644 --- a/plotnine/geoms/annotation_logticks.py +++ b/plotnine/geoms/annotation_logticks.py @@ -63,14 +63,14 @@ def _check_log_scale( Parameters ---------- - base : float | None + base : Base of the logarithm in which the ticks will be calculated. If `None`, the base of the log transform the scale will be used. - sides : str, default="bl" + sides : Panel sides to mark, using any combination of `b`, `t`, `l`, and `r`. Resolve any axis flip before calling. - panel_params : panel_view + panel_params : `x` and `y` view scale values. Returns @@ -192,8 +192,7 @@ def _draw( ): for position, length in zip(tick_positions, lengths): data = pd.DataFrame({axis: position, **_aesthetics}) - params["length"] = length - stroke_rugs(data, panel_params, ax, params, sides) + stroke_rugs(data, panel_params, ax, params, sides, length) base_x, base_y = self._check_log_scale( params["base"], sides, panel_params @@ -220,8 +219,8 @@ class annotation_logticks(annotate): sides : Sides onto which to draw the marks. Any combination chosen from the characters `btlr`, for *bottom*, *top*, - *left* or *right* side marks. If `coord_flip()` is used, - these are the sides *after* the flip. + *left* or *right* side marks. With `coord_flip()`, specify + sides before the flip. alpha : Transparency of the ticks color : diff --git a/plotnine/geoms/geom_rug.py b/plotnine/geoms/geom_rug.py index 747b4ac887..808338f042 100644 --- a/plotnine/geoms/geom_rug.py +++ b/plotnine/geoms/geom_rug.py @@ -65,7 +65,7 @@ def draw_group( if isinstance(coord, coord_flip): sides = sides.translate(str.maketrans("tblr", "rlbt")) - stroke_rugs(data, panel_params, ax, params, sides) + stroke_rugs(data, panel_params, ax, params, sides, params["length"]) def stroke_rugs( @@ -74,6 +74,7 @@ def stroke_rugs( ax: Axes, params: dict[str, Any], sides: str, + length: float, ) -> None: """ Draw rug marks in panel coordinates @@ -92,6 +93,8 @@ def stroke_rugs( sides : Panel sides to mark, using any combination of `b`, `t`, `l`, and `r`. Resolve any axis flip before calling. + length : + Length of each mark as a fraction of the panel width or height. """ from matplotlib.collections import LineCollection @@ -107,8 +110,8 @@ def stroke_rugs( rugs = [] xmin, xmax = panel_params.x.range ymin, ymax = panel_params.y.range - xheight = (xmax - xmin) * params["length"] - yheight = (ymax - ymin) * params["length"] + xheight = (xmax - xmin) * length + yheight = (ymax - ymin) * length if has_x: x = cast("FloatArray", np.repeat(data["x"].to_numpy(), 2))