Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2970b4f
Add inset_element class and ggplot._insets attribute
has2k1 Apr 19, 2026
241ff8e
Draw insets into the host figure
has2k1 Apr 29, 2026
2ece891
Arrange insets within the host figure
has2k1 Apr 29, 2026
e9b7cbc
Set axes zorder at creation
has2k1 Apr 29, 2026
2d15fcd
Stack inset figure-level artists above the host
has2k1 Apr 29, 2026
e1c0bda
Use Insets container class for ggplot._insets
has2k1 May 4, 2026
59a341b
Clarify align_to region descriptions on inset_element
has2k1 May 4, 2026
208068e
Give each inset its own zorder band
has2k1 May 7, 2026
9dc3152
Inherit figure_size/dpi onto insets and Compose items
has2k1 May 7, 2026
7ef818b
Add on_top parameter to inset_element
has2k1 May 8, 2026
274ce91
Bind theme to its owner
has2k1 May 8, 2026
cb36e01
Introduce p9Figure with dormant zorder stamping
has2k1 May 9, 2026
bb41610
Split Insets.draw into "above" and "below" bands
has2k1 May 9, 2026
7048409
Reorder ggplot.draw() to semantic paint order
has2k1 May 9, 2026
26ef177
Reorder Compose.draw to semantic paint order
has2k1 May 9, 2026
772826d
Replace flat-zorder bookkeeping with insertion-order stamping
has2k1 May 9, 2026
9d9ddce
Make inset_element renderable on its own
has2k1 May 11, 2026
424c93f
Validate GridSpecParams on construction
has2k1 May 11, 2026
8d3d3ec
Add inset images
has2k1 May 11, 2026
8f6bfb5
Add lines, areas, texts and tiles to yippie._Geom
has2k1 May 13, 2026
0d85e56
Broadcast `&` / `*` from a host plot into its insets
has2k1 May 13, 2026
d071c64
Anchor parameter for image insets
has2k1 May 14, 2026
b290bcd
Background wraps the user bbox for image insets
has2k1 May 14, 2026
a66d74a
Add tests for inset_element
has2k1 May 13, 2026
5ec9cfb
Add inset_element to API reference
has2k1 May 14, 2026
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
1 change: 1 addition & 0 deletions doc/_quartodoc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,7 @@ quartodoc:
- plot_annotation
- plot_spacer
- plot_layout
- inset_element

