Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 40d4ff6

Browse files
committed
Add plot_layout
1 parent 84e6988 commit 40d4ff6

File tree

8 files changed

+191
-32
lines changed

8 files changed

+191
-32
lines changed

doc/_quartodoc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,7 @@ quartodoc:
541541
- Stack
542542
- Beside
543543
- plot_spacer
544+
- plot_layout
544545

545546
- title: Options
546547
desc: |

doc/changelog.qmd

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
title: Changelog
33
---
44

5-
## v0.15.1
5+
## v0.16.0
66
(not-yet-released)
77

88
### New
@@ -12,6 +12,9 @@ title: Changelog
1212
- Added [](:class:`~plotnine.composition.Compose.show`), making it possible to output/show
1313
plot composition objects when they are not the last object in a cell.
1414

15+
- Added [](:class:`~plotnine.composition.plot_layout`) with which you can customise the
16+
layout of plots in composition.
17+
1518
### Enhancements
1619

1720
- In a jupyter environment, the output when the plot is the last in a cell is

plotnine/_mpl/layout_manager/_layout_tree.py

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from __future__ import annotations
22

33
import abc
4-
from dataclasses import dataclass
4+
from dataclasses import dataclass, field
55
from functools import cached_property
66
from typing import TYPE_CHECKING
77

@@ -72,7 +72,18 @@ class LayoutTree:
7272
Each composition is a tree or subtree
7373
"""
7474

75-
gridspec: p9GridSpec
75+
cmp: Compose
76+
"""
77+
Composition that this tree represents
78+
"""
79+
80+
nodes: list[LayoutSpaces | LayoutTree]
81+
"""
82+
The spaces or tree of spaces in the composition that the tree
83+
represents.
84+
"""
85+
86+
gridspec: p9GridSpec = field(init=False)
7687
"""
7788
Gridspec of the composition
7889
@@ -86,11 +97,8 @@ class LayoutTree:
8697
LayoutSpaces.
8798
"""
8899

89-
nodes: list[LayoutSpaces | LayoutTree]
90-
"""
91-
The spaces or tree of spaces in the composition that the tree
92-
represents.
93-
"""
100+
def __post_init__(self):
101+
self.gridspec = self.cmp.gridspec
94102

95103
@staticmethod
96104
def create(
@@ -122,9 +130,9 @@ def create(
122130
nodes.append(LayoutTree.create(item, lookup_spaces))
123131

124132
if isinstance(cmp, Beside):
125-
return ColumnsTree(cmp.gridspec, nodes)
133+
return ColumnsTree(cmp, nodes)
126134
else:
127-
return RowsTree(cmp.gridspec, nodes)
135+
return RowsTree(cmp, nodes)
128136

129137
@cached_property
130138
def sub_compositions(self) -> list[LayoutTree]:
@@ -310,13 +318,15 @@ def resize(self):
310318
"""
311319
Resize the widths of gridspec so that panels have equal widths
312320
"""
313-
# The new width of each panel is the average width of all
314-
# the panels plus all the space to the left and right
315-
# of the panels.
321+
# The new width of each panel is the average width (scaled by
322+
# the plot_layout widths) of all the panels plus all the space
323+
# to the left and right of the panels.
324+
widths = np.array(self.cmp._plot_layout.widths)
316325
plot_widths = np.array(self.plot_widths)
317326
panel_widths = np.array(self.panel_widths)
318327
non_panel_space = plot_widths - panel_widths
319-
new_plot_widths = panel_widths.mean() + non_panel_space
328+
scaled_panel_widths = panel_widths.mean() * widths
329+
new_plot_widths = scaled_panel_widths + non_panel_space
320330
width_ratios = new_plot_widths / new_plot_widths.min()
321331
self.gridspec.set_width_ratios(width_ratios)
322332
self.resize_sub_compositions()
@@ -472,12 +482,15 @@ def resize(self):
472482
473483
This method resizes (recursively) the contained compositions
474484
"""
475-
# The new height of each panel is the average width of all
476-
# the panels plus all the space above and below the panels.
485+
# The new height of each panel is the average width (scaled by
486+
# the plot_layout heights) of all the panels plus all the space
487+
# above and below the panels.
488+
heights = np.array(self.cmp._plot_layout.heights)
477489
plot_heights = np.array(self.plot_heights)
478490
panel_heights = np.array(self.panel_heights)
479491
non_panel_space = plot_heights - panel_heights
480-
new_plot_heights = panel_heights.mean() + non_panel_space
492+
scaled_panel_heights = panel_heights.mean() * heights
493+
new_plot_heights = scaled_panel_heights + non_panel_space
481494
height_ratios = new_plot_heights / new_plot_heights.max()
482495
self.gridspec.set_height_ratios(height_ratios)
483496
self.resize_sub_compositions()

