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 @@ -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.
Expand Down
53 changes: 19 additions & 34 deletions plotnine/geoms/annotation_logticks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -58,26 +57,21 @@ def _check_log_scale(
base: Optional[float],
sides: str,
panel_params: panel_view,
coord: coord,
) -> tuple[float, float]:
"""
Check the log transforms

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 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_params : panel_view
sides :
Panel sides to mark, using any combination of `b`, `t`, `l`,
and `r`. Resolve any axis flip before calling.
panel_params :
`x` and `y` view scale values.
coord : coord
Coordinate (e.g. coord_cartesian) system of the geom.

Returns
-------
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -191,35 +181,30 @@ 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, length)

# 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):
Expand All @@ -234,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 :
Expand Down
6 changes: 3 additions & 3 deletions plotnine/geoms/annotation_stripes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -190,4 +190,4 @@ def draw_group(
}
)

return geom_rect.draw_group(data, panel_params, coord, ax, params)
fill_rects(data, ax, params)
61 changes: 41 additions & 20 deletions plotnine/geoms/geom_rect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
135 changes: 82 additions & 53 deletions plotnine/geoms/geom_rug.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,62 +57,91 @@ 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, params["length"])


def stroke_rugs(
data: pd.DataFrame,
panel_params: panel_view,
ax: Axes,
params: dict[str, Any],
sides: str,
length: float,
) -> 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.
length :
Length of each mark as a fraction of the panel width or height.
"""
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) * length
yheight = (ymax - ymin) * 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)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading