From 65ef6d26918fca77a3badab24d70ec8dcd8b6658 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 26 Sep 2025 15:52:11 -0700 Subject: [PATCH 01/24] Add plot_annotation class --- plotnine/_utils/dataclasses.py | 24 ++++++ plotnine/composition/__init__.py | 2 + plotnine/composition/_compose.py | 26 ++++++ plotnine/composition/_plot_annotation.py | 101 +++++++++++++++++++++++ 4 files changed, 153 insertions(+) create mode 100644 plotnine/_utils/dataclasses.py create mode 100644 plotnine/composition/_plot_annotation.py diff --git a/plotnine/_utils/dataclasses.py b/plotnine/_utils/dataclasses.py new file mode 100644 index 0000000000..57726c1219 --- /dev/null +++ b/plotnine/_utils/dataclasses.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from dataclasses import fields +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Any, Iterable + + +def non_none_init_items(obj) -> Iterable[tuple[str, Any]]: + """ + Yield (name, value) pairs of dataclass fields of `obj` that: + + 1. Have `init=True` in their definition + 2. Have a value that is not `None` + + This function is shallow and does not recursively yield nested + dataclasses. + """ + return ( + (f.name, value) + for f in fields(obj) + if f.init and (value := getattr(obj, f.name)) is not None + ) diff --git a/plotnine/composition/__init__.py b/plotnine/composition/__init__.py index a613f60eab..602b1e24c9 100644 --- a/plotnine/composition/__init__.py +++ b/plotnine/composition/__init__.py @@ -1,5 +1,6 @@ from ._beside import Beside from ._compose import Compose +from ._plot_annotation import plot_annotation from ._plot_layout import plot_layout from ._plot_spacer import plot_spacer from ._stack import Stack @@ -10,6 +11,7 @@ "Stack", "Beside", "Wrap", + "plot_annotation", "plot_layout", "plot_spacer", ) diff --git a/plotnine/composition/_compose.py b/plotnine/composition/_compose.py index 63defc0665..48cdfb2bc9 100644 --- a/plotnine/composition/_compose.py +++ b/plotnine/composition/_compose.py @@ -13,6 +13,7 @@ is_inline_backend, ) from .._utils.quarto import is_knitr_engine, is_quarto_environment +from ..composition._plot_annotation import plot_annotation from ..composition._plot_layout import plot_layout from ..composition._types import ComposeAddable from ..options import get_option @@ -105,6 +106,10 @@ class Compose: is drawn, or they are overwritten by a layout added by the user. """ + _annotation: plot_annotation = field( + init=False, repr=False, default_factory=plot_annotation + ) + # These are created in the _create_figure method figure: Figure = field(init=False, repr=False) plotspecs: list[plotspec] = field(init=False, repr=False) @@ -150,6 +155,21 @@ def layout(self, value: plot_layout): self._layout = copy(self.layout) self._layout.update(value) + @property + def annotation(self) -> plot_annotation: + """ + The plot_annotation of this composition + """ + return self._annotation + + @annotation.setter + def annotation(self, value: plot_annotation): + """ + Add (or merge) a plot_annotation to this composition + """ + self._annotation = copy(self.annotation) + self._annotation.update(value) + @property def nrow(self) -> int: return cast("int", self.layout.nrow) @@ -225,8 +245,13 @@ def __and__(self, rhs: PlotAddable) -> Compose: rhs: What to add. """ + from plotnine import theme + self = deepcopy(self) + if isinstance(rhs, theme): + self.annotation.theme = self.annotation.theme + rhs + for i, item in enumerate(self): if isinstance(item, Compose): self[i] = item & rhs @@ -466,6 +491,7 @@ def draw(self, *, show: bool = False) -> Figure: with plot_composition_context(self, show): figure = self._setup() self._draw_plots() + self.annotation.draw() figure.set_layout_engine(PlotnineCompositionLayoutEngine(self)) return figure diff --git a/plotnine/composition/_plot_annotation.py b/plotnine/composition/_plot_annotation.py new file mode 100644 index 0000000000..f44a5b46df --- /dev/null +++ b/plotnine/composition/_plot_annotation.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +from copy import copy +from dataclasses import dataclass, field +from typing import TYPE_CHECKING + +from plotnine.themes.targets import ThemeTargets + +from .. import theme +from .._utils.dataclasses import non_none_init_items +from ..composition._types import ComposeAddable + +if TYPE_CHECKING: + from ._compose import Compose + + +@dataclass(kw_only=True) +class plot_annotation(ComposeAddable): + """ + Annotate a composition + """ + + title: str | None = None + """ + The title of the composition + """ + + subtitle: str | None = None + """ + The subtitle of the composition + """ + + caption: str | None = None + """ + The caption of the composition + """ + + theme: theme = field(default_factory=theme) # pyright: ignore[reportUnboundVariable] + """ + Theme to use for the plot title, subtitle, caption, margin and background + """ + + def __radd__(self, cmp: Compose) -> Compose: + """ + Add plot annotation to composition + """ + cmp.annotation = self + return cmp + + def update(self, other: plot_annotation): + """ + Update this annotation with the contents of other + """ + for name, value in non_none_init_items(other): + if name == "theme": + self.theme = self.theme + value + else: + setattr(self, name, value) + + def _setup(self, cmp: Compose): + """ + Setup annotation + """ + # We mimick theme.setup instead of call it because + # we cannot have have a ggplot object. + self.theme = copy(self.theme) + self.theme.figure = cmp.figure + self.theme.targets = ThemeTargets() + self.theme._add_default_themeable_properties() + self.theme.T.setup(self.theme) + + def empty(self) -> bool: + """ + Whether the annotation has any content + """ + for name, value in non_none_init_items(self): + if name == "theme": + return len(value.themeables) == 0 + else: + return False + + return True + + def draw(self): + """ + Render the items in the annotation + """ + if self.empty(): + return + + figure = self.theme.figure + targets = self.theme.targets + + if self.title: + targets.plot_title = figure.text(0, 0, self.title) + + if subtitle := self.subtitle: + targets.plot_subtitle = figure.text(0, 0, subtitle) + + if caption := self.caption: + targets.plot_caption = figure.text(0, 0, caption) From 3acb97c7493c8a02c721e8e420f157cbd544f820 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Fri, 26 Sep 2025 21:49:17 -0700 Subject: [PATCH 02/24] Rename helper class Calc to ArtistGeometry --- plotnine/_mpl/layout_manager/_layout_items.py | 181 +++--------------- plotnine/_mpl/layout_manager/_spaces.py | 28 +-- plotnine/_mpl/utils.py | 125 +++++++++++- 3 files changed, 165 insertions(+), 169 deletions(-) diff --git a/plotnine/_mpl/layout_manager/_layout_items.py b/plotnine/_mpl/layout_manager/_layout_items.py index 9ba0272a99..0389a36f02 100644 --- a/plotnine/_mpl/layout_manager/_layout_items.py +++ b/plotnine/_mpl/layout_manager/_layout_items.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from itertools import chain -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING from matplotlib.text import Text @@ -11,10 +11,9 @@ from plotnine.exceptions import PlotnineError from ..utils import ( - bbox_in_figure_space, + ArtistGeometry, get_subplotspecs, rel_position, - tight_bbox_in_figure_space, ) if TYPE_CHECKING: @@ -22,15 +21,12 @@ Any, Iterator, Literal, - Sequence, TypeAlias, ) - from matplotlib.artist import Artist from matplotlib.axes import Axes from matplotlib.axis import Tick - from matplotlib.backend_bases import RendererBase - from matplotlib.transforms import Bbox, Transform + from matplotlib.transforms import Transform from plotnine import ggplot from plotnine._mpl.offsetbox import FlexibleAnchoredOffsetbox @@ -64,131 +60,6 @@ ) -@dataclass -class Calc: - """ - Calculate space taken up by an artist - """ - - # fig: Figure - # renderer: RendererBase - plot: ggplot - - def __post_init__(self): - self.figure = self.plot.figure - self.renderer = cast("RendererBase", self.plot.figure._get_renderer()) # pyright: ignore - - def bbox(self, artist: Artist) -> Bbox: - """ - Bounding box of artist in figure coordinates - """ - return bbox_in_figure_space(artist, self.figure, self.renderer) - - def tight_bbox(self, artist: Artist) -> Bbox: - """ - Bounding box of artist and its children in figure coordinates - """ - return tight_bbox_in_figure_space(artist, self.figure, self.renderer) - - def width(self, artist: Artist) -> float: - """ - Width of artist in figure space - """ - return self.bbox(artist).width - - def tight_width(self, artist: Artist) -> float: - """ - Width of artist and its children in figure space - """ - return self.tight_bbox(artist).width - - def height(self, artist: Artist) -> float: - """ - Height of artist in figure space - """ - return self.bbox(artist).height - - def tight_height(self, artist: Artist) -> float: - """ - Height of artist and its children in figure space - """ - return self.tight_bbox(artist).height - - def size(self, artist: Artist) -> tuple[float, float]: - """ - (width, height) of artist in figure space - """ - bbox = self.bbox(artist) - return (bbox.width, bbox.height) - - def tight_size(self, artist: Artist) -> tuple[float, float]: - """ - (width, height) of artist and its children in figure space - """ - bbox = self.tight_bbox(artist) - return (bbox.width, bbox.height) - - def left_x(self, artist: Artist) -> float: - """ - x value of the left edge of the artist - - --- - x | - --- - """ - return self.bbox(artist).min[0] - - def right_x(self, artist: Artist) -> float: - """ - x value of the left edge of the artist - - --- - | x - --- - """ - return self.bbox(artist).max[0] - - def top_y(self, artist: Artist) -> float: - """ - y value of the top edge of the artist - - -y- - | | - --- - """ - return self.bbox(artist).max[1] - - def bottom_y(self, artist: Artist) -> float: - """ - y value of the bottom edge of the artist - - --- - | | - -y- - """ - return self.bbox(artist).min[1] - - def max_width(self, artists: Sequence[Artist]) -> float: - """ - Return the maximum width of list of artists - """ - widths = [ - bbox_in_figure_space(a, self.figure, self.renderer).width - for a in artists - ] - return max(widths) if len(widths) else 0 - - def max_height(self, artists: Sequence[Artist]) -> float: - """ - Return the maximum height of list of artists - """ - heights = [ - bbox_in_figure_space(a, self.figure, self.renderer).height - for a in artists - ] - return max(heights) if len(heights) else 0 - - @dataclass class LayoutItems: """ @@ -210,7 +81,7 @@ def get(name: str) -> Any: return None return t - self.calc = Calc(self.plot) + self.geometry = ArtistGeometry(self.plot.figure) self.axis_title_x: Text | None = get("axis_title_x") self.axis_title_y: Text | None = get("axis_title_y") @@ -326,7 +197,7 @@ def strip_text_x_extra_height(self, position: StripPosition) -> float: if isinstance(a, StripTextPatch) else a.draw_info ) - h = self.calc.height(a) + h = self.geometry.height(a) heights.append(max(h + h * info.strip_align, 0)) return max(heights) @@ -352,7 +223,7 @@ def strip_text_y_extra_width(self, position: StripPosition) -> float: if isinstance(a, StripTextPatch) else a.draw_info ) - w = self.calc.width(a) + w = self.geometry.width(a) widths.append(max(w + w * info.strip_align, 0)) return max(widths) @@ -362,7 +233,7 @@ def axis_ticks_x_max_height_at(self, location: AxesLocation) -> float: Return maximum height[figure space] of x ticks """ heights = [ - self.calc.tight_height(tick.tick1line) + self.geometry.tight_height(tick.tick1line) for ax in self._filter_axes(location) for tick in self.axis_ticks_x(ax) ] @@ -373,7 +244,7 @@ def axis_text_x_max_height(self, ax: Axes) -> float: Return maximum height[figure space] of x tick labels """ heights = [ - self.calc.tight_height(label) for label in self.axis_text_x(ax) + self.geometry.tight_height(label) for label in self.axis_text_x(ax) ] return max(heights) if len(heights) else 0 @@ -392,7 +263,7 @@ def axis_ticks_y_max_width_at(self, location: AxesLocation) -> float: Return maximum width[figure space] of y ticks """ widths = [ - self.calc.tight_width(tick.tick1line) + self.geometry.tight_width(tick.tick1line) for ax in self._filter_axes(location) for tick in self.axis_ticks_y(ax) ] @@ -403,7 +274,7 @@ def axis_text_y_max_width(self, ax: Axes) -> float: Return maximum width[figure space] of y tick labels """ widths = [ - self.calc.tight_width(label) for label in self.axis_text_y(ax) + self.geometry.tight_width(label) for label in self.axis_text_y(ax) ] return max(widths) if len(widths) else 0 @@ -423,9 +294,9 @@ def axis_text_y_top_protrusion(self, location: AxesLocation) -> float: """ extras = [] for ax in self._filter_axes(location): - ax_top_y = self.calc.top_y(ax) + ax_top_y = self.geometry.top_y(ax) for label in self.axis_text_y(ax): - label_top_y = self.calc.top_y(label) + label_top_y = self.geometry.top_y(label) extras.append(max(0, label_top_y - ax_top_y)) return max(extras) if len(extras) else 0 @@ -436,9 +307,9 @@ def axis_text_y_bottom_protrusion(self, location: AxesLocation) -> float: """ extras = [] for ax in self._filter_axes(location): - ax_bottom_y = self.calc.bottom_y(ax) + ax_bottom_y = self.geometry.bottom_y(ax) for label in self.axis_text_y(ax): - label_bottom_y = self.calc.bottom_y(label) + label_bottom_y = self.geometry.bottom_y(label) protrusion = abs(min(label_bottom_y - ax_bottom_y, 0)) extras.append(protrusion) @@ -450,9 +321,9 @@ def axis_text_x_left_protrusion(self, location: AxesLocation) -> float: """ extras = [] for ax in self._filter_axes(location): - ax_left_x = self.calc.left_x(ax) + ax_left_x = self.geometry.left_x(ax) for label in self.axis_text_x(ax): - label_left_x = self.calc.left_x(label) + label_left_x = self.geometry.left_x(label) protrusion = abs(min(label_left_x - ax_left_x, 0)) extras.append(protrusion) @@ -464,9 +335,9 @@ def axis_text_x_right_protrusion(self, location: AxesLocation) -> float: """ extras = [] for ax in self._filter_axes(location): - ax_right_x = self.calc.right_x(ax) + ax_right_x = self.geometry.right_x(ax) for label in self.axis_text_x(ax): - label_right_x = self.calc.right_x(label) + label_right_x = self.geometry.right_x(label) extras.append(max(0, label_right_x - ax_right_x)) return max(extras) if len(extras) else 0 @@ -547,7 +418,7 @@ def to_vertical_axis_dimensions(value: float, ax: Axes) -> float: ) for text in texts: height = to_vertical_axis_dimensions( - self.calc.tight_height(text), ax + self.geometry.tight_height(text), ax ) justify.vertically( text, va, -axis_text_row_height, 0, height=height @@ -600,7 +471,7 @@ def to_horizontal_axis_dimensions(value: float, ax: Axes) -> float: ) for text in texts: width = to_horizontal_axis_dimensions( - self.calc.tight_width(text), ax + self.geometry.tight_width(text), ax ) justify.horizontally( text, ha, -axis_text_col_width, 0, width=width @@ -615,7 +486,9 @@ def _strip_text_x_background_equal_heights(self): if not self.strip_text_x: return - heights = [self.calc.bbox(t.patch).height for t in self.strip_text_x] + heights = [ + self.geometry.bbox(t.patch).height for t in self.strip_text_x + ] max_height = max(heights) relative_heights = [max_height / h for h in heights] for text, scale in zip(self.strip_text_x, relative_heights): @@ -630,7 +503,7 @@ def _strip_text_y_background_equal_widths(self): if not self.strip_text_y: return - widths = [self.calc.bbox(t.patch).width for t in self.strip_text_y] + widths = [self.geometry.bbox(t.patch).width for t in self.strip_text_y] max_width = max(widths) relative_widths = [max_width / w for w in widths] for text, scale in zip(self.strip_text_y, relative_widths): @@ -668,7 +541,7 @@ def horizontally( """ rel = ha_as_float(ha) if width is None: - width = self.spaces.items.calc.width(text) + width = self.spaces.items.geometry.width(text) x = rel_position(rel, width, left, right) text.set_x(x) text.set_horizontalalignment("left") @@ -687,7 +560,7 @@ def vertically( rel = va_as_float(va) if height is None: - height = self.spaces.items.calc.height(text) + height = self.spaces.items.geometry.height(text) y = rel_position(rel, height, bottom, top) text.set_y(y) text.set_verticalalignment("bottom") @@ -866,7 +739,7 @@ def set_plot_tag_position(tag: Text, spaces: LayoutSpaces): # Calculate the position when the tag has no margins rel_x, rel_y = lookup[position] - width, height = spaces.items.calc.size(tag) + width, height = spaces.items.geometry.size(tag) x = rel_position(rel_x, width, x1, x2) y = rel_position(rel_y, height, y1, y2) diff --git a/plotnine/_mpl/layout_manager/_spaces.py b/plotnine/_mpl/layout_manager/_spaces.py index 9fa3f4e618..61645aa4f2 100644 --- a/plotnine/_mpl/layout_manager/_spaces.py +++ b/plotnine/_mpl/layout_manager/_spaces.py @@ -134,7 +134,7 @@ def _legend_size(self) -> tuple[float, float]: return (0, 0) ol: outside_legend = getattr(self.items.legends, self.side) - return self.items.calc.size(ol.box) + return self.items.geometry.size(ol.box) @cached_property def legend_width(self) -> float: @@ -355,7 +355,7 @@ class left_spaces(_side_spaces): def _calculate(self): theme = self.items.plot.theme - calc = self.items.calc + geometry = self.items.geometry items = self.items self.plot_margin = theme.getp("plot_margin_left") @@ -363,7 +363,7 @@ def _calculate(self): if self.has_tag and items.plot_tag: m = theme.get_margin("plot_tag").fig self.plot_tag_margin_left = m.l - self.plot_tag = calc.width(items.plot_tag) + self.plot_tag = geometry.width(items.plot_tag) self.plot_tag_margin_right = m.r if items.legends and items.legends.left: @@ -373,7 +373,7 @@ def _calculate(self): if items.axis_title_y: m = theme.get_margin("axis_title_y").fig self.axis_title_y_margin_left = m.l - self.axis_title_y = calc.width(items.axis_title_y) + self.axis_title_y = geometry.width(items.axis_title_y) self.axis_title_y_margin_right = m.r # Account for the space consumed by the axis @@ -475,14 +475,14 @@ class right_spaces(_side_spaces): def _calculate(self): items = self.items theme = self.items.plot.theme - calc = self.items.calc + geometry = self.items.geometry self.plot_margin = theme.getp("plot_margin_right") if self.has_tag and items.plot_tag: m = theme.get_margin("plot_tag").fig self.plot_tag_margin_right = m.r - self.plot_tag = calc.width(items.plot_tag) + self.plot_tag = geometry.width(items.plot_tag) self.plot_tag_margin_left = m.l if items.legends and items.legends.right: @@ -587,7 +587,7 @@ class top_spaces(_side_spaces): def _calculate(self): items = self.items theme = self.items.plot.theme - calc = self.items.calc + geometry = self.items.geometry W, H = theme.getp("figure_size") F = W / H @@ -596,19 +596,19 @@ def _calculate(self): if self.has_tag and items.plot_tag: m = theme.get_margin("plot_tag").fig self.plot_tag_margin_top = m.t - self.plot_tag = calc.height(items.plot_tag) + self.plot_tag = geometry.height(items.plot_tag) self.plot_tag_margin_bottom = m.b if items.plot_title: m = theme.get_margin("plot_title").fig self.plot_title_margin_top = m.t * F - self.plot_title = calc.height(items.plot_title) + self.plot_title = geometry.height(items.plot_title) self.plot_title_margin_bottom = m.b * F if items.plot_subtitle: m = theme.get_margin("plot_subtitle").fig self.plot_subtitle_margin_top = m.t * F - self.plot_subtitle = calc.height(items.plot_subtitle) + self.plot_subtitle = geometry.height(items.plot_subtitle) self.plot_subtitle_margin_bottom = m.b * F if items.legends and items.legends.top: @@ -728,7 +728,7 @@ class bottom_spaces(_side_spaces): def _calculate(self): items = self.items theme = self.items.plot.theme - calc = self.items.calc + geometry = self.items.geometry W, H = theme.getp("figure_size") F = W / H @@ -737,13 +737,13 @@ def _calculate(self): if self.has_tag and items.plot_tag: m = theme.get_margin("plot_tag").fig self.plot_tag_margin_bottom = m.b - self.plot_tag = calc.height(items.plot_tag) + self.plot_tag = geometry.height(items.plot_tag) self.plot_tag_margin_top = m.t if items.plot_caption: m = theme.get_margin("plot_caption").fig self.plot_caption_margin_bottom = m.b * F - self.plot_caption = calc.height(items.plot_caption) + self.plot_caption = geometry.height(items.plot_caption) self.plot_caption_margin_top = m.t * F if items.legends and items.legends.bottom: @@ -753,7 +753,7 @@ def _calculate(self): if items.axis_title_x: m = theme.get_margin("axis_title_x").fig self.axis_title_x_margin_bottom = m.b * F - self.axis_title_x = calc.height(items.axis_title_x) + self.axis_title_x = geometry.height(items.axis_title_x) self.axis_title_x_margin_top = m.t * F # Account for the space consumed by the axis diff --git a/plotnine/_mpl/utils.py b/plotnine/_mpl/utils.py index af1aaff235..c60684c35b 100644 --- a/plotnine/_mpl/utils.py +++ b/plotnine/_mpl/utils.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from dataclasses import dataclass +from typing import TYPE_CHECKING, Sequence, cast from matplotlib.transforms import Affine2D, Bbox @@ -144,3 +145,125 @@ def draw_bbox(bbox, figure, color="black", **kwargs): **kwargs, ) ) + + +@dataclass +class ArtistGeometry: + """ + Helper to calculate the position & extents (space) of an artist + """ + + figure: Figure + + def __post_init__(self): + self.renderer = cast("RendererBase", self.figure._get_renderer()) # pyright: ignore + + def bbox(self, artist: Artist) -> Bbox: + """ + Bounding box of artist in figure coordinates + """ + return bbox_in_figure_space(artist, self.figure, self.renderer) + + def tight_bbox(self, artist: Artist) -> Bbox: + """ + Bounding box of artist and its children in figure coordinates + """ + return tight_bbox_in_figure_space(artist, self.figure, self.renderer) + + def width(self, artist: Artist) -> float: + """ + Width of artist in figure space + """ + return self.bbox(artist).width + + def tight_width(self, artist: Artist) -> float: + """ + Width of artist and its children in figure space + """ + return self.tight_bbox(artist).width + + def height(self, artist: Artist) -> float: + """ + Height of artist in figure space + """ + return self.bbox(artist).height + + def tight_height(self, artist: Artist) -> float: + """ + Height of artist and its children in figure space + """ + return self.tight_bbox(artist).height + + def size(self, artist: Artist) -> tuple[float, float]: + """ + (width, height) of artist in figure space + """ + bbox = self.bbox(artist) + return (bbox.width, bbox.height) + + def tight_size(self, artist: Artist) -> tuple[float, float]: + """ + (width, height) of artist and its children in figure space + """ + bbox = self.tight_bbox(artist) + return (bbox.width, bbox.height) + + def left_x(self, artist: Artist) -> float: + """ + x value of the left edge of the artist + + --- + x | + --- + """ + return self.bbox(artist).min[0] + + def right_x(self, artist: Artist) -> float: + """ + x value of the left edge of the artist + + --- + | x + --- + """ + return self.bbox(artist).max[0] + + def top_y(self, artist: Artist) -> float: + """ + y value of the top edge of the artist + + -y- + | | + --- + """ + return self.bbox(artist).max[1] + + def bottom_y(self, artist: Artist) -> float: + """ + y value of the bottom edge of the artist + + --- + | | + -y- + """ + return self.bbox(artist).min[1] + + def max_width(self, artists: Sequence[Artist]) -> float: + """ + Return the maximum width of list of artists + """ + widths = [ + bbox_in_figure_space(a, self.figure, self.renderer).width + for a in artists + ] + return max(widths) if len(widths) else 0 + + def max_height(self, artists: Sequence[Artist]) -> float: + """ + Return the maximum height of list of artists + """ + heights = [ + bbox_in_figure_space(a, self.figure, self.renderer).height + for a in artists + ] + return max(heights) if len(heights) else 0 From 24a6f13472e306ba7be3da767080b9764e40ca18 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Sat, 27 Sep 2025 00:05:59 -0700 Subject: [PATCH 03/24] Refactor the setup of compositions and themes --- plotnine/_mpl/layout_manager/_layout_tree.py | 2 +- plotnine/composition/_compose.py | 33 ++++++++++----- plotnine/composition/_plot_annotation.py | 11 +---- plotnine/ggplot.py | 7 +++- plotnine/guides/guide.py | 2 +- plotnine/themes/theme.py | 42 +++++++++----------- 6 files changed, 49 insertions(+), 48 deletions(-) diff --git a/plotnine/_mpl/layout_manager/_layout_tree.py b/plotnine/_mpl/layout_manager/_layout_tree.py index 72ba046788..c5f2f4c7aa 100644 --- a/plotnine/_mpl/layout_manager/_layout_tree.py +++ b/plotnine/_mpl/layout_manager/_layout_tree.py @@ -126,7 +126,7 @@ class LayoutTree: """ def __post_init__(self): - self.gridspec = self.cmp.gridspec + self.gridspec = self.cmp._gridspec self.grid = Grid["Node"]( self.nrow, self.ncol, diff --git a/plotnine/composition/_compose.py b/plotnine/composition/_compose.py index 48cdfb2bc9..21f2a34595 100644 --- a/plotnine/composition/_compose.py +++ b/plotnine/composition/_compose.py @@ -113,7 +113,7 @@ class Compose: # These are created in the _create_figure method figure: Figure = field(init=False, repr=False) plotspecs: list[plotspec] = field(init=False, repr=False) - gridspec: p9GridSpec = field(init=False, repr=False) + _gridspec: p9GridSpec = field(init=False, repr=False) def __post_init__(self): # The way we handle the plots has consequences that would @@ -383,15 +383,18 @@ def _to_retina(self): else: item._to_retina() - def _create_gridspec(self, figure, nest_into): + def _create_gridspec(self, nest_into): """ Create the gridspec for this composition """ from plotnine._mpl.gridspec import p9GridSpec + # NOTE: These two should be in ._setup self.layout._setup(self) - self.gridspec = p9GridSpec.from_layout( - self.layout, figure=figure, nest_into=nest_into + self.annotation._setup(self) + + self._gridspec = p9GridSpec.from_layout( + self.layout, figure=self.figure, nest_into=nest_into ) def _setup(self) -> Figure: @@ -409,6 +412,8 @@ def _create_figure(self): from plotnine import ggplot from plotnine._mpl.gridspec import p9GridSpec + figure = plt.figure() + def _make_plotspecs( cmp: Compose, parent_gridspec: p9GridSpec | None ) -> Generator[plotspec]: @@ -418,7 +423,9 @@ def _make_plotspecs( # This gridspec contains a composition group e.g. # (p2 | p3) of p1 | (p2 | p3) ss_or_none = parent_gridspec[0] if parent_gridspec else None - cmp._create_gridspec(self.figure, ss_or_none) + + cmp.figure = figure + cmp._create_gridspec(ss_or_none) # Each subplot in the composition will contain one of: # 1. A plot @@ -428,22 +435,21 @@ def _make_plotspecs( # "subplot" in the grid. The SubplotSpec is the handle that # allows us to set it up for a plot or to nest another gridspec # in it. - for item, subplot_spec in zip(cmp, cmp.gridspec): + for item, subplot_spec in zip(cmp, cmp._gridspec): if isinstance(item, ggplot): yield plotspec( item, - self.figure, - cmp.gridspec, + figure, + cmp._gridspec, subplot_spec, - p9GridSpec(1, 1, self.figure, nest_into=subplot_spec), + p9GridSpec(1, 1, figure, nest_into=subplot_spec), ) elif item: yield from _make_plotspecs( item, - p9GridSpec(1, 1, self.figure, nest_into=subplot_spec), + p9GridSpec(1, 1, figure, nest_into=subplot_spec), ) - self.figure = plt.figure() self.plotspecs = list(_make_plotspecs(self, None)) def _draw_plots(self): @@ -486,6 +492,11 @@ def draw(self, *, show: bool = False) -> Figure: : Matplotlib figure """ + # NOTE: This method is not recursive though considering the order in + # which the methods are called we may expect it to be. + # The outmost composition has a list (the plotspecs created recursively + # in ._create_figure), so we can draw all the plots. + # Consider a refactoring. from .._mpl.layout_manager import PlotnineCompositionLayoutEngine with plot_composition_context(self, show): diff --git a/plotnine/composition/_plot_annotation.py b/plotnine/composition/_plot_annotation.py index f44a5b46df..1668ada113 100644 --- a/plotnine/composition/_plot_annotation.py +++ b/plotnine/composition/_plot_annotation.py @@ -1,11 +1,8 @@ from __future__ import annotations -from copy import copy from dataclasses import dataclass, field from typing import TYPE_CHECKING -from plotnine.themes.targets import ThemeTargets - from .. import theme from .._utils.dataclasses import non_none_init_items from ..composition._types import ComposeAddable @@ -61,13 +58,7 @@ def _setup(self, cmp: Compose): """ Setup annotation """ - # We mimick theme.setup instead of call it because - # we cannot have have a ggplot object. - self.theme = copy(self.theme) - self.theme.figure = cmp.figure - self.theme.targets = ThemeTargets() - self.theme._add_default_themeable_properties() - self.theme.T.setup(self.theme) + self.theme._setup(cmp.figure) def empty(self) -> bool: """ diff --git a/plotnine/ggplot.py b/plotnine/ggplot.py index 6fd225e03f..16db7077da 100755 --- a/plotnine/ggplot.py +++ b/plotnine/ggplot.py @@ -326,7 +326,12 @@ def draw(self, *, show: bool = False) -> Figure: # setup self.axs = self.facet.setup(self) self.guides._setup(self) - self.theme.setup(self) + self.theme._setup( + figure, + self.axs, + self.labels.title, + self.labels.subtitle, + ) # Drawing self._draw_layers() diff --git a/plotnine/guides/guide.py b/plotnine/guides/guide.py index c772dc638a..0863d789c3 100644 --- a/plotnine/guides/guide.py +++ b/plotnine/guides/guide.py @@ -113,7 +113,7 @@ def setup(self, guides: guides): # guide theme has priority and its targets are tracked # independently. self.theme = guides.plot.theme + self.theme - self.theme.setup(guides.plot) + self.theme._setup(guides.plot.figure) self.plot_layers = guides.plot.layers self.plot_mapping = guides.plot.mapping self.elements = self._elements_cls(self.theme, self) diff --git a/plotnine/themes/theme.py b/plotnine/themes/theme.py index 2f8be31be3..4a7aa6de2b 100644 --- a/plotnine/themes/theme.py +++ b/plotnine/themes/theme.py @@ -295,7 +295,13 @@ def apply(self): for th in self.T.values(): th.apply(self) - def setup(self, plot: ggplot): + def _setup( + self, + figure: Figure, + axs: list[Axes] | None = None, + title: str | None = None, + subtitle: str | None = None, + ): """ Setup theme for applying @@ -306,24 +312,14 @@ def setup(self, plot: ggplot): It also initialises where the artists to be themed will be stored. """ - self.plot = plot - self.figure = plot.figure - self.axs = plot.axs - self.targets = ThemeTargets() - self._add_default_themeable_properties() - self.T.setup(self) - - def _add_default_themeable_properties(self): - """ - Add default themeable properties that depend depend on the plot + self.figure = figure + self.axs = axs if axs is not None else [] - Some properties may be left unset (None) and their final values are - best worked out dynamically after the plot has been built, but - before the themeables are applied. + if title or subtitle: + self._smart_title_and_subtitle_ha(title, subtitle) - This is where the theme is modified to add those values. - """ - self._smart_title_and_subtitle_ha() + self.targets = ThemeTargets() + self.T.setup(self) @property def rcParams(self): @@ -466,18 +462,16 @@ def to_retina(self) -> theme: dpi = self.getp("dpi") return self + theme(dpi=dpi * 2) - def _smart_title_and_subtitle_ha(self): + def _smart_title_and_subtitle_ha( + self, title: str | None, subtitle: str | None + ): """ Smartly add the horizontal alignment for the title and subtitle """ from .elements import element_text - has_title = bool( - self.plot.labels.get("title", "") - ) and not self.T.is_blank("plot_title") - has_subtitle = bool( - self.plot.labels.get("subtitle", "") - ) and not self.T.is_blank("plot_subtitle") + has_title = bool(title) and not self.T.is_blank("plot_title") + has_subtitle = bool(subtitle) and not self.T.is_blank("plot_subtitle") title_ha = self.getp(("plot_title", "ha")) subtitle_ha = self.getp(("plot_subtitle", "ha")) From e93dc6d412d94ff4d47e38da724c383636b0c086 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Sat, 27 Sep 2025 00:11:32 -0700 Subject: [PATCH 04/24] Rename facet._panels_gridspec to _gridspec --- plotnine/_mpl/layout_manager/_engine.py | 4 ++-- plotnine/_mpl/layout_manager/_layout_items.py | 4 ++-- plotnine/facets/facet.py | 8 +++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/plotnine/_mpl/layout_manager/_engine.py b/plotnine/_mpl/layout_manager/_engine.py index 9f6c3e0b82..7eb825b0bb 100644 --- a/plotnine/_mpl/layout_manager/_engine.py +++ b/plotnine/_mpl/layout_manager/_engine.py @@ -42,7 +42,7 @@ def execute(self, fig: Figure): spaces = LayoutSpaces(self.plot) gsparams = spaces.get_gridspec_params() - self.plot.facet._panels_gridspec.update_params_and_artists(gsparams) + self.plot.facet._gridspec.update_params_and_artists(gsparams) spaces.items._adjust_positions(spaces) @@ -83,5 +83,5 @@ def execute(self, fig: Figure): PlotnineWarning, ) break - plot.facet._panels_gridspec.update_params_and_artists(gsparams) + plot.facet._gridspec.update_params_and_artists(gsparams) spaces.items._adjust_positions(spaces) diff --git a/plotnine/_mpl/layout_manager/_layout_items.py b/plotnine/_mpl/layout_manager/_layout_items.py index 0389a36f02..75398eaaab 100644 --- a/plotnine/_mpl/layout_manager/_layout_items.py +++ b/plotnine/_mpl/layout_manager/_layout_items.py @@ -632,7 +632,7 @@ def set_legends_position(legends: legend_artists, spaces: LayoutSpaces): """ Place legend on the figure and justify is a required """ - panels_gs = spaces.plot.facet._panels_gridspec + panels_gs = spaces.plot.facet._gridspec params = panels_gs.get_subplot_params() transFigure = spaces.plot.figure.transFigure @@ -711,7 +711,7 @@ def set_plot_tag_position(tag: Text, spaces: LayoutSpaces): Set the postion of the plot_tag """ theme = spaces.plot.theme - panels_gs = spaces.plot.facet._panels_gridspec + panels_gs = spaces.plot.facet._gridspec location: TagLocation = theme.getp("plot_tag_location") position: TagPosition = theme.getp("plot_tag_position") margin = theme.get_margin("plot_tag") diff --git a/plotnine/facets/facet.py b/plotnine/facets/facet.py index 8b0a5d7e93..4809cca816 100644 --- a/plotnine/facets/facet.py +++ b/plotnine/facets/facet.py @@ -93,7 +93,7 @@ class facet: # Axes axs: list[Axes] - _panels_gridspec: p9GridSpec + _gridspec: p9GridSpec # ggplot object that the facet belongs to plot: ggplot @@ -395,14 +395,12 @@ def _make_axes(self) -> list[Axes]: num_panels = len(self.layout.layout) axsarr = np.empty((self.nrow, self.ncol), dtype=object) - self._panels_gridspec = self._get_panels_gridspec() + self._gridspec = self._get_panels_gridspec() # Create axes it = itertools.product(range(self.nrow), range(self.ncol)) for i, (row, col) in enumerate(it): - axsarr[row, col] = self.figure.add_subplot( - self._panels_gridspec[i] - ) + axsarr[row, col] = self.figure.add_subplot(self._gridspec[i]) # Rearrange axes # They are ordered to match the positions in the layout table From c1efcd54c4f31c57b20f6d61689ce76fe72ff421 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Sat, 27 Sep 2025 00:24:22 -0700 Subject: [PATCH 05/24] Rename _*_spaces to _*_space --- plotnine/_mpl/layout_manager/_engine.py | 2 +- plotnine/_mpl/layout_manager/_layout_items.py | 2 +- plotnine/_mpl/layout_manager/_layout_tree.py | 52 +++++++++---------- .../{_spaces.py => _side_space.py} | 28 +++++----- 4 files changed, 42 insertions(+), 42 deletions(-) rename plotnine/_mpl/layout_manager/{_spaces.py => _side_space.py} (98%) diff --git a/plotnine/_mpl/layout_manager/_engine.py b/plotnine/_mpl/layout_manager/_engine.py index 7eb825b0bb..b91cf5208f 100644 --- a/plotnine/_mpl/layout_manager/_engine.py +++ b/plotnine/_mpl/layout_manager/_engine.py @@ -7,7 +7,7 @@ from ...exceptions import PlotnineWarning from ._layout_tree import LayoutTree -from ._spaces import LayoutSpaces +from ._side_space import LayoutSpaces if TYPE_CHECKING: from matplotlib.figure import Figure diff --git a/plotnine/_mpl/layout_manager/_layout_items.py b/plotnine/_mpl/layout_manager/_layout_items.py index 75398eaaab..13d039ab70 100644 --- a/plotnine/_mpl/layout_manager/_layout_items.py +++ b/plotnine/_mpl/layout_manager/_layout_items.py @@ -39,7 +39,7 @@ VerticalJustification, ) - from ._spaces import LayoutSpaces + from ._side_space import LayoutSpaces AxesLocation: TypeAlias = Literal[ "all", "first_row", "last_row", "first_col", "last_col" diff --git a/plotnine/_mpl/layout_manager/_layout_tree.py b/plotnine/_mpl/layout_manager/_layout_tree.py index c5f2f4c7aa..60914078fd 100644 --- a/plotnine/_mpl/layout_manager/_layout_tree.py +++ b/plotnine/_mpl/layout_manager/_layout_tree.py @@ -7,12 +7,12 @@ import numpy as np from ._grid import Grid -from ._spaces import ( +from ._side_space import ( LayoutSpaces, - bottom_spaces, - left_spaces, - right_spaces, - top_spaces, + bottom_space, + left_space, + right_space, + top_space, ) if TYPE_CHECKING: @@ -20,11 +20,11 @@ from plotnine import ggplot from plotnine._mpl.gridspec import p9GridSpec - from plotnine._mpl.layout_manager._spaces import ( - bottom_spaces, - left_spaces, - right_spaces, - top_spaces, + from plotnine._mpl.layout_manager._side_space import ( + bottom_space, + left_space, + right_space, + top_space, ) from plotnine.composition import Compose @@ -233,28 +233,28 @@ def resize_sub_compositions(self): tree.resize() @cached_property - def bottom_most_spaces(self) -> list[bottom_spaces]: + def bottom_most_spaces(self) -> list[bottom_space]: """ Bottom spaces of items in the last row """ return [s for s in self.bottom_spaces_in_row(self.nrow - 1)] @cached_property - def top_most_spaces(self) -> list[top_spaces]: + def top_most_spaces(self) -> list[top_space]: """ Top spaces of items in the top row """ return [s for s in self.top_spaces_in_row(0)] @cached_property - def left_most_spaces(self) -> list[left_spaces]: + def left_most_spaces(self) -> list[left_space]: """ Left spaces of items in the last column """ return [s for s in self.left_spaces_in_col(0)] @cached_property - def right_most_spaces(self) -> list[right_spaces]: + def right_most_spaces(self) -> list[right_space]: """ Right spaces of items the last column """ @@ -357,8 +357,8 @@ def panel_height_ratios(self) -> Sequence[float]: """ return cast("Sequence[float]", self.cmp._layout.heights) - def bottom_spaces_in_row(self, r: int) -> list[bottom_spaces]: - spaces: list[bottom_spaces] = [] + def bottom_spaces_in_row(self, r: int) -> list[bottom_space]: + spaces: list[bottom_space] = [] for node in self.grid[r, :]: if isinstance(node, LayoutSpaces): spaces.append(node.b) @@ -366,8 +366,8 @@ def bottom_spaces_in_row(self, r: int) -> list[bottom_spaces]: spaces.extend(node.bottom_most_spaces) return spaces - def top_spaces_in_row(self, r: int) -> list[top_spaces]: - spaces: list[top_spaces] = [] + def top_spaces_in_row(self, r: int) -> list[top_space]: + spaces: list[top_space] = [] for node in self.grid[r, :]: if isinstance(node, LayoutSpaces): spaces.append(node.t) @@ -375,8 +375,8 @@ def top_spaces_in_row(self, r: int) -> list[top_spaces]: spaces.extend(node.top_most_spaces) return spaces - def left_spaces_in_col(self, c: int) -> list[left_spaces]: - spaces: list[left_spaces] = [] + def left_spaces_in_col(self, c: int) -> list[left_space]: + spaces: list[left_space] = [] for node in self.grid[:, c]: if isinstance(node, LayoutSpaces): spaces.append(node.l) @@ -384,8 +384,8 @@ def left_spaces_in_col(self, c: int) -> list[left_spaces]: spaces.extend(node.left_most_spaces) return spaces - def right_spaces_in_col(self, c: int) -> list[right_spaces]: - spaces: list[right_spaces] = [] + def right_spaces_in_col(self, c: int) -> list[right_space]: + spaces: list[right_space] = [] for node in self.grid[:, c]: if isinstance(node, LayoutSpaces): spaces.append(node.r) @@ -393,7 +393,7 @@ def right_spaces_in_col(self, c: int) -> list[right_spaces]: spaces.extend(node.right_most_spaces) return spaces - def iter_left_spaces(self) -> Iterator[list[left_spaces]]: + def iter_left_spaces(self) -> Iterator[list[left_space]]: """ Left spaces for each non-empty column @@ -404,7 +404,7 @@ def iter_left_spaces(self) -> Iterator[list[left_spaces]]: if spaces: yield spaces - def iter_right_spaces(self) -> Iterator[list[right_spaces]]: + def iter_right_spaces(self) -> Iterator[list[right_space]]: """ Right spaces for each non-empty column @@ -415,7 +415,7 @@ def iter_right_spaces(self) -> Iterator[list[right_spaces]]: if spaces: yield spaces - def iter_bottom_spaces(self) -> Iterator[list[bottom_spaces]]: + def iter_bottom_spaces(self) -> Iterator[list[bottom_space]]: """ Bottom spaces for each non-empty row """ @@ -424,7 +424,7 @@ def iter_bottom_spaces(self) -> Iterator[list[bottom_spaces]]: if spaces: yield spaces - def iter_top_spaces(self) -> Iterator[list[top_spaces]]: + def iter_top_spaces(self) -> Iterator[list[top_space]]: """ Top spaces for each non-empty row """ diff --git a/plotnine/_mpl/layout_manager/_spaces.py b/plotnine/_mpl/layout_manager/_side_space.py similarity index 98% rename from plotnine/_mpl/layout_manager/_spaces.py rename to plotnine/_mpl/layout_manager/_side_space.py index 61645aa4f2..9dc8cc2e09 100644 --- a/plotnine/_mpl/layout_manager/_spaces.py +++ b/plotnine/_mpl/layout_manager/_side_space.py @@ -59,7 +59,7 @@ def valid(self) -> bool: @dataclass -class _side_spaces(ABC): +class _side_space(ABC): """ Base class to for spaces @@ -73,7 +73,7 @@ class _side_spaces(ABC): items: LayoutItems def __post_init__(self): - self.side: Side = cast("Side", self.__class__.__name__[:-7]) + self.side: Side = cast("Side", self.__class__.__name__[:-6]) """ Side of the panel(s) that this class applies to """ @@ -283,7 +283,7 @@ def axis_title_clearance(self) -> float: @dataclass -class left_spaces(_side_spaces): +class left_space(_side_space): """ Space in the figure for artists on the left of the panel area @@ -455,7 +455,7 @@ def tag_width(self): @dataclass -class right_spaces(_side_spaces): +class right_space(_side_space): """ Space in the figure for artists on the right of the panel area @@ -561,7 +561,7 @@ def tag_width(self): @dataclass -class top_spaces(_side_spaces): +class top_space(_side_space): """ Space in the figure for artists above the panel area @@ -690,7 +690,7 @@ def tag_height(self): @dataclass -class bottom_spaces(_side_spaces): +class bottom_space(_side_space): """ Space in the figure for artists below the panel area @@ -855,16 +855,16 @@ class LayoutSpaces: plot: ggplot - l: left_spaces = field(init=False) + l: left_space = field(init=False) """All subspaces to the left of the panels""" - r: right_spaces = field(init=False) + r: right_space = field(init=False) """All subspaces to the right of the panels""" - t: top_spaces = field(init=False) + t: top_space = field(init=False) """All subspaces above the top of the panels""" - b: bottom_spaces = field(init=False) + b: bottom_space = field(init=False) """All subspaces below the bottom of the panels""" W: float = field(init=False, default=0) @@ -894,10 +894,10 @@ def __post_init__(self): # Calculate the spacing along the edges of the panel area # (spacing required by plotnine) - self.l = left_spaces(self.items) - self.r = right_spaces(self.items) - self.t = top_spaces(self.items) - self.b = bottom_spaces(self.items) + self.l = left_space(self.items) + self.r = right_space(self.items) + self.t = top_space(self.items) + self.b = bottom_space(self.items) def get_gridspec_params(self) -> GridSpecParams: # Calculate the gridspec params From 2b951fdff3808372d50b531c033f143131c7683d Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Sat, 27 Sep 2025 16:17:19 -0700 Subject: [PATCH 06/24] Refactor _side_space to introduce _plot_side_space --- plotnine/_mpl/gridspec.py | 2 +- plotnine/_mpl/layout_manager/_engine.py | 2 +- plotnine/_mpl/layout_manager/_layout_items.py | 2 +- plotnine/_mpl/layout_manager/_layout_tree.py | 4 +- .../_mpl/layout_manager/_plot_side_space.py | 1027 +++++++++++++++++ plotnine/_mpl/layout_manager/_side_space.py | 1004 +--------------- 6 files changed, 1038 insertions(+), 1003 deletions(-) create mode 100644 plotnine/_mpl/layout_manager/_plot_side_space.py diff --git a/plotnine/_mpl/gridspec.py b/plotnine/_mpl/gridspec.py index 07fc66d6eb..2b8ff60b78 100644 --- a/plotnine/_mpl/gridspec.py +++ b/plotnine/_mpl/gridspec.py @@ -19,7 +19,7 @@ from matplotlib.patches import Rectangle from matplotlib.transforms import Transform - from plotnine._mpl.layout_manager._spaces import GridSpecParams + from plotnine._mpl.layout_manager._side_space import GridSpecParams from plotnine.composition._plot_layout import plot_layout diff --git a/plotnine/_mpl/layout_manager/_engine.py b/plotnine/_mpl/layout_manager/_engine.py index b91cf5208f..8d66392343 100644 --- a/plotnine/_mpl/layout_manager/_engine.py +++ b/plotnine/_mpl/layout_manager/_engine.py @@ -7,7 +7,7 @@ from ...exceptions import PlotnineWarning from ._layout_tree import LayoutTree -from ._side_space import LayoutSpaces +from ._plot_side_space import LayoutSpaces if TYPE_CHECKING: from matplotlib.figure import Figure diff --git a/plotnine/_mpl/layout_manager/_layout_items.py b/plotnine/_mpl/layout_manager/_layout_items.py index 13d039ab70..ea0c098a3a 100644 --- a/plotnine/_mpl/layout_manager/_layout_items.py +++ b/plotnine/_mpl/layout_manager/_layout_items.py @@ -39,7 +39,7 @@ VerticalJustification, ) - from ._side_space import LayoutSpaces + from ._plot_side_space import LayoutSpaces AxesLocation: TypeAlias = Literal[ "all", "first_row", "last_row", "first_col", "last_col" diff --git a/plotnine/_mpl/layout_manager/_layout_tree.py b/plotnine/_mpl/layout_manager/_layout_tree.py index 60914078fd..54915ce8db 100644 --- a/plotnine/_mpl/layout_manager/_layout_tree.py +++ b/plotnine/_mpl/layout_manager/_layout_tree.py @@ -7,7 +7,7 @@ import numpy as np from ._grid import Grid -from ._side_space import ( +from ._plot_side_space import ( LayoutSpaces, bottom_space, left_space, @@ -20,7 +20,7 @@ from plotnine import ggplot from plotnine._mpl.gridspec import p9GridSpec - from plotnine._mpl.layout_manager._side_space import ( + from plotnine._mpl.layout_manager._plot_side_space import ( bottom_space, left_space, right_space, diff --git a/plotnine/_mpl/layout_manager/_plot_side_space.py b/plotnine/_mpl/layout_manager/_plot_side_space.py new file mode 100644 index 0000000000..3cce2113cc --- /dev/null +++ b/plotnine/_mpl/layout_manager/_plot_side_space.py @@ -0,0 +1,1027 @@ +""" +Routines to adjust subplot params so that subplots are +nicely fit in the figure. In doing so, only axis labels, tick labels, axes +titles and offsetboxes that are anchored to axes are currently considered. + +Internally, this module assumes that the margins (left margin, etc.) which are +differences between `Axes.get_tightbbox` and `Axes.bbox` are independent of +Axes position. This may fail if `Axes.adjustable` is `datalim` as well as +such cases as when left or right margin are affected by xlabel. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from functools import cached_property +from typing import TYPE_CHECKING + +from plotnine.exceptions import PlotnineError +from plotnine.facets import facet_grid, facet_null, facet_wrap + +from ._layout_items import LayoutItems +from ._side_space import GridSpecParams, _side_space + +if TYPE_CHECKING: + from plotnine import ggplot + from plotnine._mpl.gridspec import p9GridSpec + from plotnine.iapi import outside_legend + + +@dataclass +class _plot_side_space(_side_space): + """ + Base class for the side space around a plot + """ + + items: LayoutItems + + @cached_property + def _legend_size(self) -> tuple[float, float]: + """ + Return size of legend in figure coordinates + + We need this to accurately justify the legend by proportional + values e.g. 0.2, instead of just left, right, top, bottom & + center. + """ + if not self.has_legend: + return (0, 0) + + ol: outside_legend = getattr(self.items.legends, self.side) + return self.items.geometry.size(ol.box) + + @cached_property + def legend_width(self) -> float: + """ + Return width of legend in figure coordinates + """ + return self._legend_size[0] + + @cached_property + def legend_height(self) -> float: + """ + Return height of legend in figure coordinates + """ + return self._legend_size[1] + + @cached_property + def gs(self) -> p9GridSpec: + """ + The gridspec of the plot + """ + return self.items.plot._gridspec + + @property + def has_tag(self) -> bool: + """ + Return True if the space/margin to this side of the panel has a tag + + If it does, then it will be included in the layout + """ + getp = self.items.plot.theme.getp + return getp("plot_tag_location") == "margin" and self.side in getp( + "plot_tag_position" + ) + + @property + def has_legend(self) -> bool: + """ + Return True if the space/margin to this side of the panel has a legend + + If it does, then it will be included in the layout + """ + if not self.items.legends: + return False + return hasattr(self.items.legends, self.side) + + @property + def tag_width(self) -> float: + """ + The width of the tag including the margins + + The value is zero except if all these are true: + - The tag is in the margin `theme(plot_tag_position = "margin")` + - The tag at one one of the the following locations; + left, right, topleft, topright, bottomleft or bottomright + """ + return 0 + + @property + def tag_height(self) -> float: + """ + The height of the tag including the margins + + The value is zero except if all these are true: + - The tag is in the margin `theme(plot_tag_position = "margin")` + - The tag at one one of the the following locations; + top, bottom, topleft, topright, bottomleft or bottomright + """ + return 0 + + @property + def axis_title_clearance(self) -> float: + """ + The distance between the axis title and the panel + + Figure + ---------------------------- + | Panel | + | ----------- | + | | | | + | | | | + | Y<--->| | | + | | | | + | | | | + | ----------- | + | | + ---------------------------- + + We use this value to when aligning axis titles in a + plot composition. + """ + + try: + return self.total - self.sum_upto("axis_title_alignment") + except AttributeError as err: + # There is probably an error in in the layout manager + raise PlotnineError("Side has no axis title") from err + + +@dataclass +class left_space(_plot_side_space): + """ + Space in the figure for artists on the left of the panel area + + Ordered from the edge of the figure and going inwards + """ + + plot_margin: float = 0 + tag_alignment: float = 0 + """ + Space added to align the tag in this plot with others in a composition + + This value is calculated during the layout process, and it ensures that + all tags on this side of the plot take up the same amount of space in + the margin. e.g. from + + ------------------------------------ + | plot_margin | tag | artists | + |------------------------------------| + | plot_margin | A long tag | artists | + ------------------------------------ + + to + + ------------------------------------ + | plot_margin | tag | artists | + |------------------------------------| + | plot_margin | A long tag | artists | + ------------------------------------ + + And the tag is justified within that space e.g if ha="left" we get + + ------------------------------------ + | plot_margin | tag | artists | + |------------------------------------| + | plot_margin | A long tag | artists | + ------------------------------------ + + So, contrary to the order in which the space items are laid out, the + tag_alignment does not necessarily come before the plot_tag. + """ + plot_tag_margin_left: float = 0 + plot_tag: float = 0 + plot_tag_margin_right: float = 0 + margin_alignment: float = 0 + """ + Space added to align this plot with others in a composition + + This value is calculated during the layout process in a tree structure + that has convenient access to the sides/edges of the panels in the + composition. + """ + legend: float = 0 + legend_box_spacing: float = 0 + axis_title_y_margin_left: float = 0 + axis_title_y: float = 0 + axis_title_y_margin_right: float = 0 + axis_title_alignment: float = 0 + """ + Space added to align the axis title with others in a composition + + This value is calculated during the layout process. The amount is + the difference between the largest and smallest axis_title_clearance + among the items in the composition. + """ + axis_text_y_margin_left: float = 0 + axis_text_y: float = 0 + axis_text_y_margin_right: float = 0 + axis_ticks_y: float = 0 + + def _calculate(self): + theme = self.items.plot.theme + geometry = self.items.geometry + items = self.items + + self.plot_margin = theme.getp("plot_margin_left") + + if self.has_tag and items.plot_tag: + m = theme.get_margin("plot_tag").fig + self.plot_tag_margin_left = m.l + self.plot_tag = geometry.width(items.plot_tag) + self.plot_tag_margin_right = m.r + + if items.legends and items.legends.left: + self.legend = self.legend_width + self.legend_box_spacing = theme.getp("legend_box_spacing") + + if items.axis_title_y: + m = theme.get_margin("axis_title_y").fig + self.axis_title_y_margin_left = m.l + self.axis_title_y = geometry.width(items.axis_title_y) + self.axis_title_y_margin_right = m.r + + # Account for the space consumed by the axis + self.axis_text_y = items.axis_text_y_max_width_at("first_col") + if self.axis_text_y: + m = theme.get_margin("axis_text_y").fig + self.axis_text_y_margin_left = m.l + self.axis_text_y_margin_right = m.r + + self.axis_ticks_y = items.axis_ticks_y_max_width_at("first_col") + + # Adjust plot_margin to make room for ylabels that protude well + # beyond the axes + # NOTE: This adjustment breaks down when the protrusion is large + protrusion = items.axis_text_x_left_protrusion("all") + adjustment = protrusion - (self.total - self.plot_margin) + if adjustment > 0: + self.plot_margin += adjustment + + @property + def offset(self) -> float: + """ + Distance from left of the figure to the left of the plot gridspec + + ----------------(1, 1) + | ---- | + | dx | | | + |<--->| | | + | | | | + | ---- | + (0, 0)---------------- + + """ + return self.gs.bbox_relative.x0 + + def x1(self, item: str) -> float: + """ + Lower x-coordinate in figure space of the item + """ + return self.to_figure_space(self.sum_upto(item)) + + def x2(self, item: str) -> float: + """ + Higher x-coordinate in figure space of the item + """ + return self.to_figure_space(self.sum_incl(item)) + + @property + def panel_left_relative(self): + """ + Left (relative to the gridspec) of the panels in figure dimensions + """ + return self.total + + @property + def panel_left(self): + """ + Left of the panels in figure space + """ + return self.to_figure_space(self.panel_left_relative) + + @property + def plot_left(self): + """ + Distance up to the left-most artist in figure space + """ + return self.x1("legend") + + @property + def tag_width(self): + """ + The width of the tag including the margins + """ + return ( + self.plot_tag_margin_left + + self.plot_tag + + self.plot_tag_margin_right + ) + + +@dataclass +class right_space(_plot_side_space): + """ + Space in the figure for artists on the right of the panel area + + Ordered from the edge of the figure and going inwards + """ + + plot_margin: float = 0 + tag_alignment: float = 0 + plot_tag_margin_right: float = 0 + plot_tag: float = 0 + plot_tag_margin_left: float = 0 + margin_alignment: float = 0 + legend: float = 0 + legend_box_spacing: float = 0 + strip_text_y_extra_width: float = 0 + + def _calculate(self): + items = self.items + theme = self.items.plot.theme + geometry = self.items.geometry + + self.plot_margin = theme.getp("plot_margin_right") + + if self.has_tag and items.plot_tag: + m = theme.get_margin("plot_tag").fig + self.plot_tag_margin_right = m.r + self.plot_tag = geometry.width(items.plot_tag) + self.plot_tag_margin_left = m.l + + if items.legends and items.legends.right: + self.legend = self.legend_width + self.legend_box_spacing = theme.getp("legend_box_spacing") + + self.strip_text_y_extra_width = items.strip_text_y_extra_width("right") + + # Adjust plot_margin to make room for ylabels that protude well + # beyond the axes + # NOTE: This adjustment breaks down when the protrusion is large + protrusion = items.axis_text_x_right_protrusion("all") + adjustment = protrusion - (self.total - self.plot_margin) + if adjustment > 0: + self.plot_margin += adjustment + + @property + def offset(self): + """ + Distance from right of the figure to the right of the plot gridspec + + ---------------(1, 1) + | ---- | + | | | -dx | + | | |<--->| + | | | | + | ---- | + (0, 0)--------------- + + """ + return self.gs.bbox_relative.x1 - 1 + + def x1(self, item: str) -> float: + """ + Lower x-coordinate in figure space of the item + """ + return self.to_figure_space(1 - self.sum_incl(item)) + + def x2(self, item: str) -> float: + """ + Higher x-coordinate in figure space of the item + """ + return self.to_figure_space(1 - self.sum_upto(item)) + + @property + def panel_right_relative(self): + """ + Right (relative to the gridspec) of the panels in figure dimensions + """ + return 1 - self.total + + @property + def panel_right(self): + """ + Right of the panels in figure space + """ + return self.to_figure_space(self.panel_right_relative) + + @property + def plot_right(self): + """ + Distance up to the right-most artist in figure space + """ + return self.x2("legend") + + @property + def tag_width(self): + """ + The width of the tag including the margins + """ + return ( + self.plot_tag_margin_right + + self.plot_tag + + self.plot_tag_margin_left + ) + + +@dataclass +class top_space(_plot_side_space): + """ + Space in the figure for artists above the panel area + + Ordered from the edge of the figure and going inwards + """ + + plot_margin: float = 0 + tag_alignment: float = 0 + plot_tag_margin_top: float = 0 + plot_tag: float = 0 + plot_tag_margin_bottom: float = 0 + margin_alignment: float = 0 + plot_title_margin_top: float = 0 + plot_title: float = 0 + plot_title_margin_bottom: float = 0 + plot_subtitle_margin_top: float = 0 + plot_subtitle: float = 0 + plot_subtitle_margin_bottom: float = 0 + legend: float = 0 + legend_box_spacing: float = 0 + strip_text_x_extra_height: float = 0 + + def _calculate(self): + items = self.items + theme = self.items.plot.theme + geometry = self.items.geometry + W, H = theme.getp("figure_size") + F = W / H + + self.plot_margin = theme.getp("plot_margin_top") * F + + if self.has_tag and items.plot_tag: + m = theme.get_margin("plot_tag").fig + self.plot_tag_margin_top = m.t + self.plot_tag = geometry.height(items.plot_tag) + self.plot_tag_margin_bottom = m.b + + if items.plot_title: + m = theme.get_margin("plot_title").fig + self.plot_title_margin_top = m.t * F + self.plot_title = geometry.height(items.plot_title) + self.plot_title_margin_bottom = m.b * F + + if items.plot_subtitle: + m = theme.get_margin("plot_subtitle").fig + self.plot_subtitle_margin_top = m.t * F + self.plot_subtitle = geometry.height(items.plot_subtitle) + self.plot_subtitle_margin_bottom = m.b * F + + if items.legends and items.legends.top: + self.legend = self.legend_height + self.legend_box_spacing = theme.getp("legend_box_spacing") * F + + self.strip_text_x_extra_height = items.strip_text_x_extra_height("top") + + # Adjust plot_margin to make room for ylabels that protude well + # beyond the axes + # NOTE: This adjustment breaks down when the protrusion is large + protrusion = items.axis_text_y_top_protrusion("all") + adjustment = protrusion - (self.total - self.plot_margin) + if adjustment > 0: + self.plot_margin += adjustment + + @property + def offset(self) -> float: + """ + Distance from top of the figure to the top of the plot gridspec + + ----------------(1, 1) + | ^ | + | |-dy | + | v | + | ---- | + | | | | + | | | | + | | | | + | ---- | + | | + (0, 0)---------------- + """ + return self.gs.bbox_relative.y1 - 1 + + def y1(self, item: str) -> float: + """ + Lower y-coordinate in figure space of the item + """ + return self.to_figure_space(1 - self.sum_incl(item)) + + def y2(self, item: str) -> float: + """ + Higher y-coordinate in figure space of the item + """ + return self.to_figure_space(1 - self.sum_upto(item)) + + @property + def panel_top_relative(self): + """ + Top (relative to the gridspec) of the panels in figure dimensions + """ + return 1 - self.total + + @property + def panel_top(self): + """ + Top of the panels in figure space + """ + return self.to_figure_space(self.panel_top_relative) + + @property + def plot_top(self): + """ + Distance up to the top-most artist in figure space + """ + return self.y2("legend") + + @property + def tag_height(self): + """ + The height of the tag including the margins + """ + return ( + self.plot_tag_margin_top + + self.plot_tag + + self.plot_tag_margin_bottom + ) + + +@dataclass +class bottom_space(_plot_side_space): + """ + Space in the figure for artists below the panel area + + Ordered from the edge of the figure and going inwards + """ + + plot_margin: float = 0 + tag_alignment: float = 0 + plot_tag_margin_bottom: float = 0 + plot_tag: float = 0 + plot_tag_margin_top: float = 0 + margin_alignment: float = 0 + plot_caption_margin_bottom: float = 0 + plot_caption: float = 0 + plot_caption_margin_top: float = 0 + legend: float = 0 + legend_box_spacing: float = 0 + axis_title_x_margin_bottom: float = 0 + axis_title_x: float = 0 + axis_title_x_margin_top: float = 0 + axis_title_alignment: float = 0 + """ + Space added to align the axis title with others in a composition + + This value is calculated during the layout process in a tree structure + that has convenient access to the sides/edges of the panels in the + composition. It's amount is the difference in height between this axis + text (and it's margins) and the tallest axis text (and it's margin). + """ + axis_text_x_margin_bottom: float = 0 + axis_text_x: float = 0 + axis_text_x_margin_top: float = 0 + axis_ticks_x: float = 0 + + def _calculate(self): + items = self.items + theme = self.items.plot.theme + geometry = self.items.geometry + W, H = theme.getp("figure_size") + F = W / H + + self.plot_margin = theme.getp("plot_margin_bottom") * F + + if self.has_tag and items.plot_tag: + m = theme.get_margin("plot_tag").fig + self.plot_tag_margin_bottom = m.b + self.plot_tag = geometry.height(items.plot_tag) + self.plot_tag_margin_top = m.t + + if items.plot_caption: + m = theme.get_margin("plot_caption").fig + self.plot_caption_margin_bottom = m.b * F + self.plot_caption = geometry.height(items.plot_caption) + self.plot_caption_margin_top = m.t * F + + if items.legends and items.legends.bottom: + self.legend = self.legend_height + self.legend_box_spacing = theme.getp("legend_box_spacing") * F + + if items.axis_title_x: + m = theme.get_margin("axis_title_x").fig + self.axis_title_x_margin_bottom = m.b * F + self.axis_title_x = geometry.height(items.axis_title_x) + self.axis_title_x_margin_top = m.t * F + + # Account for the space consumed by the axis + self.axis_text_x = items.axis_text_x_max_height_at("last_row") + if self.axis_text_x: + m = theme.get_margin("axis_text_x").fig + self.axis_text_x_margin_bottom = m.b + self.axis_text_x_margin_top = m.t + self.axis_ticks_x = items.axis_ticks_x_max_height_at("last_row") + + # Adjust plot_margin to make room for ylabels that protude well + # beyond the axes + # NOTE: This adjustment breaks down when the protrusion is large + protrusion = items.axis_text_y_bottom_protrusion("all") + adjustment = protrusion - (self.total - self.plot_margin) + if adjustment > 0: + self.plot_margin += adjustment + + @property + def offset(self) -> float: + """ + Distance from bottom of the figure to the bottom of the plot gridspec + + ----------------(1, 1) + | | + | ---- | + | | | | + | | | | + | | | | + | ---- | + | ^ | + | |dy | + | v | + (0, 0)---------------- + """ + return self.gs.bbox_relative.y0 + + def y1(self, item: str) -> float: + """ + Lower y-coordinate in figure space of the item + """ + return self.to_figure_space(self.sum_upto(item)) + + def y2(self, item: str) -> float: + """ + Higher y-coordinate in figure space of the item + """ + return self.to_figure_space(self.sum_incl(item)) + + @property + def panel_bottom_relative(self): + """ + Bottom (relative to the gridspec) of the panels in figure dimensions + """ + return self.total + + @property + def panel_bottom(self): + """ + Bottom of the panels in figure space + """ + return self.to_figure_space(self.panel_bottom_relative) + + @property + def plot_bottom(self): + """ + Distance up to the bottom-most artist in figure space + """ + return self.y1("legend") + + @property + def tag_height(self): + """ + The height of the tag including the margins + """ + return ( + self.plot_tag_margin_bottom + + self.plot_tag + + self.plot_tag_margin_top + ) + + +@dataclass +class LayoutSpaces: + """ + Compute the all the spaces required in the layout + + These are: + + 1. The space of each artist between the panel and the edge of the + figure. + 2. The space in-between the panels + + From these values, we put together the grid-spec parameters required + by matplotblib to position the axes. We also use the values to adjust + the coordinates of all the artists that occupy these spaces, placing + them in their final positions. + """ + + plot: ggplot + + l: left_space = field(init=False) + """All subspaces to the left of the panels""" + + r: right_space = field(init=False) + """All subspaces to the right of the panels""" + + t: top_space = field(init=False) + """All subspaces above the top of the panels""" + + b: bottom_space = field(init=False) + """All subspaces below the bottom of the panels""" + + W: float = field(init=False, default=0) + """Figure Width [inches]""" + + H: float = field(init=False, default=0) + """Figure Height [inches]""" + + w: float = field(init=False, default=0) + """Axes width w.r.t figure in [0, 1]""" + + h: float = field(init=False, default=0) + """Axes height w.r.t figure in [0, 1]""" + + sh: float = field(init=False, default=0) + """horizontal spacing btn panels w.r.t figure""" + + sw: float = field(init=False, default=0) + """vertical spacing btn panels w.r.t figure""" + + gsparams: GridSpecParams = field(init=False, repr=False) + """Grid spacing btn panels w.r.t figure""" + + def __post_init__(self): + self.items = LayoutItems(self.plot) + self.W, self.H = self.plot.theme.getp("figure_size") + + # Calculate the spacing along the edges of the panel area + # (spacing required by plotnine) + self.l = left_space(self.items) + self.r = right_space(self.items) + self.t = top_space(self.items) + self.b = bottom_space(self.items) + + def get_gridspec_params(self) -> GridSpecParams: + # Calculate the gridspec params + # (spacing required by mpl) + self.gsparams = self._calculate_panel_spacing() + + # Adjust the spacing parameters for the desired aspect ratio + # It is simpler to adjust for the aspect ratio than to calculate + # the final parameters that are true to the aspect ratio in + # one-short + if (ratio := self.plot.facet._aspect_ratio()) is not None: + current_ratio = self.aspect_ratio + if ratio > current_ratio: + # Increase aspect ratio, taller panels + self._reduce_width(ratio) + elif ratio < current_ratio: + # Increase aspect ratio, wider panels + self._reduce_height(ratio) + + return self.gsparams + + @property + def plot_width(self) -> float: + """ + Width [figure dimensions] of the whole plot + """ + return float(self.plot._gridspec.width) + + @property + def plot_height(self) -> float: + """ + Height [figure dimensions] of the whole plot + """ + return float(self.plot._gridspec.height) + + @property + def panel_width(self) -> float: + """ + Width [figure dimensions] of panels + """ + return self.r.panel_right - self.l.panel_left + + @property + def panel_height(self) -> float: + """ + Height [figure dimensions] of panels + """ + return self.t.panel_top - self.b.panel_bottom + + def increase_horizontal_plot_margin(self, dw: float): + """ + Increase the plot_margin to the right & left of the panels + """ + self.l.plot_margin += dw + self.r.plot_margin += dw + + def increase_vertical_plot_margin(self, dh: float): + """ + Increase the plot_margin to the above & below of the panels + """ + self.t.plot_margin += dh + self.b.plot_margin += dh + + @property + def plot_area_coordinates( + self, + ) -> tuple[tuple[float, float], tuple[float, float]]: + """ + Lower-left and upper-right coordinates of the plot area + + This is the area surrounded by the plot_margin. + """ + x1, x2 = self.l.x2("plot_margin"), self.r.x1("plot_margin") + y1, y2 = self.b.y2("plot_margin"), self.t.y1("plot_margin") + return ((x1, y1), (x2, y2)) + + @property + def panel_area_coordinates( + self, + ) -> tuple[tuple[float, float], tuple[float, float]]: + """ + Lower-left and upper-right coordinates of the panel area + + This is the area in which the panels are drawn. + """ + x1, x2 = self.l.panel_left, self.r.panel_right + y1, y2 = self.b.panel_bottom, self.t.panel_top + return ((x1, y1), (x2, y2)) + + def _calculate_panel_spacing(self) -> GridSpecParams: + """ + Spacing between the panels (wspace & hspace) + + Both spaces are calculated from a fraction of the width. + This ensures that the same fraction gives equals space + in both directions. + """ + if isinstance(self.plot.facet, facet_wrap): + wspace, hspace = self._calculate_panel_spacing_facet_wrap() + elif isinstance(self.plot.facet, facet_grid): + wspace, hspace = self._calculate_panel_spacing_facet_grid() + elif isinstance(self.plot.facet, facet_null): + wspace, hspace = self._calculate_panel_spacing_facet_null() + else: + raise TypeError(f"Unknown type of facet: {type(self.plot.facet)}") + + return GridSpecParams( + self.l.panel_left_relative, + self.r.panel_right_relative, + self.t.panel_top_relative, + self.b.panel_bottom_relative, + wspace, + hspace, + ) + + def _calculate_panel_spacing_facet_grid(self) -> tuple[float, float]: + """ + Calculate spacing parts for facet_grid + """ + theme = self.plot.theme + + ncol = self.plot.facet.ncol + nrow = self.plot.facet.nrow + + left, right = self.l.panel_left, self.r.panel_right + top, bottom = self.t.panel_top, self.b.panel_bottom + + # Both spacings are specified as fractions of the figure width + # Multiply the vertical by (W/H) so that the gullies along both + # directions are equally spaced. + self.sw = theme.getp("panel_spacing_x") + self.sh = theme.getp("panel_spacing_y") * self.W / self.H + + # width and height of axes as fraction of figure width & height + self.w = ((right - left) - self.sw * (ncol - 1)) / ncol + self.h = ((top - bottom) - self.sh * (nrow - 1)) / nrow + + # Spacing as fraction of axes width & height + wspace = self.sw / self.w + hspace = self.sh / self.h + return (wspace, hspace) + + def _calculate_panel_spacing_facet_wrap(self) -> tuple[float, float]: + """ + Calculate spacing parts for facet_wrap + """ + facet = self.plot.facet + theme = self.plot.theme + + ncol = facet.ncol + nrow = facet.nrow + + left, right = self.l.panel_left, self.r.panel_right + top, bottom = self.t.panel_top, self.b.panel_bottom + + # Both spacings are specified as fractions of the figure width + self.sw = theme.getp("panel_spacing_x") + self.sh = theme.getp("panel_spacing_y") * self.W / self.H + + # A fraction of the strip height + # Effectively slides the strip + # +ve: Away from the panel + # 0: Top of the panel + # -ve: Into the panel + # Where values <= -1, put the strip completely into + # the panel. We do not worry about larger -ves. + strip_align_x = theme.getp("strip_align_x") + + # Only interested in the proportion of the strip that + # does not overlap with the panel + if strip_align_x > -1: + self.sh += self.t.strip_text_x_extra_height * (1 + strip_align_x) + + if facet.free["x"]: + self.sh += self.items.axis_text_x_max_height_at( + "all" + ) + self.items.axis_ticks_x_max_height_at("all") + if facet.free["y"]: + self.sw += self.items.axis_text_y_max_width_at( + "all" + ) + self.items.axis_ticks_y_max_width_at("all") + + # width and height of axes as fraction of figure width & height + self.w = ((right - left) - self.sw * (ncol - 1)) / ncol + self.h = ((top - bottom) - self.sh * (nrow - 1)) / nrow + + # Spacing as fraction of axes width & height + wspace = self.sw / self.w + hspace = self.sh / self.h + return (wspace, hspace) + + def _calculate_panel_spacing_facet_null(self) -> tuple[float, float]: + """ + Calculate spacing parts for facet_null + """ + self.w = self.r.panel_right - self.l.panel_left + self.h = self.t.panel_top - self.b.panel_bottom + self.sw = 0 + self.sh = 0 + return 0, 0 + + def _reduce_height(self, ratio: float): + """ + Reduce the height of axes to get the aspect ratio + """ + # 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 + self.gsparams.top -= dh + self.gsparams.bottom += dh + self.gsparams.hspace = self.sh / h1 + + # Add more vertical plot margin + self.increase_vertical_plot_margin(dh) + + def _reduce_width(self, ratio: float): + """ + Reduce the width of axes to get the aspect ratio + """ + # 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 + self.gsparams.left += dw + self.gsparams.right -= dw + self.gsparams.wspace = self.sw / w1 + + # Add more horizontal margin + self.increase_horizontal_plot_margin(dw) + + @property + def aspect_ratio(self) -> float: + """ + Default aspect ratio of the panels + """ + return (self.h * self.H) / (self.w * self.W) + + @cached_property + def gs(self) -> p9GridSpec: + """ + The gridspec + """ + return self.plot._gridspec + + def to_figure_space( + self, + position: tuple[float, float], + ) -> tuple[float, float]: + """ + Convert position from gridspec space to figure space + """ + _x, _y = position + x = self.l.plot_left + (self.r.plot_right - self.l.plot_left) * _x + y = self.b.plot_bottom + (self.t.plot_top - self.b.plot_bottom) * _y + return (x, y) diff --git a/plotnine/_mpl/layout_manager/_side_space.py b/plotnine/_mpl/layout_manager/_side_space.py index 9dc8cc2e09..2cc487df80 100644 --- a/plotnine/_mpl/layout_manager/_side_space.py +++ b/plotnine/_mpl/layout_manager/_side_space.py @@ -11,25 +11,19 @@ from __future__ import annotations -from abc import ABC -from dataclasses import dataclass, field, fields +from abc import ABC, abstractmethod +from dataclasses import dataclass, fields from functools import cached_property from typing import TYPE_CHECKING, cast -from plotnine.exceptions import PlotnineError -from plotnine.facets import facet_grid, facet_null, facet_wrap - -from ._layout_items import LayoutItems - if TYPE_CHECKING: from dataclasses import Field from typing import Generator - from plotnine import ggplot from plotnine._mpl.gridspec import p9GridSpec - from plotnine.iapi import outside_legend from plotnine.typing import Side + # Note # Margins around the plot are specified in figure coordinates # We interpret that value to be a fraction of the width. So along @@ -70,10 +64,8 @@ class _side_space(ABC): The amount of space for each artist is computed in figure coordinates. """ - items: LayoutItems - def __post_init__(self): - self.side: Side = cast("Side", self.__class__.__name__[:-6]) + self.side: Side = cast("Side", self.__class__.__name__.split("_")[0]) """ Side of the panel(s) that this class applies to """ @@ -122,40 +114,11 @@ def _fields_upto(item: str) -> Generator[Field, None, None]: return sum(getattr(self, f.name) for f in _fields_upto(item)) @cached_property - def _legend_size(self) -> tuple[float, float]: - """ - Return size of legend in figure coordinates - - We need this to accurately justify the legend by proportional - values e.g. 0.2, instead of just left, right, top, bottom & - center. - """ - if not self.has_legend: - return (0, 0) - - ol: outside_legend = getattr(self.items.legends, self.side) - return self.items.geometry.size(ol.box) - - @cached_property - def legend_width(self) -> float: - """ - Return width of legend in figure coordinates - """ - return self._legend_size[0] - - @cached_property - def legend_height(self) -> float: - """ - Return height of legend in figure coordinates - """ - return self._legend_size[1] - - @cached_property + @abstractmethod def gs(self) -> p9GridSpec: """ - The gridspec of the plot + The gridspec of the plot or composition """ - return self.items.plot._gridspec @property def offset(self) -> float: @@ -205,958 +168,3 @@ def to_figure_space(self, rel_value: float) -> float: Position relative to the position of the gridspec """ return self.offset + rel_value - - @property - def has_tag(self) -> bool: - """ - Return True if the space/margin to this side of the panel has a tag - - If it does, then it will be included in the layout - """ - getp = self.items.plot.theme.getp - return getp("plot_tag_location") == "margin" and self.side in getp( - "plot_tag_position" - ) - - @property - def has_legend(self) -> bool: - """ - Return True if the space/margin to this side of the panel has a legend - - If it does, then it will be included in the layout - """ - if not self.items.legends: - return False - return hasattr(self.items.legends, self.side) - - @property - def tag_width(self) -> float: - """ - The width of the tag including the margins - - The value is zero except if all these are true: - - The tag is in the margin `theme(plot_tag_position = "margin")` - - The tag at one one of the the following locations; - left, right, topleft, topright, bottomleft or bottomright - """ - return 0 - - @property - def tag_height(self) -> float: - """ - The height of the tag including the margins - - The value is zero except if all these are true: - - The tag is in the margin `theme(plot_tag_position = "margin")` - - The tag at one one of the the following locations; - top, bottom, topleft, topright, bottomleft or bottomright - """ - return 0 - - @property - def axis_title_clearance(self) -> float: - """ - The distance between the axis title and the panel - - Figure - ---------------------------- - | Panel | - | ----------- | - | | | | - | | | | - | Y<--->| | | - | | | | - | | | | - | ----------- | - | | - ---------------------------- - - We use this value to when aligning axis titles in a - plot composition. - """ - - try: - return self.total - self.sum_upto("axis_title_alignment") - except AttributeError as err: - # There is probably an error in in the layout manager - raise PlotnineError("Side has no axis title") from err - - -@dataclass -class left_space(_side_space): - """ - Space in the figure for artists on the left of the panel area - - Ordered from the edge of the figure and going inwards - """ - - plot_margin: float = 0 - tag_alignment: float = 0 - """ - Space added to align the tag in this plot with others in a composition - - This value is calculated during the layout process, and it ensures that - all tags on this side of the plot take up the same amount of space in - the margin. e.g. from - - ------------------------------------ - | plot_margin | tag | artists | - |------------------------------------| - | plot_margin | A long tag | artists | - ------------------------------------ - - to - - ------------------------------------ - | plot_margin | tag | artists | - |------------------------------------| - | plot_margin | A long tag | artists | - ------------------------------------ - - And the tag is justified within that space e.g if ha="left" we get - - ------------------------------------ - | plot_margin | tag | artists | - |------------------------------------| - | plot_margin | A long tag | artists | - ------------------------------------ - - So, contrary to the order in which the space items are laid out, the - tag_alignment does not necessarily come before the plot_tag. - """ - plot_tag_margin_left: float = 0 - plot_tag: float = 0 - plot_tag_margin_right: float = 0 - margin_alignment: float = 0 - """ - Space added to align this plot with others in a composition - - This value is calculated during the layout process in a tree structure - that has convenient access to the sides/edges of the panels in the - composition. - """ - legend: float = 0 - legend_box_spacing: float = 0 - axis_title_y_margin_left: float = 0 - axis_title_y: float = 0 - axis_title_y_margin_right: float = 0 - axis_title_alignment: float = 0 - """ - Space added to align the axis title with others in a composition - - This value is calculated during the layout process. The amount is - the difference between the largest and smallest axis_title_clearance - among the items in the composition. - """ - axis_text_y_margin_left: float = 0 - axis_text_y: float = 0 - axis_text_y_margin_right: float = 0 - axis_ticks_y: float = 0 - - def _calculate(self): - theme = self.items.plot.theme - geometry = self.items.geometry - items = self.items - - self.plot_margin = theme.getp("plot_margin_left") - - if self.has_tag and items.plot_tag: - m = theme.get_margin("plot_tag").fig - self.plot_tag_margin_left = m.l - self.plot_tag = geometry.width(items.plot_tag) - self.plot_tag_margin_right = m.r - - if items.legends and items.legends.left: - self.legend = self.legend_width - self.legend_box_spacing = theme.getp("legend_box_spacing") - - if items.axis_title_y: - m = theme.get_margin("axis_title_y").fig - self.axis_title_y_margin_left = m.l - self.axis_title_y = geometry.width(items.axis_title_y) - self.axis_title_y_margin_right = m.r - - # Account for the space consumed by the axis - self.axis_text_y = items.axis_text_y_max_width_at("first_col") - if self.axis_text_y: - m = theme.get_margin("axis_text_y").fig - self.axis_text_y_margin_left = m.l - self.axis_text_y_margin_right = m.r - - self.axis_ticks_y = items.axis_ticks_y_max_width_at("first_col") - - # Adjust plot_margin to make room for ylabels that protude well - # beyond the axes - # NOTE: This adjustment breaks down when the protrusion is large - protrusion = items.axis_text_x_left_protrusion("all") - adjustment = protrusion - (self.total - self.plot_margin) - if adjustment > 0: - self.plot_margin += adjustment - - @property - def offset(self) -> float: - """ - Distance from left of the figure to the left of the plot gridspec - - ----------------(1, 1) - | ---- | - | dx | | | - |<--->| | | - | | | | - | ---- | - (0, 0)---------------- - - """ - return self.gs.bbox_relative.x0 - - def x1(self, item: str) -> float: - """ - Lower x-coordinate in figure space of the item - """ - return self.to_figure_space(self.sum_upto(item)) - - def x2(self, item: str) -> float: - """ - Higher x-coordinate in figure space of the item - """ - return self.to_figure_space(self.sum_incl(item)) - - @property - def panel_left_relative(self): - """ - Left (relative to the gridspec) of the panels in figure dimensions - """ - return self.total - - @property - def panel_left(self): - """ - Left of the panels in figure space - """ - return self.to_figure_space(self.panel_left_relative) - - @property - def plot_left(self): - """ - Distance up to the left-most artist in figure space - """ - return self.x1("legend") - - @property - def tag_width(self): - """ - The width of the tag including the margins - """ - return ( - self.plot_tag_margin_left - + self.plot_tag - + self.plot_tag_margin_right - ) - - -@dataclass -class right_space(_side_space): - """ - Space in the figure for artists on the right of the panel area - - Ordered from the edge of the figure and going inwards - """ - - plot_margin: float = 0 - tag_alignment: float = 0 - plot_tag_margin_right: float = 0 - plot_tag: float = 0 - plot_tag_margin_left: float = 0 - margin_alignment: float = 0 - legend: float = 0 - legend_box_spacing: float = 0 - strip_text_y_extra_width: float = 0 - - def _calculate(self): - items = self.items - theme = self.items.plot.theme - geometry = self.items.geometry - - self.plot_margin = theme.getp("plot_margin_right") - - if self.has_tag and items.plot_tag: - m = theme.get_margin("plot_tag").fig - self.plot_tag_margin_right = m.r - self.plot_tag = geometry.width(items.plot_tag) - self.plot_tag_margin_left = m.l - - if items.legends and items.legends.right: - self.legend = self.legend_width - self.legend_box_spacing = theme.getp("legend_box_spacing") - - self.strip_text_y_extra_width = items.strip_text_y_extra_width("right") - - # Adjust plot_margin to make room for ylabels that protude well - # beyond the axes - # NOTE: This adjustment breaks down when the protrusion is large - protrusion = items.axis_text_x_right_protrusion("all") - adjustment = protrusion - (self.total - self.plot_margin) - if adjustment > 0: - self.plot_margin += adjustment - - @property - def offset(self): - """ - Distance from right of the figure to the right of the plot gridspec - - ---------------(1, 1) - | ---- | - | | | -dx | - | | |<--->| - | | | | - | ---- | - (0, 0)--------------- - - """ - return self.gs.bbox_relative.x1 - 1 - - def x1(self, item: str) -> float: - """ - Lower x-coordinate in figure space of the item - """ - return self.to_figure_space(1 - self.sum_incl(item)) - - def x2(self, item: str) -> float: - """ - Higher x-coordinate in figure space of the item - """ - return self.to_figure_space(1 - self.sum_upto(item)) - - @property - def panel_right_relative(self): - """ - Right (relative to the gridspec) of the panels in figure dimensions - """ - return 1 - self.total - - @property - def panel_right(self): - """ - Right of the panels in figure space - """ - return self.to_figure_space(self.panel_right_relative) - - @property - def plot_right(self): - """ - Distance up to the right-most artist in figure space - """ - return self.x2("legend") - - @property - def tag_width(self): - """ - The width of the tag including the margins - """ - return ( - self.plot_tag_margin_right - + self.plot_tag - + self.plot_tag_margin_left - ) - - -@dataclass -class top_space(_side_space): - """ - Space in the figure for artists above the panel area - - Ordered from the edge of the figure and going inwards - """ - - plot_margin: float = 0 - tag_alignment: float = 0 - plot_tag_margin_top: float = 0 - plot_tag: float = 0 - plot_tag_margin_bottom: float = 0 - margin_alignment: float = 0 - plot_title_margin_top: float = 0 - plot_title: float = 0 - plot_title_margin_bottom: float = 0 - plot_subtitle_margin_top: float = 0 - plot_subtitle: float = 0 - plot_subtitle_margin_bottom: float = 0 - legend: float = 0 - legend_box_spacing: float = 0 - strip_text_x_extra_height: float = 0 - - def _calculate(self): - items = self.items - theme = self.items.plot.theme - geometry = self.items.geometry - W, H = theme.getp("figure_size") - F = W / H - - self.plot_margin = theme.getp("plot_margin_top") * F - - if self.has_tag and items.plot_tag: - m = theme.get_margin("plot_tag").fig - self.plot_tag_margin_top = m.t - self.plot_tag = geometry.height(items.plot_tag) - self.plot_tag_margin_bottom = m.b - - if items.plot_title: - m = theme.get_margin("plot_title").fig - self.plot_title_margin_top = m.t * F - self.plot_title = geometry.height(items.plot_title) - self.plot_title_margin_bottom = m.b * F - - if items.plot_subtitle: - m = theme.get_margin("plot_subtitle").fig - self.plot_subtitle_margin_top = m.t * F - self.plot_subtitle = geometry.height(items.plot_subtitle) - self.plot_subtitle_margin_bottom = m.b * F - - if items.legends and items.legends.top: - self.legend = self.legend_height - self.legend_box_spacing = theme.getp("legend_box_spacing") * F - - self.strip_text_x_extra_height = items.strip_text_x_extra_height("top") - - # Adjust plot_margin to make room for ylabels that protude well - # beyond the axes - # NOTE: This adjustment breaks down when the protrusion is large - protrusion = items.axis_text_y_top_protrusion("all") - adjustment = protrusion - (self.total - self.plot_margin) - if adjustment > 0: - self.plot_margin += adjustment - - @property - def offset(self) -> float: - """ - Distance from top of the figure to the top of the plot gridspec - - ----------------(1, 1) - | ^ | - | |-dy | - | v | - | ---- | - | | | | - | | | | - | | | | - | ---- | - | | - (0, 0)---------------- - """ - return self.gs.bbox_relative.y1 - 1 - - def y1(self, item: str) -> float: - """ - Lower y-coordinate in figure space of the item - """ - return self.to_figure_space(1 - self.sum_incl(item)) - - def y2(self, item: str) -> float: - """ - Higher y-coordinate in figure space of the item - """ - return self.to_figure_space(1 - self.sum_upto(item)) - - @property - def panel_top_relative(self): - """ - Top (relative to the gridspec) of the panels in figure dimensions - """ - return 1 - self.total - - @property - def panel_top(self): - """ - Top of the panels in figure space - """ - return self.to_figure_space(self.panel_top_relative) - - @property - def plot_top(self): - """ - Distance up to the top-most artist in figure space - """ - return self.y2("legend") - - @property - def tag_height(self): - """ - The height of the tag including the margins - """ - return ( - self.plot_tag_margin_top - + self.plot_tag - + self.plot_tag_margin_bottom - ) - - -@dataclass -class bottom_space(_side_space): - """ - Space in the figure for artists below the panel area - - Ordered from the edge of the figure and going inwards - """ - - plot_margin: float = 0 - tag_alignment: float = 0 - plot_tag_margin_bottom: float = 0 - plot_tag: float = 0 - plot_tag_margin_top: float = 0 - margin_alignment: float = 0 - plot_caption_margin_bottom: float = 0 - plot_caption: float = 0 - plot_caption_margin_top: float = 0 - legend: float = 0 - legend_box_spacing: float = 0 - axis_title_x_margin_bottom: float = 0 - axis_title_x: float = 0 - axis_title_x_margin_top: float = 0 - axis_title_alignment: float = 0 - """ - Space added to align the axis title with others in a composition - - This value is calculated during the layout process in a tree structure - that has convenient access to the sides/edges of the panels in the - composition. It's amount is the difference in height between this axis - text (and it's margins) and the tallest axis text (and it's margin). - """ - axis_text_x_margin_bottom: float = 0 - axis_text_x: float = 0 - axis_text_x_margin_top: float = 0 - axis_ticks_x: float = 0 - - def _calculate(self): - items = self.items - theme = self.items.plot.theme - geometry = self.items.geometry - W, H = theme.getp("figure_size") - F = W / H - - self.plot_margin = theme.getp("plot_margin_bottom") * F - - if self.has_tag and items.plot_tag: - m = theme.get_margin("plot_tag").fig - self.plot_tag_margin_bottom = m.b - self.plot_tag = geometry.height(items.plot_tag) - self.plot_tag_margin_top = m.t - - if items.plot_caption: - m = theme.get_margin("plot_caption").fig - self.plot_caption_margin_bottom = m.b * F - self.plot_caption = geometry.height(items.plot_caption) - self.plot_caption_margin_top = m.t * F - - if items.legends and items.legends.bottom: - self.legend = self.legend_height - self.legend_box_spacing = theme.getp("legend_box_spacing") * F - - if items.axis_title_x: - m = theme.get_margin("axis_title_x").fig - self.axis_title_x_margin_bottom = m.b * F - self.axis_title_x = geometry.height(items.axis_title_x) - self.axis_title_x_margin_top = m.t * F - - # Account for the space consumed by the axis - self.axis_text_x = items.axis_text_x_max_height_at("last_row") - if self.axis_text_x: - m = theme.get_margin("axis_text_x").fig - self.axis_text_x_margin_bottom = m.b - self.axis_text_x_margin_top = m.t - self.axis_ticks_x = items.axis_ticks_x_max_height_at("last_row") - - # Adjust plot_margin to make room for ylabels that protude well - # beyond the axes - # NOTE: This adjustment breaks down when the protrusion is large - protrusion = items.axis_text_y_bottom_protrusion("all") - adjustment = protrusion - (self.total - self.plot_margin) - if adjustment > 0: - self.plot_margin += adjustment - - @property - def offset(self) -> float: - """ - Distance from bottom of the figure to the bottom of the plot gridspec - - ----------------(1, 1) - | | - | ---- | - | | | | - | | | | - | | | | - | ---- | - | ^ | - | |dy | - | v | - (0, 0)---------------- - """ - return self.gs.bbox_relative.y0 - - def y1(self, item: str) -> float: - """ - Lower y-coordinate in figure space of the item - """ - return self.to_figure_space(self.sum_upto(item)) - - def y2(self, item: str) -> float: - """ - Higher y-coordinate in figure space of the item - """ - return self.to_figure_space(self.sum_incl(item)) - - @property - def panel_bottom_relative(self): - """ - Bottom (relative to the gridspec) of the panels in figure dimensions - """ - return self.total - - @property - def panel_bottom(self): - """ - Bottom of the panels in figure space - """ - return self.to_figure_space(self.panel_bottom_relative) - - @property - def plot_bottom(self): - """ - Distance up to the bottom-most artist in figure space - """ - return self.y1("legend") - - @property - def tag_height(self): - """ - The height of the tag including the margins - """ - return ( - self.plot_tag_margin_bottom - + self.plot_tag - + self.plot_tag_margin_top - ) - - -@dataclass -class LayoutSpaces: - """ - Compute the all the spaces required in the layout - - These are: - - 1. The space of each artist between the panel and the edge of the - figure. - 2. The space in-between the panels - - From these values, we put together the grid-spec parameters required - by matplotblib to position the axes. We also use the values to adjust - the coordinates of all the artists that occupy these spaces, placing - them in their final positions. - """ - - plot: ggplot - - l: left_space = field(init=False) - """All subspaces to the left of the panels""" - - r: right_space = field(init=False) - """All subspaces to the right of the panels""" - - t: top_space = field(init=False) - """All subspaces above the top of the panels""" - - b: bottom_space = field(init=False) - """All subspaces below the bottom of the panels""" - - W: float = field(init=False, default=0) - """Figure Width [inches]""" - - H: float = field(init=False, default=0) - """Figure Height [inches]""" - - w: float = field(init=False, default=0) - """Axes width w.r.t figure in [0, 1]""" - - h: float = field(init=False, default=0) - """Axes height w.r.t figure in [0, 1]""" - - sh: float = field(init=False, default=0) - """horizontal spacing btn panels w.r.t figure""" - - sw: float = field(init=False, default=0) - """vertical spacing btn panels w.r.t figure""" - - gsparams: GridSpecParams = field(init=False, repr=False) - """Grid spacing btn panels w.r.t figure""" - - def __post_init__(self): - self.items = LayoutItems(self.plot) - self.W, self.H = self.plot.theme.getp("figure_size") - - # Calculate the spacing along the edges of the panel area - # (spacing required by plotnine) - self.l = left_space(self.items) - self.r = right_space(self.items) - self.t = top_space(self.items) - self.b = bottom_space(self.items) - - def get_gridspec_params(self) -> GridSpecParams: - # Calculate the gridspec params - # (spacing required by mpl) - self.gsparams = self._calculate_panel_spacing() - - # Adjust the spacing parameters for the desired aspect ratio - # It is simpler to adjust for the aspect ratio than to calculate - # the final parameters that are true to the aspect ratio in - # one-short - if (ratio := self.plot.facet._aspect_ratio()) is not None: - current_ratio = self.aspect_ratio - if ratio > current_ratio: - # Increase aspect ratio, taller panels - self._reduce_width(ratio) - elif ratio < current_ratio: - # Increase aspect ratio, wider panels - self._reduce_height(ratio) - - return self.gsparams - - @property - def plot_width(self) -> float: - """ - Width [figure dimensions] of the whole plot - """ - return float(self.plot._gridspec.width) - - @property - def plot_height(self) -> float: - """ - Height [figure dimensions] of the whole plot - """ - return float(self.plot._gridspec.height) - - @property - def panel_width(self) -> float: - """ - Width [figure dimensions] of panels - """ - return self.r.panel_right - self.l.panel_left - - @property - def panel_height(self) -> float: - """ - Height [figure dimensions] of panels - """ - return self.t.panel_top - self.b.panel_bottom - - def increase_horizontal_plot_margin(self, dw: float): - """ - Increase the plot_margin to the right & left of the panels - """ - self.l.plot_margin += dw - self.r.plot_margin += dw - - def increase_vertical_plot_margin(self, dh: float): - """ - Increase the plot_margin to the above & below of the panels - """ - self.t.plot_margin += dh - self.b.plot_margin += dh - - @property - def plot_area_coordinates( - self, - ) -> tuple[tuple[float, float], tuple[float, float]]: - """ - Lower-left and upper-right coordinates of the plot area - - This is the area surrounded by the plot_margin. - """ - x1, x2 = self.l.x2("plot_margin"), self.r.x1("plot_margin") - y1, y2 = self.b.y2("plot_margin"), self.t.y1("plot_margin") - return ((x1, y1), (x2, y2)) - - @property - def panel_area_coordinates( - self, - ) -> tuple[tuple[float, float], tuple[float, float]]: - """ - Lower-left and upper-right coordinates of the panel area - - This is the area in which the panels are drawn. - """ - x1, x2 = self.l.panel_left, self.r.panel_right - y1, y2 = self.b.panel_bottom, self.t.panel_top - return ((x1, y1), (x2, y2)) - - def _calculate_panel_spacing(self) -> GridSpecParams: - """ - Spacing between the panels (wspace & hspace) - - Both spaces are calculated from a fraction of the width. - This ensures that the same fraction gives equals space - in both directions. - """ - if isinstance(self.plot.facet, facet_wrap): - wspace, hspace = self._calculate_panel_spacing_facet_wrap() - elif isinstance(self.plot.facet, facet_grid): - wspace, hspace = self._calculate_panel_spacing_facet_grid() - elif isinstance(self.plot.facet, facet_null): - wspace, hspace = self._calculate_panel_spacing_facet_null() - else: - raise TypeError(f"Unknown type of facet: {type(self.plot.facet)}") - - return GridSpecParams( - self.l.panel_left_relative, - self.r.panel_right_relative, - self.t.panel_top_relative, - self.b.panel_bottom_relative, - wspace, - hspace, - ) - - def _calculate_panel_spacing_facet_grid(self) -> tuple[float, float]: - """ - Calculate spacing parts for facet_grid - """ - theme = self.plot.theme - - ncol = self.plot.facet.ncol - nrow = self.plot.facet.nrow - - left, right = self.l.panel_left, self.r.panel_right - top, bottom = self.t.panel_top, self.b.panel_bottom - - # Both spacings are specified as fractions of the figure width - # Multiply the vertical by (W/H) so that the gullies along both - # directions are equally spaced. - self.sw = theme.getp("panel_spacing_x") - self.sh = theme.getp("panel_spacing_y") * self.W / self.H - - # width and height of axes as fraction of figure width & height - self.w = ((right - left) - self.sw * (ncol - 1)) / ncol - self.h = ((top - bottom) - self.sh * (nrow - 1)) / nrow - - # Spacing as fraction of axes width & height - wspace = self.sw / self.w - hspace = self.sh / self.h - return (wspace, hspace) - - def _calculate_panel_spacing_facet_wrap(self) -> tuple[float, float]: - """ - Calculate spacing parts for facet_wrap - """ - facet = self.plot.facet - theme = self.plot.theme - - ncol = facet.ncol - nrow = facet.nrow - - left, right = self.l.panel_left, self.r.panel_right - top, bottom = self.t.panel_top, self.b.panel_bottom - - # Both spacings are specified as fractions of the figure width - self.sw = theme.getp("panel_spacing_x") - self.sh = theme.getp("panel_spacing_y") * self.W / self.H - - # A fraction of the strip height - # Effectively slides the strip - # +ve: Away from the panel - # 0: Top of the panel - # -ve: Into the panel - # Where values <= -1, put the strip completely into - # the panel. We do not worry about larger -ves. - strip_align_x = theme.getp("strip_align_x") - - # Only interested in the proportion of the strip that - # does not overlap with the panel - if strip_align_x > -1: - self.sh += self.t.strip_text_x_extra_height * (1 + strip_align_x) - - if facet.free["x"]: - self.sh += self.items.axis_text_x_max_height_at( - "all" - ) + self.items.axis_ticks_x_max_height_at("all") - if facet.free["y"]: - self.sw += self.items.axis_text_y_max_width_at( - "all" - ) + self.items.axis_ticks_y_max_width_at("all") - - # width and height of axes as fraction of figure width & height - self.w = ((right - left) - self.sw * (ncol - 1)) / ncol - self.h = ((top - bottom) - self.sh * (nrow - 1)) / nrow - - # Spacing as fraction of axes width & height - wspace = self.sw / self.w - hspace = self.sh / self.h - return (wspace, hspace) - - def _calculate_panel_spacing_facet_null(self) -> tuple[float, float]: - """ - Calculate spacing parts for facet_null - """ - self.w = self.r.panel_right - self.l.panel_left - self.h = self.t.panel_top - self.b.panel_bottom - self.sw = 0 - self.sh = 0 - return 0, 0 - - def _reduce_height(self, ratio: float): - """ - Reduce the height of axes to get the aspect ratio - """ - # 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 - self.gsparams.top -= dh - self.gsparams.bottom += dh - self.gsparams.hspace = self.sh / h1 - - # Add more vertical plot margin - self.increase_vertical_plot_margin(dh) - - def _reduce_width(self, ratio: float): - """ - Reduce the width of axes to get the aspect ratio - """ - # 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 - self.gsparams.left += dw - self.gsparams.right -= dw - self.gsparams.wspace = self.sw / w1 - - # Add more horizontal margin - self.increase_horizontal_plot_margin(dw) - - @property - def aspect_ratio(self) -> float: - """ - Default aspect ratio of the panels - """ - return (self.h * self.H) / (self.w * self.W) - - @cached_property - def gs(self) -> p9GridSpec: - """ - The gridspec - """ - return self.plot._gridspec - - def to_figure_space( - self, - position: tuple[float, float], - ) -> tuple[float, float]: - """ - Convert position from gridspec space to figure space - """ - _x, _y = position - x = self.l.plot_left + (self.r.plot_right - self.l.plot_left) * _x - y = self.b.plot_bottom + (self.t.plot_top - self.b.plot_bottom) * _y - return (x, y) From 1d11c39793a865bd58d8020751cc12cf0bfd5ba9 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Sun, 28 Sep 2025 00:38:21 -0700 Subject: [PATCH 07/24] Refactor to use CompositionItems This allows us to associate gridspecs more consistently, where the a plots and compositions can both a "container" 1x1 gridspec. So that composition items are to a composition what facets(panels) are to a plot. --- plotnine/_mpl/layout_manager/_layout_tree.py | 2 +- plotnine/composition/_beside.py | 2 - plotnine/composition/_compose.py | 117 ++++++++++--------- plotnine/composition/_plotspec.py | 18 +-- plotnine/composition/_stack.py | 2 - plotnine/composition/_types.py | 16 +++ 6 files changed, 82 insertions(+), 75 deletions(-) diff --git a/plotnine/_mpl/layout_manager/_layout_tree.py b/plotnine/_mpl/layout_manager/_layout_tree.py index 54915ce8db..6304f2c4c5 100644 --- a/plotnine/_mpl/layout_manager/_layout_tree.py +++ b/plotnine/_mpl/layout_manager/_layout_tree.py @@ -126,7 +126,7 @@ class LayoutTree: """ def __post_init__(self): - self.gridspec = self.cmp._gridspec + self.gridspec = self.cmp.items._gridspec self.grid = Grid["Node"]( self.nrow, self.ncol, diff --git a/plotnine/composition/_beside.py b/plotnine/composition/_beside.py index 260fc5658e..58570fa534 100644 --- a/plotnine/composition/_beside.py +++ b/plotnine/composition/_beside.py @@ -1,6 +1,5 @@ from __future__ import annotations -from dataclasses import dataclass from typing import TYPE_CHECKING from ._compose import Compose @@ -9,7 +8,6 @@ from plotnine.ggplot import ggplot -@dataclass(repr=False) class Beside(Compose): """ Place plots or compositions side by side diff --git a/plotnine/composition/_compose.py b/plotnine/composition/_compose.py index 21f2a34595..6d885ae25c 100644 --- a/plotnine/composition/_compose.py +++ b/plotnine/composition/_compose.py @@ -2,7 +2,6 @@ import abc from copy import copy, deepcopy -from dataclasses import dataclass, field from io import BytesIO from typing import TYPE_CHECKING, cast, overload @@ -15,7 +14,7 @@ from .._utils.quarto import is_knitr_engine, is_quarto_environment from ..composition._plot_annotation import plot_annotation from ..composition._plot_layout import plot_layout -from ..composition._types import ComposeAddable +from ..composition._types import ComposeAddable, CompositionItems from ..options import get_option from ._plotspec import plotspec @@ -30,7 +29,6 @@ from plotnine.typing import FigureFormat, MimeBundle -@dataclass class Compose: """ Base class for those that create plot compositions @@ -84,45 +82,61 @@ class Compose: : Add right hand side to the last plot in the composition. + Parameters + ---------- + items : + The objects to be arranged (composed) + + See Also -------- plotnine.composition.Beside : To arrange plots side by side plotnine.composition.Stack : To arrange plots vertically - plotnine.composition.Stack : To arrange in a grid + plotnine.composition.Wrap : To arrange in a grid plotnine.composition.plot_spacer : To add a blank space between plots """ - items: list[ggplot | Compose] - """ - The objects to be arranged (composed) - """ - - _layout: plot_layout = field( - init=False, repr=False, default_factory=plot_layout - ) + # These are created in the ._create_figure + figure: Figure + plotspecs: list[plotspec] + _gridspec: p9GridSpec """ - Every composition gets initiated with an empty plot_layout whose - attributes are either dynamically generated before the composition - is drawn, or they are overwritten by a layout added by the user. + Gridspec (1x1) that contains the annotations and the composition items + + ------------------- + | title |<----- This one + | subtitle | + | | + | ------------- | + | | | |<-+----- .items._gridspec + | | | | | + | ------------- | + | | + | caption | + ------------------- + + plot_layout's theme parameter affects this gridspec. """ - _annotation: plot_annotation = field( - init=False, repr=False, default_factory=plot_annotation - ) - - # These are created in the _create_figure method - figure: Figure = field(init=False, repr=False) - plotspecs: list[plotspec] = field(init=False, repr=False) - _gridspec: p9GridSpec = field(init=False, repr=False) - - def __post_init__(self): + def __init__(self, items: list[ggplot | Compose]): # The way we handle the plots has consequences that would # prevent having a duplicate plot in the composition. # Using copies prevents this. - self.items = [ - op if isinstance(op, Compose) else deepcopy(op) - for op in self.items - ] + self.items = CompositionItems( + [op if isinstance(op, Compose) else deepcopy(op) for op in items] + ) + + self._layout = plot_layout() + """ + Every composition gets initiated with an empty plot_layout whose + attributes are either dynamically generated before the composition + is drawn, or they are overwritten by a layout added by the user. + """ + + self._annotation = plot_annotation() + """ + The annotations around the composition + """ def __repr__(self): """ @@ -145,6 +159,7 @@ def layout(self) -> plot_layout: """ The plot_layout of this composition """ + self.items return self._layout @layout.setter @@ -393,7 +408,7 @@ def _create_gridspec(self, nest_into): self.layout._setup(self) self.annotation._setup(self) - self._gridspec = p9GridSpec.from_layout( + self.items._gridspec = p9GridSpec.from_layout( self.layout, figure=self.figure, nest_into=nest_into ) @@ -415,42 +430,36 @@ def _create_figure(self): figure = plt.figure() def _make_plotspecs( - cmp: Compose, parent_gridspec: p9GridSpec | None + cmp: Compose, _gridspec: p9GridSpec ) -> Generator[plotspec]: """ Return the plot specification for each subplot in the composition """ # This gridspec contains a composition group e.g. # (p2 | p3) of p1 | (p2 | p3) - ss_or_none = parent_gridspec[0] if parent_gridspec else None + ss_or_none = _gridspec[0] cmp.figure = figure cmp._create_gridspec(ss_or_none) - # Each subplot in the composition will contain one of: - # 1. A plot - # 2. A plot composition - # 3. Nothing # Iterating over the gridspec yields the SubplotSpecs for each - # "subplot" in the grid. The SubplotSpec is the handle that - # allows us to set it up for a plot or to nest another gridspec - # in it. - for item, subplot_spec in zip(cmp, cmp._gridspec): + # "subplot" in the grid. The SubplotSpec is the handle for the + # area in the grid; it allows us to put a plot or a nested + # composion in that area. + for item, subplot_spec in zip(cmp, cmp.items._gridspec): + # This container gs will contain a plot or a composition, + # i.e. it will be assigned to one of: + # 1. ggplot._gridspec + # 2. compose._gridspec + container_gs = p9GridSpec(1, 1, figure, nest_into=subplot_spec) if isinstance(item, ggplot): - yield plotspec( - item, - figure, - cmp._gridspec, - subplot_spec, - p9GridSpec(1, 1, figure, nest_into=subplot_spec), - ) - elif item: - yield from _make_plotspecs( - item, - p9GridSpec(1, 1, figure, nest_into=subplot_spec), - ) - - self.plotspecs = list(_make_plotspecs(self, None)) + yield plotspec(item, figure, container_gs) + else: + yield from _make_plotspecs(item, container_gs) + + self.plotspecs = list( + _make_plotspecs(self, p9GridSpec(1, 1, figure, nest_into=None)) + ) def _draw_plots(self): """ diff --git a/plotnine/composition/_plotspec.py b/plotnine/composition/_plotspec.py index 1895afad65..7a7a69f5bc 100644 --- a/plotnine/composition/_plotspec.py +++ b/plotnine/composition/_plotspec.py @@ -5,7 +5,6 @@ if TYPE_CHECKING: from matplotlib.figure import Figure - from matplotlib.gridspec import SubplotSpec from plotnine._mpl.gridspec import p9GridSpec from plotnine.ggplot import ggplot @@ -27,24 +26,11 @@ class plotspec: Figure in which the draw the plot """ - composition_gridspec: p9GridSpec - """ - The gridspec of the innermost composition group that contains the plot - """ - - subplotspec: SubplotSpec - """ - The subplotspec that contains the plot - - This is the subplot within the composition gridspec and it will - contain the plot's gridspec. - """ - - plot_gridspec: p9GridSpec + gridspec: p9GridSpec """ The gridspec in which the plot is drawn """ def __post_init__(self): self.plot.figure = self.figure - self.plot._gridspec = self.plot_gridspec + self.plot._gridspec = self.gridspec diff --git a/plotnine/composition/_stack.py b/plotnine/composition/_stack.py index 87b3853f14..0a5ffa99b0 100644 --- a/plotnine/composition/_stack.py +++ b/plotnine/composition/_stack.py @@ -1,6 +1,5 @@ from __future__ import annotations -from dataclasses import dataclass from typing import TYPE_CHECKING from ._compose import Compose @@ -9,7 +8,6 @@ from plotnine.ggplot import ggplot -@dataclass(repr=False) class Stack(Compose): """ Place plots or compositions on top of each other diff --git a/plotnine/composition/_types.py b/plotnine/composition/_types.py index bb95739536..a7d94e07f5 100644 --- a/plotnine/composition/_types.py +++ b/plotnine/composition/_types.py @@ -3,6 +3,9 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: + from plotnine import ggplot + from plotnine._mpl.gridspec import p9GridSpec + from ._compose import Compose @@ -26,3 +29,16 @@ def __radd__(self, other: Compose) -> Compose: Compose object """ return other + + +class CompositionItems(list["ggplot | Compose"]): + """ + The items in a composition + """ + + _gridspec: p9GridSpec + """ + Gridspec (nxm) that contains the composition items + + plot_layout's widths & heights parameters affect this gridspec. + """ From 3d8752ef982177d7b3227b584176ac4c73c9af1f Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 16 Oct 2025 12:11:00 +0200 Subject: [PATCH 08/24] Make TextJustifier independent of the Spaces --- plotnine/_mpl/layout_manager/_layout_items.py | 131 +++-------------- plotnine/_mpl/utils.py | 137 +++++++++++++++++- 2 files changed, 155 insertions(+), 113 deletions(-) diff --git a/plotnine/_mpl/layout_manager/_layout_items.py b/plotnine/_mpl/layout_manager/_layout_items.py index ea0c098a3a..fcd945b223 100644 --- a/plotnine/_mpl/layout_manager/_layout_items.py +++ b/plotnine/_mpl/layout_manager/_layout_items.py @@ -7,11 +7,12 @@ from matplotlib.text import Text from plotnine._mpl.patches import StripTextPatch -from plotnine._utils import ha_as_float, va_as_float from plotnine.exceptions import PlotnineError from ..utils import ( ArtistGeometry, + JustifyBoundaries, + TextJustifier, get_subplotspecs, rel_position, ) @@ -34,9 +35,7 @@ from plotnine.iapi import legend_artists from plotnine.themes.elements import margin as Margin from plotnine.typing import ( - HorizontalJustification, StripPosition, - VerticalJustification, ) from ._plot_side_space import LayoutSpaces @@ -349,7 +348,7 @@ def _adjust_positions(self, spaces: LayoutSpaces): theme = self.plot.theme plot_title_position = theme.getp("plot_title_position", "panel") plot_caption_position = theme.getp("plot_caption_position", "panel") - justify = TextJustifier(spaces) + justify = PlotTextJustifier(spaces) if self.plot_tag: set_plot_tag_position(self.plot_tag, spaces) @@ -393,7 +392,7 @@ def _adjust_positions(self, spaces: LayoutSpaces): self._strip_text_x_background_equal_heights() self._strip_text_y_background_equal_widths() - def _adjust_axis_text_x(self, justify: TextJustifier): + def _adjust_axis_text_x(self, justify: PlotTextJustifier): """ Adjust x-axis text, justifying vertically as necessary """ @@ -424,7 +423,7 @@ def to_vertical_axis_dimensions(value: float, ax: Axes) -> float: text, va, -axis_text_row_height, 0, height=height ) - def _adjust_axis_text_y(self, justify: TextJustifier): + def _adjust_axis_text_y(self, justify: PlotTextJustifier): """ Adjust x-axis text, justifying horizontally as necessary """ @@ -517,115 +516,23 @@ def _text_is_visible(text: Text) -> bool: return text.get_visible() and text._text # type: ignore -@dataclass -class TextJustifier: +class PlotTextJustifier(TextJustifier): """ - Justify Text - - The justification methods reinterpret alignment values to be justification - about a span. + Justify Text about a plot or it's panels """ - spaces: LayoutSpaces - - def horizontally( - self, - text: Text, - ha: HorizontalJustification | float, - left: float, - right: float, - width: float | None = None, - ): - """ - Horizontally Justify text between left and right - """ - rel = ha_as_float(ha) - if width is None: - width = self.spaces.items.geometry.width(text) - x = rel_position(rel, width, left, right) - text.set_x(x) - text.set_horizontalalignment("left") - - def vertically( - self, - text: Text, - va: VerticalJustification | float, - bottom: float, - top: float, - height: float | None = None, - ): - """ - Vertically Justify text between bottom and top - """ - rel = va_as_float(va) - - if height is None: - height = self.spaces.items.geometry.height(text) - y = rel_position(rel, height, bottom, top) - text.set_y(y) - text.set_verticalalignment("bottom") - - def horizontally_across_panel( - self, text: Text, ha: HorizontalJustification | float - ): - """ - Horizontally Justify text accross the panel(s) width - """ - self.horizontally( - text, ha, self.spaces.l.panel_left, self.spaces.r.panel_right - ) - - def horizontally_across_plot( - self, text: Text, ha: HorizontalJustification | float - ): - """ - Horizontally Justify text across the plot's width - """ - self.horizontally( - text, ha, self.spaces.l.plot_left, self.spaces.r.plot_right - ) - - def vertically_along_panel( - self, text: Text, va: VerticalJustification | float - ): - """ - Horizontally Justify text along the panel(s) height - """ - self.vertically( - text, va, self.spaces.b.panel_bottom, self.spaces.t.panel_top - ) - - def vertically_along_plot( - self, text: Text, va: VerticalJustification | float - ): - """ - Vertically Justify text along the plot's height - """ - self.vertically( - text, va, self.spaces.b.plot_bottom, self.spaces.t.plot_top + def __init__(self, spaces: LayoutSpaces): + boundaries = JustifyBoundaries( + plot_left=spaces.l.plot_left, + plot_right=spaces.r.plot_right, + plot_bottom=spaces.b.plot_bottom, + plot_top=spaces.t.plot_top, + panel_left=spaces.l.panel_left, + panel_right=spaces.r.panel_right, + panel_bottom=spaces.b.panel_bottom, + panel_top=spaces.t.panel_top, ) - - def horizontally_about( - self, text: Text, ratio: float, how: Literal["panel", "plot"] - ): - """ - Horizontally Justify text across the panel or plot - """ - if how == "panel": - self.horizontally_across_panel(text, ratio) - else: - self.horizontally_across_plot(text, ratio) - - def vertically_about( - self, text: Text, ratio: float, how: Literal["panel", "plot"] - ): - """ - Vertically Justify text along the panel or plot - """ - if how == "panel": - self.vertically_along_panel(text, ratio) - else: - self.vertically_along_plot(text, ratio) + super().__init__(spaces.plot.figure, boundaries) def set_legends_position(legends: legend_artists, spaces: LayoutSpaces): @@ -805,7 +712,7 @@ def set_plot_tag_position_in_margin(tag: Text, spaces: LayoutSpaces): tag.set_y(y) tag.set_verticalalignment("bottom") - justify = TextJustifier(spaces) + justify = PlotTextJustifier(spaces) if position in ("left", "right"): justify.vertically_along_plot(tag, va) elif position in ("top", "bottom"): diff --git a/plotnine/_mpl/utils.py b/plotnine/_mpl/utils.py index c60684c35b..65cf8737b0 100644 --- a/plotnine/_mpl/utils.py +++ b/plotnine/_mpl/utils.py @@ -1,20 +1,27 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING, Sequence, cast +from typing import TYPE_CHECKING, cast from matplotlib.transforms import Affine2D, Bbox +from plotnine._utils import ha_as_float, va_as_float + from .transforms import ZEROS_BBOX if TYPE_CHECKING: + from typing import Literal, Sequence + from matplotlib.artist import Artist from matplotlib.axes import Axes from matplotlib.backend_bases import RendererBase from matplotlib.figure import Figure from matplotlib.gridspec import SubplotSpec + from matplotlib.text import Text from matplotlib.transforms import Transform + from plotnine.typing import HorizontalJustification, VerticalJustification + from .gridspec import p9GridSpec @@ -267,3 +274,131 @@ def max_height(self, artists: Sequence[Artist]) -> float: for a in artists ] return max(heights) if len(heights) else 0 + + +@dataclass +class JustifyBoundaries: + """ + Limits about which text can be justified + """ + + plot_left: float + plot_right: float + plot_bottom: float + plot_top: float + panel_left: float + panel_right: float + panel_bottom: float + panel_top: float + + +class TextJustifier: + """ + Justify Text + + The justification methods reinterpret alignment values to be justification + about a span. + """ + + def __init__(self, figure: Figure, boundaries: JustifyBoundaries): + self.geometry = ArtistGeometry(figure) + self.boundaries = boundaries + + def horizontally( + self, + text: Text, + ha: HorizontalJustification | float, + left: float, + right: float, + width: float | None = None, + ): + """ + Horizontally Justify text between left and right + """ + rel = ha_as_float(ha) + if width is None: + width = self.geometry.width(text) + x = rel_position(rel, width, left, right) + text.set_x(x) + text.set_horizontalalignment("left") + + def vertically( + self, + text: Text, + va: VerticalJustification | float, + bottom: float, + top: float, + height: float | None = None, + ): + """ + Vertically Justify text between bottom and top + """ + rel = va_as_float(va) + + if height is None: + height = self.geometry.height(text) + y = rel_position(rel, height, bottom, top) + text.set_y(y) + text.set_verticalalignment("bottom") + + def horizontally_across_panel( + self, text: Text, ha: HorizontalJustification | float + ): + """ + Horizontally Justify text accross the panel(s) width + """ + self.horizontally( + text, ha, self.boundaries.panel_left, self.boundaries.panel_right + ) + + def horizontally_across_plot( + self, text: Text, ha: HorizontalJustification | float + ): + """ + Horizontally Justify text across the plot's width + """ + self.horizontally( + text, ha, self.boundaries.plot_left, self.boundaries.plot_right + ) + + def vertically_along_panel( + self, text: Text, va: VerticalJustification | float + ): + """ + Horizontally Justify text along the panel(s) height + """ + self.vertically( + text, va, self.boundaries.panel_bottom, self.boundaries.panel_top + ) + + def vertically_along_plot( + self, text: Text, va: VerticalJustification | float + ): + """ + Vertically Justify text along the plot's height + """ + self.vertically( + text, va, self.boundaries.plot_bottom, self.boundaries.plot_top + ) + + def horizontally_about( + self, text: Text, ratio: float, how: Literal["panel", "plot"] + ): + """ + Horizontally Justify text across the panel or plot + """ + if how == "panel": + self.horizontally_across_panel(text, ratio) + else: + self.horizontally_across_plot(text, ratio) + + def vertically_about( + self, text: Text, ratio: float, how: Literal["panel", "plot"] + ): + """ + Vertically Justify text along the panel or plot + """ + if how == "panel": + self.vertically_along_panel(text, ratio) + else: + self.vertically_along_plot(text, ratio) From b2b41e34ab3ba1be6f9618a7cee09b1040bab147 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Thu, 16 Oct 2025 12:54:33 +0200 Subject: [PATCH 09/24] Rename LayoutSpaces to PlotLayoutSpaces This change is in preparation for an similar function that will work for compositions. --- plotnine/_mpl/layout_manager/_engine.py | 8 +-- plotnine/_mpl/layout_manager/_layout_tree.py | 18 +++---- ..._layout_items.py => _plot_layout_items.py} | 14 ++--- .../_mpl/layout_manager/_plot_side_space.py | 54 ++++++++----------- 4 files changed, 43 insertions(+), 51 deletions(-) rename plotnine/_mpl/layout_manager/{_layout_items.py => _plot_layout_items.py} (98%) diff --git a/plotnine/_mpl/layout_manager/_engine.py b/plotnine/_mpl/layout_manager/_engine.py index 8d66392343..54df0a3937 100644 --- a/plotnine/_mpl/layout_manager/_engine.py +++ b/plotnine/_mpl/layout_manager/_engine.py @@ -7,7 +7,7 @@ from ...exceptions import PlotnineWarning from ._layout_tree import LayoutTree -from ._plot_side_space import LayoutSpaces +from ._plot_side_space import PlotLayoutSpaces if TYPE_CHECKING: from matplotlib.figure import Figure @@ -39,7 +39,7 @@ def execute(self, fig: Figure): renderer = fig._get_renderer() # pyright: ignore[reportAttributeAccessIssue] with getattr(renderer, "_draw_disabled", nullcontext)(): - spaces = LayoutSpaces(self.plot) + spaces = PlotLayoutSpaces(self.plot) gsparams = spaces.get_gridspec_params() self.plot.facet._gridspec.update_params_and_artists(gsparams) @@ -63,10 +63,10 @@ def execute(self, fig: Figure): renderer = fig._get_renderer() # pyright: ignore[reportAttributeAccessIssue] # Caculate the space taken up by all plot artists - lookup_spaces: dict[ggplot, LayoutSpaces] = {} + lookup_spaces: dict[ggplot, PlotLayoutSpaces] = {} with getattr(renderer, "_draw_disabled", nullcontext)(): for ps in self.composition.plotspecs: - lookup_spaces[ps.plot] = LayoutSpaces(ps.plot) + lookup_spaces[ps.plot] = PlotLayoutSpaces(ps.plot) # Adjust the size and placements of the plots tree = LayoutTree.create(self.composition, lookup_spaces) diff --git a/plotnine/_mpl/layout_manager/_layout_tree.py b/plotnine/_mpl/layout_manager/_layout_tree.py index 6304f2c4c5..84bc6d4234 100644 --- a/plotnine/_mpl/layout_manager/_layout_tree.py +++ b/plotnine/_mpl/layout_manager/_layout_tree.py @@ -8,7 +8,7 @@ from ._grid import Grid from ._plot_side_space import ( - LayoutSpaces, + PlotLayoutSpaces, bottom_space, left_space, right_space, @@ -28,7 +28,7 @@ ) from plotnine.composition import Compose - Node: TypeAlias = "LayoutSpaces | LayoutTree" + Node: TypeAlias = "PlotLayoutSpaces | LayoutTree" @dataclass @@ -105,7 +105,7 @@ class LayoutTree: Composition that this tree represents """ - nodes: list[LayoutSpaces | LayoutTree] + nodes: list[PlotLayoutSpaces | LayoutTree] """ The spaces or tree of spaces in the composition that the tree represents. @@ -151,7 +151,7 @@ def nrow(self) -> int: @staticmethod def create( cmp: Compose, - lookup_spaces: dict[ggplot, LayoutSpaces], + lookup_spaces: dict[ggplot, PlotLayoutSpaces], ) -> LayoutTree: """ Create a LayoutTree for this composition @@ -171,7 +171,7 @@ def create( from plotnine import ggplot # Create subtree - nodes: list[LayoutSpaces | LayoutTree] = [] + nodes: list[PlotLayoutSpaces | LayoutTree] = [] for item in cmp: if isinstance(item, ggplot): nodes.append(lookup_spaces[item]) @@ -360,7 +360,7 @@ def panel_height_ratios(self) -> Sequence[float]: def bottom_spaces_in_row(self, r: int) -> list[bottom_space]: spaces: list[bottom_space] = [] for node in self.grid[r, :]: - if isinstance(node, LayoutSpaces): + if isinstance(node, PlotLayoutSpaces): spaces.append(node.b) elif isinstance(node, LayoutTree): spaces.extend(node.bottom_most_spaces) @@ -369,7 +369,7 @@ def bottom_spaces_in_row(self, r: int) -> list[bottom_space]: def top_spaces_in_row(self, r: int) -> list[top_space]: spaces: list[top_space] = [] for node in self.grid[r, :]: - if isinstance(node, LayoutSpaces): + if isinstance(node, PlotLayoutSpaces): spaces.append(node.t) elif isinstance(node, LayoutTree): spaces.extend(node.top_most_spaces) @@ -378,7 +378,7 @@ def top_spaces_in_row(self, r: int) -> list[top_space]: def left_spaces_in_col(self, c: int) -> list[left_space]: spaces: list[left_space] = [] for node in self.grid[:, c]: - if isinstance(node, LayoutSpaces): + if isinstance(node, PlotLayoutSpaces): spaces.append(node.l) elif isinstance(node, LayoutTree): spaces.extend(node.left_most_spaces) @@ -387,7 +387,7 @@ def left_spaces_in_col(self, c: int) -> list[left_space]: def right_spaces_in_col(self, c: int) -> list[right_space]: spaces: list[right_space] = [] for node in self.grid[:, c]: - if isinstance(node, LayoutSpaces): + if isinstance(node, PlotLayoutSpaces): spaces.append(node.r) elif isinstance(node, LayoutTree): spaces.extend(node.right_most_spaces) diff --git a/plotnine/_mpl/layout_manager/_layout_items.py b/plotnine/_mpl/layout_manager/_plot_layout_items.py similarity index 98% rename from plotnine/_mpl/layout_manager/_layout_items.py rename to plotnine/_mpl/layout_manager/_plot_layout_items.py index fcd945b223..182be4f90f 100644 --- a/plotnine/_mpl/layout_manager/_layout_items.py +++ b/plotnine/_mpl/layout_manager/_plot_layout_items.py @@ -38,7 +38,7 @@ StripPosition, ) - from ._plot_side_space import LayoutSpaces + from ._plot_side_space import PlotLayoutSpaces AxesLocation: TypeAlias = Literal[ "all", "first_row", "last_row", "first_col", "last_col" @@ -60,7 +60,7 @@ @dataclass -class LayoutItems: +class PlotLayoutItems: """ Objects required to compute the layout """ @@ -341,7 +341,7 @@ def axis_text_x_right_protrusion(self, location: AxesLocation) -> float: return max(extras) if len(extras) else 0 - def _adjust_positions(self, spaces: LayoutSpaces): + def _adjust_positions(self, spaces: PlotLayoutSpaces): """ Set the x,y position of the artists around the panels """ @@ -521,7 +521,7 @@ class PlotTextJustifier(TextJustifier): Justify Text about a plot or it's panels """ - def __init__(self, spaces: LayoutSpaces): + def __init__(self, spaces: PlotLayoutSpaces): boundaries = JustifyBoundaries( plot_left=spaces.l.plot_left, plot_right=spaces.r.plot_right, @@ -535,7 +535,7 @@ def __init__(self, spaces: LayoutSpaces): super().__init__(spaces.plot.figure, boundaries) -def set_legends_position(legends: legend_artists, spaces: LayoutSpaces): +def set_legends_position(legends: legend_artists, spaces: PlotLayoutSpaces): """ Place legend on the figure and justify is a required """ @@ -613,7 +613,7 @@ def set_position( set_position(l.box, l.position, l.justification, transPanels) -def set_plot_tag_position(tag: Text, spaces: LayoutSpaces): +def set_plot_tag_position(tag: Text, spaces: PlotLayoutSpaces): """ Set the postion of the plot_tag """ @@ -674,7 +674,7 @@ def set_plot_tag_position(tag: Text, spaces: LayoutSpaces): tag.set_position(position) -def set_plot_tag_position_in_margin(tag: Text, spaces: LayoutSpaces): +def set_plot_tag_position_in_margin(tag: Text, spaces: PlotLayoutSpaces): """ Place the tag in an inner margin around the plot diff --git a/plotnine/_mpl/layout_manager/_plot_side_space.py b/plotnine/_mpl/layout_manager/_plot_side_space.py index 3cce2113cc..c33aa45263 100644 --- a/plotnine/_mpl/layout_manager/_plot_side_space.py +++ b/plotnine/_mpl/layout_manager/_plot_side_space.py @@ -11,14 +11,14 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass from functools import cached_property from typing import TYPE_CHECKING from plotnine.exceptions import PlotnineError from plotnine.facets import facet_grid, facet_null, facet_wrap -from ._layout_items import LayoutItems +from ._plot_layout_items import PlotLayoutItems from ._side_space import GridSpecParams, _side_space if TYPE_CHECKING: @@ -33,7 +33,7 @@ class _plot_side_space(_side_space): Base class for the side space around a plot """ - items: LayoutItems + items: PlotLayoutItems @cached_property def _legend_size(self) -> tuple[float, float]: @@ -701,8 +701,7 @@ def tag_height(self): ) -@dataclass -class LayoutSpaces: +class PlotLayoutSpaces: """ Compute the all the spaces required in the layout @@ -718,51 +717,44 @@ class LayoutSpaces: them in their final positions. """ - plot: ggplot - - l: left_space = field(init=False) - """All subspaces to the left of the panels""" - - r: right_space = field(init=False) - """All subspaces to the right of the panels""" - - t: top_space = field(init=False) - """All subspaces above the top of the panels""" - - b: bottom_space = field(init=False) - """All subspaces below the bottom of the panels""" - - W: float = field(init=False, default=0) + W: float """Figure Width [inches]""" - H: float = field(init=False, default=0) + H: float """Figure Height [inches]""" - w: float = field(init=False, default=0) + w: float """Axes width w.r.t figure in [0, 1]""" - h: float = field(init=False, default=0) + h: float """Axes height w.r.t figure in [0, 1]""" - sh: float = field(init=False, default=0) + sh: float """horizontal spacing btn panels w.r.t figure""" - sw: float = field(init=False, default=0) + sw: float """vertical spacing btn panels w.r.t figure""" - gsparams: GridSpecParams = field(init=False, repr=False) + gsparams: GridSpecParams """Grid spacing btn panels w.r.t figure""" - def __post_init__(self): - self.items = LayoutItems(self.plot) - self.W, self.H = self.plot.theme.getp("figure_size") + def __init__(self, plot: ggplot): + self.plot = plot + self.items = PlotLayoutItems(plot) - # Calculate the spacing along the edges of the panel area - # (spacing required by plotnine) self.l = left_space(self.items) + """All subspaces to the left of the panels""" + self.r = right_space(self.items) + """All subspaces to the right of the panels""" + self.t = top_space(self.items) + """All subspaces above the top of the panels""" + self.b = bottom_space(self.items) + """All subspaces below the bottom of the panels""" + + self.W, self.H = plot.theme.getp("figure_size") def get_gridspec_params(self) -> GridSpecParams: # Calculate the gridspec params From c3be061850ffc44dec100b01f4d5aee9fb4af49a Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Mon, 20 Oct 2025 18:37:15 +0300 Subject: [PATCH 10/24] Apply theme to the plot composition --- plotnine/composition/_compose.py | 35 +++++++++++++++++------- plotnine/composition/_plot_annotation.py | 3 +- plotnine/ggplot.py | 4 +-- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/plotnine/composition/_compose.py b/plotnine/composition/_compose.py index 6d885ae25c..d5fb86aba4 100644 --- a/plotnine/composition/_compose.py +++ b/plotnine/composition/_compose.py @@ -25,7 +25,7 @@ from matplotlib.figure import Figure from plotnine._mpl.gridspec import p9GridSpec - from plotnine.ggplot import PlotAddable, ggplot + from plotnine.ggplot import PlotAddable, ggplot, theme from plotnine.typing import FigureFormat, MimeBundle @@ -193,6 +193,14 @@ def nrow(self) -> int: def ncol(self) -> int: return cast("int", self.layout.ncol) + @property + def theme(self) -> theme: + return self.annotation.theme + + @theme.setter + def theme(self, value: theme): + self.annotation.theme = value + @abc.abstractmethod def __or__(self, rhs: ggplot | Compose) -> Compose: """ @@ -338,7 +346,7 @@ def _repr_mimebundle_(self, include=None, exclude=None) -> MimeBundle: buf = BytesIO() self.save(buf, "png" if format == "retina" else format) - figure_size_px = self.last_plot.theme._figure_size_px + figure_size_px = self.annotation.theme._figure_size_px return get_mimebundle(buf.getvalue(), format, figure_size_px) @property @@ -398,7 +406,7 @@ def _to_retina(self): else: item._to_retina() - def _create_gridspec(self, nest_into): + def _create_gridspec(self, container_gs): """ Create the gridspec for this composition """ @@ -408,8 +416,9 @@ def _create_gridspec(self, nest_into): self.layout._setup(self) self.annotation._setup(self) + self._gridspec = container_gs self.items._gridspec = p9GridSpec.from_layout( - self.layout, figure=self.figure, nest_into=nest_into + self.layout, figure=self.figure, nest_into=container_gs[0] ) def _setup(self) -> Figure: @@ -430,17 +439,13 @@ def _create_figure(self): figure = plt.figure() def _make_plotspecs( - cmp: Compose, _gridspec: p9GridSpec + cmp: Compose, container_gs: p9GridSpec ) -> Generator[plotspec]: """ Return the plot specification for each subplot in the composition """ - # This gridspec contains a composition group e.g. - # (p2 | p3) of p1 | (p2 | p3) - ss_or_none = _gridspec[0] - cmp.figure = figure - cmp._create_gridspec(ss_or_none) + cmp._create_gridspec(container_gs) # Iterating over the gridspec yields the SubplotSpecs for each # "subplot" in the grid. The SubplotSpec is the handle for the @@ -512,9 +517,19 @@ def draw(self, *, show: bool = False) -> Figure: figure = self._setup() self._draw_plots() self.annotation.draw() + self._draw_composition_background() + self.annotation.theme.apply() figure.set_layout_engine(PlotnineCompositionLayoutEngine(self)) return figure + def _draw_composition_background(self): + from matplotlib.patches import Rectangle + + rect = Rectangle((0, 0), 0, 0, facecolor="none", zorder=-1000) + self.figure.add_artist(rect) + self._gridspec.patch = rect + self.annotation.theme.targets.plot_background = rect + def save( self, filename: str | Path | BytesIO, diff --git a/plotnine/composition/_plot_annotation.py b/plotnine/composition/_plot_annotation.py index 1668ada113..ce2ae58f68 100644 --- a/plotnine/composition/_plot_annotation.py +++ b/plotnine/composition/_plot_annotation.py @@ -58,7 +58,8 @@ def _setup(self, cmp: Compose): """ Setup annotation """ - self.theme._setup(cmp.figure) + self.theme = cmp.last_plot.theme + self.theme + self.theme._setup(cmp.figure, None, self.title, self.subtitle) def empty(self) -> bool: """ diff --git a/plotnine/ggplot.py b/plotnine/ggplot.py index 16db7077da..52dc9dba44 100755 --- a/plotnine/ggplot.py +++ b/plotnine/ggplot.py @@ -340,7 +340,7 @@ def draw(self, *, show: bool = False) -> Figure: self.guides.draw() self._draw_figure_texts() self._draw_watermarks() - self._draw_figure_background() + self._draw_plot_background() # Artist object theming self.theme.apply() @@ -559,7 +559,7 @@ def _draw_watermarks(self): for wm in self.watermarks: wm.draw(self.figure) - def _draw_figure_background(self): + def _draw_plot_background(self): from matplotlib.patches import Rectangle rect = Rectangle((0, 0), 0, 0, facecolor="none", zorder=-1000) From 2bd79dea81a0a4710e8358ca8726ce626e5087a5 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 21 Oct 2025 18:43:57 +0300 Subject: [PATCH 11/24] Make compose.theme combine default theme with annotation theme --- plotnine/_utils/context.py | 2 +- plotnine/composition/_compose.py | 55 ++++++++++++++++++++---- plotnine/composition/_plot_annotation.py | 26 ----------- tests/conftest.py | 4 +- 4 files changed, 50 insertions(+), 37 deletions(-) diff --git a/plotnine/_utils/context.py b/plotnine/_utils/context.py index 36812059a4..6deda3bf1e 100644 --- a/plotnine/_utils/context.py +++ b/plotnine/_utils/context.py @@ -116,7 +116,7 @@ def __post_init__(self): # https://github.com/matplotlib/matplotlib/issues/24644 # When drawing the Composition, the dpi themeable is infective # because it sets the rcParam after this figure is created. - rcParams = {"figure.dpi": self.cmp.last_plot.theme.getp("dpi")} + rcParams = {"figure.dpi": self.cmp.theme.getp("dpi")} self._rc_context = mpl.rc_context(rcParams) def __enter__(self) -> Self: diff --git a/plotnine/composition/_compose.py b/plotnine/composition/_compose.py index d5fb86aba4..7c59fda047 100644 --- a/plotnine/composition/_compose.py +++ b/plotnine/composition/_compose.py @@ -5,6 +5,8 @@ from io import BytesIO from typing import TYPE_CHECKING, cast, overload +from plotnine.themes.theme import theme_get + from .._utils.context import plot_composition_context from .._utils.ipython import ( get_ipython, @@ -195,11 +197,19 @@ def ncol(self) -> int: @property def theme(self) -> theme: - return self.annotation.theme + """ + Theme for this composition + + This is the default theme plus combined with theme from the + annotation. + """ + if not getattr(self, "_theme", None): + self._theme = theme_get() + self.annotation.theme + return self._theme @theme.setter def theme(self, value: theme): - self.annotation.theme = value + self._theme = value @abc.abstractmethod def __or__(self, rhs: ggplot | Compose) -> Compose: @@ -346,7 +356,7 @@ def _repr_mimebundle_(self, include=None, exclude=None) -> MimeBundle: buf = BytesIO() self.save(buf, "png" if format == "retina" else format) - figure_size_px = self.annotation.theme._figure_size_px + figure_size_px = self.theme._figure_size_px return get_mimebundle(buf.getvalue(), format, figure_size_px) @property @@ -400,6 +410,8 @@ def __deepcopy__(self, memo): def _to_retina(self): from plotnine import ggplot + self.theme = self.theme.to_retina() + for item in self: if isinstance(item, ggplot): item.theme = item.theme.to_retina() @@ -412,9 +424,8 @@ def _create_gridspec(self, container_gs): """ from plotnine._mpl.gridspec import p9GridSpec - # NOTE: These two should be in ._setup + # NOTE: This should be in ._setup self.layout._setup(self) - self.annotation._setup(self) self._gridspec = container_gs self.items._gridspec = p9GridSpec.from_layout( @@ -516,19 +527,47 @@ def draw(self, *, show: bool = False) -> Figure: with plot_composition_context(self, show): figure = self._setup() self._draw_plots() - self.annotation.draw() + self.theme._setup( + self.figure, + None, + self.annotation.title, + self.annotation.subtitle, + ) + self._draw_annotation() self._draw_composition_background() - self.annotation.theme.apply() + self.theme.apply() figure.set_layout_engine(PlotnineCompositionLayoutEngine(self)) return figure def _draw_composition_background(self): + """ + Draw the background rectangle of the composition + """ from matplotlib.patches import Rectangle rect = Rectangle((0, 0), 0, 0, facecolor="none", zorder=-1000) self.figure.add_artist(rect) self._gridspec.patch = rect - self.annotation.theme.targets.plot_background = rect + self.theme.targets.plot_background = rect + + def _draw_annotation(self): + """ + Draw the items in the annotation + """ + if self.annotation.empty(): + return + + figure = self.theme.figure + targets = self.theme.targets + + if title := self.annotation.title: + targets.plot_title = figure.text(0, 0, title) + + if subtitle := self.annotation.subtitle: + targets.plot_subtitle = figure.text(0, 0, subtitle) + + if caption := self.annotation.caption: + targets.plot_caption = figure.text(0, 0, caption) def save( self, diff --git a/plotnine/composition/_plot_annotation.py b/plotnine/composition/_plot_annotation.py index ce2ae58f68..ea89812a47 100644 --- a/plotnine/composition/_plot_annotation.py +++ b/plotnine/composition/_plot_annotation.py @@ -54,13 +54,6 @@ def update(self, other: plot_annotation): else: setattr(self, name, value) - def _setup(self, cmp: Compose): - """ - Setup annotation - """ - self.theme = cmp.last_plot.theme + self.theme - self.theme._setup(cmp.figure, None, self.title, self.subtitle) - def empty(self) -> bool: """ Whether the annotation has any content @@ -72,22 +65,3 @@ def empty(self) -> bool: return False return True - - def draw(self): - """ - Render the items in the annotation - """ - if self.empty(): - return - - figure = self.theme.figure - targets = self.theme.targets - - if self.title: - targets.plot_title = figure.text(0, 0, self.title) - - if subtitle := self.subtitle: - targets.plot_subtitle = figure.text(0, 0, subtitle) - - if caption := self.caption: - targets.plot_caption = figure.text(0, 0, caption) diff --git a/tests/conftest.py b/tests/conftest.py index 051fa459a3..9d3e6593e4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,7 @@ from matplotlib.testing.compare import compare_images from plotnine import ggplot, theme -from plotnine.composition import Beside, Compose, Stack +from plotnine.composition import Beside, Compose, Stack, plot_annotation from plotnine.themes.theme import DEFAULT_RCPARAMS TOLERANCE = 2 # Default tolerance for the tests @@ -256,7 +256,7 @@ def composition_equals(cmp: Compose, name: str) -> bool: test_file = inspect.stack()[1][1] filenames = make_test_image_filenames(name, test_file) - _cmp = cmp + theme(dpi=DPI) + _cmp = cmp + plot_annotation(theme=theme(figure_size=(8, 6), dpi=DPI)) with _test_cleanup(): _cmp.save(filenames.result) From e27e502a0f29dd2b291562c73132fd424f40b294 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 21 Oct 2025 18:47:03 +0300 Subject: [PATCH 12/24] Don't use dataclasses for _side_space --- .../_mpl/layout_manager/_plot_side_space.py | 10 ++-- plotnine/_mpl/layout_manager/_side_space.py | 53 +++++++++---------- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/plotnine/_mpl/layout_manager/_plot_side_space.py b/plotnine/_mpl/layout_manager/_plot_side_space.py index c33aa45263..9468d9cc11 100644 --- a/plotnine/_mpl/layout_manager/_plot_side_space.py +++ b/plotnine/_mpl/layout_manager/_plot_side_space.py @@ -11,7 +11,6 @@ from __future__ import annotations -from dataclasses import dataclass from functools import cached_property from typing import TYPE_CHECKING @@ -27,13 +26,14 @@ from plotnine.iapi import outside_legend -@dataclass class _plot_side_space(_side_space): """ Base class for the side space around a plot """ - items: PlotLayoutItems + def __init__(self, items: PlotLayoutItems): + self.items = items + self._calculate() @cached_property def _legend_size(self) -> tuple[float, float]: @@ -147,7 +147,6 @@ def axis_title_clearance(self) -> float: raise PlotnineError("Side has no axis title") from err -@dataclass class left_space(_plot_side_space): """ Space in the figure for artists on the left of the panel area @@ -319,7 +318,6 @@ def tag_width(self): ) -@dataclass class right_space(_plot_side_space): """ Space in the figure for artists on the right of the panel area @@ -425,7 +423,6 @@ def tag_width(self): ) -@dataclass class top_space(_plot_side_space): """ Space in the figure for artists above the panel area @@ -554,7 +551,6 @@ def tag_height(self): ) -@dataclass class bottom_space(_plot_side_space): """ Space in the figure for artists below the panel area diff --git a/plotnine/_mpl/layout_manager/_side_space.py b/plotnine/_mpl/layout_manager/_side_space.py index 2cc487df80..08d75cbd58 100644 --- a/plotnine/_mpl/layout_manager/_side_space.py +++ b/plotnine/_mpl/layout_manager/_side_space.py @@ -12,14 +12,11 @@ from __future__ import annotations from abc import ABC, abstractmethod -from dataclasses import dataclass, fields +from dataclasses import dataclass from functools import cached_property from typing import TYPE_CHECKING, cast if TYPE_CHECKING: - from dataclasses import Field - from typing import Generator - from plotnine._mpl.gridspec import p9GridSpec from plotnine.typing import Side @@ -52,7 +49,6 @@ def valid(self) -> bool: return self.top - self.bottom > 0 and self.right - self.left > 0 -@dataclass class _side_space(ABC): """ Base class to for spaces @@ -64,24 +60,39 @@ class _side_space(ABC): The amount of space for each artist is computed in figure coordinates. """ - def __post_init__(self): - self.side: Side = cast("Side", self.__class__.__name__.split("_")[0]) + def _calculate(self): + """ + Calculate the space taken up by each artist + """ + + @cached_property + def side(self) -> Side: """ Side of the panel(s) that this class applies to """ - self._calculate() + return cast("Side", self.__class__.__name__.split("_")[0]) - def _calculate(self): + @cached_property + def parts(self) -> list[str]: """ - Calculate the space taken up by each artist + The names of the part of the spaces """ + return [ + name + for name, value in self.__class__.__dict__.items() + if not ( + name.startswith("_") + or callable(value) + or isinstance(value, property) + ) + ] @property def total(self) -> float: """ Total space """ - return sum(getattr(self, f.name) for f in fields(self)[1:]) + return sum(getattr(self, name) for name in self.parts) def sum_upto(self, item: str) -> float: """ @@ -89,14 +100,8 @@ def sum_upto(self, item: str) -> float: Sums from the edge of the figure i.e. the "plot_margin". """ - - def _fields_upto(item: str) -> Generator[Field, None, None]: - for f in fields(self)[1:]: - if f.name == item: - break - yield f - - return sum(getattr(self, f.name) for f in _fields_upto(item)) + stop = self.parts.index(item) + return sum(getattr(self, name) for name in self.parts[:stop]) def sum_incl(self, item: str) -> float: """ @@ -104,14 +109,8 @@ def sum_incl(self, item: str) -> float: Sums from the edge of the figure i.e. the "plot_margin". """ - - def _fields_upto(item: str) -> Generator[Field, None, None]: - for f in fields(self)[1:]: - yield f - if f.name == item: - break - - return sum(getattr(self, f.name) for f in _fields_upto(item)) + stop = self.parts.index(item) + 1 + return sum(getattr(self, name) for name in self.parts[:stop]) @cached_property @abstractmethod From 4e78597bd1afdd1c43fbe138601ab61b7d5e8478 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 21 Oct 2025 19:08:03 +0300 Subject: [PATCH 13/24] Don't use a dataclass for PlotLayoutItems --- plotnine/_mpl/layout_manager/_plot_layout_items.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plotnine/_mpl/layout_manager/_plot_layout_items.py b/plotnine/_mpl/layout_manager/_plot_layout_items.py index 182be4f90f..09e0b9e94a 100644 --- a/plotnine/_mpl/layout_manager/_plot_layout_items.py +++ b/plotnine/_mpl/layout_manager/_plot_layout_items.py @@ -59,15 +59,12 @@ ) -@dataclass class PlotLayoutItems: """ Objects required to compute the layout """ - plot: ggplot - - def __post_init__(self): + def __init__(self, plot: ggplot): def get(name: str) -> Any: """ Return themeable target or None @@ -80,6 +77,7 @@ def get(name: str) -> Any: return None return t + self.plot = plot self.geometry = ArtistGeometry(self.plot.figure) self.axis_title_x: Text | None = get("axis_title_x") From d3576c6021b47bf39960021c90481322b6115948 Mon Sep 17 00:00:00 2001 From: Hassan Kibirige Date: Tue, 21 Oct 2025 19:11:19 +0300 Subject: [PATCH 14/24] Compute side_space for the compositions The plot_composition test images have changed because the compositions now have a plot margin. --- .../_composition_layout_items.py | 95 +++++ .../layout_manager/_composition_side_space.py | 381 ++++++++++++++++++ plotnine/_mpl/layout_manager/_engine.py | 7 + .../_mpl/layout_manager/_plot_layout_items.py | 1 - .../test_plot_composition/add_into_ncol.png | Bin 7683 -> 7794 bytes .../test_plot_composition/add_into_stack.png | Bin 7823 -> 8071 bytes .../test_plot_composition/and_operator.png | Bin 7081 -> 7820 bytes .../basic_horizontal_align_resize.png | Bin 6333 -> 6457 bytes .../basic_vertical_align_resize.png | Bin 6181 -> 6376 bytes .../complex_composition.png | Bin 22286 -> 22517 bytes .../test_plot_composition/facets.png | Bin 20341 -> 20673 bytes .../horizontal_tag_align.png | Bin 11317 -> 11399 bytes .../test_plot_composition/minus.png | Bin 9867 -> 10043 bytes .../test_plot_composition/mul_operator.png | Bin 8646 -> 8763 bytes .../nested_horizontal_align_resize.png | Bin 10688 -> 10797 bytes .../nested_vertical_align_resize.png | Bin 10741 -> 10997 bytes .../plot_layout_association.png | Bin 11252 -> 11417 bytes .../plot_layout_byrow.png | Bin 9714 -> 9836 bytes .../plot_layout_extra_col_width.png | Bin 13996 -> 14111 bytes .../plot_layout_extra_cols.png | Bin 7733 -> 7806 bytes .../plot_layout_extra_row_width.png | Bin 9271 -> 9555 bytes .../plot_layout_extra_rows.png | Bin 7828 -> 8077 bytes .../plot_layout_heights.png | Bin 6082 -> 6265 bytes .../plot_layout_nested_resize.png | Bin 9735 -> 9900 bytes .../plot_layout_widths.png | Bin 5963 -> 6073 bytes .../test_plot_composition/plus_operator.png | Bin 8996 -> 9912 bytes .../test_nested_horizontal_align_1.png | Bin 10088 -> 10181 bytes .../test_nested_horizontal_align_2.png | Bin 10908 -> 11009 bytes .../test_nested_vertical_align_1.png | Bin 10205 -> 10333 bytes .../test_nested_vertical_align_2.png | Bin 10939 -> 11109 bytes .../vertical_tag_align.png | Bin 11449 -> 11776 bytes .../wrap_complicated.png | Bin 20225 -> 20353 bytes 32 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 plotnine/_mpl/layout_manager/_composition_layout_items.py create mode 100644 plotnine/_mpl/layout_manager/_composition_side_space.py diff --git a/plotnine/_mpl/layout_manager/_composition_layout_items.py b/plotnine/_mpl/layout_manager/_composition_layout_items.py new file mode 100644 index 0000000000..0b5c510241 --- /dev/null +++ b/plotnine/_mpl/layout_manager/_composition_layout_items.py @@ -0,0 +1,95 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from matplotlib.text import Text + +from plotnine._mpl.utils import ( + ArtistGeometry, + JustifyBoundaries, + TextJustifier, +) + +if TYPE_CHECKING: + from typing import Any + + from plotnine.composition._compose import Compose + + from ._composition_side_space import CompositionLayoutSpaces + + +@dataclass +class CompositionLayoutItems: + """ + plot_annotation artists + """ + + def __init__(self, cmp: Compose): + def get(name: str) -> Any: + """ + Return themeable target or None + """ + if self._is_blank(name): + return None + else: + t = getattr(cmp.theme.targets, name) + if isinstance(t, Text) and t.get_text() == "": + return None + return t + + self.cmp = cmp + self.geometry = ArtistGeometry(cmp.figure) + + self.plot_title: Text | None = get("plot_title") + self.plot_subtitle: Text | None = get("plot_subtitle") + self.plot_caption: Text | None = get("plot_caption") + + def _is_blank(self, name: str) -> bool: + return self.cmp.theme.T.is_blank(name) + + def _adjust_positions(self, spaces: CompositionLayoutSpaces): + theme = self.cmp.theme + plot_title_position = theme.getp("plot_title_position", "panel") + plot_caption_position = theme.getp("plot_caption_position", "panel") + justify = CompositionTextJustifier(spaces) + + if self.plot_title: + ha = theme.getp(("plot_title", "ha")) + self.plot_title.set_y(spaces.t.y2("plot_title")) + justify.horizontally_about( + self.plot_title, ha, plot_title_position + ) + + if self.plot_subtitle: + ha = theme.getp(("plot_subtitle", "ha")) + self.plot_subtitle.set_y(spaces.t.y2("plot_subtitle")) + justify.horizontally_about( + self.plot_subtitle, ha, plot_title_position + ) + + if self.plot_caption: + ha = theme.getp(("plot_caption", "ha"), "right") + self.plot_caption.set_y(spaces.b.y1("plot_caption")) + justify.horizontally_about( + self.plot_caption, ha, plot_caption_position + ) + + +class CompositionTextJustifier(TextJustifier): + """ + Justify Text about a composition or it's panels + """ + + def __init__(self, spaces: CompositionLayoutSpaces): + boundaries = JustifyBoundaries( + plot_left=spaces.l.composition_left, + plot_right=spaces.r.composition_right, + plot_bottom=spaces.b.composition_bottom, + plot_top=spaces.t.composition_top, + panel_left=spaces.l.composition_panel_left, + panel_right=spaces.r.composition_panel_right, + panel_bottom=spaces.b.composition_panel_bottom, + panel_top=spaces.t.composition_panel_top, + ) + super().__init__(spaces.cmp.figure, boundaries) diff --git a/plotnine/_mpl/layout_manager/_composition_side_space.py b/plotnine/_mpl/layout_manager/_composition_side_space.py new file mode 100644 index 0000000000..da25cb4475 --- /dev/null +++ b/plotnine/_mpl/layout_manager/_composition_side_space.py @@ -0,0 +1,381 @@ +from __future__ import annotations + +from functools import cached_property +from typing import TYPE_CHECKING + +from ._composition_layout_items import CompositionLayoutItems +from ._side_space import GridSpecParams, _side_space + +if TYPE_CHECKING: + from plotnine._mpl.gridspec import p9GridSpec + from plotnine.composition._compose import Compose + + +class _composition_side_space(_side_space): + """ + Base class for the side space around a composition + """ + + def __init__(self, items: CompositionLayoutItems): + self.items = items + self._calculate() + + @cached_property + def gs(self) -> p9GridSpec: + """ + The gridspec of the composition + """ + return self.items.cmp._gridspec + + +class composition_left_space(_composition_side_space): + plot_margin: float = 0 + + def _calculate(self): + theme = self.items.cmp.theme + self.plot_margin = theme.getp("plot_margin_left") + + @property + def offset(self) -> float: + """ + Distance from left of the figure to the left of the plot gridspec + + ----------------(1, 1) + | ---- | + | dx | | | + |<--->| | | + | | | | + | ---- | + (0, 0)---------------- + + """ + return self.gs.bbox_relative.x0 + + def x1(self, item: str) -> float: + """ + Lower x-coordinate in figure space of the item + """ + return self.to_figure_space(self.sum_upto(item)) + + def x2(self, item: str) -> float: + """ + Higher x-coordinate in figure space of the item + """ + return self.to_figure_space(self.sum_incl(item)) + + @property + def items_left_relative(self): + """ + Left (relative to the gridspec) of the cmp items in figure dimensions + """ + return self.total + + @property + def items_left(self): + """ + Left of the composition items in figure space + """ + return self.to_figure_space(self.items_left_relative) + + @property + def composition_left(self): + """ + Distance up to the left-most artist in figure space + """ + return self.x2("plot_margin") + + @property + def composition_panel_left(self): + """ + Left of the panels in figure space + """ + # TODO: Fixme. Workout the actual panel left + return self.composition_left + + +class composition_right_space(_composition_side_space): + """ + Space for annotations to the right of the actual composition + + Ordered from the edge of the figure and going inwards + """ + + plot_margin: float = 0 + + def _calculate(self): + theme = self.items.cmp.theme + self.plot_margin = theme.getp("plot_margin_right") + + @property + def offset(self): + """ + Distance from right of the figure to the right of the plot gridspec + + ---------------(1, 1) + | ---- | + | | | -dx | + | | |<--->| + | | | | + | ---- | + (0, 0)--------------- + + """ + return self.gs.bbox_relative.x1 - 1 + + def x1(self, item: str) -> float: + """ + Lower x-coordinate in figure space of the item + """ + return self.to_figure_space(1 - self.sum_incl(item)) + + def x2(self, item: str) -> float: + """ + Higher x-coordinate in figure space of the item + """ + return self.to_figure_space(1 - self.sum_upto(item)) + + @property + def items_right_relative(self): + """ + Right (relative to the gridspec) of the panels in figure dimensions + """ + return 1 - self.total + + @property + def items_right(self): + """ + Right of the panels in figure space + """ + return self.to_figure_space(self.items_right_relative) + + @property + def composition_right(self): + """ + Distance up to the right-most artist in figure space + """ + return self.x1("plot_margin") + + @property + def composition_panel_right(self): + """ + Right of the panels in figure space + """ + # TODO: Fixme. Workout the actual panel right + return self.composition_right + + +class composition_top_space(_composition_side_space): + """ + Space for annotations above the actual composition + + Ordered from the edge of the figure and going inwards + """ + + plot_margin: float = 0 + plot_title_margin_top: float = 0 + plot_title: float = 0 + plot_title_margin_bottom: float = 0 + plot_subtitle_margin_top: float = 0 + plot_subtitle: float = 0 + plot_subtitle_margin_bottom: float = 0 + + def _calculate(self): + items = self.items + theme = self.items.cmp.theme + geometry = self.items.geometry + W, H = theme.getp("figure_size") + F = W / H + + self.plot_margin = theme.getp("plot_margin_top") * F + + if items.plot_title: + m = theme.get_margin("plot_title").fig + self.plot_title_margin_top = m.t * F + self.plot_title = geometry.height(items.plot_title) + self.plot_title_margin_bottom = m.b * F + + if items.plot_subtitle: + m = theme.get_margin("plot_subtitle").fig + self.plot_subtitle_margin_top = m.t * F + self.plot_subtitle = geometry.height(items.plot_subtitle) + self.plot_subtitle_margin_bottom = m.b * F + + @property + def offset(self) -> float: + """ + Distance from top of the figure to the top of the composition gridspec + + ----------------(1, 1) + | ^ | + | |-dy | + | v | + | ---- | + | | | | + | | | | + | | | | + | ---- | + | | + (0, 0)---------------- + """ + return self.gs.bbox_relative.y1 - 1 + + def y1(self, item: str) -> float: + """ + Lower y-coordinate in figure space of the item + """ + return self.to_figure_space(1 - self.sum_incl(item)) + + def y2(self, item: str) -> float: + """ + Higher y-coordinate in figure space of the item + """ + return self.to_figure_space(1 - self.sum_upto(item)) + + @property + def items_top_relative(self): + """ + Top (relative to the gridspec) of the panels in figure dimensions + """ + return 1 - self.total + + @property + def items_top(self): + """ + Top of the composition items in figure space + """ + return self.to_figure_space(self.items_top_relative) + + @property + def composition_top(self): + """ + Distance up to the top-most artist in figure space + """ + return self.y2("plot_margin") + + @property + def composition_panel_top(self): + """ + Top of the panels in figure space + """ + # TODO: Fixme. Workout the actual panel top + return self.composition_top + + +class composition_bottom_space(_composition_side_space): + """ + Space in the figure for artists below the panel area + + Ordered from the edge of the figure and going inwards + """ + + plot_margin: float = 0 + plot_caption_margin_bottom: float = 0 + plot_caption: float = 0 + plot_caption_margin_top: float = 0 + + def _calculate(self): + items = self.items + theme = self.items.cmp.theme + geometry = self.items.geometry + W, H = theme.getp("figure_size") + F = W / H + + self.plot_margin = theme.getp("plot_margin_bottom") * F + + if items.plot_caption: + m = theme.get_margin("plot_caption").fig + self.plot_caption_margin_bottom = m.b * F + self.plot_caption = geometry.height(items.plot_caption) + self.plot_caption_margin_top = m.t * F + + @property + def offset(self) -> float: + """ + Distance from bottom of the figure to the composition gridspec + + ----------------(1, 1) + | | + | ---- | + | | | | + | | | | + | | | | + | ---- | + | ^ | + | |dy | + | v | + (0, 0)---------------- + """ + return self.gs.bbox_relative.y0 + + def y1(self, item: str) -> float: + """ + Lower y-coordinate in figure space of the item + """ + return self.to_figure_space(self.sum_upto(item)) + + def y2(self, item: str) -> float: + """ + Higher y-coordinate in figure space of the item + """ + return self.to_figure_space(self.sum_incl(item)) + + @property + def items_bottom_relative(self): + """ + Bottom (relative to the gridspec) of the panels in figure dimensions + """ + return self.total + + @property + def items_bottom(self): + """ + Bottom of the panels in figure space + """ + return self.to_figure_space(self.items_bottom_relative) + + @property + def composition_bottom(self): + """ + Distance up to the bottom-most artist in figure space + """ + return self.y1("plot_margin") + + @property + def composition_panel_bottom(self): + """ + Bottom of the panels in figure space + """ + # TODO: Fixme. Workout the actual panel bottom + return self.composition_bottom + + +class CompositionLayoutSpaces: + gsparams: GridSpecParams + """Grid spacing btn panels w.r.t figure""" + + def __init__(self, cmp: Compose): + self.cmp = cmp + self.items = CompositionLayoutItems(cmp) + + self.l = composition_left_space(self.items) + """All subspaces to the left of the panels""" + + self.r = composition_right_space(self.items) + """All subspaces to the right of the panels""" + + self.t = composition_top_space(self.items) + """All subspaces above the top of the panels""" + + self.b = composition_bottom_space(self.items) + """All subspaces below the bottom of the panels""" + + def get_gridspec_params(self) -> GridSpecParams: + self.gsparams = GridSpecParams( + self.l.items_left_relative, + self.r.items_right_relative, + self.t.items_top_relative, + self.b.items_bottom_relative, + 0, + 0, + ) + return self.gsparams diff --git a/plotnine/_mpl/layout_manager/_engine.py b/plotnine/_mpl/layout_manager/_engine.py index 54df0a3937..966bbb2e81 100644 --- a/plotnine/_mpl/layout_manager/_engine.py +++ b/plotnine/_mpl/layout_manager/_engine.py @@ -6,6 +6,7 @@ from matplotlib.layout_engine import LayoutEngine from ...exceptions import PlotnineWarning +from ._composition_side_space import CompositionLayoutSpaces from ._layout_tree import LayoutTree from ._plot_side_space import PlotLayoutSpaces @@ -61,10 +62,16 @@ def execute(self, fig: Figure): from contextlib import nullcontext renderer = fig._get_renderer() # pyright: ignore[reportAttributeAccessIssue] + cmp = self.composition # Caculate the space taken up by all plot artists lookup_spaces: dict[ggplot, PlotLayoutSpaces] = {} with getattr(renderer, "_draw_disabled", nullcontext)(): + cmp_spaces = CompositionLayoutSpaces(cmp) + gsparams = cmp_spaces.get_gridspec_params() + cmp.items._gridspec.update_params_and_artists(gsparams) + cmp_spaces.items._adjust_positions(cmp_spaces) + for ps in self.composition.plotspecs: lookup_spaces[ps.plot] = PlotLayoutSpaces(ps.plot) diff --git a/plotnine/_mpl/layout_manager/_plot_layout_items.py b/plotnine/_mpl/layout_manager/_plot_layout_items.py index 09e0b9e94a..a54f858c3a 100644 --- a/plotnine/_mpl/layout_manager/_plot_layout_items.py +++ b/plotnine/_mpl/layout_manager/_plot_layout_items.py @@ -1,6 +1,5 @@ from __future__ import annotations -from dataclasses import dataclass from itertools import chain from typing import TYPE_CHECKING diff --git a/tests/baseline_images/test_plot_composition/add_into_ncol.png b/tests/baseline_images/test_plot_composition/add_into_ncol.png index ac9289f53ecd07326c8353f35b4cca1a5c350a08..76b5a6c2a59a9ab6e63a2e52f2d5e25047fa2bdc 100644 GIT binary patch delta 6087 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 e?^IVWh$OM&9jKxPZP^~P(y5B>-5&<2eF delta 5899 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 literal 7823 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 T2n0ME2271DjLLs;fB1g@+OblZ diff --git a/tests/baseline_images/test_plot_composition/and_operator.png b/tests/baseline_images/test_plot_composition/and_operator.png index fb429f6f8509998ea1ffa4a2c409039d26833b69..ad5ff6d220ab077fc6ed33a936302e0f7a874b23 100644 GIT binary patch literal 7820 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 BmE`~c diff --git a/tests/baseline_images/test_plot_composition/basic_horizontal_align_resize.png b/tests/baseline_images/test_plot_composition/basic_horizontal_align_resize.png index 8cbec230a1490d0547f1f57713141249729bca77..50bc62b4ca74535401d7fd9dbe6bbfb1cc73361f 100644 GIT binary patch literal 6457 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 literal 6181 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 diff --git a/tests/baseline_images/test_plot_composition/complex_composition.png b/tests/baseline_images/test_plot_composition/complex_composition.png index b8ccc7c709dadaea0a50add82657e984dad6591c..1521b8ec7daec0455c8e51301fd12191dae89ff7 100644 GIT binary patch literal 22517 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 literal 22286 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 Kl|Oy)_CEl1+?%)n literal 11317 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 diff --git a/tests/baseline_images/test_plot_composition/minus.png b/tests/baseline_images/test_plot_composition/minus.png index 9fca0718f1c9185f25c0cbef9f833c8199810164..28ec18a37fd6456b2b109caf5ad858bab96a439b 100644 GIT binary patch literal 10043 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 literal 9867 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 BF!BHZ literal 8646 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 diff --git a/tests/baseline_images/test_plot_composition/nested_horizontal_align_resize.png b/tests/baseline_images/test_plot_composition/nested_horizontal_align_resize.png index 114c77f50116ea7f2c6f873c4a7405401523bb23..7226305150feb1c4b934ecba265fb0c8dbf070b2 100644 GIT binary patch literal 10797 zcmeHtXH-*L*KX7oQ4moPQKYDd2neV&0YgBMBGL&xASg9-2nYd!qM{U$uCxeJLlSx@ z5l}gZ(p!iSLhm67EpX^}pLTSI*|89~&wMK9v0*SooWGIr#_L`#OU3?ESsnz5Lx>9WDhp z`ue$gdCE%Px+yJtn!6WS``Ic<+!)Dr5^~y$#d{N1WL;| z4+4pQ)X`8keU?F|a)!KPP3|vmd|1`5>^*ZWQQYiyBs4_CIl=g3@mut&K;z|e`{$nM z=P_3dj>RkF|81Oe+9c;DB&njeU7%dRclgjYm;ryI@Tfg>RJCMwvaDrMb#X7I3s0M_ z!*AEw+?JJR`_o=z_2m*|9kdM56(}x#y!ln z(5+#-7yIz7dDnRk*E2aLV)}!~>tE&Bj@He!e_ZsF1B3N0Uw*EPSTG>QaMo^&N1`Qf z`_|P*KBow(#ihuU++*Xou6lVqAXInk-F1t?PibF%YxJ1p(7|WN@_BDqXzQhY!OYDq zuM2T_e)5}5NmKC1)?Y$P&2oJ*e|-}ASo;m8()Ban(G%TeJeFQcp5(&#d}#}BV#~2% zg|zTf{190lK5&PP9fIex!xm1r&B7Y$ z?VAWzHQs5kme-9j7dPGpuM7BX(ne8`oCMK}5+ais{0i8--@hFL1cf~>hvytUVfYi=253|jz-4NOh8w*s4Jy}yWi)KCz zU1#A6qj=HXU6B1}n+-@ttzF}OmU~Ojc)}UcyQG{4f*om97PFn1eOki1=GoaY)*Dn* zV|7Ae#dqFy1YK6$t_c%+kv0A$$VxV}jv-}hbIT5e6JMRR-pI?(D+(_zW*xedURf!} zE3ra0yZrTIZeAY7F#8JNh0+UDVytbDVJCDB$2N-`Z^5kC-8+5=2k*yyU^sl z+})MpIp>zshshhwe8Se~#DoC&Y-cRSIB&piHE>^Oa}!EfJ?5j6ED=^WD`dc;DcXXO zP9D2ugAKci;Pu~40fHCz>_PHjD>})jC3W!5MD&(986TYE(A>7syt@=(MJf)g7(G)X z;!dpjt{GWmpe;w2;N|7x3#8(zs+h9DG7+QME;~a*!|?jZ*QhgU&XnwVjy|F#{O`E(8p7)fD*V3d^cKxB`(+ z^Q78V5)La}{4=c;ZNPru0jwpKRvgSaJiU;c+Bk8^1q6BzXXg&{$Q%RW%@Je8;+6Lp z(wb~RNjOht3<5B^IMJ~EaBe!4R_s5YAYn2S7P_;Xy0y365xmPXpMLcFFo5X3xS-Xr zy8S&(e&x0MG@1cK5dT>#wQaDJ?O?rDXjKm=aeY$4IJY$8!a)Y`#o0Gm4G}!z5Vtih zQk24hs1b8+k_Jrl9$w)Tns<~d*sU)gHV{eg>gmy)a{p%%dS;+1=`~jcwwOv0P+p7V zmtVk{+qzzH{(kNf2;^-d-P|?0zxe1F@Z&vvx(!mewDe2a`%?4Pc?I_Ki}S5i4J+Hm(o^Qhg%UcMeW zMfP$ykjJ=UFDlZ~Hfn2X4$|5fgGU%*8OZZz?0foR7kTxk2=}j#f6l#5lcxnzVULmP zH&& zR=Ig_3Eeg7%a83JtSIO&8C6*qy5eiUxq=u1V1czj(v0L5CM{2jv6h+~rvYoRFK>Rh zg;|^}kZRn?5wi`2>Y;-lat|X*>Lb}~6c$NYT- zI`8opRZ7F_ho+vkf9RT?HMay{%RE)Os>-1`1j&6|(SB|2g4LI2RVRgZnm9hKt)1BI z+CM?w7&S-)%pnjX^&$%S%w>PPNjYe%aw}H21oBR}ns9;zC+iDZSC$GXqMAACH@~q< z*c848pvswPKPjDBDW5K=>L|7dAk#6>C(nL2UBaoea&h;-xJ|&q((=qeJpnXFJ_9@ zt%S0Kwh*%=zN_ASzvF(iZmZ6I890TP0fR|H98=>)I~ae{5%lU+XkpF+>f)No2j}(0JEB+Hjbr)gxDw%z(p>$FJ5tW~K}sJ;=?cFn zZMt#bozfS|0j6tjPOogRsvNH0ysDvo=#T`1ftME1a_gvc#mv!)%VeF0kiTo%=)>hE zj;+b@k*O{6!WE8_=N_~?Xe)l1n!uANPlxo$U3PlGdx!90e$`Dlv}#Y%!-i^?khL|Hh!`L<8zI`lsJjor$an3NMw1Cw9&i+? zJuhDUcA@(3GioO%^3)@+FpkEnVzi2$&#bnF(4jqwm)rJ?9obx`?PsZH8(IdPRcO%@ zC7h0zX(&|Zx%S&TER%B!NS}CLbU`cLe?x=lGP$SYEhkf zBPcF|%Q{@3yU@1Yluk0QnuF~3%w3R`^BlO5+32k_x4QM?Wb?Dwl^A~ zFC#wrM1I*Fp`>l0i`+&l>uoq|tgJ&BQb8rkYYU&eQp3`vK3ut`aXkn1ve&pTDLy{C z1p~>~5=NOC^atO|j=0APZXqMaci3nRTOG7wV>MCYu6t?MEj7ZCw6@d|tTb$-cX7}t zm04oWd?00gt~k{ele^87`TQ^$18XR`ANg%Sy@yUQH*W~p#8Pj<>RuMxD9vapZdSC0G>^j^%_Kjz4 z(0SBlWl9mK&Gh~!aTz{rz2z}{UWz-8IGLe)K)!0SUHe9QsV@Vc{}(BH*Ccdg>f-sr z`gNiI2v}JP3yX$r<_Q>$i9GK`uM4pWeC&!)-SWjvc8q>iqlgArY}x*5DZn=D+%|gm zgRE$X+$^YD@c8@fTRUYGXmC%@PgVW&`{wB}b)_5DJ6}AQ>R;mpLnubGGP(Kz#tj}Lfr;Htdmu=PcSQmoGwi2s&H+TG#dO3$`) z2fTAg5uQIX`D6Oe1Vh_>Q!|Wwsf*QfkzOdniqtgMVZ#~ejHXIgk*S%PY$@x3`AFf3 zeyk-j_9B@w(sb{nX3U3c+}w&d>*@zSK9IuDZKnllx?^k~yWt^obJ}Wo6`WM-sLQ74 ziS4hf{1Rq4mQrh*L4-G27pKszFw2~=$v=i)C3+*I9*5cz13p?27O_8KBa%~6+?e*4 z+1WU3Yz(BWi#s!|CGOOe8Jk!R+zAB%1-}LH+sq9ZW=r19qB_s{>D0mPJ2Hj>BBXX5 ziro4;(})+^VR!m=VEn~-y0e9vv0DQk7cR`N3!TGPWyQcSN`5Jru-$0dFaH#IePK18 zqbK_pYwo~l2DQd{l`7&oh$BZ`M@049-S{7teC{&Ak(eMy$q@j#yZte&(`l(JMdc0N;)ep zuCqRI=6qoW`_ZR9@kt-wzo)OC73LNdr;QmPkKf?EUhhG^{I#cEN_4SH6$Sp2h-}LN zB0^Ja^DJS0ut|FVewUdWvUxTrXwU~df2I(Q{T4elamCTeX+JZeRl81+g0oZ+T2<>` z99*E_Ge_P;wJz_Kg>KJ~bdOX&UGE=sBiCl;XinV?+&<~Z`0mq@C72gL-;$76h!tQm zDS?ZE!aTm`c;&4_w+Ve0y=Tm-dGsqS&{+(^QEpWFf4rl{v4YQ@B*TMmAd1pepbhzCv z;JMNd=~5e<7%)(m_;qFH(%jhP`~F*tgKc)g+B!y52KAqZkUM)S6TC_u)>BH#VaUh= zPXGGE@d1ER$)GY5eATE#zf+YW94Ccx`U*-c41~0esC75sD^^+ThRx-Uyyws4=&1y{ z{aPS#Ec`YSOjGQlrLuWi4z4)z_FmOcsb1_~;4LPVJKK0IciozhbMM~aD!