Skip to content

Commit

Permalink
Use LayoutSpaces & LayoutItems
Browse files Browse the repository at this point in the history
LayoutItems has replaced LayoutPack
  • Loading branch information
has2k1 committed Nov 12, 2024
1 parent 3da542f commit 78d368e
Show file tree
Hide file tree
Showing 4 changed files with 276 additions and 298 deletions.
12 changes: 5 additions & 7 deletions plotnine/_mpl/layout_manager/_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from matplotlib.layout_engine import LayoutEngine

from ._layout_pack import LayoutPack
from ._spaces import LayoutSpaces

if TYPE_CHECKING:
from matplotlib.figure import Figure
Expand Down Expand Up @@ -33,12 +33,10 @@ def __init__(self, plot: ggplot):
def execute(self, fig: Figure):
from contextlib import nullcontext

from ._tight_layout import adjust_figure_artists, compute_layout
renderer = fig._get_renderer() # pyright: ignore[reportAttributeAccessIssue]

pack = LayoutPack(self.plot)

with getattr(pack.renderer, "_draw_disabled", nullcontext)():
spaces = compute_layout(pack)
with getattr(renderer, "_draw_disabled", nullcontext)():
spaces = LayoutSpaces(self.plot)

fig.subplots_adjust(**asdict(spaces.gsparams))
adjust_figure_artists(pack, spaces)
spaces.items._adjust_positions(spaces)
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from ..utils import (
bbox_in_figure_space,
get_transPanels,
tight_bbox_in_figure_space,
)

Expand All @@ -25,35 +26,46 @@
from matplotlib.artist import Artist
from matplotlib.axes import Axes
from matplotlib.axis import Tick
from matplotlib.figure import Figure
from matplotlib.transforms import Bbox
from matplotlib.transforms import Bbox, Transform

from plotnine import ggplot
from plotnine._mpl.offsetbox import FlexibleAnchoredOffsetbox
from plotnine._mpl.text import StripText
from plotnine.iapi import legend_artists
from plotnine.typing import StripPosition

from ._spaces import LayoutSpaces

AxesLocation: TypeAlias = Literal[
"all", "first_row", "last_row", "first_col", "last_col"
]


@dataclass
class Calc:
fig: Figure
renderer: RendererBase
"""
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.fig, self.renderer)
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.fig, self.renderer)
return tight_bbox_in_figure_space(artist, self.figure, self.renderer)

def width(self, artist: Artist) -> float:
"""
Expand Down Expand Up @@ -138,7 +150,7 @@ def max_width(self, artists: Sequence[Artist]) -> float:
Return the maximum width of list of artists
"""
widths = [
bbox_in_figure_space(a, self.fig, self.renderer).width
bbox_in_figure_space(a, self.figure, self.renderer).width
for a in artists
]
return max(widths) if len(widths) else 0
Expand All @@ -148,14 +160,14 @@ def max_height(self, artists: Sequence[Artist]) -> float:
Return the maximum height of list of artists
"""
heights = [
bbox_in_figure_space(a, self.fig, self.renderer).height
bbox_in_figure_space(a, self.figure, self.renderer).height
for a in artists
]
return max(heights) if len(heights) else 0


@dataclass
class LayoutPack:
class LayoutItems:
"""
Objects required to compute the layout
"""
Expand All @@ -167,20 +179,15 @@ def get(name: str) -> Any:
"""
Return themeable target or None
"""
if self.theme.T.is_blank(name):
if self._is_blank(name):
return None
else:
t = getattr(self.theme.targets, name)
t = getattr(self.plot.theme.targets, name)
if isinstance(t, Text) and t.get_text() == "":
return None
return t

self.axs = self.plot.axs
self.theme = self.plot.theme
self.figure = self.plot.figure
self.facet = self.plot.facet
self.renderer = cast(RendererBase, self.plot.figure._get_renderer()) # pyright: ignore
self.calc = Calc(self.figure, self.renderer)
self.calc = Calc(self.plot)

self.axis_title_x: Text | None = get("axis_title_x")
self.axis_title_y: Text | None = get("axis_title_y")
Expand All @@ -195,20 +202,22 @@ def get(name: str) -> Any:
self.strip_text_y: list[StripText] | None = get("strip_text_y")

def _is_blank(self, name: str) -> bool:
return self.theme.T.is_blank(name)
return self.plot.theme.T.is_blank(name)

def _filter_axes(self, location: AxesLocation = "all") -> list[Axes]:
"""
Return subset of axes
"""
axs = self.plot.axs

if location == "all":
return self.axs
return axs

# e.g. is_first_row, is_last_row, ..
pred_method = f"is_{location}"
return [
ax
for spec, ax in zip(get_subplotspec_list(self.axs), self.axs)
for spec, ax in zip(get_subplotspec_list(axs), axs)
if getattr(spec, pred_method)()
]