- title: Options
desc: |
Expand Down
3 changes: 3 additions & 0 deletions doc/changelog.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ title: Changelog
theme(plot_footer_line=element_line(color="black"))
```

- Added [](:class:`~plotnine.composition.inset_element`) with which you can insert plot
compositions or images into another plot.

### API Changes

- Removed `geom.to_layer()`, `stat.to_layer()`, `annotate.to_layer()`,
Expand Down
48 changes: 48 additions & 0 deletions plotnine/_mpl/figure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import annotations

from typing import TypeVar

from matplotlib.artist import Artist
from matplotlib.figure import Figure

TArtist = TypeVar("TArtist", bound=Artist)


class p9Figure(Figure):
"""
Figure for plotnine plots

Stamps figure-level artists (added through the public methods
``add_artist``, ``add_subplot``, ``add_axes``, ``figimage``,
``text``) with strictly increasing zorders, so insertion order
dictates paint order — matplotlib stable-sorts by zorder before
rendering.
"""

_next_zorder: int

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# figure.patch sits at zorder 1; start above it.
self._next_zorder = 2

def _stamp(self, artist: TArtist) -> TArtist:
artist.set_zorder(self._next_zorder)
self._next_zorder += 1
return artist

def add_artist(self, artist: TArtist, *args, **kwargs) -> TArtist:
super().add_artist(artist, *args, **kwargs)
return self._stamp(artist)

def add_subplot(self, *args, **kwargs):
return self._stamp(super().add_subplot(*args, **kwargs))

def add_axes(self, *args, **kwargs):
return self._stamp(super().add_axes(*args, **kwargs))

def figimage(self, *args, **kwargs):
return self._stamp(super().figimage(*args, **kwargs))

def text(self, *args, **kwargs):
return self._stamp(super().text(*args, **kwargs))
1 change: 0 additions & 1 deletion plotnine/_mpl/layout_manager/_composition_side_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,6 @@ def resize_gridspec(self):
sized to accomodate the content of the annotations.
"""
gsparams = self.calculate_gridspec_params()
gsparams.validate()
self.sub_gridspec.update_params_and_artists(gsparams)

def calculate_gridspec_params(self) -> GridSpecParams:
Expand Down
86 changes: 68 additions & 18 deletions plotnine/_mpl/layout_manager/_plot_side_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from __future__ import annotations

from copy import copy
from dataclasses import replace
from functools import cached_property
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -772,6 +772,59 @@ def arrange(self):
"""
self.resize_gridspec()
self.items._move_artists(self)
self._arrange_insets()

def _arrange_insets(self):
"""
Position and arrange every inset attached to this plot

The host's panel/plot/full region is now finalised, so the
inset's fractional bounding box is scaled into figure
coordinates. For ggplot / Compose insets the bbox drives the
inset's gridspec and its side-space layout runs to position
its content. For image insets the adapter's `_arrange_in_box`
does the aspect-fit math directly — no gridspec or side-space
work needed.
"""
from plotnine import ggplot
from plotnine.composition import Compose
from plotnine.composition._inset_image import _InsetImage

from ._composition_side_space import CompositionSideSpaces

for inset in self.plot._insets:
if inset.align_to == "panel":
(x1, y1), (x2, y2) = self.panel_area_coordinates
elif inset.align_to == "plot":
(x1, y1), (x2, y2) = self.plot_area_coordinates
else: # "full"
# Note that this isn't necessarily the figure's coordinates,
# rather the entire ggplot area.
bbox = self.plot._gridspec.bbox_relative
(x1, y1), (x2, y2) = (bbox.x0, bbox.y0), (bbox.x1, bbox.y1)

left = x1 + inset.left * (x2 - x1)
bottom = y1 + inset.bottom * (y2 - y1)
right = x1 + inset.right * (x2 - x1)
top = y1 + inset.top * (y2 - y1)

if isinstance(inset.obj, (ggplot, Compose)):
params = GridSpecParams(
left=left,
bottom=bottom,
right=right,
top=top,
wspace=0,
hspace=0,
)
inset.obj._gridspec.update_params_and_artists(params)
if isinstance(inset.obj, ggplot):
inset.obj._sidespaces = PlotSideSpaces(inset.obj)
else:
inset.obj._sidespaces = CompositionSideSpaces(inset.obj)
inset.obj._sidespaces.arrange()
elif isinstance(inset.obj, _InsetImage):
inset.obj._arrange_in_box(left, bottom, right, top)

def resize_gridspec(self):
"""
Expand All @@ -781,7 +834,6 @@ def resize_gridspec(self):
sized to accomodate the artists around the panels.
"""
gsparams = self.calculate_gridspec_params()
gsparams.validate()
self.sub_gridspec.update_params_and_artists(gsparams)

def calculate_gridspec_params(self) -> GridSpecParams:
Expand Down Expand Up @@ -1031,43 +1083,41 @@ def _reduce_height(self, gsparams: GridSpecParams, ratio: float):
"""
Reduce the height of axes to get the aspect ratio
"""
gsparams = copy(gsparams)

# New height w.r.t figure height
h1 = ratio * self.w * (self.W / self.H)

# Half of the total vertical reduction w.r.t figure height
dh = (self.h - h1) * self.plot.facet.nrow / 2

# Reduce plot area height
gsparams.top -= dh
gsparams.bottom += dh
gsparams.hspace = self.sh / h1

# Add more vertical plot margin
self.increase_vertical_plot_margin(dh)
return gsparams

return replace(
gsparams,
top=gsparams.top - dh,
bottom=gsparams.bottom + dh,
hspace=self.sh / h1,
)

def _reduce_width(self, gsparams: GridSpecParams, ratio: float):
"""
Reduce the width of axes to get the aspect ratio
"""
gsparams = copy(gsparams)

# New width w.r.t figure width
w1 = (self.h * self.H) / (ratio * self.W)

# Half of the total horizontal reduction w.r.t figure width
dw = (self.w - w1) * self.plot.facet.ncol / 2

# Reduce width
gsparams.left += dw
gsparams.right -= dw
gsparams.wspace = self.sw / w1

# Add more horizontal margin
self.increase_horizontal_plot_margin(dw)
return gsparams

return replace(
gsparams,
left=gsparams.left + dw,
right=gsparams.right - dw,
wspace=self.sw / w1,
)

@property
def aspect_ratio(self) -> float:
Expand Down
7 changes: 5 additions & 2 deletions plotnine/_mpl/layout_manager/_side_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,12 @@ class GridSpecParams:
wspace: float
hspace: float

def validate(self):
def __post_init__(self):
self._validate()

def _validate(self):
"""
Return True if the params will create a non-empty area
Raise if the params do not enclose a positive-area rectangle
"""
if not (self.top - self.bottom > 0 and self.right - self.left > 0):
raise GridSpecParamsError(
Expand Down
24 changes: 24 additions & 0 deletions plotnine/_utils/yippie.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
element_blank,
element_rect,
element_text,
geom_area,
geom_col,
geom_line,
geom_point,
geom_text,
geom_tile,
ggplot,
labs,
theme,
Expand Down Expand Up @@ -69,6 +73,26 @@ def points(self):
def cols(self):
return geom_col(aes("cat", "value", fill="cat"), self.data)

@property
def lines(self):
return geom_line(
aes("cat", "value", color="cat2", group="cat2"), self.data
)

@property
def areas(self):
return geom_area(
aes("cat", "value", fill="cat2", group="cat2"), self.data
)

@property
def texts(self):
return geom_text(aes("cat", "value", label="cat"), self.data, size=16)

@property
def tiles(self):
return geom_tile(aes("cat", "cat2", fill="value"), self.data)


class _Legend:
"""
Expand Down
4 changes: 3 additions & 1 deletion plotnine/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,9 @@ def check_scale_limits(scales: list[scale], frame_no: int):
artists.append(get_frame_artists(axs))

if figure is None:
figure = plt.figure()
from plotnine._mpl.figure import p9Figure

figure = plt.figure(FigureClass=p9Figure)

# Prevent Jupyter from plotting any static figure
plt.close(figure)
Expand Down
2 changes: 2 additions & 0 deletions plotnine/composition/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from ._beside import Beside
from ._compose import Compose
from ._inset_element import inset_element
from ._plot_annotation import plot_annotation
from ._plot_layout import plot_layout
from ._plot_spacer import plot_spacer
Expand All @@ -11,6 +12,7 @@
"Stack",
"Beside",
"Wrap",
"inset_element",
"plot_annotation",
"plot_layout",
"plot_spacer",
Expand Down
Loading
Loading