plotnine/composition/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from ._beside import Beside
22
from ._compose import Compose
3+
from ._plot_layout import plot_layout
34
from ._plot_spacer import plot_spacer
45
from ._stack import Stack
56

67
__all__ = (
78
"Compose",
89
"Stack",
910
"Beside",
11+
"plot_layout",
1012
"plot_spacer",
1113
)

plotnine/composition/_compose.py

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
from io import BytesIO
77
from typing import TYPE_CHECKING, overload
88

9+
from plotnine.composition._plot_layout import plot_layout
10+
from plotnine.composition._types import ComposeAddable
11+
912
from .._utils.context import plot_composition_context
1013
from .._utils.ipython import (
1114
get_ipython,
@@ -85,7 +88,12 @@ class Compose:
8588

8689
items: list[ggplot | Compose]
8790
"""
88-
The objects to be arranged (composed).
91+
The objects to be arranged (composed)
92+
"""
93+
94+
_plot_layout: plot_layout = field(init=False, repr=False)
95+
"""
96+
The instance of plot_layout added to the composition
8997
"""
9098

9199
# These are created in the _create_figure method
@@ -130,7 +138,10 @@ def __truediv__(self, rhs: ggplot | Compose) -> Compose:
130138
Add rhs as a row
131139
"""
132140

133-
def __add__(self, rhs: ggplot | Compose | PlotAddable) -> Compose:
141+
def __add__(
142+
self,
143+
rhs: ggplot | Compose | PlotAddable | ComposeAddable,
144+
) -> Compose:
134145
"""
135146
Add rhs to the composition
136147
@@ -141,10 +152,13 @@ def __add__(self, rhs: ggplot | Compose | PlotAddable) -> Compose:
141152
"""
142153
from plotnine import ggplot
143154

144-
if not isinstance(rhs, (ggplot, Compose)):
145-
cmp = deepcopy(self)
146-
cmp.last_plot = cmp.last_plot + rhs
147-
return cmp
155+
self = deepcopy(self)
156+
157+
if isinstance(rhs, ComposeAddable):
158+
return rhs.__radd__(self)
159+
elif not isinstance(rhs, (ggplot, Compose)):
160+
self.last_plot = self.last_plot + rhs
161+
return self
148162

149163
t1, t2 = type(self).__name__, type(rhs).__name__
150164
msg = f"unsupported operand type(s) for +: '{t1}' and '{t2}'"
@@ -181,14 +195,12 @@ def __and__(self, rhs: PlotAddable) -> Compose:
181195
"""
182196
self = deepcopy(self)
183197

184-
def add_other(cmp: Compose):
185-
for i, item in enumerate(cmp):
186-
if isinstance(item, Compose):
187-
add_other(item)
188-
else:
189-
cmp[i] = item + copy(rhs)
198+
for i, item in enumerate(self):
199+
if isinstance(item, Compose):
200+
self[i] = item & rhs
201+
else:
202+
item += copy(rhs)
190203

191-
add_other(self)
192204
return self
193205

194206
def __mul__(self, rhs: PlotAddable) -> Compose:
@@ -204,9 +216,9 @@ def __mul__(self, rhs: PlotAddable) -> Compose:
204216

205217
self = deepcopy(self)
206218

207-
for i, item in enumerate(self):
219+
for item in self:
208220
if isinstance(item, ggplot):
209-
self[i] = item + copy(rhs)
221+
item += copy(rhs)
210222

211223
return self
212224

@@ -338,6 +350,11 @@ def _create_gridspec(self, figure, nest_into):
338350
self.nrow, self.ncol, figure, nest_into=nest_into
339351
)
340352

