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
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""Force facet panel sizes (port of ggh4x ``R/force_panelsize.R``).
|
|
2
|
+
|
|
3
|
+
:func:`force_panelsizes` returns a :class:`ForcedSize` add-on object. When added
|
|
4
|
+
to a plot with ``+``, its handler (registered on
|
|
5
|
+
:func:`ggplot2_py.plot.update_ggplot`) clones the plot's live facet into a
|
|
6
|
+
dynamic ``Forced<FacetClass>`` subclass whose ``draw_panels`` calls the original
|
|
7
|
+
facet's ``draw_panels`` and then overwrites the resulting gtable's panel row
|
|
8
|
+
heights / column widths with the user-forced ``"null"`` units (and sets
|
|
9
|
+
``respect``).
|
|
10
|
+
|
|
11
|
+
This mirrors R, where ``ggplot_add.forcedsize`` wraps ``old.facet$draw_panels``
|
|
12
|
+
in a new function that mutates the panel gtable's ``widths`` / ``heights`` after
|
|
13
|
+
the parent produced it. Because the rewrite happens *inside* ``draw_panels``
|
|
14
|
+
(post-parent), it overrules the theme ``panel.widths`` / ``panel.heights``
|
|
15
|
+
applied by ``Facet.set_panel_size`` afterwards, exactly as the R ``space``
|
|
16
|
+
argument is overruled.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
from ggplot2_py import ggproto
|
|
24
|
+
from ggplot2_py.ggproto import ggproto_parent
|
|
25
|
+
from ggplot2_py.plot import update_ggplot
|
|
26
|
+
from grid_py import Unit, convert_height, convert_width, is_unit, unit_type
|
|
27
|
+
|
|
28
|
+
from ggh4x._facet_utils import panel_cols, panel_rows
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"force_panelsizes",
|
|
32
|
+
"ForcedSize",
|
|
33
|
+
"is_null_unit",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# is_null_unit (R force_panelsize.R:197-202)
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
def is_null_unit(x: Any) -> bool:
|
|
41
|
+
"""Return whether *x* is a unit composed entirely of ``"null"`` units.
|
|
42
|
+
|
|
43
|
+
Faithful port of ggh4x's ``is_null_unit`` (``R/force_panelsize.R:197-202``):
|
|
44
|
+
a non-unit returns ``False``; otherwise every element's :func:`unit_type`
|
|
45
|
+
must equal ``"null"``.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
x : Any
|
|
50
|
+
Candidate object.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
bool
|
|
55
|
+
``True`` when *x* is a unit whose every element is ``"null"``.
|
|
56
|
+
"""
|
|
57
|
+
if not is_unit(x):
|
|
58
|
+
return False
|
|
59
|
+
types = unit_type(x)
|
|
60
|
+
if isinstance(types, str):
|
|
61
|
+
types = [types]
|
|
62
|
+
return all(t == "null" for t in types)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_ABSOLUTE_TOTAL_UNITS = ("cm", "mm", "inches", "points", "bigpts")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# ForcedSize container (R: structure(list(...), class = "forcedsize"))
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
class ForcedSize:
|
|
72
|
+
"""Deferred container of forced panel sizes (R ``forcedsize`` S3 object).
|
|
73
|
+
|
|
74
|
+
Holds the five fields produced by :func:`force_panelsizes`. It is *not* a
|
|
75
|
+
facet; it is consumed by :func:`_update_forcedsize` at ``+``-time, which
|
|
76
|
+
rewrites the plot's facet.
|
|
77
|
+
|
|
78
|
+
Attributes
|
|
79
|
+
----------
|
|
80
|
+
rows, cols : grid_py.Unit or None
|
|
81
|
+
Forced panel heights (rows) / widths (cols), as ``"null"`` units (or
|
|
82
|
+
absolute when supplied directly).
|
|
83
|
+
respect : bool or None
|
|
84
|
+
Forced ``respect`` flag for the panel gtable, or ``None`` to inherit.
|
|
85
|
+
total_width, total_height : grid_py.Unit or None
|
|
86
|
+
Absolute total width / height of all panels plus inter-panel decoration.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
rows: Optional[Unit] = None,
|
|
92
|
+
cols: Optional[Unit] = None,
|
|
93
|
+
respect: Optional[bool] = None,
|
|
94
|
+
total_width: Optional[Unit] = None,
|
|
95
|
+
total_height: Optional[Unit] = None,
|
|
96
|
+
) -> None:
|
|
97
|
+
self.rows = rows
|
|
98
|
+
self.cols = cols
|
|
99
|
+
self.respect = respect
|
|
100
|
+
self.total_width = total_width
|
|
101
|
+
self.total_height = total_height
|
|
102
|
+
|
|
103
|
+
def _lengths_sum(self) -> int:
|
|
104
|
+
"""Return ``sum(lengths(object))`` (R ``force_panelsize.R:96``)."""
|
|
105
|
+
total = 0
|
|
106
|
+
for field in (self.rows, self.cols, self.respect,
|
|
107
|
+
self.total_width, self.total_height):
|
|
108
|
+
if field is None:
|
|
109
|
+
continue
|
|
110
|
+
if is_unit(field):
|
|
111
|
+
total += len(field)
|
|
112
|
+
else:
|
|
113
|
+
total += 1
|
|
114
|
+
return total
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# force_panelsizes constructor (R force_panelsize.R:51-85)
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
def force_panelsizes(
|
|
121
|
+
rows: Any = None,
|
|
122
|
+
cols: Any = None,
|
|
123
|
+
respect: Optional[bool] = None,
|
|
124
|
+
total_width: Any = None,
|
|
125
|
+
total_height: Any = None,
|
|
126
|
+
) -> ForcedSize:
|
|
127
|
+
"""Force a facetted plot to have specified panel sizes.
|
|
128
|
+
|
|
129
|
+
Faithful port of ggh4x's ``force_panelsizes`` (``R/force_panelsize.R:51-85``).
|
|
130
|
+
``rows`` / ``cols`` set panel heights / widths; bare numerics become relative
|
|
131
|
+
``"null"`` units (ratios), recycled / shortened to the number of panel rows /
|
|
132
|
+
columns. ``total_width`` / ``total_height`` set the absolute total of all
|
|
133
|
+
panels plus the decoration between them, and require the corresponding
|
|
134
|
+
``rows`` / ``cols`` to be relative (numeric or ``"null"`` units).
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
rows, cols : numeric or grid_py.Unit or None, default None
|
|
139
|
+
Panel heights (rows) / widths (cols). ``None`` leaves that direction
|
|
140
|
+
unchanged.
|
|
141
|
+
respect : bool or None, default None
|
|
142
|
+
When ``True``, ``"null"`` widths and heights are proportional. ``None``
|
|
143
|
+
inherits the behaviour specified elsewhere.
|
|
144
|
+
total_width, total_height : grid_py.Unit or None, default None
|
|
145
|
+
Absolute total width / height (length-1 unit) of all panels plus the
|
|
146
|
+
decoration between panels.
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
ForcedSize
|
|
151
|
+
An add-on object that can be added to a plot with ``+``.
|
|
152
|
+
|
|
153
|
+
Raises
|
|
154
|
+
------
|
|
155
|
+
ValueError
|
|
156
|
+
When ``total_width`` is set but ``cols`` is a non-relative unit (or vice
|
|
157
|
+
versa for ``total_height`` / ``rows``), or when a ``total_*`` argument is
|
|
158
|
+
not an absolute unit of the allowed types.
|
|
159
|
+
"""
|
|
160
|
+
if rows is not None and not is_unit(rows):
|
|
161
|
+
rows = Unit(rows, "null")
|
|
162
|
+
if cols is not None and not is_unit(cols):
|
|
163
|
+
cols = Unit(cols, "null")
|
|
164
|
+
|
|
165
|
+
if total_width is not None:
|
|
166
|
+
if is_unit(cols) and not is_null_unit(cols):
|
|
167
|
+
raise ValueError(
|
|
168
|
+
"Cannot set `total_width` when `cols` is not relative."
|
|
169
|
+
)
|
|
170
|
+
if not is_unit(total_width):
|
|
171
|
+
raise ValueError("`total_width` must be a unit object.")
|
|
172
|
+
_arg_match_unit(total_width, "total_width")
|
|
173
|
+
if total_height is not None:
|
|
174
|
+
if is_unit(rows) and not is_null_unit(rows):
|
|
175
|
+
raise ValueError(
|
|
176
|
+
"Cannot set `total_height` when `rows` is not relative."
|
|
177
|
+
)
|
|
178
|
+
if not is_unit(total_height):
|
|
179
|
+
raise ValueError("`total_height` must be a unit object.")
|
|
180
|
+
_arg_match_unit(total_height, "total_height")
|
|
181
|
+
|
|
182
|
+
return ForcedSize(
|
|
183
|
+
rows=rows,
|
|
184
|
+
cols=cols,
|
|
185
|
+
respect=respect,
|
|
186
|
+
total_width=total_width,
|
|
187
|
+
total_height=total_height,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _arg_match_unit(u: Unit, nm: str) -> None:
|
|
192
|
+
"""Validate that *u*'s unit type is one of the allowed absolute units.
|
|
193
|
+
|
|
194
|
+
Mirrors R's ``arg_match0(unitType(total_width), c("cm","mm",...))``.
|
|
195
|
+
"""
|
|
196
|
+
t = unit_type(u)
|
|
197
|
+
if isinstance(t, (list, tuple)):
|
|
198
|
+
t = t[0] if t else ""
|
|
199
|
+
if t not in _ABSOLUTE_TOTAL_UNITS:
|
|
200
|
+
allowed = ", ".join(_ABSOLUTE_TOTAL_UNITS)
|
|
201
|
+
raise ValueError(
|
|
202
|
+
f"`{nm}` unit type {t!r} must be one of: {allowed}."
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
# draw_panels override builder
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
def _seq_range_int(values: List[int]) -> List[int]:
|
|
210
|
+
"""Return the contiguous integer range ``min:max`` over *values* (R ``seq_range``)."""
|
|
211
|
+
if not values:
|
|
212
|
+
return []
|
|
213
|
+
lo = min(values)
|
|
214
|
+
hi = max(values)
|
|
215
|
+
return list(range(lo, hi + 1))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _make_forced_draw_panels(parent_cls: Any) -> Any:
|
|
219
|
+
"""Build the ``draw_panels`` override for a ``Forced<FacetClass>`` clone.
|
|
220
|
+
|
|
221
|
+
Closes over the *concrete parent class* so the override can dispatch to the
|
|
222
|
+
original facet's ``draw_panels`` via :func:`ggproto_parent`. Faithful port
|
|
223
|
+
of R's ``new.fun`` (``R/force_panelsize.R:107-179``).
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
----------
|
|
227
|
+
parent_cls : type
|
|
228
|
+
The concrete facet class the forced facet was cloned from.
|
|
229
|
+
|
|
230
|
+
Returns
|
|
231
|
+
-------
|
|
232
|
+
callable
|
|
233
|
+
A ``draw_panels(self, ...)`` method.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
def draw_panels(
|
|
237
|
+
self: Any,
|
|
238
|
+
panels: list,
|
|
239
|
+
layout: Any,
|
|
240
|
+
x_scales: list,
|
|
241
|
+
y_scales: list,
|
|
242
|
+
ranges: list,
|
|
243
|
+
coord: Any,
|
|
244
|
+
data: Any,
|
|
245
|
+
theme: Any,
|
|
246
|
+
params: Dict[str, Any],
|
|
247
|
+
) -> Any:
|
|
248
|
+
# Call the original facet's draw_panels to build the panel gtable.
|
|
249
|
+
panel_table = ggproto_parent(parent_cls, self).draw_panels(
|
|
250
|
+
panels, layout, x_scales, y_scales, ranges, coord, data, theme, params
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
force = self.params
|
|
254
|
+
prows = panel_rows(panel_table)
|
|
255
|
+
pcols = panel_cols(panel_table)
|
|
256
|
+
t_pos = [int(v) for v in prows["t"]]
|
|
257
|
+
l_pos = [int(v) for v in pcols["l"]]
|
|
258
|
+
n_rows = len(t_pos)
|
|
259
|
+
n_cols = len(l_pos)
|
|
260
|
+
# seq_range over ALL pcols / prows values (l & r, t & b) per R's
|
|
261
|
+
# `seq_range(pcols)` / `seq_range(prows)`.
|
|
262
|
+
all_col = [int(v) for v in pcols["l"]] + [int(v) for v in pcols["r"]]
|
|
263
|
+
all_row = [int(v) for v in prows["t"]] + [int(v) for v in prows["b"]]
|
|
264
|
+
|
|
265
|
+
force_rows = force.get("force_rows")
|
|
266
|
+
force_cols = force.get("force_cols")
|
|
267
|
+
force_respect = force.get("force_respect")
|
|
268
|
+
total_width = force.get("force_total_width")
|
|
269
|
+
total_height = force.get("force_total_height")
|
|
270
|
+
|
|
271
|
+
# --- total_width branch (R:122-141) -------------------------------
|
|
272
|
+
if total_width is not None:
|
|
273
|
+
if force_cols is None:
|
|
274
|
+
colwidths = [
|
|
275
|
+
float(panel_table.widths[p - 1]._values[0])
|
|
276
|
+
for p in l_pos
|
|
277
|
+
]
|
|
278
|
+
else:
|
|
279
|
+
colwidths = [
|
|
280
|
+
float(_recycle(force_cols, i)) for i in range(n_cols)
|
|
281
|
+
]
|
|
282
|
+
# extra_width = columns between panel cells (the decoration):
|
|
283
|
+
# setdiff(seq_range(pcols), unique(unlist(pcols))).
|
|
284
|
+
uniq_cols = set(all_col)
|
|
285
|
+
extra_idx = [i for i in _seq_range_int(all_col) if i not in uniq_cols]
|
|
286
|
+
if len(extra_idx) > 1:
|
|
287
|
+
extra_width = sum(
|
|
288
|
+
_to_scalar(convert_width(panel_table.widths[i - 1], "cm", valueOnly=True))
|
|
289
|
+
for i in extra_idx
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
extra_width = 0.0
|
|
293
|
+
tw = _to_scalar(convert_width(total_width, "cm", valueOnly=True))
|
|
294
|
+
avail = tw - extra_width
|
|
295
|
+
denom = sum(colwidths)
|
|
296
|
+
new_widths = [avail * c / denom for c in colwidths]
|
|
297
|
+
for pos, w in zip(l_pos, new_widths):
|
|
298
|
+
panel_table.widths[pos - 1] = Unit(w, "cm")
|
|
299
|
+
force_cols = None
|
|
300
|
+
|
|
301
|
+
# --- total_height branch (R:143-161) ------------------------------
|
|
302
|
+
if total_height is not None:
|
|
303
|
+
if force_rows is None:
|
|
304
|
+
rowheights = [
|
|
305
|
+
float(panel_table.heights[p - 1]._values[0])
|
|
306
|
+
for p in t_pos
|
|
307
|
+
]
|
|
308
|
+
else:
|
|
309
|
+
rowheights = [
|
|
310
|
+
float(_recycle(force_rows, i)) for i in range(n_rows)
|
|
311
|
+
]
|
|
312
|
+
uniq_rows = set(all_row)
|
|
313
|
+
extra_idx = [i for i in _seq_range_int(all_row) if i not in uniq_rows]
|
|
314
|
+
if len(extra_idx) > 1:
|
|
315
|
+
extra_height = sum(
|
|
316
|
+
_to_scalar(convert_height(panel_table.heights[i - 1], "cm", valueOnly=True))
|
|
317
|
+
for i in extra_idx
|
|
318
|
+
)
|
|
319
|
+
else:
|
|
320
|
+
extra_height = 0.0
|
|
321
|
+
th = _to_scalar(convert_height(total_height, "cm", valueOnly=True))
|
|
322
|
+
avail = th - extra_height
|
|
323
|
+
denom = sum(rowheights)
|
|
324
|
+
new_heights = [avail * r / denom for r in rowheights]
|
|
325
|
+
for pos, h in zip(t_pos, new_heights):
|
|
326
|
+
panel_table.heights[pos - 1] = Unit(h, "cm")
|
|
327
|
+
force_rows = None
|
|
328
|
+
|
|
329
|
+
# --- plain override (R:163-176) -----------------------------------
|
|
330
|
+
if force_rows is not None:
|
|
331
|
+
for i, pos in enumerate(t_pos):
|
|
332
|
+
panel_table.heights[pos - 1] = _recycle_unit(force_rows, i)
|
|
333
|
+
if force_cols is not None:
|
|
334
|
+
for i, pos in enumerate(l_pos):
|
|
335
|
+
panel_table.widths[pos - 1] = _recycle_unit(force_cols, i)
|
|
336
|
+
if force_respect is not None:
|
|
337
|
+
panel_table.respect = force_respect
|
|
338
|
+
|
|
339
|
+
return panel_table
|
|
340
|
+
|
|
341
|
+
return draw_panels
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _to_scalar(x: Any) -> float:
|
|
345
|
+
"""Coerce a ``convert_*`` result (scalar or length-1 array) to a float."""
|
|
346
|
+
import numpy as np
|
|
347
|
+
|
|
348
|
+
arr = np.asarray(x).ravel()
|
|
349
|
+
return float(arr[0]) if arr.size else 0.0
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _recycle(u: Unit, i: int) -> float:
|
|
353
|
+
"""Return the numeric value of element ``i mod len`` of unit *u*."""
|
|
354
|
+
return float(u._values[i % len(u)])
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _recycle_unit(u: Unit, i: int) -> Unit:
|
|
358
|
+
"""Return element ``i mod len`` of unit *u* as a length-1 Unit (R ``rep(..., length.out)``)."""
|
|
359
|
+
return u[i % len(u)]
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# ---------------------------------------------------------------------------
|
|
363
|
+
# ggplot_add.forcedsize (R force_panelsize.R:94-195)
|
|
364
|
+
# ---------------------------------------------------------------------------
|
|
365
|
+
@update_ggplot.register(ForcedSize)
|
|
366
|
+
def _update_forcedsize(obj: ForcedSize, plot: Any, object_name: str = "") -> Any:
|
|
367
|
+
"""Add a :class:`ForcedSize` to *plot* (R ``ggplot_add.forcedsize``).
|
|
368
|
+
|
|
369
|
+
Clones the plot's current facet into a dynamic ``Forced<FacetClass>``
|
|
370
|
+
subclass whose ``draw_panels`` mutates the panel gtable's panel-row heights /
|
|
371
|
+
panel-column widths to the forced sizes. The forced parameters are merged
|
|
372
|
+
into a *copied* params dict under ``force_rows`` / ``force_cols`` /
|
|
373
|
+
``force_respect`` / ``force_total_width`` / ``force_total_height``.
|
|
374
|
+
|
|
375
|
+
Parameters
|
|
376
|
+
----------
|
|
377
|
+
obj : ForcedSize
|
|
378
|
+
The container produced by :func:`force_panelsizes`.
|
|
379
|
+
plot : ggplot2_py.plot.GGPlot
|
|
380
|
+
The plot to mutate.
|
|
381
|
+
object_name : str, optional
|
|
382
|
+
Unused (kept for the ``update_ggplot`` dispatch signature).
|
|
383
|
+
|
|
384
|
+
Returns
|
|
385
|
+
-------
|
|
386
|
+
ggplot2_py.plot.GGPlot
|
|
387
|
+
The mutated plot (returned unchanged when *obj* carries no sizes).
|
|
388
|
+
"""
|
|
389
|
+
if obj._lengths_sum() < 1:
|
|
390
|
+
return plot
|
|
391
|
+
|
|
392
|
+
old_facet = plot.facet
|
|
393
|
+
parent_cls = type(old_facet)
|
|
394
|
+
|
|
395
|
+
# Merge force params into a copy of the facet's params (R:184).
|
|
396
|
+
old_params = dict(old_facet.params) if old_facet.params else {}
|
|
397
|
+
old_params["force_rows"] = obj.rows
|
|
398
|
+
old_params["force_cols"] = obj.cols
|
|
399
|
+
old_params["force_respect"] = obj.respect
|
|
400
|
+
old_params["force_total_width"] = obj.total_width
|
|
401
|
+
old_params["force_total_height"] = obj.total_height
|
|
402
|
+
|
|
403
|
+
new_facet = ggproto(
|
|
404
|
+
f"Forced{parent_cls.__name__}",
|
|
405
|
+
old_facet,
|
|
406
|
+
draw_panels=_make_forced_draw_panels(parent_cls),
|
|
407
|
+
params=old_params,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
plot.facet = new_facet
|
|
411
|
+
return plot
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Single per-panel position scales (port of ggh4x ``R/scale_facet.R``).
|
|
2
|
+
|
|
3
|
+
:func:`scale_x_facet` / :func:`scale_y_facet` build a :class:`ScaleFacet` add-on
|
|
4
|
+
carrying a panel *predicate* and a position scale. When added to a plot, the
|
|
5
|
+
handler either appends to an existing ``FreeScaled<...>`` facet's scale lists or
|
|
6
|
+
lowers to a single-entry :class:`~ggh4x.panel_scales.facetted_pos_scales.FacettedPosScales`,
|
|
7
|
+
reusing that machinery.
|
|
8
|
+
|
|
9
|
+
NSE deviation
|
|
10
|
+
-------------
|
|
11
|
+
R captures ``expr`` via ``enquo`` and tidy-evaluates it against the plot layout.
|
|
12
|
+
Python has no NSE: ``expr`` is a *predicate* -- either a callable
|
|
13
|
+
``layout_df -> bool-array`` or a string evaluated with
|
|
14
|
+
:meth:`pandas.DataFrame.eval` over the layout columns (``PANEL`` / ``ROW`` /
|
|
15
|
+
``COL`` / ``SCALE_*`` + facet variables).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import ggplot2_py as _gg
|
|
23
|
+
from ggplot2_py import ggproto
|
|
24
|
+
from ggplot2_py.plot import update_ggplot
|
|
25
|
+
|
|
26
|
+
from ggh4x._cli import cli_abort
|
|
27
|
+
from ggh4x._rlang import arg_match0
|
|
28
|
+
|
|
29
|
+
from .facetted_pos_scales import FacettedPosScales, _ScaleList
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"scale_facet",
|
|
33
|
+
"scale_x_facet",
|
|
34
|
+
"scale_y_facet",
|
|
35
|
+
"ScaleFacet",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# ScaleFacet container (R: structure(list(lhs=, rhs=), class = "scale_facet"))
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
class ScaleFacet:
|
|
43
|
+
"""Deferred container of one per-panel position scale + its predicate.
|
|
44
|
+
|
|
45
|
+
Port of R's ``structure(list(lhs =, rhs =), class = "scale_facet")``.
|
|
46
|
+
Consumed by :func:`_update_scale_facet` at ``+``-time.
|
|
47
|
+
|
|
48
|
+
Attributes
|
|
49
|
+
----------
|
|
50
|
+
lhs : callable or str
|
|
51
|
+
The panel predicate (formula LHS equivalent).
|
|
52
|
+
rhs : Scale
|
|
53
|
+
The position scale to apply (formula RHS equivalent).
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, lhs: Any, rhs: Any) -> None:
|
|
57
|
+
self.lhs = lhs
|
|
58
|
+
self.rhs = rhs
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# scale_facet (R scale_facet.R:76-116)
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
def scale_facet(expr: Any, aes: str, *args: Any, type: str = "continuous", **kwargs: Any) -> ScaleFacet:
|
|
65
|
+
"""Build a per-panel position scale (generic over aesthetic).
|
|
66
|
+
|
|
67
|
+
Faithful port of ggh4x's ``scale_facet`` (``R/scale_facet.R:76-116``).
|
|
68
|
+
Resolves the constructor ``scale_<aes>_<type>`` from the :mod:`ggplot2_py`
|
|
69
|
+
namespace (R's ``find_global``), instantiates it with the extra arguments,
|
|
70
|
+
and pairs it with the panel predicate *expr*.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
expr : callable or str
|
|
75
|
+
Panel predicate evaluated against the plot layout (see module docstring).
|
|
76
|
+
aes : {"x", "y"}
|
|
77
|
+
The position aesthetic.
|
|
78
|
+
*args, **kwargs
|
|
79
|
+
Extra arguments forwarded to the resolved scale constructor.
|
|
80
|
+
type : str, default "continuous"
|
|
81
|
+
Scale type, such that ``scale_<aes>_<type>`` names a constructor.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
ScaleFacet
|
|
86
|
+
An add-on object that can be added to a plot with ``+``.
|
|
87
|
+
|
|
88
|
+
Raises
|
|
89
|
+
------
|
|
90
|
+
ValueError
|
|
91
|
+
When ``type == "facet"`` (circular), the constructor cannot be found, or
|
|
92
|
+
*expr* is missing.
|
|
93
|
+
"""
|
|
94
|
+
candidate = f"scale_{aes}_{type}"
|
|
95
|
+
if type == "facet":
|
|
96
|
+
cli_abort(
|
|
97
|
+
f"Cannot circularly define `{candidate}` as template for `{candidate}`."
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
fun = getattr(_gg, candidate, None)
|
|
101
|
+
scale = None
|
|
102
|
+
if fun is not None:
|
|
103
|
+
scale = fun(*args, **kwargs)
|
|
104
|
+
|
|
105
|
+
if scale is None or not hasattr(scale, "aesthetics"):
|
|
106
|
+
cli_abort(
|
|
107
|
+
f"Cannot find a `{candidate}` function. "
|
|
108
|
+
"Did you misspell the `type` argument?"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if expr is None:
|
|
112
|
+
cli_abort("`expr` must be a valid expression.")
|
|
113
|
+
|
|
114
|
+
return ScaleFacet(lhs=expr, rhs=scale)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def scale_x_facet(expr: Any, *args: Any, type: str = "continuous", **kwargs: Any) -> ScaleFacet:
|
|
118
|
+
"""Per-panel x position scale (thin wrapper, R ``scale_x_facet``).
|
|
119
|
+
|
|
120
|
+
Parameters
|
|
121
|
+
----------
|
|
122
|
+
expr : callable or str
|
|
123
|
+
Panel predicate (see :func:`scale_facet`).
|
|
124
|
+
*args, **kwargs
|
|
125
|
+
Forwarded to the resolved ``scale_x_<type>`` constructor.
|
|
126
|
+
type : str, default "continuous"
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
ScaleFacet
|
|
131
|
+
"""
|
|
132
|
+
return scale_facet(expr, "x", *args, type=type, **kwargs)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def scale_y_facet(expr: Any, *args: Any, type: str = "continuous", **kwargs: Any) -> ScaleFacet:
|
|
136
|
+
"""Per-panel y position scale (thin wrapper, R ``scale_y_facet``).
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
expr : callable or str
|
|
141
|
+
Panel predicate (see :func:`scale_facet`).
|
|
142
|
+
*args, **kwargs
|
|
143
|
+
Forwarded to the resolved ``scale_y_<type>`` constructor.
|
|
144
|
+
type : str, default "continuous"
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
ScaleFacet
|
|
149
|
+
"""
|
|
150
|
+
return scale_facet(expr, "y", *args, type=type, **kwargs)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
# ggplot_add.scale_facet (R scale_facet.R:133-184)
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
@update_ggplot.register(ScaleFacet)
|
|
157
|
+
def _update_scale_facet(obj: ScaleFacet, plot: Any, object_name: str = "") -> Any:
|
|
158
|
+
"""Add a :class:`ScaleFacet` to *plot* (R ``ggplot_add.scale_facet``).
|
|
159
|
+
|
|
160
|
+
Appends to an existing ``FreeScaled<...>`` facet's per-panel scale lists (and
|
|
161
|
+
its parallel predicate list), or lowers to a single-entry
|
|
162
|
+
:class:`FacettedPosScales` otherwise.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
obj : ScaleFacet
|
|
167
|
+
plot : ggplot2_py.plot.GGPlot
|
|
168
|
+
object_name : str, optional
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
ggplot2_py.plot.GGPlot
|
|
173
|
+
|
|
174
|
+
Raises
|
|
175
|
+
------
|
|
176
|
+
ValueError
|
|
177
|
+
When the plot has no facets (``FacetNull``).
|
|
178
|
+
"""
|
|
179
|
+
aes = obj.rhs.aesthetics[0]
|
|
180
|
+
aes = arg_match0(aes, ["x", "y"], arg_name="scale$aesthetics[1]")
|
|
181
|
+
|
|
182
|
+
facet = plot.facet
|
|
183
|
+
cls_name = type(facet).__name__
|
|
184
|
+
|
|
185
|
+
if cls_name.startswith("FacetNull") or cls_name == "FacetNull":
|
|
186
|
+
nm = f"scale_{aes}_facet"
|
|
187
|
+
cli_abort(
|
|
188
|
+
f"`{nm}` cannot be added to a plot without facets. "
|
|
189
|
+
f"Try adding facets before adding `{nm}`."
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
if cls_name.startswith("FreeScaled"):
|
|
193
|
+
if aes == "x":
|
|
194
|
+
old = facet.new_x_scales
|
|
195
|
+
old_lhs = getattr(old, "lhs", None) or []
|
|
196
|
+
new = _ScaleList(
|
|
197
|
+
list(old) + [obj.rhs],
|
|
198
|
+
lhs=list(old_lhs) + [obj.lhs],
|
|
199
|
+
)
|
|
200
|
+
plot.facet = ggproto(None, facet, new_x_scales=new)
|
|
201
|
+
else:
|
|
202
|
+
old = facet.new_y_scales
|
|
203
|
+
old_lhs = getattr(old, "lhs", None) or []
|
|
204
|
+
new = _ScaleList(
|
|
205
|
+
list(old) + [obj.rhs],
|
|
206
|
+
lhs=list(old_lhs) + [obj.lhs],
|
|
207
|
+
)
|
|
208
|
+
plot.facet = ggproto(None, facet, new_y_scales=new)
|
|
209
|
+
else:
|
|
210
|
+
if aes == "x":
|
|
211
|
+
fps = FacettedPosScales(
|
|
212
|
+
x=_ScaleList([obj.rhs], lhs=[obj.lhs]),
|
|
213
|
+
y=_ScaleList([None], lhs=None),
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
fps = FacettedPosScales(
|
|
217
|
+
x=_ScaleList([None], lhs=None),
|
|
218
|
+
y=_ScaleList([obj.rhs], lhs=[obj.lhs]),
|
|
219
|
+
)
|
|
220
|
+
plot = plot + fps
|
|
221
|
+
|
|
222
|
+
return plot
|