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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions plotnine/_mpl/gridspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ def get_subplot_params(self, figure=None) -> SubplotParams:
parent_bbox = self._parent_subplot_spec.get_position(figure) # pyright: ignore
_left, _bottom, _right, _top = parent_bbox.extents

# params.left/bottom are insets from the parent's left/bottom
# edges. params.right/top represent where the right/top edges
# are (e.g. 0.95 means 5% margin), so (1 - params.right) is
# the right margin, subtracted from the parent's right edge.
left = _left + params.left
bottom = _bottom + params.bottom
right = _right - (1 - params.right)
Expand Down
76 changes: 28 additions & 48 deletions plotnine/_mpl/layout_manager/_composition_layout_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

from plotnine._mpl.utils import (
ArtistGeometry,
JustifyBoundaries,
TextJustifier,
resize_footer_background,
resize_footer_line,
)

if TYPE_CHECKING:
Expand Down Expand Up @@ -64,7 +65,17 @@ def _move_artists(self, spaces: CompositionSideSpaces):
plot_title_position = theme.getp("plot_title_position", "panel")
plot_caption_position = theme.getp("plot_caption_position", "panel")
plot_footer_position = theme.getp("plot_footer_position", "plot")
justify = CompositionTextJustifier(spaces)
justify = TextJustifier.from_boundaries(
spaces.cmp.figure,
plot_left=spaces.plot_left,
plot_right=spaces.plot_right,
plot_bottom=spaces.plot_bottom,
plot_top=spaces.plot_top,
panel_left=spaces.panel_left,
panel_right=spaces.panel_right,
panel_bottom=spaces.panel_bottom,
panel_top=spaces.panel_top,
)

if self.plot_title:
ha = theme.getp(("plot_title", "ha"))
Expand Down Expand Up @@ -93,49 +104,18 @@ def _move_artists(self, spaces: CompositionSideSpaces):
justify.horizontally_about(
self.plot_footer, ha, plot_footer_position
)
self._resize_plot_footer_background(spaces)
self._resize_plot_footer_line(spaces)

def _resize_plot_footer_background(self, spaces: CompositionSideSpaces):
"""
Resize the plot footer to the size of the footer
"""
if not self.plot_footer_background:
return

self.plot_footer_background.set_x(spaces.l.offset)
self.plot_footer_background.set_y(spaces.b.offset)
self.plot_footer_background.set_height(spaces.b.footer_height)
self.plot_footer_background.set_width(spaces.plot_width)

def _resize_plot_footer_line(self, spaces: CompositionSideSpaces):
"""
Resize the footer line to be a border above the footer
"""
if not self.plot_footer_line:
return

x1 = spaces.l.offset
x2 = x1 + spaces.plot_width
y1 = y2 = spaces.b.offset + spaces.b.footer_height
self.plot_footer_line.set_xdata([x1, x2])
self.plot_footer_line.set_ydata([y1, y2])


class CompositionTextJustifier(TextJustifier):
"""
Justify Text about a composition or it's panels
"""

def __init__(self, spaces: CompositionSideSpaces):
boundaries = JustifyBoundaries(
plot_left=spaces.plot_left,
plot_right=spaces.plot_right,
plot_bottom=spaces.plot_bottom,
plot_top=spaces.plot_top,
panel_left=spaces.panel_left,
panel_right=spaces.panel_right,
panel_bottom=spaces.panel_bottom,
panel_top=spaces.panel_top,
)
super().__init__(spaces.cmp.figure, boundaries)
if self.plot_footer_background:
resize_footer_background(
self.plot_footer_background,
x=spaces.l.offset,
y=spaces.b.offset,
height=spaces.b.footer_height,
width=spaces.plot_width,
)
if self.plot_footer_line:
resize_footer_line(
self.plot_footer_line,
x=spaces.l.offset,
width=spaces.plot_width,
y=spaces.b.offset + spaces.b.footer_height,
)
152 changes: 73 additions & 79 deletions plotnine/_mpl/layout_manager/_layout_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from ._plot_side_space import PlotSideSpaces

if TYPE_CHECKING:
from typing import Sequence, TypeAlias
from typing import Any, Callable, Literal, Sequence, TypeAlias

from plotnine._mpl.gridspec import p9GridSpec
from plotnine._mpl.layout_manager._plot_side_space import (
Expand Down Expand Up @@ -460,7 +460,8 @@ def right_spaces_in_col(self, c: int) -> list[right_space]:
spaces.extend(node.right_most_spaces)
return spaces

def iter_left_spaces(self) -> Iterator[list[left_space]]:
@property
def left_spaces(self) -> Iterator[list[left_space]]:
"""
Left spaces for each non-empty column

Expand All @@ -471,7 +472,8 @@ def iter_left_spaces(self) -> Iterator[list[left_space]]:
if spaces:
yield spaces

def iter_right_spaces(self) -> Iterator[list[right_space]]:
@property
def right_spaces(self) -> Iterator[list[right_space]]:
"""
Right spaces for each non-empty column

Expand All @@ -482,7 +484,8 @@ def iter_right_spaces(self) -> Iterator[list[right_space]]:
if spaces:
yield spaces

def iter_bottom_spaces(self) -> Iterator[list[bottom_space]]:
@property
def bottom_spaces(self) -> Iterator[list[bottom_space]]:
"""
Bottom spaces for each non-empty row

Expand All @@ -493,7 +496,8 @@ def iter_bottom_spaces(self) -> Iterator[list[bottom_space]]:
if spaces:
yield spaces

def iter_top_spaces(self) -> Iterator[list[top_space]]:
@property
def top_spaces(self) -> Iterator[list[top_space]]:
"""
Top spaces for each non-empty row

Expand All @@ -508,73 +512,27 @@ def align_panels(self):
"""
Align the edges of the panels in the composition
"""
for spaces in self.iter_bottom_spaces():
bottoms = [space.panel_bottom for space in spaces]
high = max(bottoms)
diffs = [high - b for b in bottoms]
for space, diff in zip(spaces, diffs):
space.margin_alignment += diff

for spaces in self.iter_top_spaces():
tops = [space.panel_top for space in spaces]
low = min(tops)
diffs = [b - low for b in tops]
for space, diff in zip(spaces, diffs):
space.margin_alignment += diff

for spaces in self.iter_left_spaces():
lefts = [space.panel_left for space in spaces]
high = max(lefts)
diffs = [high - l for l in lefts]
for space, diff in zip(spaces, diffs):
space.margin_alignment += diff

for spaces in self.iter_right_spaces():
rights = [space.panel_right for space in spaces]
low = min(rights)
diffs = [r - low for r in rights]
for space, diff in zip(spaces, diffs):
space.margin_alignment += diff
align_args = [
(self.bottom_spaces, lambda s: s.panel_bottom, "max"),
(self.top_spaces, lambda s: s.panel_top, "min"),
(self.left_spaces, lambda s: s.panel_left, "max"),
(self.right_spaces, lambda s: s.panel_right, "min"),
]
for spaces, measure, how in align_args:
_align(spaces, measure, "margin_alignment", how)

def align_tags(self):
"""
Align the tags in the composition
"""
for spaces in self.iter_bottom_spaces():
heights = [
space.tag_height + space.tag_alignment for space in spaces
]
high = max(heights)
diffs = [high - h for h in heights]
for space, diff in zip(spaces, diffs):
space.tag_alignment += diff

for spaces in self.iter_top_spaces():
heights = [
space.tag_height + space.tag_alignment for space in spaces
]
high = max(heights)
diffs = [high - h for h in heights]
for space, diff in zip(spaces, diffs):
space.tag_alignment += diff

for spaces in self.iter_left_spaces():
widths = [
space.tag_width + space.tag_alignment for space in spaces
]
high = max(widths)
diffs = [high - w for w in widths]
for space, diff in zip(spaces, diffs):
space.tag_alignment += diff

for spaces in self.iter_right_spaces():
widths = [
space.tag_width + space.tag_alignment for space in spaces
]
high = max(widths)
diffs = [high - w for w in widths]
for space, diff in zip(spaces, diffs):
space.tag_alignment += diff
align_args = [
(self.bottom_spaces, lambda s: s.tag_height + s.tag_alignment),
(self.top_spaces, lambda s: s.tag_height + s.tag_alignment),
(self.left_spaces, lambda s: s.tag_width + s.tag_alignment),
(self.right_spaces, lambda s: s.tag_width + s.tag_alignment),
]
for spaces, measure in align_args:
_align(spaces, measure, "tag_alignment")

def align_axis_titles(self):
"""
Expand All @@ -589,19 +547,11 @@ def align_axis_titles(self):
setting the position of the texts!
"""

for spaces in self.iter_bottom_spaces():
clearances = [space.axis_title_clearance for space in spaces]
high = max(clearances)
diffs = [high - b for b in clearances]
for space, diff in zip(spaces, diffs):
space.axis_title_alignment += diff
def axis_title_clearance(s):
return s.axis_title_clearance

for spaces in self.iter_left_spaces():
clearances = [space.axis_title_clearance for space in spaces]
high = max(clearances)
diffs = [high - l for l in clearances]
for space, diff in zip(spaces, diffs):
space.axis_title_alignment += diff
for spaces in [self.bottom_spaces, self.left_spaces]:
_align(spaces, axis_title_clearance, "axis_title_alignment")

for tree in self.sub_compositions:
tree.align_axis_titles()
Expand Down Expand Up @@ -632,6 +582,50 @@ def resize_heights(self):
self.sub_gridspec.set_height_ratios(height_ratios)


def _align(
spaces_iter: Iterator[Sequence[Any]],
measure: Callable[[Any], float],
attr: str,
how: Literal["max", "min"] = "max",
):
"""
Align spaces by adjusting an attribute

For each group of spaces yielded by the iterator, find the extreme
value (max or min) of the measurement, then add the difference to
each space's alignment attribute so that all spaces in the group end
up with the same measurement.

Parameters
----------
spaces_iter
Iterator yielding groups of side spaces to equalize.
Each group is a sequence of spaces along the same row or column of
the composition.
measure
Function that extracts the value to equalize from a side space.
attr
Name of the alignment attribute on the side space to adjust.
The difference is added to the current value.
how
Whether to equalize to the maximum or minimum value in each group.
For "max", spaces with smaller measurements get extra alignment to
match the largest.
For "min", spaces with larger measurements get extra alignment to
match the smallest.
"""
for spaces in spaces_iter:
values = [measure(s) for s in spaces]
if how == "max":
target = max(values)
diffs = [target - v for v in values]
else:
target = min(values)
diffs = [v - target for v in values]
for space, diff in zip(spaces, diffs):
setattr(space, attr, getattr(space, attr) + diff)


# For debugging
def _draw_gridspecs(tree: LayoutTree):
from ..utils import draw_bbox
Expand Down
Loading
Loading