353+
if not hasattr(self, "_plot_layout"):
354+
self._plot_layout = plot_layout()
355+
356+
self._plot_layout._setup(self.nrow, self.ncol)
357+
341358
def _setup(self) -> Figure:
342359
"""
343360
Setup this instance for the building process

plotnine/composition/_plot_layout.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from __future__ import annotations
2+
3+
from copy import copy
4+
from dataclasses import dataclass
5+
from itertools import cycle
6+
from typing import TYPE_CHECKING, Sequence
7+
8+
from plotnine.composition._types import ComposeAddable
9+
10+
if TYPE_CHECKING:
11+
from ._compose import Compose
12+
13+
14+
@dataclass(kw_only=True)
15+
class plot_layout(ComposeAddable):
16+
"""
17+
Customise the layout of plots in a composition
18+
"""
19+
20+
widths: Sequence[float] | None = None
21+
"""
22+
Relative widths of each column
23+
"""
24+
25+
heights: Sequence[float] | None = None
26+
"""
27+
Relative heights of each column
28+
"""
29+
30+
def __radd__(self, cmp: Compose) -> Compose:
31+
"""
32+
Add plot layout to composition
33+
"""
34+
cmp._plot_layout = copy(self)
35+
return cmp
36+
37+
def _setup(self, nrow: int, ncol: int):
38+
"""
39+
Setup default parameters as they are expected by the layout manager
40+
"""
41+
# from mizani.bounds import rescale
42+
43+
ws, hs = self.widths, self.heights
44+
if ws is None:
45+
ws = (1 / ncol,) * ncol
46+
elif len(ws) != ncol:
47+
ws = repeat(ws, ncol)
48+
49+
if hs is None:
50+
hs = (1 / nrow,) * nrow
51+
elif len(hs) != nrow:
52+
hs = repeat(hs, nrow)
53+
54+
self.widths = rescale(ws)
55+
self.heights = rescale(hs)
56+
57+
58+
def repeat(seq: Sequence[float], n: int) -> list[float]:
59+
"""
60+
Ensure returned sequence has n values, repeat as necessary
61+
"""
62+
return [val for _, val in zip(range(n), cycle(seq))]
63+
64+
65+
def rescale(seq) -> list[float]:
66+
high = max(seq)
67+
return [x / high for x in seq]

plotnine/composition/_types.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
if TYPE_CHECKING:
6+
from ._compose import Compose
7+
8+
9+
class ComposeAddable:
10+
"""
11+
Object that can be added to a ggplot object
12+
"""
13+
14+
def __radd__(self, other: Compose) -> Compose:
15+
"""
16+
Add to compose object
17+
18+
Parameters
19+
----------
20+
other :
21+
Compose object
22+
23+
Returns
24+
-------
25+
:
26+
Compose object
27+
"""
28+
return other

tests/test_plot_composition.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from plotnine import element_text, facet_grid, facet_wrap, theme, theme_gray
22
from plotnine._utils.yippie import geom as g
33
from plotnine._utils.yippie import legend, plot, rotate, tag
4+
from plotnine.composition._plot_layout import plot_layout
45

56

67
def test_basic_horizontal_align_resize():
@@ -151,3 +152,30 @@ def test_plus_operator():
151152
p4 = plot.brown
152153
p = (p1 | p2 | (p3 / p4)) + theme_gray()
153154
assert p == "plus_operator"
155+
156+
157+
def test_plot_layout_widths():
158+
p1 = plot.red
159+
p2 = plot.green
160+
p = (p1 | p2) + plot_layout(widths=[1, 4])
161+
assert p == "plot_layout_widths"
162+
163+
164+
def test_plot_layout_heights():
165+
p1 = plot.red
166+
p2 = plot.green
167+
p = (p1 / p2) + plot_layout(heights=[1, 4])
168+
assert p == "plot_layout_heights"
169+
170+
171+
def test_plot_layout_nested_resize():
172+
p1 = plot.red
173+
p2 = plot.green
174+
p3 = plot.blue
175+
p4 = plot.brown
176+
177+
ws = plot_layout(widths=[1, 4])
178+
hs = plot_layout(heights=[1, 3])
179+
180+
p = (((p1 | p2) + ws) / ((p3 | p4) + ws)) + hs
181+
assert p == "plot_layout_nested_resize"

0 commit comments

Comments
 (0)