ggh4x-python 0.3.1.9000__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ggh4x/__init__.py +140 -0
- ggh4x/_aimed_text_grob.py +432 -0
- ggh4x/_borrowed_ggplot2.py +273 -0
- ggh4x/_cli.py +84 -0
- ggh4x/_datasets.py +106 -0
- ggh4x/_download.py +111 -0
- ggh4x/_facet_helpers.py +313 -0
- ggh4x/_facet_utils.py +649 -0
- ggh4x/_gap_grobs.py +606 -0
- ggh4x/_registry.py +10 -0
- ggh4x/_rlang.py +93 -0
- ggh4x/_utils.py +150 -0
- ggh4x/_vctrs.py +233 -0
- ggh4x/conveniences.py +601 -0
- ggh4x/coord_axes_inside.py +380 -0
- ggh4x/element_part_rect.py +545 -0
- ggh4x/facet_grid2.py +1018 -0
- ggh4x/facet_manual.py +901 -0
- ggh4x/facet_nested.py +776 -0
- ggh4x/facet_nested_wrap.py +193 -0
- ggh4x/facet_wrap2.py +896 -0
- ggh4x/geom_box.py +536 -0
- ggh4x/geom_outline_point.py +444 -0
- ggh4x/geom_pointpath.py +259 -0
- ggh4x/geom_polygonraster.py +252 -0
- ggh4x/geom_rectrug.py +489 -0
- ggh4x/geom_text_aimed.py +279 -0
- ggh4x/guide_stringlegend.py +354 -0
- ggh4x/help_secondary.py +549 -0
- ggh4x/multiscale/__init__.py +51 -0
- ggh4x/multiscale/_multiscale_add.py +207 -0
- ggh4x/multiscale/scale_listed.py +167 -0
- ggh4x/multiscale/scale_manual.py +478 -0
- ggh4x/multiscale/scale_multi.py +393 -0
- ggh4x/panel_scales/__init__.py +58 -0
- ggh4x/panel_scales/at_panel.py +115 -0
- ggh4x/panel_scales/facetted_pos_scales.py +647 -0
- ggh4x/panel_scales/force_panelsize.py +411 -0
- ggh4x/panel_scales/scale_facet.py +222 -0
- ggh4x/position_disjoint_ranges.py +229 -0
- ggh4x/position_lineartrans.py +242 -0
- ggh4x/py.typed +0 -0
- ggh4x/resources/faithful.csv +273 -0
- ggh4x/resources/iris.csv +151 -0
- ggh4x/resources/mtcars.csv +33 -0
- ggh4x/resources/pressure.csv +20 -0
- ggh4x/resources/volcano.csv +87 -0
- ggh4x/save.py +255 -0
- ggh4x/stat_difference.py +388 -0
- ggh4x/stat_funxy.py +436 -0
- ggh4x/stat_rle.py +290 -0
- ggh4x/stat_rollingkernel.py +369 -0
- ggh4x/stat_theodensity.py +681 -0
- ggh4x/strip_nested.py +448 -0
- ggh4x/strip_split.py +687 -0
- ggh4x/strip_tag.py +636 -0
- ggh4x/strip_themed.py +232 -0
- ggh4x/strip_vanilla.py +1464 -0
- ggh4x/themes.py +31 -0
- ggh4x/themes_ggh4x.py +67 -0
- ggh4x_python-0.3.1.9000.dist-info/METADATA +40 -0
- ggh4x_python-0.3.1.9000.dist-info/RECORD +64 -0
- ggh4x_python-0.3.1.9000.dist-info/WHEEL +4 -0
- ggh4x_python-0.3.1.9000.dist-info/licenses/LICENSE +3 -0
ggh4x/facet_grid2.py
ADDED
|
@@ -0,0 +1,1018 @@
|
|
|
1
|
+
"""Extended grid facets (port of ggh4x ``R/facet_grid2.R``).
|
|
2
|
+
|
|
3
|
+
``facet_grid2`` behaves like :func:`ggplot2_py.facet_grid` but adds three things:
|
|
4
|
+
|
|
5
|
+
* axes may be drawn (and optionally label-purged) at inner panels (``axes`` /
|
|
6
|
+
``remove_labels``),
|
|
7
|
+
* position scales may be *independent* within a row or column (``independent``),
|
|
8
|
+
* empty panels may be rendered as blanks (``render_empty``).
|
|
9
|
+
|
|
10
|
+
The :class:`FacetGrid2` ggproto subclasses :class:`ggplot2_py.facet.FacetGrid` and
|
|
11
|
+
*fully replaces* its ``draw_panels`` with a decomposed, strip-pluggable pipeline
|
|
12
|
+
(the explicit reason ggh4x exists -- to give ``facet_nested`` clean override
|
|
13
|
+
seams). Every step (``setup_axes``, ``setup_aspect_ratio``, ``setup_panel_table``,
|
|
14
|
+
``attach_axes``, ``finish_panels``) is an overridable method.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
import pandas as pd
|
|
23
|
+
|
|
24
|
+
from ggplot2_py import calc_element, ggproto
|
|
25
|
+
from ggplot2_py.facet import FacetGrid, _combine_vars, _resolve_facet_vars, facet_grid
|
|
26
|
+
from grid_py import GList, GTree, Unit, edit_viewport, null_grob
|
|
27
|
+
from gtable_py import (
|
|
28
|
+
Gtable,
|
|
29
|
+
gtable_add_col_space,
|
|
30
|
+
gtable_add_grob,
|
|
31
|
+
gtable_add_row_space,
|
|
32
|
+
gtable_filter,
|
|
33
|
+
gtable_height,
|
|
34
|
+
gtable_trim,
|
|
35
|
+
gtable_width,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from ggh4x._borrowed_ggplot2 import id, is_zero, snake_class, ulevels
|
|
39
|
+
from ggh4x._cli import cli_abort
|
|
40
|
+
from ggh4x._facet_helpers import (
|
|
41
|
+
AspectRatio,
|
|
42
|
+
_match_facet_arg,
|
|
43
|
+
_validate_independent,
|
|
44
|
+
reshape_add_margins,
|
|
45
|
+
)
|
|
46
|
+
from ggh4x._facet_utils import (
|
|
47
|
+
df_grid,
|
|
48
|
+
render_axes,
|
|
49
|
+
weave_tables_col,
|
|
50
|
+
weave_tables_row,
|
|
51
|
+
)
|
|
52
|
+
from ggh4x._rlang import arg_match0
|
|
53
|
+
from ggh4x.strip_vanilla import resolve_strip
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"facet_grid2",
|
|
57
|
+
"FacetGrid2",
|
|
58
|
+
"new_grid_facets",
|
|
59
|
+
"purge_guide_labels",
|
|
60
|
+
"_measure_axes",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ---------------------------------------------------------------------------
|
|
65
|
+
# Shared axis helpers (R: facet_wrap2.R purge_guide_labels / .measure_axes)
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
def purge_guide_labels(guide: Any) -> Any:
|
|
68
|
+
"""Strip the label grobs from an axis guide, keeping line + ticks.
|
|
69
|
+
|
|
70
|
+
Faithful port of ggh4x's ``purge_guide_labels`` (``R/facet_wrap2.R:396-416``)
|
|
71
|
+
adapted to the ``ggplot2_py`` axis grob layout. R reaches into
|
|
72
|
+
``guide$children$axis$grobs`` and drops every grob that is a ``titleGrob`` /
|
|
73
|
+
``richtext_grob`` / ``zeroGrob`` (i.e. the labels), trims the inner axis
|
|
74
|
+
gtable, then resets the guide's reported width/height.
|
|
75
|
+
|
|
76
|
+
In ``ggplot2_py`` :func:`ggplot2_py._guide_axis.draw_axis` returns an
|
|
77
|
+
``_AbsoluteAxisGrob`` whose children are ``[axis_line, inner_gtable]`` and the
|
|
78
|
+
inner gtable carries the ``"axis.labels"`` cells. This port locates that
|
|
79
|
+
inner gtable, deletes its ``"axis.labels"`` grobs via :func:`gtable_filter`,
|
|
80
|
+
trims it, and refreshes the wrapper's measured width / height.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
guide : grob
|
|
85
|
+
An axis grob (``_AbsoluteAxisGrob``) or a zero grob.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
grob
|
|
90
|
+
The label-purged axis grob (zero grobs pass through unchanged).
|
|
91
|
+
"""
|
|
92
|
+
if is_zero(guide):
|
|
93
|
+
return guide
|
|
94
|
+
|
|
95
|
+
children = list(guide.get_children()) if hasattr(guide, "get_children") else []
|
|
96
|
+
inner_idx = None
|
|
97
|
+
inner_gt = None
|
|
98
|
+
for i, child in enumerate(children):
|
|
99
|
+
if isinstance(child, Gtable):
|
|
100
|
+
inner_idx = i
|
|
101
|
+
inner_gt = child
|
|
102
|
+
break
|
|
103
|
+
if inner_gt is None:
|
|
104
|
+
return guide
|
|
105
|
+
|
|
106
|
+
# Drop the label cells, keep everything else (line, ticks).
|
|
107
|
+
purged = gtable_filter(inner_gt, "axis.labels", trim=False, invert=True)
|
|
108
|
+
purged = gtable_trim(purged)
|
|
109
|
+
|
|
110
|
+
# Replace the inner gtable child in place.
|
|
111
|
+
order = list(guide._children_order) if hasattr(guide, "_children_order") else None
|
|
112
|
+
if order is not None and inner_idx < len(order):
|
|
113
|
+
key = order[inner_idx]
|
|
114
|
+
guide._children[key] = purged
|
|
115
|
+
else:
|
|
116
|
+
# Fallback: rebuild children list.
|
|
117
|
+
new_children = list(children)
|
|
118
|
+
new_children[inner_idx] = purged
|
|
119
|
+
guide.set_children(GList(*new_children))
|
|
120
|
+
|
|
121
|
+
# Reset reported width/height (R: guide$width/height <- sum(axis$...)).
|
|
122
|
+
new_w = gtable_width(purged)
|
|
123
|
+
new_h = gtable_height(purged)
|
|
124
|
+
if hasattr(guide, "_abs_width"):
|
|
125
|
+
guide._abs_width = new_w
|
|
126
|
+
guide._abs_height = new_h
|
|
127
|
+
vp = getattr(guide, "vp", None)
|
|
128
|
+
if vp is not None:
|
|
129
|
+
edits = {}
|
|
130
|
+
if getattr(vp, "width", None) is not None:
|
|
131
|
+
edits["width"] = new_w
|
|
132
|
+
if getattr(vp, "height", None) is not None:
|
|
133
|
+
edits["height"] = new_h
|
|
134
|
+
if edits:
|
|
135
|
+
# grid_py Viewport is immutable (read-only width/height); rebuild it
|
|
136
|
+
# via edit_viewport rather than assigning in place.
|
|
137
|
+
guide.vp = edit_viewport(vp, **edits)
|
|
138
|
+
return guide
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _measure_axes(axes: Dict[str, List[List[Any]]]) -> Dict[str, Unit]:
|
|
142
|
+
"""Measure per-row / per-column axis bands in centimetres.
|
|
143
|
+
|
|
144
|
+
Faithful port of ggh4x's ``.measure_axes`` (``R/facet_wrap2.R:435-441``).
|
|
145
|
+
For the ``top`` / ``bottom`` grob matrices the maximum *height* (cm) is taken
|
|
146
|
+
over each matrix **row**; for ``left`` / ``right`` the maximum *width* (cm)
|
|
147
|
+
over each matrix **column**.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
axes : dict
|
|
152
|
+
``{"top", "bottom", "left", "right"}`` -- each a row-major grob matrix
|
|
153
|
+
(list of lists), one row per panel row and one column per panel column.
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
dict
|
|
158
|
+
``{"top", "bottom", "left", "right"}`` -- each a ``"cm"`` :class:`Unit`
|
|
159
|
+
of per-band sizes (length = nrow for top/bottom, ncol for left/right).
|
|
160
|
+
"""
|
|
161
|
+
from ggplot2_py.facet import _axis_height_cm, _axis_width_cm
|
|
162
|
+
|
|
163
|
+
def _h(g: Any) -> float:
|
|
164
|
+
return 0.0 if is_zero(g) else _axis_height_cm(g)
|
|
165
|
+
|
|
166
|
+
def _w(g: Any) -> float:
|
|
167
|
+
return 0.0 if is_zero(g) else _axis_width_cm(g)
|
|
168
|
+
|
|
169
|
+
top_m = axes["top"]
|
|
170
|
+
bottom_m = axes["bottom"]
|
|
171
|
+
left_m = axes["left"]
|
|
172
|
+
right_m = axes["right"]
|
|
173
|
+
nrow = len(top_m)
|
|
174
|
+
ncol = len(top_m[0]) if nrow else 0
|
|
175
|
+
|
|
176
|
+
top = [max((_h(top_m[r][c]) for c in range(ncol)), default=0.0) for r in range(nrow)]
|
|
177
|
+
bottom = [
|
|
178
|
+
max((_h(bottom_m[r][c]) for c in range(ncol)), default=0.0) for r in range(nrow)
|
|
179
|
+
]
|
|
180
|
+
left = [max((_w(left_m[r][c]) for r in range(nrow)), default=0.0) for c in range(ncol)]
|
|
181
|
+
right = [
|
|
182
|
+
max((_w(right_m[r][c]) for r in range(nrow)), default=0.0) for c in range(ncol)
|
|
183
|
+
]
|
|
184
|
+
return {
|
|
185
|
+
"top": Unit(top, "cm"),
|
|
186
|
+
"bottom": Unit(bottom, "cm"),
|
|
187
|
+
"left": Unit(left, "cm"),
|
|
188
|
+
"right": Unit(right, "cm"),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# Constructor
|
|
194
|
+
# ---------------------------------------------------------------------------
|
|
195
|
+
def facet_grid2(
|
|
196
|
+
rows: Any = None,
|
|
197
|
+
cols: Any = None,
|
|
198
|
+
scales: Any = "fixed",
|
|
199
|
+
space: Any = "fixed",
|
|
200
|
+
axes: Any = "margins",
|
|
201
|
+
remove_labels: Any = "none",
|
|
202
|
+
independent: Any = "none",
|
|
203
|
+
shrink: bool = True,
|
|
204
|
+
labeller: Any = "label_value",
|
|
205
|
+
as_table: bool = True,
|
|
206
|
+
switch: Optional[str] = None,
|
|
207
|
+
drop: bool = True,
|
|
208
|
+
margins: Any = False,
|
|
209
|
+
render_empty: bool = True,
|
|
210
|
+
strip: Any = "vanilla",
|
|
211
|
+
) -> "FacetGrid2":
|
|
212
|
+
"""Extended grid facets.
|
|
213
|
+
|
|
214
|
+
Port of ggh4x's ``facet_grid2()`` (``R/facet_grid2.R:100-124``). Like
|
|
215
|
+
:func:`ggplot2_py.facet_grid` but can draw partial / full axis guides at inner
|
|
216
|
+
panels and supports independent position scales.
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
----------
|
|
220
|
+
rows, cols : formula / list / dict / None
|
|
221
|
+
Faceting variables for rows and columns (same spec as ``facet_grid``).
|
|
222
|
+
scales : {"fixed", "free_x", "free_y", "free"} or bool, default "fixed"
|
|
223
|
+
Whether scales are shared or free across facets.
|
|
224
|
+
space : {"fixed", "free_x", "free_y", "free"} or bool, default "fixed"
|
|
225
|
+
Whether panel sizes are proportional to the scales.
|
|
226
|
+
axes : {"margins", "x", "y", "all"} or bool, default "margins"
|
|
227
|
+
Where inner axes are drawn.
|
|
228
|
+
remove_labels : {"none", "x", "y", "all"} or bool, default "none"
|
|
229
|
+
Whether inner-axis text is removed.
|
|
230
|
+
independent : {"none", "x", "y", "all"} or bool, default "none"
|
|
231
|
+
Whether scales vary within a row / column.
|
|
232
|
+
shrink : bool, default True
|
|
233
|
+
Shrink scales to fit stat output.
|
|
234
|
+
labeller : callable or str, default "label_value"
|
|
235
|
+
Strip labeller.
|
|
236
|
+
as_table : bool, default True
|
|
237
|
+
When ``False``, reverse the row factor level order.
|
|
238
|
+
switch : {"x", "y", "both", None}, default None
|
|
239
|
+
Which strips switch sides.
|
|
240
|
+
drop : bool, default True
|
|
241
|
+
Drop unused factor levels.
|
|
242
|
+
margins : bool or list of str, default False
|
|
243
|
+
Add marginal panels.
|
|
244
|
+
render_empty : bool, default True
|
|
245
|
+
Draw data-less panels (``True``) or blank them (``False``).
|
|
246
|
+
strip : Strip or callable or str, default "vanilla"
|
|
247
|
+
Strip specification.
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
FacetGrid2
|
|
252
|
+
A ggproto facet object that can be added to a plot.
|
|
253
|
+
"""
|
|
254
|
+
return new_grid_facets(
|
|
255
|
+
rows,
|
|
256
|
+
cols,
|
|
257
|
+
scales,
|
|
258
|
+
space,
|
|
259
|
+
axes,
|
|
260
|
+
remove_labels,
|
|
261
|
+
independent,
|
|
262
|
+
shrink,
|
|
263
|
+
labeller,
|
|
264
|
+
as_table,
|
|
265
|
+
switch,
|
|
266
|
+
drop,
|
|
267
|
+
margins,
|
|
268
|
+
render_empty,
|
|
269
|
+
strip,
|
|
270
|
+
super_=FacetGrid2,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def new_grid_facets(
|
|
275
|
+
rows: Any,
|
|
276
|
+
cols: Any,
|
|
277
|
+
scales: Any,
|
|
278
|
+
space: Any,
|
|
279
|
+
axes: Any,
|
|
280
|
+
rmlab: Any,
|
|
281
|
+
indy: Any,
|
|
282
|
+
shrink: bool,
|
|
283
|
+
labeller: Any,
|
|
284
|
+
as_table: bool,
|
|
285
|
+
switch: Optional[str],
|
|
286
|
+
drop: bool,
|
|
287
|
+
margins: Any,
|
|
288
|
+
render_empty: bool,
|
|
289
|
+
strip: Any,
|
|
290
|
+
params: Optional[Dict[str, Any]] = None,
|
|
291
|
+
super_: Any = None,
|
|
292
|
+
) -> "FacetGrid2":
|
|
293
|
+
"""Build a :class:`FacetGrid2` instance from raw arguments.
|
|
294
|
+
|
|
295
|
+
Port of ggh4x's ``new_grid_facets()`` (``R/facet_grid2.R:128-171``).
|
|
296
|
+
Normalises the option arguments, validates the ``independent`` interactions,
|
|
297
|
+
resolves the formula spec via :func:`ggplot2_py.facet_grid`, resolves the
|
|
298
|
+
strip, assembles the parameter dict and instantiates the ggproto.
|
|
299
|
+
|
|
300
|
+
Parameters
|
|
301
|
+
----------
|
|
302
|
+
rows, cols : Any
|
|
303
|
+
Faceting variable specs.
|
|
304
|
+
scales, space, axes, rmlab, indy : Any
|
|
305
|
+
Option arguments (normalised via :func:`_match_facet_arg`).
|
|
306
|
+
shrink : bool
|
|
307
|
+
labeller : Any
|
|
308
|
+
as_table : bool
|
|
309
|
+
switch : str or None
|
|
310
|
+
drop : bool
|
|
311
|
+
margins : Any
|
|
312
|
+
render_empty : bool
|
|
313
|
+
strip : Any
|
|
314
|
+
params : dict, optional
|
|
315
|
+
Extra params merged in (used by ``facet_nested``).
|
|
316
|
+
super_ : type, optional
|
|
317
|
+
The ggproto class to instantiate (default :class:`FacetGrid2`).
|
|
318
|
+
|
|
319
|
+
Returns
|
|
320
|
+
-------
|
|
321
|
+
FacetGrid2
|
|
322
|
+
"""
|
|
323
|
+
if super_ is None:
|
|
324
|
+
super_ = FacetGrid2
|
|
325
|
+
params = dict(params or {})
|
|
326
|
+
|
|
327
|
+
switch = switch if switch is not None else "none"
|
|
328
|
+
switch = arg_match0(switch, ["none", "both", "x", "y"], arg_name="switch")
|
|
329
|
+
switch_param = None if switch == "none" else switch
|
|
330
|
+
|
|
331
|
+
axes = _match_facet_arg(axes, ["margins", "x", "y", "all"], nm="axes")
|
|
332
|
+
free = _match_facet_arg(scales, ["fixed", "free_x", "free_y", "free"], nm="scales")
|
|
333
|
+
space = _match_facet_arg(space, ["fixed", "free_x", "free_y", "free"], nm="space")
|
|
334
|
+
rmlab = _match_facet_arg(rmlab, ["none", "x", "y", "all"], nm="remove_labels")
|
|
335
|
+
indy = _match_facet_arg(indy, ["none", "x", "y", "all"], nm="independent")
|
|
336
|
+
strip = resolve_strip(strip)
|
|
337
|
+
|
|
338
|
+
axis_params = _validate_independent(indy, free, space, rmlab)
|
|
339
|
+
|
|
340
|
+
# Resolve the formula -> rows / cols var lists via the base facet_grid.
|
|
341
|
+
# Store as name lists (R keeps a named quosure list; the strip / layout read
|
|
342
|
+
# the names) so both the strip subsystem and ``compute_layout`` agree.
|
|
343
|
+
facets = facet_grid(rows=rows, cols=cols).params
|
|
344
|
+
proto_rows = _resolve_facet_vars(facets["rows"])
|
|
345
|
+
proto_cols = _resolve_facet_vars(facets["cols"])
|
|
346
|
+
|
|
347
|
+
params.update(axis_params)
|
|
348
|
+
params.update(
|
|
349
|
+
{
|
|
350
|
+
"rows": proto_rows,
|
|
351
|
+
"cols": proto_cols,
|
|
352
|
+
"margins": margins,
|
|
353
|
+
"labeller": labeller,
|
|
354
|
+
"as_table": as_table,
|
|
355
|
+
"switch": switch_param,
|
|
356
|
+
"drop": drop,
|
|
357
|
+
"axes": axes,
|
|
358
|
+
"render_empty": render_empty is not False,
|
|
359
|
+
}
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
obj = super_()
|
|
363
|
+
obj._set(shrink=shrink, strip=strip, params=params)
|
|
364
|
+
return obj
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# ---------------------------------------------------------------------------
|
|
368
|
+
# ggproto
|
|
369
|
+
# ---------------------------------------------------------------------------
|
|
370
|
+
def _as_name_list(spec: Any) -> List[str]:
|
|
371
|
+
"""Return the faceting-variable names from a ``rows``/``cols`` spec."""
|
|
372
|
+
if spec is None:
|
|
373
|
+
return []
|
|
374
|
+
if isinstance(spec, dict):
|
|
375
|
+
return list(spec.keys())
|
|
376
|
+
if isinstance(spec, (list, tuple)):
|
|
377
|
+
return [str(s) for s in spec]
|
|
378
|
+
if isinstance(spec, str):
|
|
379
|
+
return [spec]
|
|
380
|
+
return []
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class FacetGrid2(FacetGrid):
|
|
384
|
+
"""Extended grid facet ggproto (port of R ``FacetGrid2``).
|
|
385
|
+
|
|
386
|
+
Subclasses :class:`ggplot2_py.facet.FacetGrid`. Replaces ``compute_layout``
|
|
387
|
+
(adds a ``_render`` column, ``id()``-stable ordering and independent-axis
|
|
388
|
+
``SCALE_X``/``SCALE_Y`` assignment) and ``draw_panels`` (a decomposed,
|
|
389
|
+
strip-pluggable pipeline). All drawing sub-steps are overridable methods so
|
|
390
|
+
``facet_nested`` can hook them.
|
|
391
|
+
|
|
392
|
+
Attributes
|
|
393
|
+
----------
|
|
394
|
+
shrink : bool
|
|
395
|
+
strip : Strip
|
|
396
|
+
The pluggable strip instance.
|
|
397
|
+
params : dict
|
|
398
|
+
Facet parameters (carries ``independent``, ``free``, ``space_free``,
|
|
399
|
+
``rmlab``, ``axes``, ``render_empty``, ``rows``, ``cols``, ...).
|
|
400
|
+
"""
|
|
401
|
+
|
|
402
|
+
_class_name = "FacetGrid2"
|
|
403
|
+
|
|
404
|
+
shrink: bool = True
|
|
405
|
+
strip: Any = None
|
|
406
|
+
|
|
407
|
+
# -- vars_combine (extension seam) --------------------------------------
|
|
408
|
+
def vars_combine(
|
|
409
|
+
self,
|
|
410
|
+
data: List[pd.DataFrame],
|
|
411
|
+
env: Any,
|
|
412
|
+
vars_: Any,
|
|
413
|
+
drop: bool = True,
|
|
414
|
+
) -> pd.DataFrame:
|
|
415
|
+
"""Combine the faceting variables across datasets.
|
|
416
|
+
|
|
417
|
+
Extension seam mirroring ggh4x's ``FacetGrid2$vars_combine``
|
|
418
|
+
(``R/facet_grid2.R:190-192``) which delegates to ggplot2's
|
|
419
|
+
``combine_vars``. ``facet_nested`` overrides this to substitute its own
|
|
420
|
+
variable-combination logic, so it must remain a real method called via
|
|
421
|
+
``self.vars_combine(...)``.
|
|
422
|
+
|
|
423
|
+
Parameters
|
|
424
|
+
----------
|
|
425
|
+
data : list of DataFrame
|
|
426
|
+
The plot + layer data frames.
|
|
427
|
+
env : Any
|
|
428
|
+
The plot environment (unused -- kept for R signature parity).
|
|
429
|
+
vars_ : dict or list of str
|
|
430
|
+
The variable names (the ``rows`` / ``cols`` spec).
|
|
431
|
+
drop : bool, default True
|
|
432
|
+
Drop unused factor combinations.
|
|
433
|
+
|
|
434
|
+
Returns
|
|
435
|
+
-------
|
|
436
|
+
pandas.DataFrame
|
|
437
|
+
Unique combinations of the requested variables.
|
|
438
|
+
"""
|
|
439
|
+
names = _as_name_list(vars_)
|
|
440
|
+
return _combine_vars(data, names, drop=drop)
|
|
441
|
+
|
|
442
|
+
# -- compute_layout -----------------------------------------------------
|
|
443
|
+
def compute_layout(
|
|
444
|
+
self,
|
|
445
|
+
data: List[pd.DataFrame],
|
|
446
|
+
params: Dict[str, Any],
|
|
447
|
+
) -> pd.DataFrame:
|
|
448
|
+
"""Build the panel layout with ``_render`` + independent-scale columns.
|
|
449
|
+
|
|
450
|
+
Port of ggh4x's ``FacetGrid2$compute_layout`` (``R/facet_grid2.R:193-283``).
|
|
451
|
+
|
|
452
|
+
Parameters
|
|
453
|
+
----------
|
|
454
|
+
data : list of DataFrame
|
|
455
|
+
params : dict
|
|
456
|
+
|
|
457
|
+
Returns
|
|
458
|
+
-------
|
|
459
|
+
pandas.DataFrame
|
|
460
|
+
Layout with ``PANEL``, ``ROW``, ``COL``, ``SCALE_X``, ``SCALE_Y``, a
|
|
461
|
+
ggh4x-specific boolean ``_render`` column, and the faceting-var
|
|
462
|
+
columns.
|
|
463
|
+
"""
|
|
464
|
+
rows = params["rows"]
|
|
465
|
+
cols = params["cols"]
|
|
466
|
+
row_names = _as_name_list(rows)
|
|
467
|
+
col_names = _as_name_list(cols)
|
|
468
|
+
|
|
469
|
+
dups = [d for d in row_names if d in col_names]
|
|
470
|
+
if dups:
|
|
471
|
+
cli_abort(
|
|
472
|
+
"Facetting variables can only appear in `rows` or `cols`, not "
|
|
473
|
+
f"both. Duplicated variables: {dups}"
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
drop = params.get("drop", True)
|
|
477
|
+
env = params.get("plot_env")
|
|
478
|
+
|
|
479
|
+
base_rows = self.vars_combine(data, env, rows, drop=drop)
|
|
480
|
+
if not params.get("as_table", True):
|
|
481
|
+
base_rows = base_rows.copy()
|
|
482
|
+
for c in base_rows.columns:
|
|
483
|
+
levels = list(ulevels(base_rows[c]))[::-1]
|
|
484
|
+
base_rows[c] = pd.Categorical(base_rows[c], categories=levels)
|
|
485
|
+
|
|
486
|
+
base_cols = self.vars_combine(data, env, cols, drop=drop)
|
|
487
|
+
base = df_grid(base_rows, base_cols)
|
|
488
|
+
|
|
489
|
+
if base is None or len(base) == 0:
|
|
490
|
+
return pd.DataFrame(
|
|
491
|
+
{
|
|
492
|
+
"PANEL": pd.Categorical([1]),
|
|
493
|
+
"ROW": [1],
|
|
494
|
+
"COL": [1],
|
|
495
|
+
"SCALE_X": [1],
|
|
496
|
+
"SCALE_Y": [1],
|
|
497
|
+
"_render": [True],
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
base = reshape_add_margins(
|
|
502
|
+
base, [row_names, col_names], params.get("margins", False)
|
|
503
|
+
)
|
|
504
|
+
base = base.drop_duplicates().reset_index(drop=True)
|
|
505
|
+
|
|
506
|
+
if not params.get("render_empty", True):
|
|
507
|
+
both = {**(rows or {}), **(cols or {})} if isinstance(rows, dict) else None
|
|
508
|
+
if both is not None:
|
|
509
|
+
universe = self.vars_combine(data, env, both, drop=drop)
|
|
510
|
+
else:
|
|
511
|
+
universe = self.vars_combine(
|
|
512
|
+
data, env, row_names + col_names, drop=drop
|
|
513
|
+
)
|
|
514
|
+
keys = [c for c in base.columns if c in universe.columns]
|
|
515
|
+
if keys:
|
|
516
|
+
uni_rows = set(
|
|
517
|
+
tuple(r) for r in universe[keys].itertuples(index=False, name=None)
|
|
518
|
+
)
|
|
519
|
+
render = [
|
|
520
|
+
tuple(r) in uni_rows
|
|
521
|
+
for r in base[keys].itertuples(index=False, name=None)
|
|
522
|
+
]
|
|
523
|
+
else:
|
|
524
|
+
render = [True] * len(base)
|
|
525
|
+
else:
|
|
526
|
+
render = [True] * len(base)
|
|
527
|
+
|
|
528
|
+
# PANEL / ROW / COL via id() (R radix ordering, NOT Categorical.codes).
|
|
529
|
+
panel = id(base, drop=True)
|
|
530
|
+
n_panel = int(panel.n)
|
|
531
|
+
|
|
532
|
+
if not row_names:
|
|
533
|
+
row_ids = np.array([1] * len(base), dtype=int)
|
|
534
|
+
else:
|
|
535
|
+
row_ids = np.asarray(id(base[row_names], drop=True), dtype=int)
|
|
536
|
+
if not col_names:
|
|
537
|
+
col_ids = np.array([1] * len(base), dtype=int)
|
|
538
|
+
else:
|
|
539
|
+
col_ids = np.asarray(id(base[col_names], drop=True), dtype=int)
|
|
540
|
+
|
|
541
|
+
panel_int = np.asarray(panel, dtype=int)
|
|
542
|
+
panels = base.copy()
|
|
543
|
+
panels.insert(0, "PANEL", pd.Categorical(panel_int, categories=range(1, n_panel + 1)))
|
|
544
|
+
panels.insert(1, "ROW", row_ids)
|
|
545
|
+
panels.insert(2, "COL", col_ids)
|
|
546
|
+
panels["_render"] = render
|
|
547
|
+
|
|
548
|
+
# order(PANEL)
|
|
549
|
+
order = np.argsort(panel_int, kind="mergesort")
|
|
550
|
+
panels = panels.iloc[order].reset_index(drop=True)
|
|
551
|
+
|
|
552
|
+
free = params["free"]
|
|
553
|
+
independent = params["independent"]
|
|
554
|
+
n = len(panels)
|
|
555
|
+
|
|
556
|
+
if free["x"]:
|
|
557
|
+
if independent["x"]:
|
|
558
|
+
panels["SCALE_X"] = np.arange(1, n + 1)
|
|
559
|
+
else:
|
|
560
|
+
panels["SCALE_X"] = panels["COL"].to_numpy()
|
|
561
|
+
else:
|
|
562
|
+
panels["SCALE_X"] = 1
|
|
563
|
+
|
|
564
|
+
if free["y"]:
|
|
565
|
+
if independent["y"]:
|
|
566
|
+
panels["SCALE_Y"] = np.arange(1, n + 1)
|
|
567
|
+
else:
|
|
568
|
+
panels["SCALE_Y"] = panels["ROW"].to_numpy()
|
|
569
|
+
else:
|
|
570
|
+
panels["SCALE_Y"] = 1
|
|
571
|
+
|
|
572
|
+
return panels
|
|
573
|
+
|
|
574
|
+
# -- setup_aspect_ratio -------------------------------------------------
|
|
575
|
+
def setup_aspect_ratio(
|
|
576
|
+
self,
|
|
577
|
+
coord: Any,
|
|
578
|
+
free: Dict[str, bool],
|
|
579
|
+
theme: Any,
|
|
580
|
+
ranges: Sequence[Any],
|
|
581
|
+
) -> AspectRatio:
|
|
582
|
+
"""Resolve the panel aspect ratio + ``respect`` flag.
|
|
583
|
+
|
|
584
|
+
Port of ggh4x's ``FacetGrid2$setup_aspect_ratio``
|
|
585
|
+
(``R/facet_grid2.R:284-296``). Uses ``theme$aspect.ratio`` if set; else
|
|
586
|
+
``coord$aspect(ranges[[1]])`` when neither dimension is free; else ``1``
|
|
587
|
+
with ``respect=False``.
|
|
588
|
+
|
|
589
|
+
Parameters
|
|
590
|
+
----------
|
|
591
|
+
coord : Coord
|
|
592
|
+
free : dict
|
|
593
|
+
``{"x": bool, "y": bool}``.
|
|
594
|
+
theme : Theme
|
|
595
|
+
ranges : sequence
|
|
596
|
+
Per-panel ``panel_params`` dicts.
|
|
597
|
+
|
|
598
|
+
Returns
|
|
599
|
+
-------
|
|
600
|
+
AspectRatio
|
|
601
|
+
"""
|
|
602
|
+
aspect_ratio = calc_element("aspect.ratio", theme) if theme is not None else None
|
|
603
|
+
if aspect_ratio is None and not free["x"] and not free["y"] and ranges:
|
|
604
|
+
aspect_ratio = coord.aspect(ranges[0])
|
|
605
|
+
if aspect_ratio is None:
|
|
606
|
+
return AspectRatio(1.0, False)
|
|
607
|
+
return AspectRatio(float(aspect_ratio), True)
|
|
608
|
+
|
|
609
|
+
# -- setup_panel_table --------------------------------------------------
|
|
610
|
+
def setup_panel_table(
|
|
611
|
+
self,
|
|
612
|
+
panels: List[Any],
|
|
613
|
+
layout: pd.DataFrame,
|
|
614
|
+
space: Dict[str, bool],
|
|
615
|
+
ranges: Sequence[Any],
|
|
616
|
+
aspect: AspectRatio,
|
|
617
|
+
clip: str,
|
|
618
|
+
theme: Any,
|
|
619
|
+
) -> Gtable:
|
|
620
|
+
"""Build the panel gtable (byrow matrix, null-unit / proportional sizes).
|
|
621
|
+
|
|
622
|
+
Port of ggh4x's ``FacetGrid2$setup_panel_table``
|
|
623
|
+
(``R/facet_grid2.R:297-340``). Blanks non-rendered panels, lays panels
|
|
624
|
+
into a ``nrow x ncol`` gtable at ``(ROW, COL)`` with ``z=1``, sizes
|
|
625
|
+
columns / rows as ``"null"`` units (proportional to ``diff(range)`` when
|
|
626
|
+
``space`` is free, heights scaled by ``abs(aspect)`` otherwise), then adds
|
|
627
|
+
panel spacing.
|
|
628
|
+
|
|
629
|
+
Parameters
|
|
630
|
+
----------
|
|
631
|
+
panels : list of grob
|
|
632
|
+
One decorated panel grob per PANEL (PANEL-ordered).
|
|
633
|
+
layout : pandas.DataFrame
|
|
634
|
+
The facet layout (carries ``ROW``, ``COL``, ``_render``).
|
|
635
|
+
space : dict
|
|
636
|
+
``{"x": bool, "y": bool}`` -- ``space_free`` flags.
|
|
637
|
+
ranges : sequence
|
|
638
|
+
Per-panel ``panel_params`` dicts.
|
|
639
|
+
aspect : AspectRatio
|
|
640
|
+
clip : str
|
|
641
|
+
Panel clip setting (``coord.clip``).
|
|
642
|
+
theme : Theme
|
|
643
|
+
|
|
644
|
+
Returns
|
|
645
|
+
-------
|
|
646
|
+
Gtable
|
|
647
|
+
"""
|
|
648
|
+
panels = list(panels)
|
|
649
|
+
render = list(layout["_render"]) if "_render" in layout.columns else [True] * len(panels)
|
|
650
|
+
panels = [p if render[i] else null_grob() for i, p in enumerate(panels)]
|
|
651
|
+
|
|
652
|
+
ncol = int(layout["COL"].max())
|
|
653
|
+
nrow = int(layout["ROW"].max())
|
|
654
|
+
|
|
655
|
+
panel_arr = np.asarray(layout["PANEL"]).astype(int)
|
|
656
|
+
|
|
657
|
+
if space["x"]:
|
|
658
|
+
row1 = layout[layout["ROW"] == 1]
|
|
659
|
+
ps = [int(p) for p in row1["PANEL"]]
|
|
660
|
+
widths_vals = [
|
|
661
|
+
_range_diff(ranges[i - 1], "x") for i in ps
|
|
662
|
+
]
|
|
663
|
+
widths = Unit(widths_vals, "null")
|
|
664
|
+
else:
|
|
665
|
+
widths = Unit([1.0] * ncol, "null")
|
|
666
|
+
|
|
667
|
+
if space["y"]:
|
|
668
|
+
col1 = layout[layout["COL"] == 1]
|
|
669
|
+
ps = [int(p) for p in col1["PANEL"]]
|
|
670
|
+
heights_vals = [
|
|
671
|
+
_range_diff(ranges[i - 1], "y") for i in ps
|
|
672
|
+
]
|
|
673
|
+
heights = Unit(heights_vals, "null")
|
|
674
|
+
else:
|
|
675
|
+
heights = Unit([1.0 * abs(aspect.value)] * nrow, "null")
|
|
676
|
+
|
|
677
|
+
panel_table = Gtable(widths=widths, heights=heights, respect=aspect.respect)
|
|
678
|
+
|
|
679
|
+
rows_idx = [int(v) for v in layout["ROW"]]
|
|
680
|
+
cols_idx = [int(v) for v in layout["COL"]]
|
|
681
|
+
# R: paste0("panel-", rep(seq_len(nrow), ncol), "-", rep(seq_len(ncol), each = nrow))
|
|
682
|
+
# This is a fixed *positional* name vector applied to the PANEL-ordered
|
|
683
|
+
# ``panels`` list -- the suffixes do NOT track each panel's actual
|
|
684
|
+
# ROW/COL, they enumerate row-fastest, column-slowest.
|
|
685
|
+
name_i = [((k % nrow) + 1) for k in range(nrow * ncol)]
|
|
686
|
+
name_j = [((k // nrow) + 1) for k in range(nrow * ncol)]
|
|
687
|
+
names = [f"panel-{name_i[k]}-{name_j[k]}" for k in range(len(panels))]
|
|
688
|
+
panel_table = gtable_add_grob(
|
|
689
|
+
panel_table,
|
|
690
|
+
panels,
|
|
691
|
+
t=rows_idx,
|
|
692
|
+
l=cols_idx,
|
|
693
|
+
z=1,
|
|
694
|
+
clip=clip,
|
|
695
|
+
name=names,
|
|
696
|
+
)
|
|
697
|
+
panel_table = gtable_add_col_space(
|
|
698
|
+
panel_table, calc_element("panel.spacing.x", theme)
|
|
699
|
+
)
|
|
700
|
+
panel_table = gtable_add_row_space(
|
|
701
|
+
panel_table, calc_element("panel.spacing.y", theme)
|
|
702
|
+
)
|
|
703
|
+
return panel_table
|
|
704
|
+
|
|
705
|
+
# -- attach_axes --------------------------------------------------------
|
|
706
|
+
def attach_axes(self, panel_table: Gtable, axes: Dict[str, Any]) -> Gtable:
|
|
707
|
+
"""Weave the four axis bands into the panel gtable.
|
|
708
|
+
|
|
709
|
+
Port of ggh4x's ``FacetGrid2$attach_axes`` (``R/facet_grid2.R:341-356``).
|
|
710
|
+
Measures the axes (:func:`_measure_axes`) then weaves the top (shift -1),
|
|
711
|
+
bottom (shift 0), left (shift -1) and right (shift 0) bands at ``z=3``.
|
|
712
|
+
|
|
713
|
+
Parameters
|
|
714
|
+
----------
|
|
715
|
+
panel_table : Gtable
|
|
716
|
+
axes : dict
|
|
717
|
+
``{"top", "bottom", "left", "right"}`` grob matrices from
|
|
718
|
+
:meth:`setup_axes`.
|
|
719
|
+
|
|
720
|
+
Returns
|
|
721
|
+
-------
|
|
722
|
+
Gtable
|
|
723
|
+
"""
|
|
724
|
+
sizes = _measure_axes(axes)
|
|
725
|
+
panel_table = weave_tables_row(
|
|
726
|
+
panel_table, axes["top"], -1, sizes["top"], "axis-t", 3
|
|
727
|
+
)
|
|
728
|
+
panel_table = weave_tables_row(
|
|
729
|
+
panel_table, axes["bottom"], 0, sizes["bottom"], "axis-b", 3
|
|
730
|
+
)
|
|
731
|
+
panel_table = weave_tables_col(
|
|
732
|
+
panel_table, axes["left"], -1, sizes["left"], "axis-l", 3
|
|
733
|
+
)
|
|
734
|
+
panel_table = weave_tables_col(
|
|
735
|
+
panel_table, axes["right"], 0, sizes["right"], "axis-r", 3
|
|
736
|
+
)
|
|
737
|
+
return panel_table
|
|
738
|
+
|
|
739
|
+
# -- setup_axes ---------------------------------------------------------
|
|
740
|
+
def setup_axes(
|
|
741
|
+
self,
|
|
742
|
+
axes: Dict[str, Any],
|
|
743
|
+
empty: List[List[Any]],
|
|
744
|
+
position: Sequence[int],
|
|
745
|
+
layout: pd.DataFrame,
|
|
746
|
+
params: Dict[str, Any],
|
|
747
|
+
) -> Dict[str, List[List[Any]]]:
|
|
748
|
+
"""Fill the 4 axis grob matrices by scale-id and blank interior axes.
|
|
749
|
+
|
|
750
|
+
Port of ggh4x's ``FacetGrid2$setup_axes`` (``R/facet_grid2.R:358-394``).
|
|
751
|
+
Fills the top/bottom/left/right ``nrow x ncol`` matrices by the per-cell
|
|
752
|
+
``position`` (panel) index, blanks redundant interior axes unless they
|
|
753
|
+
must repeat (``independent`` | ``axes``), and purges labels from interior
|
|
754
|
+
axes when ``axes & rmlab & !independent``.
|
|
755
|
+
|
|
756
|
+
Parameters
|
|
757
|
+
----------
|
|
758
|
+
axes : dict
|
|
759
|
+
The transposed batch axes ``{"x": {"top", "bottom"},
|
|
760
|
+
"y": {"left", "right"}}`` from :func:`render_axes`.
|
|
761
|
+
empty : list of list
|
|
762
|
+
A ``nrow x ncol`` zero-grob matrix template.
|
|
763
|
+
position : sequence of int
|
|
764
|
+
The per-cell panel index (row-major / byrow), 1-based.
|
|
765
|
+
layout : pandas.DataFrame
|
|
766
|
+
params : dict
|
|
767
|
+
|
|
768
|
+
Returns
|
|
769
|
+
-------
|
|
770
|
+
dict
|
|
771
|
+
``{"top", "bottom", "left", "right"}`` grob matrices (list of lists).
|
|
772
|
+
"""
|
|
773
|
+
nrow = len(empty)
|
|
774
|
+
ncol = len(empty[0]) if nrow else 0
|
|
775
|
+
|
|
776
|
+
def _new() -> List[List[Any]]:
|
|
777
|
+
return [[null_grob() for _ in range(ncol)] for _ in range(nrow)]
|
|
778
|
+
|
|
779
|
+
top = _new()
|
|
780
|
+
bottom = _new()
|
|
781
|
+
left = _new()
|
|
782
|
+
right = _new()
|
|
783
|
+
|
|
784
|
+
x_top = axes["x"]["top"]
|
|
785
|
+
x_bottom = axes["x"]["bottom"]
|
|
786
|
+
y_left = axes["y"]["left"]
|
|
787
|
+
y_right = axes["y"]["right"]
|
|
788
|
+
|
|
789
|
+
# position is row-major: position[r*ncol + c] = panel index (already the
|
|
790
|
+
# PANEL-ordered axis index because axes were rendered over ranges[panel_pos]).
|
|
791
|
+
k = 0
|
|
792
|
+
for r in range(nrow):
|
|
793
|
+
for c in range(ncol):
|
|
794
|
+
# axes$x$top[position] where the rendered list is itself indexed
|
|
795
|
+
# by the same row-major order, so cell (r,c) takes element k.
|
|
796
|
+
top[r][c] = x_top[k]
|
|
797
|
+
bottom[r][c] = x_bottom[k]
|
|
798
|
+
left[r][c] = y_left[k]
|
|
799
|
+
right[r][c] = y_right[k]
|
|
800
|
+
k += 1
|
|
801
|
+
|
|
802
|
+
independent = params["independent"]
|
|
803
|
+
axes_p = params["axes"]
|
|
804
|
+
rmlab = params["rmlab"]
|
|
805
|
+
repeat_x = independent["x"] or axes_p["x"]
|
|
806
|
+
repeat_y = independent["y"] or axes_p["y"]
|
|
807
|
+
|
|
808
|
+
if not repeat_x:
|
|
809
|
+
# top[-1, ]: blank all rows except first.
|
|
810
|
+
for r in range(1, nrow):
|
|
811
|
+
for c in range(ncol):
|
|
812
|
+
top[r][c] = null_grob()
|
|
813
|
+
# bottom[-nrow, ]: blank all rows except last.
|
|
814
|
+
for r in range(nrow - 1):
|
|
815
|
+
for c in range(ncol):
|
|
816
|
+
bottom[r][c] = null_grob()
|
|
817
|
+
if not repeat_y:
|
|
818
|
+
# left[, -1]: blank all cols except first.
|
|
819
|
+
for r in range(nrow):
|
|
820
|
+
for c in range(1, ncol):
|
|
821
|
+
left[r][c] = null_grob()
|
|
822
|
+
# right[, -ncol]: blank all cols except last.
|
|
823
|
+
for r in range(nrow):
|
|
824
|
+
for c in range(ncol - 1):
|
|
825
|
+
right[r][c] = null_grob()
|
|
826
|
+
|
|
827
|
+
if axes_p["x"] and rmlab["x"] and not independent["x"]:
|
|
828
|
+
for r in range(1, nrow):
|
|
829
|
+
for c in range(ncol):
|
|
830
|
+
top[r][c] = purge_guide_labels(top[r][c])
|
|
831
|
+
for r in range(nrow - 1):
|
|
832
|
+
for c in range(ncol):
|
|
833
|
+
bottom[r][c] = purge_guide_labels(bottom[r][c])
|
|
834
|
+
if axes_p["y"] and rmlab["y"] and not independent["y"]:
|
|
835
|
+
for r in range(nrow):
|
|
836
|
+
for c in range(1, ncol):
|
|
837
|
+
left[r][c] = purge_guide_labels(left[r][c])
|
|
838
|
+
for r in range(nrow):
|
|
839
|
+
for c in range(ncol - 1):
|
|
840
|
+
right[r][c] = purge_guide_labels(right[r][c])
|
|
841
|
+
|
|
842
|
+
return {"top": top, "bottom": bottom, "left": left, "right": right}
|
|
843
|
+
|
|
844
|
+
# -- finish_panels (identity seam) --------------------------------------
|
|
845
|
+
def finish_panels(
|
|
846
|
+
self,
|
|
847
|
+
panels: Any,
|
|
848
|
+
layout: pd.DataFrame,
|
|
849
|
+
params: Dict[str, Any],
|
|
850
|
+
theme: Any,
|
|
851
|
+
) -> Any:
|
|
852
|
+
"""Identity post-processing hook (extension seam).
|
|
853
|
+
|
|
854
|
+
Port of ggh4x's ``FacetGrid2$finish_panels`` (``R/facet_grid2.R:395-397``).
|
|
855
|
+
Returns *panels* unchanged; subclasses (``facet_nested``) override it.
|
|
856
|
+
"""
|
|
857
|
+
return panels
|
|
858
|
+
|
|
859
|
+
# -- draw_panels (full override) ----------------------------------------
|
|
860
|
+
def draw_panels(
|
|
861
|
+
self,
|
|
862
|
+
panels: list,
|
|
863
|
+
layout: pd.DataFrame,
|
|
864
|
+
x_scales: list,
|
|
865
|
+
y_scales: list,
|
|
866
|
+
ranges: list,
|
|
867
|
+
coord: Any,
|
|
868
|
+
data: Any,
|
|
869
|
+
theme: Any,
|
|
870
|
+
params: Dict[str, Any],
|
|
871
|
+
) -> Gtable:
|
|
872
|
+
"""Assemble the grid panel gtable (full replacement of the base pipeline).
|
|
873
|
+
|
|
874
|
+
Port of ggh4x's ``FacetGrid2$draw_panels`` (``R/facet_grid2.R:398-433``).
|
|
875
|
+
Builds the byrow ``panel_pos`` vector, batch-renders the axes
|
|
876
|
+
(``transpose=True``), then runs ``setup_axes`` -> ``setup_aspect_ratio``
|
|
877
|
+
-> ``setup_panel_table`` -> ``attach_axes`` -> strip ``setup`` /
|
|
878
|
+
``incorporate_grid`` -> ``finish_panels``. Does **not** chain to the base
|
|
879
|
+
``draw_panels``.
|
|
880
|
+
|
|
881
|
+
Parameters
|
|
882
|
+
----------
|
|
883
|
+
panels : list
|
|
884
|
+
Per-layer lists of per-panel geom grobs (``ggplot2_py`` shape), or an
|
|
885
|
+
already-flat list of decorated panel grobs.
|
|
886
|
+
layout : pandas.DataFrame
|
|
887
|
+
x_scales, y_scales : list
|
|
888
|
+
ranges : list
|
|
889
|
+
Per-panel ``panel_params`` dicts.
|
|
890
|
+
coord : Coord
|
|
891
|
+
data : Any
|
|
892
|
+
theme : Theme
|
|
893
|
+
params : dict
|
|
894
|
+
|
|
895
|
+
Returns
|
|
896
|
+
-------
|
|
897
|
+
Gtable
|
|
898
|
+
"""
|
|
899
|
+
if (params["free"]["x"] or params["free"]["y"]) and not coord.is_free():
|
|
900
|
+
cli_abort(f"`{snake_class(coord)}` doesn't support free scales.")
|
|
901
|
+
|
|
902
|
+
strip = self.strip
|
|
903
|
+
ncol = int(layout["COL"].max())
|
|
904
|
+
nrow = int(layout["ROW"].max())
|
|
905
|
+
empty_table = [[null_grob() for _ in range(ncol)] for _ in range(nrow)]
|
|
906
|
+
|
|
907
|
+
# Decorate per-layer grobs into one panel grob per PANEL.
|
|
908
|
+
panel_grobs = _decorate_panels(panels, layout, ranges, coord, theme)
|
|
909
|
+
|
|
910
|
+
# panel_pos: byrow reshape of as.integer(PANEL).
|
|
911
|
+
panel_int = [int(p) for p in layout["PANEL"]]
|
|
912
|
+
# Map (ROW,COL) -> PANEL for byrow ordering.
|
|
913
|
+
rc_to_panel: Dict[tuple, int] = {}
|
|
914
|
+
for _, row in layout.iterrows():
|
|
915
|
+
rc_to_panel[(int(row["ROW"]), int(row["COL"]))] = int(row["PANEL"])
|
|
916
|
+
panel_pos: List[int] = []
|
|
917
|
+
for r in range(1, nrow + 1):
|
|
918
|
+
for c in range(1, ncol + 1):
|
|
919
|
+
panel_pos.append(rc_to_panel.get((r, c), 1))
|
|
920
|
+
|
|
921
|
+
ranges_pos = [ranges[p - 1] for p in panel_pos]
|
|
922
|
+
axes = render_axes(ranges_pos, ranges_pos, coord, theme, transpose=True)
|
|
923
|
+
axes = self.setup_axes(axes, empty_table, panel_pos, layout, params)
|
|
924
|
+
|
|
925
|
+
aspect_ratio = self.setup_aspect_ratio(coord, params["free"], theme, ranges)
|
|
926
|
+
|
|
927
|
+
panel_table = self.setup_panel_table(
|
|
928
|
+
panel_grobs, layout, params["space_free"], ranges, aspect_ratio,
|
|
929
|
+
coord.clip, theme,
|
|
930
|
+
)
|
|
931
|
+
panel_table = self.attach_axes(panel_table, axes)
|
|
932
|
+
|
|
933
|
+
strip.setup(layout, params, theme, type="grid")
|
|
934
|
+
panel_table = strip.incorporate_grid(panel_table, params["switch"])
|
|
935
|
+
|
|
936
|
+
return self.finish_panels(
|
|
937
|
+
panels=panel_table, layout=layout, params=params, theme=theme
|
|
938
|
+
)
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
# ---------------------------------------------------------------------------
|
|
942
|
+
# Module-private helpers
|
|
943
|
+
# ---------------------------------------------------------------------------
|
|
944
|
+
def _range_diff(panel_params: Any, axis: str) -> float:
|
|
945
|
+
"""Return ``diff(range)`` for the x or y axis of a ``panel_params`` dict.
|
|
946
|
+
|
|
947
|
+
``ggplot2_py`` exposes both ``"x_range"`` / ``"y_range"`` and the R-style
|
|
948
|
+
``"x.range"`` / ``"y.range"`` keys; prefer the R-style key to match the
|
|
949
|
+
gold-standard, falling back to the underscore variant.
|
|
950
|
+
"""
|
|
951
|
+
if panel_params is None:
|
|
952
|
+
return 1.0
|
|
953
|
+
key_dot = f"{axis}.range"
|
|
954
|
+
key_us = f"{axis}_range"
|
|
955
|
+
rng = panel_params.get(key_dot)
|
|
956
|
+
if rng is None:
|
|
957
|
+
rng = panel_params.get(key_us)
|
|
958
|
+
if rng is None:
|
|
959
|
+
return 1.0
|
|
960
|
+
return float(rng[1] - rng[0])
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def _decorate_panels(
|
|
964
|
+
panels: list,
|
|
965
|
+
layout: pd.DataFrame,
|
|
966
|
+
ranges: list,
|
|
967
|
+
coord: Any,
|
|
968
|
+
theme: Any,
|
|
969
|
+
) -> List[Any]:
|
|
970
|
+
"""Compose per-layer geom grobs into one decorated panel grob per PANEL.
|
|
971
|
+
|
|
972
|
+
``ggplot2_py`` passes ``panels`` to ``draw_panels`` as a list-of-layers (each
|
|
973
|
+
a list of per-panel grobs); R passes a flat list of already-decorated panel
|
|
974
|
+
grobs (one per PANEL). This bridges the two by running
|
|
975
|
+
``coord.draw_panel(layer_grobs_for_panel, pp, theme)`` per panel, matching the
|
|
976
|
+
base ``Facet.draw_panels`` decoration (facet.py:766-782). When *panels* is
|
|
977
|
+
already flat (one grob per panel) it is returned as-is.
|
|
978
|
+
|
|
979
|
+
Parameters
|
|
980
|
+
----------
|
|
981
|
+
panels : list
|
|
982
|
+
layout : pandas.DataFrame
|
|
983
|
+
ranges : list
|
|
984
|
+
coord : Coord
|
|
985
|
+
theme : Theme
|
|
986
|
+
|
|
987
|
+
Returns
|
|
988
|
+
-------
|
|
989
|
+
list of grob
|
|
990
|
+
One decorated panel grob per PANEL, PANEL-ordered.
|
|
991
|
+
"""
|
|
992
|
+
n_panel = len(layout)
|
|
993
|
+
# Detect the already-flat case: a list of length n_panel of non-list grobs.
|
|
994
|
+
if (
|
|
995
|
+
len(panels) == n_panel
|
|
996
|
+
and n_panel > 0
|
|
997
|
+
and not isinstance(panels[0], list)
|
|
998
|
+
):
|
|
999
|
+
return list(panels)
|
|
1000
|
+
|
|
1001
|
+
out: List[Any] = []
|
|
1002
|
+
panel_order = sorted(int(p) for p in layout["PANEL"])
|
|
1003
|
+
for panel_id in panel_order:
|
|
1004
|
+
panel_idx = panel_id - 1
|
|
1005
|
+
pp = ranges[panel_idx] if panel_idx < len(ranges) else {}
|
|
1006
|
+
layer_grobs: List[Any] = []
|
|
1007
|
+
for layer in panels:
|
|
1008
|
+
if isinstance(layer, list):
|
|
1009
|
+
if panel_idx < len(layer):
|
|
1010
|
+
layer_grobs.append(layer[panel_idx])
|
|
1011
|
+
elif layer is not None:
|
|
1012
|
+
layer_grobs.append(layer)
|
|
1013
|
+
if hasattr(coord, "draw_panel"):
|
|
1014
|
+
decorated = coord.draw_panel(layer_grobs, pp, theme)
|
|
1015
|
+
else:
|
|
1016
|
+
decorated = GTree(children=GList(*layer_grobs), name=f"panel-{panel_id}")
|
|
1017
|
+
out.append(decorated)
|
|
1018
|
+
return out
|