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_wrap2.py
ADDED
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
"""Extended wrapped facets (port of ggh4x ``R/facet_wrap2.R``).
|
|
2
|
+
|
|
3
|
+
``facet_wrap2`` behaves like :func:`ggplot2_py.facet_wrap` but adds inner-axis
|
|
4
|
+
drawing (``axes``), inner-axis label removal (``remove_labels``) and a literal
|
|
5
|
+
``trim_blank=False`` layout (``nrow`` / ``ncol`` taken verbatim).
|
|
6
|
+
|
|
7
|
+
The :class:`FacetWrap2` ggproto subclasses :class:`ggplot2_py.facet.FacetWrap` and
|
|
8
|
+
*fully replaces* ``draw_panels`` with a decomposed, strip-pluggable pipeline. Its
|
|
9
|
+
``setup_axes`` is substantially larger than ``FacetGrid2``'s: it masks interior
|
|
10
|
+
axes, measures them after deletion, then re-places marginal axes bordering empty
|
|
11
|
+
cells so dangling panels keep their axis.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
from ggplot2_py import calc_element, ggproto
|
|
22
|
+
from ggplot2_py.coord import CoordFlip
|
|
23
|
+
from ggplot2_py.facet import FacetWrap, _resolve_facet_vars, facet_wrap
|
|
24
|
+
from grid_py import GList, GTree, Unit, null_grob
|
|
25
|
+
from gtable_py import (
|
|
26
|
+
Gtable,
|
|
27
|
+
gtable_add_col_space,
|
|
28
|
+
gtable_add_grob,
|
|
29
|
+
gtable_add_row_space,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from ggh4x._borrowed_ggplot2 import is_zero, snake_class
|
|
33
|
+
from ggh4x._cli import cli_abort, cli_warn
|
|
34
|
+
from ggh4x._facet_helpers import AspectRatio, _match_facet_arg
|
|
35
|
+
from ggh4x._facet_utils import render_axes, weave_tables_col, weave_tables_row
|
|
36
|
+
from ggh4x.facet_grid2 import _decorate_panels, _measure_axes, purge_guide_labels
|
|
37
|
+
from ggh4x.strip_vanilla import resolve_strip
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"facet_wrap2",
|
|
41
|
+
"FacetWrap2",
|
|
42
|
+
"new_wrap_facets",
|
|
43
|
+
"purge_guide_labels",
|
|
44
|
+
"_measure_axes",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# Constructor
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
def facet_wrap2(
|
|
52
|
+
facets: Any,
|
|
53
|
+
nrow: Optional[int] = None,
|
|
54
|
+
ncol: Optional[int] = None,
|
|
55
|
+
scales: Any = "fixed",
|
|
56
|
+
axes: Any = "margins",
|
|
57
|
+
remove_labels: Any = "none",
|
|
58
|
+
shrink: bool = True,
|
|
59
|
+
labeller: Any = "label_value",
|
|
60
|
+
as_table: bool = True,
|
|
61
|
+
drop: bool = True,
|
|
62
|
+
dir: str = "h",
|
|
63
|
+
strip_position: str = "top",
|
|
64
|
+
trim_blank: bool = True,
|
|
65
|
+
strip: Any = "vanilla",
|
|
66
|
+
) -> "FacetWrap2":
|
|
67
|
+
"""Extended wrapped facets.
|
|
68
|
+
|
|
69
|
+
Port of ggh4x's ``facet_wrap2()`` (``R/facet_wrap2.R:67-86``). Like
|
|
70
|
+
:func:`ggplot2_py.facet_wrap` but can draw / label-purge inner axes when
|
|
71
|
+
scales are fixed, and can honour a literal ``nrow``/``ncol`` via
|
|
72
|
+
``trim_blank=False``.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
facets : formula / list / dict
|
|
77
|
+
Faceting variables.
|
|
78
|
+
nrow, ncol : int or None
|
|
79
|
+
scales : {"fixed", "free_x", "free_y", "free"} or bool, default "fixed"
|
|
80
|
+
axes : {"margins", "x", "y", "all"} or bool, default "margins"
|
|
81
|
+
remove_labels : {"none", "x", "y", "all"} or bool, default "none"
|
|
82
|
+
shrink : bool, default True
|
|
83
|
+
labeller : callable or str, default "label_value"
|
|
84
|
+
as_table : bool, default True
|
|
85
|
+
drop : bool, default True
|
|
86
|
+
dir : {"h", "v"}, default "h"
|
|
87
|
+
strip_position : {"top", "bottom", "left", "right"}, default "top"
|
|
88
|
+
trim_blank : bool, default True
|
|
89
|
+
When ``False``, ``nrow``/``ncol`` are taken literally.
|
|
90
|
+
strip : Strip or callable or str, default "vanilla"
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
FacetWrap2
|
|
95
|
+
"""
|
|
96
|
+
return new_wrap_facets(
|
|
97
|
+
facets,
|
|
98
|
+
nrow,
|
|
99
|
+
ncol,
|
|
100
|
+
scales,
|
|
101
|
+
axes,
|
|
102
|
+
remove_labels,
|
|
103
|
+
shrink,
|
|
104
|
+
labeller,
|
|
105
|
+
as_table,
|
|
106
|
+
drop,
|
|
107
|
+
dir,
|
|
108
|
+
strip_position,
|
|
109
|
+
strip,
|
|
110
|
+
trim_blank,
|
|
111
|
+
super_=FacetWrap2,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def new_wrap_facets(
|
|
116
|
+
facets: Any,
|
|
117
|
+
nrow: Optional[int],
|
|
118
|
+
ncol: Optional[int],
|
|
119
|
+
scales: Any,
|
|
120
|
+
axes: Any,
|
|
121
|
+
rmlab: Any,
|
|
122
|
+
shrink: bool,
|
|
123
|
+
labeller: Any,
|
|
124
|
+
as_table: bool,
|
|
125
|
+
drop: bool,
|
|
126
|
+
dir: str,
|
|
127
|
+
strip_position: str,
|
|
128
|
+
strip: Any,
|
|
129
|
+
trim_blank: bool,
|
|
130
|
+
params: Optional[Dict[str, Any]] = None,
|
|
131
|
+
super_: Any = None,
|
|
132
|
+
) -> "FacetWrap2":
|
|
133
|
+
"""Build a :class:`FacetWrap2` instance from raw arguments.
|
|
134
|
+
|
|
135
|
+
Port of ggh4x's ``new_wrap_facets()`` (``R/facet_wrap2.R:90-120``). Obtains
|
|
136
|
+
the full prototype param dict from :func:`ggplot2_py.facet_wrap`, normalises
|
|
137
|
+
``axes`` / ``remove_labels``, resolves the strip, computes the ``dim`` for the
|
|
138
|
+
non-trimmed case and assembles the params.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
facets : Any
|
|
143
|
+
nrow, ncol : int or None
|
|
144
|
+
scales, axes, rmlab : Any
|
|
145
|
+
shrink : bool
|
|
146
|
+
labeller : Any
|
|
147
|
+
as_table : bool
|
|
148
|
+
drop : bool
|
|
149
|
+
dir : str
|
|
150
|
+
strip_position : str
|
|
151
|
+
strip : Any
|
|
152
|
+
trim_blank : bool
|
|
153
|
+
params : dict, optional
|
|
154
|
+
super_ : type, optional
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
FacetWrap2
|
|
159
|
+
"""
|
|
160
|
+
if super_ is None:
|
|
161
|
+
super_ = FacetWrap2
|
|
162
|
+
params = dict(params or {})
|
|
163
|
+
|
|
164
|
+
prototype = facet_wrap(
|
|
165
|
+
facets=facets,
|
|
166
|
+
nrow=nrow,
|
|
167
|
+
ncol=ncol,
|
|
168
|
+
scales=scales,
|
|
169
|
+
shrink=shrink,
|
|
170
|
+
labeller=labeller,
|
|
171
|
+
as_table=as_table,
|
|
172
|
+
drop=drop,
|
|
173
|
+
dir=dir,
|
|
174
|
+
strip_position=strip_position,
|
|
175
|
+
).params
|
|
176
|
+
|
|
177
|
+
axes = _match_facet_arg(axes, ["margins", "x", "y", "all"], nm="axes")
|
|
178
|
+
rmlab = _match_facet_arg(rmlab, ["none", "x", "y", "all"], nm="remove_labels")
|
|
179
|
+
strip = resolve_strip(strip)
|
|
180
|
+
|
|
181
|
+
if trim_blank:
|
|
182
|
+
dim = None
|
|
183
|
+
else:
|
|
184
|
+
dim = [
|
|
185
|
+
prototype.get("nrow") if prototype.get("nrow") is not None else np.nan,
|
|
186
|
+
prototype.get("ncol") if prototype.get("ncol") is not None else np.nan,
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
merged = dict(prototype)
|
|
190
|
+
merged.update(params)
|
|
191
|
+
# Store ``facets`` as a name list (R keeps a named quosure list; the strip /
|
|
192
|
+
# layout read the names) so the strip subsystem agrees with the layout.
|
|
193
|
+
merged["facets"] = _resolve_facet_vars(prototype.get("facets"))
|
|
194
|
+
merged.update({"dim": dim, "axes": axes, "rmlab": rmlab})
|
|
195
|
+
# Ensure a strip.position alias exists for the R-style param name used in
|
|
196
|
+
# setup_axes warnings / strip incorporation.
|
|
197
|
+
merged.setdefault("strip.position", merged.get("strip_position", strip_position))
|
|
198
|
+
|
|
199
|
+
obj = super_()
|
|
200
|
+
obj._set(shrink=shrink, strip=strip, params=merged)
|
|
201
|
+
return obj
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
# ggproto
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
class FacetWrap2(FacetWrap):
|
|
208
|
+
"""Extended wrapped facet ggproto (port of R ``FacetWrap2``).
|
|
209
|
+
|
|
210
|
+
Subclasses :class:`ggplot2_py.facet.FacetWrap`. Replaces ``draw_panels`` with
|
|
211
|
+
a decomposed, strip-pluggable pipeline; all sub-steps are overridable.
|
|
212
|
+
|
|
213
|
+
Attributes
|
|
214
|
+
----------
|
|
215
|
+
shrink : bool
|
|
216
|
+
strip : Strip
|
|
217
|
+
params : dict
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
_class_name = "FacetWrap2"
|
|
221
|
+
|
|
222
|
+
shrink: bool = True
|
|
223
|
+
strip: Any = None
|
|
224
|
+
|
|
225
|
+
# -- setup_aspect_ratio (identical to FacetGrid2) -----------------------
|
|
226
|
+
def setup_aspect_ratio(
|
|
227
|
+
self,
|
|
228
|
+
coord: Any,
|
|
229
|
+
free: Dict[str, bool],
|
|
230
|
+
theme: Any,
|
|
231
|
+
ranges: Sequence[Any],
|
|
232
|
+
) -> AspectRatio:
|
|
233
|
+
"""Resolve the panel aspect ratio + ``respect`` flag.
|
|
234
|
+
|
|
235
|
+
Port of ggh4x's ``FacetWrap2$setup_aspect_ratio``
|
|
236
|
+
(``R/facet_wrap2.R:134-148``); identical to ``FacetGrid2``'s.
|
|
237
|
+
|
|
238
|
+
Parameters
|
|
239
|
+
----------
|
|
240
|
+
coord : Coord
|
|
241
|
+
free : dict
|
|
242
|
+
theme : Theme
|
|
243
|
+
ranges : sequence
|
|
244
|
+
|
|
245
|
+
Returns
|
|
246
|
+
-------
|
|
247
|
+
AspectRatio
|
|
248
|
+
"""
|
|
249
|
+
aspect_ratio = calc_element("aspect.ratio", theme) if theme is not None else None
|
|
250
|
+
if aspect_ratio is None and not free["x"] and not free["y"] and ranges:
|
|
251
|
+
aspect_ratio = coord.aspect(ranges[0])
|
|
252
|
+
if aspect_ratio is None:
|
|
253
|
+
return AspectRatio(1.0, False)
|
|
254
|
+
return AspectRatio(float(aspect_ratio), True)
|
|
255
|
+
|
|
256
|
+
# -- setup_layout (CoordFlip scale swap) --------------------------------
|
|
257
|
+
def setup_layout(
|
|
258
|
+
self,
|
|
259
|
+
layout: pd.DataFrame,
|
|
260
|
+
coord: Any,
|
|
261
|
+
params: Dict[str, Any],
|
|
262
|
+
) -> pd.DataFrame:
|
|
263
|
+
"""Swap ``SCALE_X`` / ``SCALE_Y`` assignment under ``CoordFlip``.
|
|
264
|
+
|
|
265
|
+
Port of ggh4x's ``FacetWrap2$setup_layout`` (``R/facet_wrap2.R:149-166``).
|
|
266
|
+
A no-op for non-flipped coords; under :class:`ggplot2_py.coord.CoordFlip`
|
|
267
|
+
each free dimension's ``SCALE_*`` becomes per-panel (``seq_len(nrow)``).
|
|
268
|
+
|
|
269
|
+
Parameters
|
|
270
|
+
----------
|
|
271
|
+
layout : pandas.DataFrame
|
|
272
|
+
coord : Coord
|
|
273
|
+
params : dict
|
|
274
|
+
|
|
275
|
+
Returns
|
|
276
|
+
-------
|
|
277
|
+
pandas.DataFrame
|
|
278
|
+
"""
|
|
279
|
+
if isinstance(coord, CoordFlip):
|
|
280
|
+
layout = layout.copy()
|
|
281
|
+
n = len(layout)
|
|
282
|
+
layout["SCALE_X"] = np.arange(1, n + 1) if params["free"]["x"] else 1
|
|
283
|
+
layout["SCALE_Y"] = np.arange(1, n + 1) if params["free"]["y"] else 1
|
|
284
|
+
return layout
|
|
285
|
+
|
|
286
|
+
# -- setup_panel_table (self-first R signature) -------------------------
|
|
287
|
+
def setup_panel_table(
|
|
288
|
+
self,
|
|
289
|
+
panels: List[Any],
|
|
290
|
+
layout: pd.DataFrame,
|
|
291
|
+
theme: Any,
|
|
292
|
+
coord: Any,
|
|
293
|
+
ranges: Sequence[Any],
|
|
294
|
+
params: Dict[str, Any],
|
|
295
|
+
) -> Gtable:
|
|
296
|
+
"""Build the wrap panel gtable (panels may span multiple cells).
|
|
297
|
+
|
|
298
|
+
Port of ggh4x's ``FacetWrap2$setup_panel_table``
|
|
299
|
+
(``R/facet_wrap2.R:167-196``). ``ncol``/``nrow`` come from ``params.dim``
|
|
300
|
+
or the spanning ``.LEFT``/``.RIGHT``/``.TOP``/``.BOTTOM`` columns; panels
|
|
301
|
+
are placed at ``t=.TOP, b=.BOTTOM, l=.LEFT, r=.RIGHT`` (``z=1``) so a
|
|
302
|
+
single panel can span empty cells.
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
panels : list of grob
|
|
307
|
+
One decorated panel grob per PANEL.
|
|
308
|
+
layout : pandas.DataFrame
|
|
309
|
+
Carries ``.TOP``/``.BOTTOM``/``.LEFT``/``.RIGHT``.
|
|
310
|
+
theme : Theme
|
|
311
|
+
coord : Coord
|
|
312
|
+
ranges : sequence
|
|
313
|
+
params : dict
|
|
314
|
+
|
|
315
|
+
Returns
|
|
316
|
+
-------
|
|
317
|
+
Gtable
|
|
318
|
+
"""
|
|
319
|
+
dim = params.get("dim")
|
|
320
|
+
if dim is not None and not _is_na(dim[1]):
|
|
321
|
+
ncol = int(dim[1])
|
|
322
|
+
else:
|
|
323
|
+
ncol = int(max(layout[".LEFT"].max(), layout[".RIGHT"].max()))
|
|
324
|
+
if dim is not None and not _is_na(dim[0]):
|
|
325
|
+
nrow = int(dim[0])
|
|
326
|
+
else:
|
|
327
|
+
nrow = int(max(layout[".TOP"].max(), layout[".BOTTOM"].max()))
|
|
328
|
+
|
|
329
|
+
aspect = self.setup_aspect_ratio(coord, params["free"], theme, ranges)
|
|
330
|
+
|
|
331
|
+
respect = params.get("respect")
|
|
332
|
+
if respect is None:
|
|
333
|
+
respect = aspect.respect
|
|
334
|
+
widths = params.get("widths")
|
|
335
|
+
if widths is None:
|
|
336
|
+
widths = Unit(1, "null")
|
|
337
|
+
heights = params.get("heights")
|
|
338
|
+
if heights is None:
|
|
339
|
+
heights = Unit(abs(aspect.value), "null")
|
|
340
|
+
|
|
341
|
+
panel_table = Gtable(
|
|
342
|
+
widths=_rep_unit(widths, ncol),
|
|
343
|
+
heights=_rep_unit(heights, nrow),
|
|
344
|
+
respect=respect,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
t = [int(v) for v in layout[".TOP"]]
|
|
348
|
+
b = [int(v) for v in layout[".BOTTOM"]]
|
|
349
|
+
l = [int(v) for v in layout[".LEFT"]]
|
|
350
|
+
r = [int(v) for v in layout[".RIGHT"]]
|
|
351
|
+
names = [f"panel-{i + 1}" for i in range(len(panels))]
|
|
352
|
+
panel_table = gtable_add_grob(
|
|
353
|
+
panel_table,
|
|
354
|
+
list(panels),
|
|
355
|
+
t=t,
|
|
356
|
+
b=b,
|
|
357
|
+
l=l,
|
|
358
|
+
r=r,
|
|
359
|
+
z=1,
|
|
360
|
+
clip=coord.clip,
|
|
361
|
+
name=names,
|
|
362
|
+
)
|
|
363
|
+
panel_table = gtable_add_col_space(
|
|
364
|
+
panel_table, calc_element("panel.spacing.x", theme)
|
|
365
|
+
)
|
|
366
|
+
panel_table = gtable_add_row_space(
|
|
367
|
+
panel_table, calc_element("panel.spacing.y", theme)
|
|
368
|
+
)
|
|
369
|
+
return panel_table
|
|
370
|
+
|
|
371
|
+
# -- setup_axes (the large one) -----------------------------------------
|
|
372
|
+
def setup_axes(
|
|
373
|
+
self,
|
|
374
|
+
axes: Dict[str, Any],
|
|
375
|
+
layout: pd.DataFrame,
|
|
376
|
+
params: Dict[str, Any],
|
|
377
|
+
theme: Any,
|
|
378
|
+
) -> Dict[str, Any]:
|
|
379
|
+
"""Fill / blank / measure / re-place the axis matrices for wrap facets.
|
|
380
|
+
|
|
381
|
+
Port of ggh4x's ``FacetWrap2$setup_axes`` (``R/facet_wrap2.R:197-335``).
|
|
382
|
+
Fills the four ``nrow x ncol`` grob matrices by ``SCALE_X`` / ``SCALE_Y``
|
|
383
|
+
at ``cbind(ROW, COL)``, blanks interior axes unless they repeat
|
|
384
|
+
(``free`` | ``axes``), purges labels when requested, **measures the axes
|
|
385
|
+
after deletion** (so dangling-panel gaps are not over-sized), then
|
|
386
|
+
re-places marginal axes bordering empty cells using ``diff()`` of the
|
|
387
|
+
``empties`` mask.
|
|
388
|
+
|
|
389
|
+
Parameters
|
|
390
|
+
----------
|
|
391
|
+
axes : dict
|
|
392
|
+
Transposed batch axes ``{"x": {"top", "bottom"},
|
|
393
|
+
"y": {"left", "right"}}`` from :func:`render_axes` (indexed by
|
|
394
|
+
``SCALE_*``).
|
|
395
|
+
layout : pandas.DataFrame
|
|
396
|
+
params : dict
|
|
397
|
+
theme : Theme
|
|
398
|
+
|
|
399
|
+
Returns
|
|
400
|
+
-------
|
|
401
|
+
dict
|
|
402
|
+
``{"top", "bottom", "left", "right", "measurements"}``.
|
|
403
|
+
"""
|
|
404
|
+
nrow = int(layout["ROW"].max())
|
|
405
|
+
ncol = int(layout["COL"].max())
|
|
406
|
+
|
|
407
|
+
rows = [int(v) for v in layout["ROW"]]
|
|
408
|
+
cols = [int(v) for v in layout["COL"]]
|
|
409
|
+
scale_x = [int(v) for v in layout["SCALE_X"]]
|
|
410
|
+
scale_y = [int(v) for v in layout["SCALE_Y"]]
|
|
411
|
+
|
|
412
|
+
# empties[r, c] = TRUE unless a panel occupies (r, c).
|
|
413
|
+
empties = np.ones((nrow, ncol), dtype=bool)
|
|
414
|
+
for rr, cc in zip(rows, cols):
|
|
415
|
+
empties[rr - 1, cc - 1] = False
|
|
416
|
+
|
|
417
|
+
def _new() -> List[List[Any]]:
|
|
418
|
+
return [[null_grob() for _ in range(ncol)] for _ in range(nrow)]
|
|
419
|
+
|
|
420
|
+
top = _new()
|
|
421
|
+
bottom = _new()
|
|
422
|
+
left = _new()
|
|
423
|
+
right = _new()
|
|
424
|
+
|
|
425
|
+
x_top = axes["x"]["top"]
|
|
426
|
+
x_bottom = axes["x"]["bottom"]
|
|
427
|
+
y_left = axes["y"]["left"]
|
|
428
|
+
y_right = axes["y"]["right"]
|
|
429
|
+
|
|
430
|
+
# Fill by SCALE id at each panel's (ROW, COL).
|
|
431
|
+
for i in range(len(rows)):
|
|
432
|
+
r0 = rows[i] - 1
|
|
433
|
+
c0 = cols[i] - 1
|
|
434
|
+
top[r0][c0] = x_top[scale_x[i] - 1]
|
|
435
|
+
bottom[r0][c0] = x_bottom[scale_x[i] - 1]
|
|
436
|
+
left[r0][c0] = y_left[scale_y[i] - 1]
|
|
437
|
+
right[r0][c0] = y_right[scale_y[i] - 1]
|
|
438
|
+
|
|
439
|
+
repeat_x = params["free"]["x"] or params["axes"]["x"]
|
|
440
|
+
repeat_y = params["free"]["y"] or params["axes"]["y"]
|
|
441
|
+
|
|
442
|
+
if not repeat_x:
|
|
443
|
+
for r in range(1, nrow): # top[-1, ]
|
|
444
|
+
for c in range(ncol):
|
|
445
|
+
top[r][c] = null_grob()
|
|
446
|
+
for r in range(nrow - 1): # bottom[-nrow, ]
|
|
447
|
+
for c in range(ncol):
|
|
448
|
+
bottom[r][c] = null_grob()
|
|
449
|
+
if not repeat_y:
|
|
450
|
+
for r in range(nrow): # left[, -1]
|
|
451
|
+
for c in range(1, ncol):
|
|
452
|
+
left[r][c] = null_grob()
|
|
453
|
+
for r in range(nrow): # right[, -ncol]
|
|
454
|
+
for c in range(ncol - 1):
|
|
455
|
+
right[r][c] = null_grob()
|
|
456
|
+
|
|
457
|
+
if params["axes"]["x"] and params["rmlab"]["x"] and not params["free"]["x"]:
|
|
458
|
+
for r in range(1, nrow):
|
|
459
|
+
for c in range(ncol):
|
|
460
|
+
top[r][c] = purge_guide_labels(top[r][c])
|
|
461
|
+
for r in range(nrow - 1):
|
|
462
|
+
for c in range(ncol):
|
|
463
|
+
bottom[r][c] = purge_guide_labels(bottom[r][c])
|
|
464
|
+
if params["axes"]["y"] and params["rmlab"]["y"] and not params["free"]["y"]:
|
|
465
|
+
for r in range(nrow):
|
|
466
|
+
for c in range(1, ncol):
|
|
467
|
+
left[r][c] = purge_guide_labels(left[r][c])
|
|
468
|
+
for r in range(nrow):
|
|
469
|
+
for c in range(ncol - 1):
|
|
470
|
+
right[r][c] = purge_guide_labels(right[r][c])
|
|
471
|
+
|
|
472
|
+
# Measure AFTER deletion, BEFORE re-placement.
|
|
473
|
+
measurements = _measure_axes(
|
|
474
|
+
{"top": top, "bottom": bottom, "left": left, "right": right}
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
if not empties.any():
|
|
478
|
+
return {
|
|
479
|
+
"top": top,
|
|
480
|
+
"bottom": bottom,
|
|
481
|
+
"left": left,
|
|
482
|
+
"right": right,
|
|
483
|
+
"measurements": measurements,
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
inside = (
|
|
487
|
+
(calc_element("strip.placement", theme) if theme is not None else None)
|
|
488
|
+
or "inside"
|
|
489
|
+
) == "inside"
|
|
490
|
+
strip_pos = params.get("strip.position", params.get("strip_position", "top"))
|
|
491
|
+
|
|
492
|
+
rc_to_panel = {(rows[i], cols[i]): i for i in range(len(rows))}
|
|
493
|
+
|
|
494
|
+
# bottom_empty: per column, c(diff(empties)==1, FALSE) — a panel whose
|
|
495
|
+
# cell below is empty (transition non-empty -> empty going down).
|
|
496
|
+
bottom_empty = _diff_down(empties, target=1, append_last=False)
|
|
497
|
+
if bottom_empty.any():
|
|
498
|
+
replace_warn = _gather_replace(x_bottom, scale=None, empties_pos=bottom_empty,
|
|
499
|
+
rc_to_panel=rc_to_panel, scale_ids=scale_x,
|
|
500
|
+
rows=rows, cols=cols, side_list=x_bottom)
|
|
501
|
+
if (
|
|
502
|
+
strip_pos == "bottom"
|
|
503
|
+
and not inside
|
|
504
|
+
and any(not is_zero(g) for g in replace_warn)
|
|
505
|
+
and not params["free"]["x"]
|
|
506
|
+
):
|
|
507
|
+
cli_warn(
|
|
508
|
+
'Suppressing axis rendering when `strip.position = "bottom"` '
|
|
509
|
+
'and `strip.placement == "outside"`'
|
|
510
|
+
)
|
|
511
|
+
else:
|
|
512
|
+
_place_back(bottom, bottom_empty, x_bottom, scale_x, rc_to_panel,
|
|
513
|
+
rows, cols)
|
|
514
|
+
|
|
515
|
+
# top_empty: per column, c(FALSE, diff(empties)==-1) — a panel whose cell
|
|
516
|
+
# above is empty.
|
|
517
|
+
top_empty = _diff_down(empties, target=-1, append_last=True)
|
|
518
|
+
if top_empty.any():
|
|
519
|
+
replace_warn = _gather_replace(x_top, scale=None, empties_pos=top_empty,
|
|
520
|
+
rc_to_panel=rc_to_panel, scale_ids=scale_x,
|
|
521
|
+
rows=rows, cols=cols, side_list=x_top)
|
|
522
|
+
if (
|
|
523
|
+
strip_pos == "top"
|
|
524
|
+
and not inside
|
|
525
|
+
and any(not is_zero(g) for g in replace_warn)
|
|
526
|
+
and not params["free"]["x"]
|
|
527
|
+
):
|
|
528
|
+
cli_warn(
|
|
529
|
+
'Suppressing axis rendering when `strip.position = "top"` '
|
|
530
|
+
'and `strip.placement == "outside"`'
|
|
531
|
+
)
|
|
532
|
+
else:
|
|
533
|
+
_place_back(top, top_empty, x_top, scale_x, rc_to_panel, rows, cols)
|
|
534
|
+
|
|
535
|
+
# right_empty: per row, c(diff(empties)==1, FALSE).
|
|
536
|
+
right_empty = _diff_right(empties, target=1, append_last=False)
|
|
537
|
+
if right_empty.any():
|
|
538
|
+
replace_warn = _gather_replace(y_right, scale=None, empties_pos=right_empty,
|
|
539
|
+
rc_to_panel=rc_to_panel, scale_ids=scale_y,
|
|
540
|
+
rows=rows, cols=cols, side_list=y_right)
|
|
541
|
+
if (
|
|
542
|
+
strip_pos == "right"
|
|
543
|
+
and not inside
|
|
544
|
+
and any(not is_zero(g) for g in replace_warn)
|
|
545
|
+
and not params["free"]["y"]
|
|
546
|
+
):
|
|
547
|
+
cli_warn(
|
|
548
|
+
'Suppressing axis rendering when `strip.position = "right"` '
|
|
549
|
+
'and `strip.placement == "outside"`'
|
|
550
|
+
)
|
|
551
|
+
# R places back unconditionally for right/left (no else guard).
|
|
552
|
+
_place_back(right, right_empty, y_right, scale_y, rc_to_panel, rows, cols)
|
|
553
|
+
|
|
554
|
+
# left_empty: per row, c(FALSE, diff(empties)==-1).
|
|
555
|
+
left_empty = _diff_right(empties, target=-1, append_last=True)
|
|
556
|
+
if left_empty.any():
|
|
557
|
+
replace_warn = _gather_replace(y_left, scale=None, empties_pos=left_empty,
|
|
558
|
+
rc_to_panel=rc_to_panel, scale_ids=scale_y,
|
|
559
|
+
rows=rows, cols=cols, side_list=y_left)
|
|
560
|
+
if (
|
|
561
|
+
strip_pos == "left"
|
|
562
|
+
and not inside
|
|
563
|
+
and any(not is_zero(g) for g in replace_warn)
|
|
564
|
+
and not params["free"]["y"]
|
|
565
|
+
):
|
|
566
|
+
cli_warn(
|
|
567
|
+
'Suppressing axis rendering when `strip.position = "left"` '
|
|
568
|
+
'and `strip.placement == "outside"`'
|
|
569
|
+
)
|
|
570
|
+
_place_back(left, left_empty, y_left, scale_y, rc_to_panel, rows, cols)
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
"top": top,
|
|
574
|
+
"bottom": bottom,
|
|
575
|
+
"left": left,
|
|
576
|
+
"right": right,
|
|
577
|
+
"measurements": measurements,
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
# -- attach_axes (explicit sizes) ---------------------------------------
|
|
581
|
+
def attach_axes(
|
|
582
|
+
self,
|
|
583
|
+
panel_table: Gtable,
|
|
584
|
+
axes: Dict[str, Any],
|
|
585
|
+
sizes: Dict[str, Unit],
|
|
586
|
+
) -> Gtable:
|
|
587
|
+
"""Weave the axis bands using pre-computed sizes.
|
|
588
|
+
|
|
589
|
+
Port of ggh4x's ``FacetWrap2$attach_axes`` (``R/facet_wrap2.R:336-346``).
|
|
590
|
+
Differs from ``FacetGrid2`` by taking an explicit ``sizes`` argument (the
|
|
591
|
+
post-deletion measurements) instead of measuring internally.
|
|
592
|
+
|
|
593
|
+
Parameters
|
|
594
|
+
----------
|
|
595
|
+
panel_table : Gtable
|
|
596
|
+
axes : dict
|
|
597
|
+
``{"top", "bottom", "left", "right"}`` grob matrices.
|
|
598
|
+
sizes : dict
|
|
599
|
+
``{"top", "bottom", "left", "right"}`` size unit vectors.
|
|
600
|
+
|
|
601
|
+
Returns
|
|
602
|
+
-------
|
|
603
|
+
Gtable
|
|
604
|
+
"""
|
|
605
|
+
panel_table = weave_tables_row(
|
|
606
|
+
panel_table, axes["top"], -1, sizes["top"], "axis-t", 3
|
|
607
|
+
)
|
|
608
|
+
panel_table = weave_tables_row(
|
|
609
|
+
panel_table, axes["bottom"], 0, sizes["bottom"], "axis-b", 3
|
|
610
|
+
)
|
|
611
|
+
panel_table = weave_tables_col(
|
|
612
|
+
panel_table, axes["left"], -1, sizes["left"], "axis-l", 3
|
|
613
|
+
)
|
|
614
|
+
panel_table = weave_tables_col(
|
|
615
|
+
panel_table, axes["right"], 0, sizes["right"], "axis-r", 3
|
|
616
|
+
)
|
|
617
|
+
return panel_table
|
|
618
|
+
|
|
619
|
+
# -- finish_panels (identity seam) --------------------------------------
|
|
620
|
+
def finish_panels(
|
|
621
|
+
self,
|
|
622
|
+
panels: Any,
|
|
623
|
+
layout: pd.DataFrame,
|
|
624
|
+
params: Dict[str, Any],
|
|
625
|
+
theme: Any,
|
|
626
|
+
) -> Any:
|
|
627
|
+
"""Identity post-processing hook (extension seam).
|
|
628
|
+
|
|
629
|
+
Port of ggh4x's ``FacetWrap2$finish_panels`` (``R/facet_wrap2.R:347-349``).
|
|
630
|
+
"""
|
|
631
|
+
return panels
|
|
632
|
+
|
|
633
|
+
# -- draw_panels (full override) ----------------------------------------
|
|
634
|
+
def draw_panels(
|
|
635
|
+
self,
|
|
636
|
+
panels: list,
|
|
637
|
+
layout: pd.DataFrame,
|
|
638
|
+
x_scales: list,
|
|
639
|
+
y_scales: list,
|
|
640
|
+
ranges: list,
|
|
641
|
+
coord: Any,
|
|
642
|
+
data: Any,
|
|
643
|
+
theme: Any,
|
|
644
|
+
params: Dict[str, Any],
|
|
645
|
+
) -> Gtable:
|
|
646
|
+
"""Assemble the wrap panel gtable (full replacement of the base pipeline).
|
|
647
|
+
|
|
648
|
+
Port of ggh4x's ``FacetWrap2$draw_panels`` (``R/facet_wrap2.R:350-391``).
|
|
649
|
+
Runs ``setup_layout``; sets the spanning ``.TOP``/``.BOTTOM``/``.LEFT``/
|
|
650
|
+
``.RIGHT = ROW/COL``; resolves ``params.dim`` NA -> nrow/ncol; then
|
|
651
|
+
``setup_panel_table`` -> ``render_axes`` -> ``setup_axes`` ->
|
|
652
|
+
``attach_axes(measurements)`` -> strip ``setup`` / ``incorporate_wrap`` ->
|
|
653
|
+
``finish_panels``.
|
|
654
|
+
|
|
655
|
+
Parameters
|
|
656
|
+
----------
|
|
657
|
+
panels : list
|
|
658
|
+
layout : pandas.DataFrame
|
|
659
|
+
x_scales, y_scales : list
|
|
660
|
+
ranges : list
|
|
661
|
+
coord : Coord
|
|
662
|
+
data : Any
|
|
663
|
+
theme : Theme
|
|
664
|
+
params : dict
|
|
665
|
+
|
|
666
|
+
Returns
|
|
667
|
+
-------
|
|
668
|
+
Gtable
|
|
669
|
+
"""
|
|
670
|
+
if (params["free"]["x"] or params["free"]["y"]) and not coord.is_free():
|
|
671
|
+
cli_abort(f"`{snake_class(coord)}` doesn't support free scales.")
|
|
672
|
+
|
|
673
|
+
strip = self.strip
|
|
674
|
+
layout = self.setup_layout(layout, coord, params)
|
|
675
|
+
|
|
676
|
+
ncol = int(layout["COL"].max())
|
|
677
|
+
nrow = int(layout["ROW"].max())
|
|
678
|
+
layout = layout.copy()
|
|
679
|
+
layout[".TOP"] = layout["ROW"].to_numpy()
|
|
680
|
+
layout[".BOTTOM"] = layout["ROW"].to_numpy()
|
|
681
|
+
layout[".LEFT"] = layout["COL"].to_numpy()
|
|
682
|
+
layout[".RIGHT"] = layout["COL"].to_numpy()
|
|
683
|
+
|
|
684
|
+
params = dict(params)
|
|
685
|
+
dim = params.get("dim")
|
|
686
|
+
if dim is not None:
|
|
687
|
+
dim = list(dim)
|
|
688
|
+
if _is_na(dim[0]):
|
|
689
|
+
dim[0] = nrow
|
|
690
|
+
if _is_na(dim[1]):
|
|
691
|
+
dim[1] = ncol
|
|
692
|
+
params["dim"] = dim
|
|
693
|
+
|
|
694
|
+
# Decorate per-layer grobs into one panel grob per PANEL.
|
|
695
|
+
panel_grobs = _decorate_panels(panels, layout, ranges, coord, theme)
|
|
696
|
+
|
|
697
|
+
panel_table = self.setup_panel_table(
|
|
698
|
+
panel_grobs, layout, theme, coord, ranges, params
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
axes = render_axes(ranges, ranges, coord, theme, transpose=True)
|
|
702
|
+
axes = self.setup_axes(axes, layout, params, theme)
|
|
703
|
+
panel_table = self.attach_axes(panel_table, axes, axes["measurements"])
|
|
704
|
+
|
|
705
|
+
strip.setup(layout, params, theme, type="wrap")
|
|
706
|
+
panel_table = strip.incorporate_wrap(
|
|
707
|
+
panel_table,
|
|
708
|
+
params.get("strip.position", params.get("strip_position", "top")),
|
|
709
|
+
clip=coord.clip,
|
|
710
|
+
sizes=axes["measurements"],
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
return self.finish_panels(
|
|
714
|
+
panels=panel_table, layout=layout, params=params, theme=theme
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
# ---------------------------------------------------------------------------
|
|
719
|
+
# Module-private helpers (empties re-placement)
|
|
720
|
+
# ---------------------------------------------------------------------------
|
|
721
|
+
def _is_na(x: Any) -> bool:
|
|
722
|
+
"""Return ``True`` when *x* is ``None`` or NaN (R ``is.na``)."""
|
|
723
|
+
if x is None:
|
|
724
|
+
return True
|
|
725
|
+
try:
|
|
726
|
+
return bool(np.isnan(x))
|
|
727
|
+
except (TypeError, ValueError):
|
|
728
|
+
return False
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def _rep_unit(u: Unit, length_out: int) -> Unit:
|
|
732
|
+
"""Recycle a (scalar) unit to ``length_out`` (R ``rep(u, length.out=n)``)."""
|
|
733
|
+
from grid_py import unit_rep
|
|
734
|
+
|
|
735
|
+
return unit_rep(u, length_out=length_out)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def _diff_down(empties: np.ndarray, target: int, append_last: bool) -> np.ndarray:
|
|
739
|
+
"""Port of ``apply(empties, 2, function(x) c(diff(x)==target, FALSE/...))``.
|
|
740
|
+
|
|
741
|
+
Computes, per column, ``diff`` of the boolean mask (as ints) compared to
|
|
742
|
+
*target*, then pads. When ``append_last`` is ``False`` the result is
|
|
743
|
+
``c(diff==target, FALSE)`` (length nrow, last row FALSE); when ``True`` it is
|
|
744
|
+
``c(FALSE, diff==target)`` (first row FALSE).
|
|
745
|
+
|
|
746
|
+
Parameters
|
|
747
|
+
----------
|
|
748
|
+
empties : numpy.ndarray
|
|
749
|
+
``nrow x ncol`` boolean mask.
|
|
750
|
+
target : int
|
|
751
|
+
``1`` (non-empty -> empty going down) or ``-1`` (empty -> non-empty).
|
|
752
|
+
append_last : bool
|
|
753
|
+
``False`` -> pad FALSE at the bottom; ``True`` -> pad FALSE at the top.
|
|
754
|
+
|
|
755
|
+
Returns
|
|
756
|
+
-------
|
|
757
|
+
numpy.ndarray
|
|
758
|
+
``nrow x ncol`` boolean mask of panels bordering a vertical hole.
|
|
759
|
+
"""
|
|
760
|
+
nrow, ncol = empties.shape
|
|
761
|
+
ints = empties.astype(int)
|
|
762
|
+
out = np.zeros((nrow, ncol), dtype=bool)
|
|
763
|
+
if nrow < 2:
|
|
764
|
+
return out
|
|
765
|
+
d = (ints[1:, :] - ints[:-1, :]) == target # (nrow-1, ncol)
|
|
766
|
+
if append_last:
|
|
767
|
+
out[1:, :] = d
|
|
768
|
+
else:
|
|
769
|
+
out[:-1, :] = d
|
|
770
|
+
return out
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _diff_right(empties: np.ndarray, target: int, append_last: bool) -> np.ndarray:
|
|
774
|
+
"""Port of ``t(apply(empties, 1, function(x) c(diff(x)==target, ...)))``.
|
|
775
|
+
|
|
776
|
+
Row-wise counterpart of :func:`_diff_down`: detects panels bordering a
|
|
777
|
+
horizontal hole. ``append_last=False`` -> ``c(diff==target, FALSE)`` per row
|
|
778
|
+
(last col FALSE); ``append_last=True`` -> ``c(FALSE, diff==target)`` (first
|
|
779
|
+
col FALSE).
|
|
780
|
+
|
|
781
|
+
Parameters
|
|
782
|
+
----------
|
|
783
|
+
empties : numpy.ndarray
|
|
784
|
+
target : int
|
|
785
|
+
``1`` (right neighbour empty) or ``-1`` (left neighbour empty).
|
|
786
|
+
append_last : bool
|
|
787
|
+
|
|
788
|
+
Returns
|
|
789
|
+
-------
|
|
790
|
+
numpy.ndarray
|
|
791
|
+
"""
|
|
792
|
+
nrow, ncol = empties.shape
|
|
793
|
+
ints = empties.astype(int)
|
|
794
|
+
out = np.zeros((nrow, ncol), dtype=bool)
|
|
795
|
+
if ncol < 2:
|
|
796
|
+
return out
|
|
797
|
+
d = (ints[:, 1:] - ints[:, :-1]) == target # (nrow, ncol-1)
|
|
798
|
+
if append_last:
|
|
799
|
+
out[:, 1:] = d
|
|
800
|
+
else:
|
|
801
|
+
out[:, :-1] = d
|
|
802
|
+
return out
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _gather_replace(
|
|
806
|
+
side_axes: Sequence[Any],
|
|
807
|
+
scale: Any,
|
|
808
|
+
empties_pos: np.ndarray,
|
|
809
|
+
rc_to_panel: Dict[tuple, int],
|
|
810
|
+
scale_ids: Sequence[int],
|
|
811
|
+
rows: Sequence[int],
|
|
812
|
+
cols: Sequence[int],
|
|
813
|
+
side_list: Sequence[Any],
|
|
814
|
+
) -> List[Any]:
|
|
815
|
+
"""Collect the would-be replacement axis grobs for the warning check.
|
|
816
|
+
|
|
817
|
+
Mirrors R's ``replace <- axes$x$bottom[panels]`` where ``panels`` are the
|
|
818
|
+
panel indices at the hole-bordering positions. Returns the rendered axis
|
|
819
|
+
grobs (by ``SCALE_*``) for each flagged ``(row, col)`` so the caller can test
|
|
820
|
+
whether any is non-zero (the strip-placement="outside" suppression warning).
|
|
821
|
+
|
|
822
|
+
Parameters
|
|
823
|
+
----------
|
|
824
|
+
side_axes, side_list : sequence
|
|
825
|
+
The rendered per-scale axis list (e.g. ``axes$x$bottom``).
|
|
826
|
+
scale : Any
|
|
827
|
+
Unused (kept for signature clarity).
|
|
828
|
+
empties_pos : numpy.ndarray
|
|
829
|
+
Boolean mask of flagged positions.
|
|
830
|
+
rc_to_panel : dict
|
|
831
|
+
``(row, col) -> panel-layout-index`` (0-based).
|
|
832
|
+
scale_ids : sequence of int
|
|
833
|
+
Per-panel ``SCALE_X`` / ``SCALE_Y`` (1-based).
|
|
834
|
+
rows, cols : sequence of int
|
|
835
|
+
Per-panel ``ROW`` / ``COL`` (1-based).
|
|
836
|
+
|
|
837
|
+
Returns
|
|
838
|
+
-------
|
|
839
|
+
list of grob
|
|
840
|
+
"""
|
|
841
|
+
out: List[Any] = []
|
|
842
|
+
nrow, ncol = empties_pos.shape
|
|
843
|
+
for r in range(nrow):
|
|
844
|
+
for c in range(ncol):
|
|
845
|
+
if not empties_pos[r, c]:
|
|
846
|
+
continue
|
|
847
|
+
panel_idx = rc_to_panel.get((r + 1, c + 1))
|
|
848
|
+
if panel_idx is None:
|
|
849
|
+
out.append(null_grob())
|
|
850
|
+
continue
|
|
851
|
+
sid = scale_ids[panel_idx]
|
|
852
|
+
out.append(side_list[sid - 1])
|
|
853
|
+
return out
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _place_back(
|
|
857
|
+
matrix: List[List[Any]],
|
|
858
|
+
empties_pos: np.ndarray,
|
|
859
|
+
side_list: Sequence[Any],
|
|
860
|
+
scale_ids: Sequence[int],
|
|
861
|
+
rc_to_panel: Dict[tuple, int],
|
|
862
|
+
rows: Sequence[int],
|
|
863
|
+
cols: Sequence[int],
|
|
864
|
+
) -> None:
|
|
865
|
+
"""Re-insert marginal axes at hole-bordering panels (in place).
|
|
866
|
+
|
|
867
|
+
Mirrors R's ``bottom[pos] <- axes$x$bottom[panels]`` block: at each flagged
|
|
868
|
+
position ``(r, c)`` look up the panel that lives there, and write its
|
|
869
|
+
``SCALE``-indexed rendered axis back into the matrix so the dangling panel
|
|
870
|
+
keeps its marginal axis.
|
|
871
|
+
|
|
872
|
+
Parameters
|
|
873
|
+
----------
|
|
874
|
+
matrix : list of list
|
|
875
|
+
The side grob matrix being mutated.
|
|
876
|
+
empties_pos : numpy.ndarray
|
|
877
|
+
Boolean mask of positions to re-place.
|
|
878
|
+
side_list : sequence
|
|
879
|
+
The rendered per-scale axis list.
|
|
880
|
+
scale_ids : sequence of int
|
|
881
|
+
Per-panel ``SCALE_*`` (1-based).
|
|
882
|
+
rc_to_panel : dict
|
|
883
|
+
``(row, col) -> panel-layout-index`` (0-based).
|
|
884
|
+
rows, cols : sequence of int
|
|
885
|
+
Per-panel ``ROW`` / ``COL`` (unused; kept for parity).
|
|
886
|
+
"""
|
|
887
|
+
nrow, ncol = empties_pos.shape
|
|
888
|
+
for r in range(nrow):
|
|
889
|
+
for c in range(ncol):
|
|
890
|
+
if not empties_pos[r, c]:
|
|
891
|
+
continue
|
|
892
|
+
panel_idx = rc_to_panel.get((r + 1, c + 1))
|
|
893
|
+
if panel_idx is None:
|
|
894
|
+
continue
|
|
895
|
+
sid = scale_ids[panel_idx]
|
|
896
|
+
matrix[r][c] = side_list[sid - 1]
|