Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/changelog.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
55 changes: 44 additions & 11 deletions plotnine/geoms/geom_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -451,6 +441,49 @@ 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.
The function adds a `linewidth` column in place so subsequent
arrowheads use the same width.
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
Expand Down
43 changes: 16 additions & 27 deletions plotnine/geoms/geom_ribbon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -154,12 +153,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],
Expand All @@ -169,25 +167,16 @@ 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"

# Each call receives one ribbon group, so an outline forms one path
# with constant aesthetics.
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)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions tests/test_geom_ribbon_area.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
aes,
after_stat,
coord_flip,
coord_trans,
facet_wrap,
geom_area,
geom_line,
Expand Down Expand Up @@ -159,3 +160,8 @@ def test_ribbon_outline_type(self):

def test_ribbon_outline_type_coord_flip(self):
assert self.p + coord_flip() == "ribbon_outline_type_coord_flip"

def test_ribbon_outline_type_coord_trans(self):
assert (
self.p + coord_trans(y="sqrt") == "ribbon_outline_type_coord_trans"
)
Loading