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/strip_tag.py
ADDED
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
"""Tag strips for ggh4x facets (port of ggh4x ``strip_tag.R``).
|
|
2
|
+
|
|
3
|
+
This module ports :class:`StripTag` and the :func:`strip_tag` constructor. Tag
|
|
4
|
+
strips render the strips as fitted text boxes *inside* the panels (anchored to a
|
|
5
|
+
panel corner via a justified viewport), rather than as full-width strips outside
|
|
6
|
+
the panels.
|
|
7
|
+
|
|
8
|
+
``StripTag`` extends :class:`ggh4x.strip_themed.StripThemed` (a *sibling* of
|
|
9
|
+
``StripNested``, not a subclass). It overrides:
|
|
10
|
+
|
|
11
|
+
* :meth:`StripTag.setup` -- builds *per-panel* (not de-duplicated) col/row var
|
|
12
|
+
frames; reuses the base ``get_strips`` x / y shape.
|
|
13
|
+
* :meth:`StripTag.draw_labels` -- a *self-less* fitted-box label builder: no
|
|
14
|
+
margin-equalisation, no ``unit(1, "null")`` cross-axis; measures grob
|
|
15
|
+
height/width in cm (per-layer max on the strip axis, exact cm on the cross
|
|
16
|
+
axis).
|
|
17
|
+
* :meth:`StripTag.finish_strip` -- *has* ``self`` (unlike the base self-less
|
|
18
|
+
``finish_strip``): builds fitted-box gtables with npc-fractional widths inside
|
|
19
|
+
a cm outer viewport justified to a panel corner.
|
|
20
|
+
* :meth:`StripTag.incorporate_grid` / :meth:`StripTag.incorporate_wrap` -- place
|
|
21
|
+
the tag grobs *onto* existing panel cells (no new rows/cols).
|
|
22
|
+
|
|
23
|
+
R source: ``ggh4x/R/strip_tag.R``.
|
|
24
|
+
|
|
25
|
+
Notes
|
|
26
|
+
-----
|
|
27
|
+
* **Viewport npc trick.** Each fitted box is a ``gtable_matrix`` whose
|
|
28
|
+
widths/heights are ``unit(w / sum(w), "npc")`` wrapped in a viewport sized to
|
|
29
|
+
``unit(sum(w), "cm")`` and justified to ``params["just"]``. When ``clip ==
|
|
30
|
+
"on"`` the viewport is clamped to ``unit(1, "npc")`` via :func:`unit_pmin`.
|
|
31
|
+
* **grid layout combine.** ``incorporate_grid`` ``rbind``s the x and y tag
|
|
32
|
+
gtables (order from ``params["order"]``), recomputing the combined viewport
|
|
33
|
+
height (sum) and width (max).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
39
|
+
|
|
40
|
+
import numpy as np
|
|
41
|
+
import pandas as pd
|
|
42
|
+
|
|
43
|
+
from ggplot2_py import element_grob
|
|
44
|
+
from ggplot2_py import is_theme_element as _is_theme_element
|
|
45
|
+
from ggplot2_py._utils import height_cm, width_cm
|
|
46
|
+
from ggplot2_py.ggproto import ggproto
|
|
47
|
+
from grid_py import (
|
|
48
|
+
Unit,
|
|
49
|
+
Viewport,
|
|
50
|
+
edit_grob,
|
|
51
|
+
grob_height,
|
|
52
|
+
grob_name,
|
|
53
|
+
grob_tree,
|
|
54
|
+
grob_width,
|
|
55
|
+
unit_c,
|
|
56
|
+
unit_pmin,
|
|
57
|
+
)
|
|
58
|
+
from gtable_py import gtable_add_grob, gtable_matrix, rbind_gtable
|
|
59
|
+
|
|
60
|
+
from ggh4x._rlang import arg_match0
|
|
61
|
+
from ggh4x.strip_themed import StripThemed
|
|
62
|
+
from ggh4x.strip_vanilla import (
|
|
63
|
+
_LabelGrobs,
|
|
64
|
+
_is_zero_grob,
|
|
65
|
+
_panel_layout,
|
|
66
|
+
_split_by,
|
|
67
|
+
validate_element_list,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
__all__ = ["StripTag", "strip_tag"]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _draw_labels_tag(
|
|
74
|
+
labels: Sequence[str],
|
|
75
|
+
element: Dict[str, List[Any]],
|
|
76
|
+
position: str,
|
|
77
|
+
layer_id: Sequence[int],
|
|
78
|
+
size: Any = None,
|
|
79
|
+
) -> _LabelGrobs:
|
|
80
|
+
"""Build fitted-box label grobs (self-less; tag variant of ``draw_labels``).
|
|
81
|
+
|
|
82
|
+
Port of R ``StripTag$draw_labels`` (``strip_tag.R:119-163``). Unlike the
|
|
83
|
+
base, there is no margin-equalisation and no ``unit(1, "null")`` cross axis:
|
|
84
|
+
label sizes are measured directly in cm with :func:`grid_py.grob_height` /
|
|
85
|
+
:func:`grid_py.grob_width` (per-layer max on the strip axis, exact cm on the
|
|
86
|
+
cross axis).
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
labels : sequence of str
|
|
91
|
+
Flattened (column-major) label strings, one per cell.
|
|
92
|
+
element : dict
|
|
93
|
+
``{"el": [...text elements...], "bg": [...background grobs...]}``.
|
|
94
|
+
position : str
|
|
95
|
+
Strip side.
|
|
96
|
+
layer_id : sequence of int
|
|
97
|
+
1-based layer id per cell.
|
|
98
|
+
size : Any, optional
|
|
99
|
+
Unused (kept for signature parity with the base ``draw_labels``).
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
_LabelGrobs
|
|
104
|
+
A list of GTree grobs with ``.width`` / ``.height`` unit attributes.
|
|
105
|
+
"""
|
|
106
|
+
aes = "x" if position in ("top", "bottom") else "y"
|
|
107
|
+
layer_id = list(layer_id)
|
|
108
|
+
|
|
109
|
+
grobs: List[Any] = []
|
|
110
|
+
for label, elem in zip(labels, element["el"]):
|
|
111
|
+
grob = element_grob(elem, label=label, margin_x=True, margin_y=True)
|
|
112
|
+
try:
|
|
113
|
+
grob.name = grob_name(grob, "strip.text." + aes)
|
|
114
|
+
except (AttributeError, TypeError):
|
|
115
|
+
pass
|
|
116
|
+
grobs.append(grob)
|
|
117
|
+
|
|
118
|
+
zeros = [_is_zero_grob(g) for g in grobs]
|
|
119
|
+
if len(grobs) == 0 or all(zeros):
|
|
120
|
+
return _LabelGrobs(grobs)
|
|
121
|
+
|
|
122
|
+
nonzero_idx = [i for i, z in enumerate(zeros) if not z]
|
|
123
|
+
nonzero_layer = [layer_id[i] for i in nonzero_idx]
|
|
124
|
+
|
|
125
|
+
heights = [grob_height(grobs[i]) for i in nonzero_idx]
|
|
126
|
+
widths = [grob_width(grobs[i]) for i in nonzero_idx]
|
|
127
|
+
|
|
128
|
+
if aes == "x":
|
|
129
|
+
# per-layer max height (of grobHeight units); exact cm width.
|
|
130
|
+
grouped = _split_by(heights, nonzero_layer)
|
|
131
|
+
height_units = [_max_unit(g) for g in grouped]
|
|
132
|
+
height = unit_c(*height_units) if len(height_units) > 1 else height_units[0]
|
|
133
|
+
width = Unit(_to_list(width_cm(widths)), "cm")
|
|
134
|
+
else:
|
|
135
|
+
# per-layer max width (of grobWidth units); exact cm height.
|
|
136
|
+
grouped = _split_by(widths, nonzero_layer)
|
|
137
|
+
width_units = [_max_unit(g) for g in grouped]
|
|
138
|
+
width = unit_c(*width_units) if len(width_units) > 1 else width_units[0]
|
|
139
|
+
height = Unit(_to_list(height_cm(heights)), "cm")
|
|
140
|
+
|
|
141
|
+
combined: List[Any] = []
|
|
142
|
+
for x, bg in zip(grobs, element["bg"]):
|
|
143
|
+
bg_grob = element_grob(bg) if _is_theme_element(bg) else bg
|
|
144
|
+
tree = grob_tree(bg_grob, x)
|
|
145
|
+
try:
|
|
146
|
+
tree.name = grob_name(tree, "strip")
|
|
147
|
+
except (AttributeError, TypeError):
|
|
148
|
+
pass
|
|
149
|
+
combined.append(tree)
|
|
150
|
+
|
|
151
|
+
result = _LabelGrobs(combined)
|
|
152
|
+
result.width = width
|
|
153
|
+
result.height = height
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _max_unit(units: Sequence[Any]) -> Any:
|
|
158
|
+
"""Port of R ``max_height`` / ``max_width`` applied to a list of *units*.
|
|
159
|
+
|
|
160
|
+
For the tag path the per-layer reduction is over already-measured unit
|
|
161
|
+
objects (``grobHeight`` / ``grobWidth`` results), so it reduces to the
|
|
162
|
+
element-wise maximum cm value wrapped back as a single ``unit``.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
units : sequence of grid_py.Unit
|
|
167
|
+
The per-cell measured units for one layer.
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
grid_py.Unit
|
|
172
|
+
A single ``cm`` unit holding the maximum measured size.
|
|
173
|
+
"""
|
|
174
|
+
if len(units) == 0:
|
|
175
|
+
return Unit(0.0, "cm")
|
|
176
|
+
values = [float(np.atleast_1d(width_cm(u))[0]) for u in units]
|
|
177
|
+
return Unit(max(values), "cm")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class StripTag(StripThemed):
|
|
181
|
+
"""Strip rendered as a fitted text box inside the panels.
|
|
182
|
+
|
|
183
|
+
Subclass of :class:`ggh4x.strip_themed.StripThemed`. See the module
|
|
184
|
+
docstring for the fitted-box / viewport algorithm.
|
|
185
|
+
|
|
186
|
+
Attributes
|
|
187
|
+
----------
|
|
188
|
+
params : dict
|
|
189
|
+
Holds ``clip``, ``order`` (``["x", "y"]`` or ``["y", "x"]``) and ``just``
|
|
190
|
+
(a length-2 numeric justification).
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
_class_name = "StripTag"
|
|
194
|
+
|
|
195
|
+
# self-less draw_labels (first param is not `self`).
|
|
196
|
+
draw_labels = staticmethod(_draw_labels_tag)
|
|
197
|
+
|
|
198
|
+
def setup(
|
|
199
|
+
self,
|
|
200
|
+
layout: pd.DataFrame,
|
|
201
|
+
params: Dict[str, Any],
|
|
202
|
+
theme: Any,
|
|
203
|
+
type: str,
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Build per-panel (non-de-duplicated) var frames and get strips.
|
|
206
|
+
|
|
207
|
+
Port of R ``StripTag$setup`` (``strip_tag.R:92-117``). Unlike the base,
|
|
208
|
+
tags are per-panel (they overlap panels), so the layout is *not*
|
|
209
|
+
de-duplicated.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
layout : pandas.DataFrame
|
|
214
|
+
The facet layout.
|
|
215
|
+
params : dict
|
|
216
|
+
Facet params (``facets`` for wrap; ``rows`` / ``cols`` for grid; plus
|
|
217
|
+
``labeller``).
|
|
218
|
+
theme : Theme
|
|
219
|
+
The active theme.
|
|
220
|
+
type : str
|
|
221
|
+
``"wrap"`` or ``"grid"``.
|
|
222
|
+
"""
|
|
223
|
+
self._set(elements=self.setup_elements(theme, type))
|
|
224
|
+
|
|
225
|
+
if type == "wrap":
|
|
226
|
+
facets = params.get("facets") or {}
|
|
227
|
+
facet_names = list(facets.keys()) if hasattr(facets, "keys") else list(facets)
|
|
228
|
+
if len(facet_names) == 0:
|
|
229
|
+
labels = pd.DataFrame({"(all)": ["(all)"]})
|
|
230
|
+
else:
|
|
231
|
+
labels = layout[facet_names].reset_index(drop=True)
|
|
232
|
+
col_vars = labels
|
|
233
|
+
row_vars = labels
|
|
234
|
+
else:
|
|
235
|
+
col_names = _names(params.get("cols"))
|
|
236
|
+
row_names = _names(params.get("rows"))
|
|
237
|
+
col_vars = layout[col_names].reset_index(drop=True) if col_names else _empty_frame(layout)
|
|
238
|
+
row_vars = layout[row_names].reset_index(drop=True) if row_names else _empty_frame(layout)
|
|
239
|
+
|
|
240
|
+
self.get_strips(
|
|
241
|
+
x=col_vars,
|
|
242
|
+
y=row_vars,
|
|
243
|
+
labeller=params.get("labeller"),
|
|
244
|
+
theme=theme,
|
|
245
|
+
params=self.params,
|
|
246
|
+
layout_x=layout,
|
|
247
|
+
layout_y=layout,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
def finish_strip( # type: ignore[override]
|
|
251
|
+
self,
|
|
252
|
+
strip: Sequence[Any],
|
|
253
|
+
width: Any,
|
|
254
|
+
height: Any,
|
|
255
|
+
position: str,
|
|
256
|
+
layout: pd.DataFrame,
|
|
257
|
+
dim: Any,
|
|
258
|
+
clip: str = "inherit",
|
|
259
|
+
) -> pd.DataFrame:
|
|
260
|
+
"""Build fitted-box gtables placed inside panels via a justified vp.
|
|
261
|
+
|
|
262
|
+
Port of R ``StripTag$finish_strip`` (``strip_tag.R:165-217``). Has
|
|
263
|
+
``self`` (unlike the base self-less ``finish_strip``) to read
|
|
264
|
+
``self.params["just"]``.
|
|
265
|
+
|
|
266
|
+
Parameters
|
|
267
|
+
----------
|
|
268
|
+
strip : sequence
|
|
269
|
+
The label grobs (one per cell).
|
|
270
|
+
width, height : grid_py.Unit
|
|
271
|
+
Per-cell width / height unit vectors (measured cm).
|
|
272
|
+
position : str
|
|
273
|
+
Strip side.
|
|
274
|
+
layout : pandas.DataFrame
|
|
275
|
+
The (full, per-panel) layout carrying ``PANEL``.
|
|
276
|
+
dim : tuple of int
|
|
277
|
+
``(nrow, ncol)`` of the label matrix (panels x layers).
|
|
278
|
+
clip : str, default ``"inherit"``
|
|
279
|
+
Clip setting (``"on"`` clamps the box to ``1 npc``).
|
|
280
|
+
|
|
281
|
+
Returns
|
|
282
|
+
-------
|
|
283
|
+
pandas.DataFrame
|
|
284
|
+
Placement frame with ``t = l = b = r = PANEL`` and a ``grobs`` column
|
|
285
|
+
of fitted-box gtables.
|
|
286
|
+
"""
|
|
287
|
+
strip = list(strip)
|
|
288
|
+
empty = len(strip) == 0 or all(_is_zero_grob(g) for g in strip)
|
|
289
|
+
|
|
290
|
+
out_grobs: List[Any] = strip
|
|
291
|
+
if not empty:
|
|
292
|
+
just = self.params["just"]
|
|
293
|
+
n = len(strip)
|
|
294
|
+
w_cm = _recycle(_to_list(width_cm(width)), n)
|
|
295
|
+
h_cm = _recycle(_to_list(height_cm(height)), n)
|
|
296
|
+
|
|
297
|
+
nrow, ncol = int(dim[0]), int(dim[1])
|
|
298
|
+
# idx = matrix(seq_along(strip), nrow, ncol) -- column-major.
|
|
299
|
+
idx = np.arange(n, dtype=int).reshape(nrow, ncol, order="F")
|
|
300
|
+
|
|
301
|
+
out_grobs = []
|
|
302
|
+
for ri in range(nrow):
|
|
303
|
+
if position in ("top", "bottom"):
|
|
304
|
+
# apply(idx, 1, matrix, ncol=1) -> column vector (ncol x 1).
|
|
305
|
+
sub_idx = idx[ri, :].reshape(ncol, 1, order="F")
|
|
306
|
+
else:
|
|
307
|
+
# apply(idx, 1, matrix, nrow=1) -> row vector (1 x ncol).
|
|
308
|
+
sub_idx = idx[ri, :].reshape(1, ncol, order="F")
|
|
309
|
+
d0, d1 = sub_idx.shape
|
|
310
|
+
flat = list(sub_idx.reshape(-1, order="F"))
|
|
311
|
+
m = [[strip[sub_idx[i, j]] for j in range(d1)] for i in range(d0)]
|
|
312
|
+
wmat = np.array([w_cm[k] for k in flat], dtype=float).reshape(d0, d1, order="F")
|
|
313
|
+
hmat = np.array([h_cm[k] for k in flat], dtype=float).reshape(d0, d1, order="F")
|
|
314
|
+
w = wmat.max(axis=0) # per column (layer)
|
|
315
|
+
h = hmat.max(axis=1) # per row
|
|
316
|
+
sum_w = float(w.sum())
|
|
317
|
+
sum_h = float(h.sum())
|
|
318
|
+
|
|
319
|
+
vp_width = Unit(sum_w, "cm")
|
|
320
|
+
vp_height = Unit(sum_h, "cm")
|
|
321
|
+
if clip == "on":
|
|
322
|
+
vp_width = unit_pmin(vp_width, Unit(1, "npc"))
|
|
323
|
+
vp_height = unit_pmin(vp_height, Unit(1, "npc"))
|
|
324
|
+
|
|
325
|
+
vp = Viewport(
|
|
326
|
+
x=just[0],
|
|
327
|
+
y=just[1],
|
|
328
|
+
just=list(just),
|
|
329
|
+
width=vp_width,
|
|
330
|
+
height=vp_height,
|
|
331
|
+
clip=clip,
|
|
332
|
+
)
|
|
333
|
+
widths = _npc_unit(w, sum_w)
|
|
334
|
+
heights = _npc_unit(h, sum_h)
|
|
335
|
+
# Build the fitted-box gtable then attach the justified viewport.
|
|
336
|
+
# (gtable_py's ``Gtable(vp=...)`` reconstruction reads
|
|
337
|
+
# ``vp.justification`` which grid_py stores as ``vp.just``, so the
|
|
338
|
+
# viewport is attached post-construction rather than via the
|
|
339
|
+
# ``vp=`` kwarg -- functionally identical to R's
|
|
340
|
+
# ``gtable_matrix(..., vp = vp)``.)
|
|
341
|
+
gt = gtable_matrix("strip-cells", m, widths, heights, clip=clip)
|
|
342
|
+
gt.vp = vp
|
|
343
|
+
out_grobs.append(gt)
|
|
344
|
+
|
|
345
|
+
panel = [int(p) for p in layout["PANEL"]]
|
|
346
|
+
return pd.DataFrame(
|
|
347
|
+
{"t": panel, "l": panel, "b": panel, "r": panel, "grobs": out_grobs}
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
def incorporate_wrap(
|
|
351
|
+
self,
|
|
352
|
+
panels: Any,
|
|
353
|
+
position: str,
|
|
354
|
+
clip: str = "off",
|
|
355
|
+
sizes: Optional[Dict[str, Any]] = None,
|
|
356
|
+
) -> Any:
|
|
357
|
+
"""Place per-panel tag grobs onto wrapped panel cells (no new rows/cols).
|
|
358
|
+
|
|
359
|
+
Port of R ``StripTag$incorporate_wrap`` (``strip_tag.R:219-230``).
|
|
360
|
+
|
|
361
|
+
Parameters
|
|
362
|
+
----------
|
|
363
|
+
panels : Gtable
|
|
364
|
+
The assembled panel gtable.
|
|
365
|
+
position : str
|
|
366
|
+
One of ``top`` / ``bottom`` / ``left`` / ``right`` -- selects the
|
|
367
|
+
strip side from ``self.strips``.
|
|
368
|
+
clip : str, default ``"off"``
|
|
369
|
+
Clip setting forwarded to ``gtable_add_grob``.
|
|
370
|
+
sizes : dict, optional
|
|
371
|
+
Unused.
|
|
372
|
+
|
|
373
|
+
Returns
|
|
374
|
+
-------
|
|
375
|
+
Gtable
|
|
376
|
+
The panel gtable with this side's tags added.
|
|
377
|
+
"""
|
|
378
|
+
strip = _flatten_strips(self.strips)[position]
|
|
379
|
+
if strip is None:
|
|
380
|
+
return panels
|
|
381
|
+
pos = _panel_layout(panels)
|
|
382
|
+
tlist = [pos["t"][ti] for ti in (int(v) for v in strip["t"])]
|
|
383
|
+
llist = [pos["l"][li] for li in (int(v) for v in strip["l"])]
|
|
384
|
+
names = ["strip-" + str(i + 1) for i in range(len(strip))]
|
|
385
|
+
panels = gtable_add_grob(
|
|
386
|
+
panels,
|
|
387
|
+
list(strip["grobs"]),
|
|
388
|
+
name=names,
|
|
389
|
+
t=tlist,
|
|
390
|
+
l=llist,
|
|
391
|
+
clip=clip,
|
|
392
|
+
)
|
|
393
|
+
return panels
|
|
394
|
+
|
|
395
|
+
def incorporate_grid(self, panels: Any, switch: Any) -> Any:
|
|
396
|
+
"""Combine x + y tags and place them onto panel cells.
|
|
397
|
+
|
|
398
|
+
Port of R ``StripTag$incorporate_grid`` (``strip_tag.R:232-271``).
|
|
399
|
+
Combines the x and y tag gtables by ``rbind`` (order from
|
|
400
|
+
``params["order"]``), recomputes the combined viewport height (sum) /
|
|
401
|
+
width (max), then adds the grobs at the panel cell ``t`` / ``l`` with
|
|
402
|
+
``z=2``, ``clip="on"``.
|
|
403
|
+
|
|
404
|
+
Parameters
|
|
405
|
+
----------
|
|
406
|
+
panels : Gtable
|
|
407
|
+
The assembled panel gtable.
|
|
408
|
+
switch : str or None
|
|
409
|
+
``"x"`` / ``"y"`` / ``"both"`` / ``None``.
|
|
410
|
+
|
|
411
|
+
Returns
|
|
412
|
+
-------
|
|
413
|
+
Gtable
|
|
414
|
+
The panel gtable with tags added.
|
|
415
|
+
"""
|
|
416
|
+
flat = _flatten_strips(self.strips)
|
|
417
|
+
xstrip = flat["bottom"] if switch in ("x", "both") else flat["top"]
|
|
418
|
+
ystrip = flat["right"] if switch in ("y", "both") else flat["left"]
|
|
419
|
+
|
|
420
|
+
if xstrip is None and ystrip is None:
|
|
421
|
+
return panels
|
|
422
|
+
elif xstrip is None:
|
|
423
|
+
strip = list(ystrip["grobs"])
|
|
424
|
+
elif ystrip is None:
|
|
425
|
+
strip = list(xstrip["grobs"])
|
|
426
|
+
else:
|
|
427
|
+
reorder = list(self.params["order"]) != ["x", "y"]
|
|
428
|
+
strip = []
|
|
429
|
+
for x, y in zip(list(xstrip["grobs"]), list(ystrip["grobs"])):
|
|
430
|
+
vp = getattr(x, "vp", None)
|
|
431
|
+
if vp is not None:
|
|
432
|
+
# R mutates vp$height/$width; grid_py viewports are immutable
|
|
433
|
+
# for these props, so rebuild one with the combined size.
|
|
434
|
+
vp = _rebuild_vp(
|
|
435
|
+
vp,
|
|
436
|
+
height=_sum_units(x.heights, y.heights),
|
|
437
|
+
width=_max_units(x.widths, y.widths),
|
|
438
|
+
)
|
|
439
|
+
new = rbind_gtable(y, x) if reorder else rbind_gtable(x, y)
|
|
440
|
+
if vp is not None:
|
|
441
|
+
new = edit_grob(new, vp=vp)
|
|
442
|
+
strip.append(new)
|
|
443
|
+
|
|
444
|
+
pos = _panel_layout(panels)
|
|
445
|
+
t_src = xstrip if xstrip is not None else ystrip
|
|
446
|
+
l_src = xstrip if xstrip is not None else ystrip
|
|
447
|
+
tlist = [pos["t"][ti] for ti in (int(v) for v in t_src["t"])]
|
|
448
|
+
llist = [pos["l"][li] for li in (int(v) for v in l_src["l"])]
|
|
449
|
+
names = ["strip-" + str(i + 1) for i in range(len(strip))]
|
|
450
|
+
panels = gtable_add_grob(
|
|
451
|
+
panels, strip, name=names, t=tlist, l=llist, clip="on", z=2
|
|
452
|
+
)
|
|
453
|
+
return panels
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# ---------------------------------------------------------------------------
|
|
457
|
+
# Helpers
|
|
458
|
+
# ---------------------------------------------------------------------------
|
|
459
|
+
def _names(param: Any) -> List[str]:
|
|
460
|
+
"""Return variable names from a facet ``rows`` / ``cols`` param."""
|
|
461
|
+
if param is None:
|
|
462
|
+
return []
|
|
463
|
+
if hasattr(param, "keys"):
|
|
464
|
+
return list(param.keys())
|
|
465
|
+
if isinstance(param, (list, tuple)):
|
|
466
|
+
return [str(p) for p in param]
|
|
467
|
+
return []
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _empty_frame(df: pd.DataFrame) -> pd.DataFrame:
|
|
471
|
+
"""Return a 0-column frame with the same row count as *df*."""
|
|
472
|
+
return pd.DataFrame(index=df.index)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _flatten_strips(strips: Dict[str, Any]) -> Dict[str, Any]:
|
|
476
|
+
"""Port of R ``unlist(unname(self$strips), recursive=FALSE)`` (side-keyed)."""
|
|
477
|
+
flat: Dict[str, Any] = {}
|
|
478
|
+
for outer in strips.values():
|
|
479
|
+
for key, value in outer.items():
|
|
480
|
+
flat[key] = value
|
|
481
|
+
return flat
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _recycle(seq: Sequence[Any], length_out: int) -> List[Any]:
|
|
485
|
+
"""Port of R ``rep(seq, length.out=n)`` for a plain list."""
|
|
486
|
+
n = len(seq)
|
|
487
|
+
if length_out <= 0 or n == 0:
|
|
488
|
+
return []
|
|
489
|
+
return [seq[i % n] for i in range(length_out)]
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _to_list(x: Any) -> List[float]:
|
|
493
|
+
"""Coerce a width_cm / height_cm result to a flat list of floats."""
|
|
494
|
+
arr = np.atleast_1d(np.asarray(x, dtype=float))
|
|
495
|
+
return [float(v) for v in arr]
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _npc_unit(values: np.ndarray, total: float) -> Any:
|
|
499
|
+
"""Build a ``unit(values / total, "npc")`` vector.
|
|
500
|
+
|
|
501
|
+
Parameters
|
|
502
|
+
----------
|
|
503
|
+
values : numpy.ndarray
|
|
504
|
+
The per-cell cm sizes.
|
|
505
|
+
total : float
|
|
506
|
+
The summed cm size (npc denominator).
|
|
507
|
+
|
|
508
|
+
Returns
|
|
509
|
+
-------
|
|
510
|
+
grid_py.Unit
|
|
511
|
+
The npc-fraction unit vector.
|
|
512
|
+
"""
|
|
513
|
+
if total == 0:
|
|
514
|
+
fractions = [0.0 for _ in values]
|
|
515
|
+
else:
|
|
516
|
+
fractions = [float(v) / total for v in values]
|
|
517
|
+
return Unit(fractions, "npc")
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _rebuild_vp(vp: Any, height: Any, width: Any) -> Any:
|
|
521
|
+
"""Rebuild a viewport with new *height* / *width*, preserving other props.
|
|
522
|
+
|
|
523
|
+
R mutates ``vp$height`` / ``vp$width`` in place; grid_py viewports expose
|
|
524
|
+
those as read-only properties, so this constructs a fresh
|
|
525
|
+
:class:`grid_py.Viewport` carrying the original anchor / justification /
|
|
526
|
+
clip and the new size.
|
|
527
|
+
|
|
528
|
+
Parameters
|
|
529
|
+
----------
|
|
530
|
+
vp : grid_py.Viewport
|
|
531
|
+
The source viewport (from the x strip's fitted box).
|
|
532
|
+
height : grid_py.Unit
|
|
533
|
+
The new (combined, summed) height.
|
|
534
|
+
width : grid_py.Unit
|
|
535
|
+
The new (combined, maximum) width.
|
|
536
|
+
|
|
537
|
+
Returns
|
|
538
|
+
-------
|
|
539
|
+
grid_py.Viewport
|
|
540
|
+
A new viewport with the combined size.
|
|
541
|
+
"""
|
|
542
|
+
just = getattr(vp, "just", None)
|
|
543
|
+
return Viewport(
|
|
544
|
+
x=getattr(vp, "x", None),
|
|
545
|
+
y=getattr(vp, "y", None),
|
|
546
|
+
just=list(just) if just is not None else "centre",
|
|
547
|
+
width=width,
|
|
548
|
+
height=height,
|
|
549
|
+
clip=getattr(vp, "clip", "inherit"),
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _sum_units(a: Any, b: Any) -> Any:
|
|
554
|
+
"""Port of R ``sum(a, b)`` over two unit vectors (cm total)."""
|
|
555
|
+
av = float(np.sum(np.atleast_1d(np.asarray(height_cm(a), dtype=float))))
|
|
556
|
+
bv = float(np.sum(np.atleast_1d(np.asarray(height_cm(b), dtype=float))))
|
|
557
|
+
return Unit(av + bv, "cm")
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _max_units(a: Any, b: Any) -> Any:
|
|
561
|
+
"""Port of R ``max(a, b)`` over two unit vectors (cm max)."""
|
|
562
|
+
av = float(np.max(np.atleast_1d(np.asarray(width_cm(a), dtype=float))))
|
|
563
|
+
bv = float(np.max(np.atleast_1d(np.asarray(width_cm(b), dtype=float))))
|
|
564
|
+
return Unit(max(av, bv), "cm")
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
# R's ``StripTag`` ggproto instance used as the parent of every clone.
|
|
568
|
+
_STRIP_TAG_SINGLETON: "StripTag" = StripTag()
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def strip_tag(
|
|
572
|
+
clip: str = "inherit",
|
|
573
|
+
order: Any = ("x", "y"),
|
|
574
|
+
just: Any = (0, 1),
|
|
575
|
+
text_x: Any = None,
|
|
576
|
+
text_y: Any = None,
|
|
577
|
+
background_x: Any = None,
|
|
578
|
+
background_y: Any = None,
|
|
579
|
+
by_layer_x: bool = False,
|
|
580
|
+
by_layer_y: bool = False,
|
|
581
|
+
) -> StripTag:
|
|
582
|
+
"""Create a tag strip (fitted text boxes inside the panels).
|
|
583
|
+
|
|
584
|
+
Port of R ``strip_tag()`` (``strip_tag.R:53-85``).
|
|
585
|
+
|
|
586
|
+
Parameters
|
|
587
|
+
----------
|
|
588
|
+
clip : str, default ``"inherit"``
|
|
589
|
+
Whether labels are clipped to background boxes.
|
|
590
|
+
order : sequence of str, default ``("x", "y")``
|
|
591
|
+
Either ``("x", "y")`` or ``("y", "x")`` -- the top-to-bottom order of
|
|
592
|
+
horizontal vs "vertical" labels in a grid layout.
|
|
593
|
+
just : sequence of float, default ``(0, 1)``
|
|
594
|
+
Horizontal and vertical justification for the text box anchor.
|
|
595
|
+
text_x, text_y, background_x, background_y : list or element or None
|
|
596
|
+
Per-strip themed elements. R defaults ``text_y`` to
|
|
597
|
+
``element_text(angle = 0)``; pass an explicit value to override.
|
|
598
|
+
by_layer_x, by_layer_y : bool, default ``False``
|
|
599
|
+
Map elements to layers (``True``) or strips (``False``).
|
|
600
|
+
|
|
601
|
+
Returns
|
|
602
|
+
-------
|
|
603
|
+
StripTag
|
|
604
|
+
A ``StripTag`` ggproto instance usable in ggh4x facets.
|
|
605
|
+
"""
|
|
606
|
+
if text_y is None:
|
|
607
|
+
# R default: text_y = element_text(angle = 0).
|
|
608
|
+
from ggplot2_py import element_text
|
|
609
|
+
|
|
610
|
+
text_y = element_text(angle=0)
|
|
611
|
+
|
|
612
|
+
params = {
|
|
613
|
+
"clip": arg_match0(clip, ["on", "off", "inherit"], arg_name="clip"),
|
|
614
|
+
"order": list(order),
|
|
615
|
+
"just": list(just),
|
|
616
|
+
# R's ``strip_tag()`` params has no ``size`` (``params$size`` is NULL and
|
|
617
|
+
# the tag's own ``draw_labels`` ignores it). The base ``assemble_strip``
|
|
618
|
+
# reads ``params["size"]`` with subscript access, so a behaviour-neutral
|
|
619
|
+
# ``"constant"`` is supplied here (consumed only as the always-ignored
|
|
620
|
+
# ``size`` argument of ``StripTag.draw_labels``). Documented deviation.
|
|
621
|
+
"size": "constant",
|
|
622
|
+
}
|
|
623
|
+
given_elements = {
|
|
624
|
+
"text_x": validate_element_list(text_x, "element_text"),
|
|
625
|
+
"text_y": validate_element_list(text_y, "element_text"),
|
|
626
|
+
"background_x": validate_element_list(background_x, "element_rect"),
|
|
627
|
+
"background_y": validate_element_list(background_y, "element_rect"),
|
|
628
|
+
"by_layer_x": bool(by_layer_x),
|
|
629
|
+
"by_layer_y": bool(by_layer_y),
|
|
630
|
+
}
|
|
631
|
+
return ggproto(
|
|
632
|
+
None,
|
|
633
|
+
_STRIP_TAG_SINGLETON,
|
|
634
|
+
params=params,
|
|
635
|
+
given_elements=given_elements,
|
|
636
|
+
)
|