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
6 changes: 6 additions & 0 deletions doc/changelog.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ title: Changelog
rectangle is now drawn as its own polygon instead of merging a bar's
segments into a single path.

- In a non-linear coordinate system (e.g. [](:class:`~plotnine.coord_trans`)),
both edges of a [](:class:`~plotnine.geom_ribbon`) or
[](:class:`~plotnine.geom_area`) now curve. Previously each edge held the
value of the point its segment started from, so the band was drawn as a
staircase.

- 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
42 changes: 23 additions & 19 deletions plotnine/coords/coord.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from .._utils import OPPOSITE_SIDE
from ..iapi import panel_ranges
from ..mapping.aes import POSITION_AESTHETICS

if typing.TYPE_CHECKING:
from typing import Any, Sequence
Expand Down Expand Up @@ -408,39 +409,42 @@ def interp(start: int, end: int, n: int) -> FloatArray:

def munch_data(data: pd.DataFrame, dist: FloatArray) -> pd.DataFrame:
"""
Breakup path into small segments
Subdivide path segments and interpolate their position aesthetics
"""
x, y = data["x"], data["y"]
segment_length = 0.01

# How many endpoints for each old segment,
# not counting the last one
# Count new points per segment, excluding the final endpoint.
dist[np.isnan(dist)] = 1
extra = np.maximum(np.floor(dist / segment_length), 1)
extra = extra.astype(int)

# Generate extra pieces for x and y values
# The final point must be manually inserted at the end
x = [interp(start, end, n) for start, end, n in zip(x[:-1], x[1:], extra)]
y = [interp(start, end, n) for start, end, n in zip(y[:-1], y[1:], extra)]
x.append(data["x"].iloc[-1])
y.append(data["y"].iloc[-1])
x = np.hstack(x)
y = np.hstack(y)

# Replicate other aesthetics: defined by start point
# but also must include final point
# Every position aesthetic defines path geometry. Replicating `ymin` and
# `ymax`, for example, would turn curved ribbon edges into steps.
position_columns = [c for c in data.columns if c in POSITION_AESTHETICS]

# Append the final endpoint after interpolating each segment.
interpolated = {}
for col in position_columns:
values = data[col].to_numpy()
pieces = [
interp(start, end, n)
for start, end, n in zip(values[:-1], values[1:], extra)
]
pieces.append(values[-1:])
interpolated[col] = np.hstack(pieces)

# Hold non-position aesthetics at each segment's starting value, then
# append the final observation.
idx = np.hstack(
[
np.repeat(data.index[:-1], extra),
len(data) - 1,
# data.index[-1] # TODO: Maybe not
]
)

munched = data.loc[idx, list(data.columns.difference(["x", "y"]))]
munched["x"] = x
munched["y"] = y
munched = data.loc[idx, list(data.columns.difference(position_columns))]
for col, values in interpolated.items():
munched[col] = values
munched.reset_index(drop=True, inplace=True)

return munched
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions tests/test_coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
geom_line,
geom_point,
geom_polygon,
geom_ribbon,
ggplot,
xlim,
)
from plotnine.coords.coord import munch_data
from plotnine.data import mtcars

n = 10 # Some even number greater than 2
Expand Down Expand Up @@ -92,6 +94,39 @@ def test_coord_trans_munches_polygon_closing_edge():
assert p == "coord_trans_munches_polygon_closing_edge"


def test_munch_interpolates_every_position_aesthetic():
# Ribbon edges use `ymin` and `ymax` as path coordinates, so both must
# vary within each munched segment.
data = pd.DataFrame(
{
"x": [0.0, 1.0],
"y": [3.0, 8.0],
"ymin": [1.0, 6.0],
"ymax": [5.0, 10.0],
"group": [1, 1],
}
)

munched = munch_data(data, np.array([1.0]))

assert len(munched) > len(data)
for column in ("x", "y", "ymin", "ymax"):
values = munched[column].to_numpy()
assert np.all(np.diff(values) > 0), f"{column} was not interpolated"


def test_coord_trans_ribbon_edges_curve():
# A smooth transformed ribbon exposes piecewise-constant `ymin` and
# `ymax` values as stepped edges.
data = pd.DataFrame({"x": range(6), "y": [3.0, 8, 5, 9, 4, 7]})
p = (
ggplot(data, aes("x", ymin="y - 2", ymax="y + 2"))
+ geom_ribbon(alpha=0.5)
+ coord_trans(y="sqrt")
)
assert p == "coord_trans_ribbon_edges_curve"


def test_coord_trans_stacked_bars_have_no_spikes():
# Each stacked segment must be its own polygon. If they merge into one
# path, the join between consecutive segments becomes a diagonal that
Expand Down
Loading