Expand Down Expand Up @@ -280,7 +289,7 @@ def axis_ticks_pad_x(self, ax: Axes) -> Iterator[float]:
# the axis_text.
major, minor = [], []
if not self._is_blank("axis_text_x"):
h = self.figure.get_figheight() * 72
h = self.plot.figure.get_figheight() * 72
major = [
(t.get_pad() or 0) / h for t in ax.xaxis.get_major_ticks()
]
Expand All @@ -297,7 +306,7 @@ def axis_ticks_pad_y(self, ax: Axes) -> Iterator[float]:
# the axis_text.
major, minor = [], []
if not self._is_blank("axis_text_y"):
w = self.figure.get_figwidth() * 72
w = self.plot.figure.get_figwidth() * 72
major = [
(t.get_pad() or 0) / w for t in ax.yaxis.get_major_ticks()
]
Expand Down Expand Up @@ -436,9 +445,165 @@ def axis_text_x_right_protrusion(self, location: AxesLocation) -> float:

return max(extras) if len(extras) else 0

def _adjust_positions(self, spaces: LayoutSpaces):
"""
Set the x,y position of the artists around the panels
"""
theme = self.plot.theme

if self.plot_title:
ha = theme.getp(("plot_title", "ha"))
self.plot_title.set_y(spaces.t.edge("plot_title"))
horizontally_align_text_with_panels(self.plot_title, ha, spaces)

if self.plot_subtitle:
ha = theme.getp(("plot_subtitle", "ha"))
self.plot_subtitle.set_y(spaces.t.edge("plot_subtitle"))
horizontally_align_text_with_panels(self.plot_subtitle, ha, spaces)

if self.plot_caption:
ha = theme.getp(("plot_caption", "ha"), "right")
self.plot_caption.set_y(spaces.b.edge("plot_caption"))
horizontally_align_text_with_panels(self.plot_caption, ha, spaces)

if self.axis_title_x:
ha = theme.getp(("axis_title_x", "ha"), "center")
self.axis_title_x.set_y(spaces.b.edge("axis_title_x"))
horizontally_align_text_with_panels(self.axis_title_x, ha, spaces)

if self.axis_title_y:
va = theme.getp(("axis_title_y", "va"), "center")
self.axis_title_y.set_x(spaces.l.edge("axis_title_y"))
vertically_align_text_with_panels(self.axis_title_y, va, spaces)

if self.legends:
set_legends_position(self.legends, spaces)


def _text_is_visible(text: Text) -> bool:
"""
Return True if text is visible and is not empty
"""
return text.get_visible() and text._text # type: ignore


def horizontally_align_text_with_panels(
text: Text, ha: str | float, spaces: LayoutSpaces
):
"""
Horizontal justification
Reinterpret horizontal alignment to be justification about the panels.
"""
if isinstance(ha, str):
lookup = {
"left": 0.0,
"center": 0.5,
"right": 1.0,
}
f = lookup[ha]
else:
f = ha

params = spaces.gsparams
width = spaces.items.calc.width(text)
x = params.left * (1 - f) + (params.right - width) * f
text.set_x(x)
text.set_horizontalalignment("left")


def vertically_align_text_with_panels(
text: Text, va: str | float, spaces: LayoutSpaces
):
"""
Vertical justification
Reinterpret vertical alignment to be justification about the panels.
"""
if isinstance(va, str):
lookup = {
"top": 1.0,
"center": 0.5,
"baseline": 0.5,
"center_baseline": 0.5,
"bottom": 0.0,
}
f = lookup[va]
else:
f = va

params = spaces.gsparams
height = spaces.items.calc.height(text)
y = params.bottom * (1 - f) + (params.top - height) * f
text.set_y(y)
text.set_verticalalignment("bottom")


def set_legends_position(legends: legend_artists, spaces: LayoutSpaces):
"""
Place legend on the figure and justify is a required
"""
figure = spaces.plot.figure
params = figure.subplotpars

def set_position(
aob: FlexibleAnchoredOffsetbox,
anchor_point: tuple[float, float],
xy_loc: tuple[float, float],
transform: Transform = figure.transFigure,
):
"""
Place box (by the anchor point) at given xy location
Parameters
----------
aob :
Offsetbox to place
anchor_point :
Point on the Offsefbox.
xy_loc :
Point where to place the offsetbox.
transform :
Transformation
"""
aob.xy_loc = xy_loc
aob.set_bbox_to_anchor(anchor_point, transform) # type: ignore

def func(a, b, length, f):
return a * (1 - f) + (b - length) * f

if legends.right:
j = legends.right.justification
y = (
params.bottom * (1 - j)
+ (params.top - spaces.r._legend_height) * j
)
x = spaces.r.edge("legend")
set_position(legends.right.box, (x, y), (1, 0))

if legends.left:
j = legends.left.justification
y = (
params.bottom * (1 - j)
+ (params.top - spaces.l._legend_height) * j
)
x = spaces.l.edge("legend")
set_position(legends.left.box, (x, y), (0, 0))

if legends.top:
j = legends.top.justification
x = params.left * (1 - j) + (params.right - spaces.t._legend_width) * j
y = spaces.t.edge("legend")
set_position(legends.top.box, (x, y), (0, 1))

if legends.bottom:
j = legends.bottom.justification
x = params.left * (1 - j) + (params.right - spaces.b._legend_width) * j
y = spaces.b.edge("legend")
set_position(legends.bottom.box, (x, y), (0, 0))

# Inside legends are placed using the panels coordinate system
if legends.inside:
transPanels = get_transPanels(figure)
for l in legends.inside:
set_position(l.box, l.position, l.justification, transPanels)
Loading

0 comments on commit 78d368e

Please sign in to comment.