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/geom_box.py
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
"""Flexible labelled rectangles (port of ggh4x ``geom_box.R``).
|
|
2
|
+
|
|
3
|
+
This module ports the R ggh4x ``geom_box()`` constructor, the ``GeomBox``
|
|
4
|
+
ggproto class and the ``resolve_box()`` helper. ``geom_box()`` is a more
|
|
5
|
+
flexible variant of :func:`ggplot2_py.geom_rect` / ``geom_tile``: instead of
|
|
6
|
+
requiring either the ``(x/y)min``/``(x/y)max`` *or* the ``(x/y)``/
|
|
7
|
+
``(width/height)`` aesthetics, *any two* out of those four aesthetics suffice
|
|
8
|
+
to define a rectangle (per axis).
|
|
9
|
+
|
|
10
|
+
R source: ``ggh4x/R/geom_box.R``.
|
|
11
|
+
|
|
12
|
+
Notes
|
|
13
|
+
-----
|
|
14
|
+
* :func:`resolve_box` resolves the ``min``/``max`` of one axis from partial
|
|
15
|
+
information with the priority cascade ``min``/``max`` verbatim ->
|
|
16
|
+
``center +/- 0.5 * dim`` -> ``opposite +/- dim``. The final ``pmin``/``pmax``
|
|
17
|
+
normalisation propagates ``NA`` (``na.rm = FALSE`` semantics) exactly like R.
|
|
18
|
+
* :meth:`GeomBox.setup_data` resolves both axes, emits a :func:`cli_warn`
|
|
19
|
+
(never an error) listing the unresolved aesthetics with axis-specific tips,
|
|
20
|
+
then writes ``xmin``/``xmax``/``ymin``/``ymax`` and drops ``x``/``width``/
|
|
21
|
+
``y``/``height``.
|
|
22
|
+
* :meth:`GeomBox.draw_panel` builds plain rectangles under linear coords
|
|
23
|
+
(``rect_grob`` when ``radius is None``, otherwise one ``roundrect_grob`` per
|
|
24
|
+
row), and expands each rectangle to a five-vertex polygon delegated to
|
|
25
|
+
:class:`ggplot2_py.GeomPolygon` under non-linear coords (``radius`` ignored).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import re
|
|
31
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
32
|
+
|
|
33
|
+
import numpy as np
|
|
34
|
+
import pandas as pd
|
|
35
|
+
|
|
36
|
+
from ggplot2_py import ggproto, ggproto_parent
|
|
37
|
+
from ggplot2_py.geom import (
|
|
38
|
+
Geom,
|
|
39
|
+
GeomPolygon,
|
|
40
|
+
FromTheme,
|
|
41
|
+
Mapping,
|
|
42
|
+
PT,
|
|
43
|
+
_coord_transform,
|
|
44
|
+
_fill_alpha,
|
|
45
|
+
_ggname,
|
|
46
|
+
_mix_ink_paper,
|
|
47
|
+
draw_key_polygon,
|
|
48
|
+
)
|
|
49
|
+
from grid_py import (
|
|
50
|
+
Gpar,
|
|
51
|
+
Unit,
|
|
52
|
+
grob_tree,
|
|
53
|
+
is_unit,
|
|
54
|
+
null_grob,
|
|
55
|
+
rect_grob,
|
|
56
|
+
roundrect_grob,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
from ggh4x._cli import cli_warn
|
|
60
|
+
from ggh4x._vctrs import vec_interleave
|
|
61
|
+
|
|
62
|
+
__all__ = [
|
|
63
|
+
"geom_box",
|
|
64
|
+
"GeomBox",
|
|
65
|
+
"resolve_box",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# resolve_box helper
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
def _as_float_array(x: Any, n: int) -> Optional[np.ndarray]:
|
|
73
|
+
"""Coerce a column-like input to a length-``n`` float array, or ``None``.
|
|
74
|
+
|
|
75
|
+
Mirrors R's treatment of a possibly-``NULL`` vector that participates in
|
|
76
|
+
:func:`resolve_box`. ``None`` propagates (so the ``%||%`` fallback can fire),
|
|
77
|
+
otherwise the value is coerced to ``float`` with ``None``/``pd.NA`` mapped to
|
|
78
|
+
``np.nan``.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
x : Any
|
|
83
|
+
A scalar, sequence, ``pandas.Series`` or ``None``.
|
|
84
|
+
n : int
|
|
85
|
+
Target length (the recycled row count).
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
numpy.ndarray or None
|
|
90
|
+
``None`` if *x* is ``None``; otherwise a float ``ndarray`` of length *n*.
|
|
91
|
+
"""
|
|
92
|
+
if x is None:
|
|
93
|
+
return None
|
|
94
|
+
if isinstance(x, pd.Series):
|
|
95
|
+
x = x.to_numpy()
|
|
96
|
+
arr = np.asarray(x, dtype="float64")
|
|
97
|
+
if arr.ndim == 0:
|
|
98
|
+
arr = np.repeat(arr, n)
|
|
99
|
+
return arr
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def resolve_box(
|
|
103
|
+
min: Any = None,
|
|
104
|
+
max: Any = None,
|
|
105
|
+
center: Any = None,
|
|
106
|
+
dim: Any = None,
|
|
107
|
+
) -> Optional[Dict[str, np.ndarray]]:
|
|
108
|
+
"""Resolve ``min``/``max`` of one axis from partial position information.
|
|
109
|
+
|
|
110
|
+
Port of R ``resolve_box()`` (``geom_box.R:230-278``). Given any two of
|
|
111
|
+
``min``, ``max``, ``center`` and ``dim`` (per element), the remaining
|
|
112
|
+
bounds are inferred with the priority order:
|
|
113
|
+
|
|
114
|
+
1. ``min``/``max`` verbatim,
|
|
115
|
+
2. ``center +/- 0.5 * dim``,
|
|
116
|
+
3. opposite bound ``+/- dim`` (where ``dim`` is itself derived from
|
|
117
|
+
``(center - min) * 2`` and then ``(max - center) * 2`` when absent).
|
|
118
|
+
|
|
119
|
+
The returned bounds are normalised with ``pmin``/``pmax`` so ``min <= max``;
|
|
120
|
+
this normalisation propagates ``NaN`` (R ``na.rm = FALSE``): if either of a
|
|
121
|
+
pair is ``NaN`` both outputs are ``NaN``.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
min, max, center, dim : array-like or None
|
|
126
|
+
The four candidate inputs for the axis. ``None`` means "not supplied".
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
dict or None
|
|
131
|
+
``{"min": ndarray, "max": ndarray}`` with float arrays, or ``None`` when
|
|
132
|
+
all four inputs are ``None`` (``n == 0``).
|
|
133
|
+
"""
|
|
134
|
+
lengths = [
|
|
135
|
+
len(np.atleast_1d(v)) for v in (min, max, center, dim) if v is not None
|
|
136
|
+
]
|
|
137
|
+
n = max_int(lengths)
|
|
138
|
+
if n == 0:
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
lo = _as_float_array(min, n)
|
|
142
|
+
hi = _as_float_array(max, n)
|
|
143
|
+
lo = np.full(n, np.nan) if lo is None else lo.astype("float64").copy()
|
|
144
|
+
hi = np.full(n, np.nan) if hi is None else hi.astype("float64").copy()
|
|
145
|
+
|
|
146
|
+
if not np.isnan(lo).any() and not np.isnan(hi).any():
|
|
147
|
+
return {"min": np.minimum(lo, hi), "max": np.maximum(lo, hi)}
|
|
148
|
+
|
|
149
|
+
ctr = _as_float_array(center, n)
|
|
150
|
+
dm = _as_float_array(dim, n)
|
|
151
|
+
ctr = np.full(n, np.nan) if ctr is None else ctr.astype("float64").copy()
|
|
152
|
+
dm = np.full(n, np.nan) if dm is None else dm.astype("float64").copy()
|
|
153
|
+
|
|
154
|
+
if np.isnan(lo).any():
|
|
155
|
+
i = np.isnan(lo)
|
|
156
|
+
lo[i] = ctr[i] - 0.5 * dm[i]
|
|
157
|
+
if np.isnan(hi).any():
|
|
158
|
+
i = np.isnan(hi)
|
|
159
|
+
hi[i] = ctr[i] + 0.5 * dm[i]
|
|
160
|
+
if not np.isnan(lo).any() and not np.isnan(hi).any():
|
|
161
|
+
return {"min": np.minimum(lo, hi), "max": np.maximum(lo, hi)}
|
|
162
|
+
|
|
163
|
+
if np.isnan(dm).any():
|
|
164
|
+
i = np.isnan(dm)
|
|
165
|
+
dm[i] = (ctr[i] - lo[i]) * 2
|
|
166
|
+
if np.isnan(dm).any():
|
|
167
|
+
i = np.isnan(dm)
|
|
168
|
+
dm[i] = (hi[i] - ctr[i]) * 2
|
|
169
|
+
|
|
170
|
+
if np.isnan(lo).any():
|
|
171
|
+
i = np.isnan(lo)
|
|
172
|
+
lo[i] = hi[i] - dm[i]
|
|
173
|
+
if np.isnan(hi).any():
|
|
174
|
+
i = np.isnan(hi)
|
|
175
|
+
hi[i] = lo[i] + dm[i]
|
|
176
|
+
|
|
177
|
+
return {"min": np.minimum(lo, hi), "max": np.maximum(lo, hi)}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def max_int(values: Sequence[int]) -> int:
|
|
181
|
+
"""Return ``max(values)`` treating an empty input as ``0`` (R ``max()``).
|
|
182
|
+
|
|
183
|
+
Parameters
|
|
184
|
+
----------
|
|
185
|
+
values : sequence of int
|
|
186
|
+
Candidate lengths.
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
int
|
|
191
|
+
The maximum, or ``0`` for an empty sequence.
|
|
192
|
+
"""
|
|
193
|
+
return max(values) if len(values) else 0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ---------------------------------------------------------------------------
|
|
197
|
+
# GeomBox ggproto class
|
|
198
|
+
# ---------------------------------------------------------------------------
|
|
199
|
+
class GeomBox(Geom):
|
|
200
|
+
"""A flexible rectangle geom defined by any two position aesthetics per axis.
|
|
201
|
+
|
|
202
|
+
Subclass of :class:`ggplot2_py.Geom` ported from R ``GeomBox``
|
|
203
|
+
(``geom_box.R:80-223``).
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
optional_aes = (
|
|
207
|
+
"xmin",
|
|
208
|
+
"xmax",
|
|
209
|
+
"x",
|
|
210
|
+
"width",
|
|
211
|
+
"ymin",
|
|
212
|
+
"ymax",
|
|
213
|
+
"y",
|
|
214
|
+
"height",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# R (geom_box.R:86-92):
|
|
218
|
+
# colour = from_theme(colour %||% NA),
|
|
219
|
+
# fill = from_theme(fill %||% col_mix(ink, paper, 0.35)),
|
|
220
|
+
# linewidth = from_theme(borderwidth),
|
|
221
|
+
# linetype = from_theme(bordertype),
|
|
222
|
+
# alpha = NA
|
|
223
|
+
default_aes: Mapping = Mapping(
|
|
224
|
+
colour=FromTheme("colour", fallback=lambda g: None),
|
|
225
|
+
fill=FromTheme("fill", fallback=_mix_ink_paper(0.35)),
|
|
226
|
+
linewidth=FromTheme("borderwidth"),
|
|
227
|
+
linetype=FromTheme("bordertype"),
|
|
228
|
+
alpha=None,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
draw_key = draw_key_polygon
|
|
232
|
+
|
|
233
|
+
def setup_data(self, data: pd.DataFrame, params: Dict[str, Any]) -> pd.DataFrame:
|
|
234
|
+
"""Resolve ``xmin``/``xmax``/``ymin``/``ymax`` from partial position info.
|
|
235
|
+
|
|
236
|
+
Port of R ``GeomBox$setup_data`` (``geom_box.R:94-139``). Each axis is
|
|
237
|
+
resolved independently with :func:`resolve_box` (``width``/``height``
|
|
238
|
+
falling back to ``params``). A :func:`cli_warn` lists any unresolved
|
|
239
|
+
aesthetics with axis-specific tips. The four corner columns are written
|
|
240
|
+
and ``x``/``width``/``y``/``height`` are dropped.
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
data : pandas.DataFrame
|
|
245
|
+
Layer data after stat computation.
|
|
246
|
+
params : dict
|
|
247
|
+
Layer parameters (may carry ``width``/``height``).
|
|
248
|
+
|
|
249
|
+
Returns
|
|
250
|
+
-------
|
|
251
|
+
pandas.DataFrame
|
|
252
|
+
Data with ``xmin``/``xmax``/``ymin``/``ymax`` set and
|
|
253
|
+
``x``/``width``/``y``/``height`` removed.
|
|
254
|
+
"""
|
|
255
|
+
data = data.copy()
|
|
256
|
+
|
|
257
|
+
def col(name: str) -> Any:
|
|
258
|
+
return data[name] if name in data.columns else None
|
|
259
|
+
|
|
260
|
+
width = col("width")
|
|
261
|
+
if width is None:
|
|
262
|
+
width = params.get("width")
|
|
263
|
+
height = col("height")
|
|
264
|
+
if height is None:
|
|
265
|
+
height = params.get("height")
|
|
266
|
+
|
|
267
|
+
x = resolve_box(col("xmin"), col("xmax"), col("x"), width)
|
|
268
|
+
y = resolve_box(col("ymin"), col("ymax"), col("y"), height)
|
|
269
|
+
|
|
270
|
+
# Check for missing rows. R: missing <- if (anyNA(x$min)) ... When an
|
|
271
|
+
# axis is entirely absent, resolve_box() returns NULL and R's
|
|
272
|
+
# anyNA(NULL) is FALSE -> that axis is NOT flagged (no spurious
|
|
273
|
+
# warning). So only flag a bound when its resolved value has NaN.
|
|
274
|
+
missing: List[str] = []
|
|
275
|
+
if x is not None and np.isnan(x["min"]).any():
|
|
276
|
+
missing.append("xmin")
|
|
277
|
+
if x is not None and np.isnan(x["max"]).any():
|
|
278
|
+
missing.append("xmax")
|
|
279
|
+
if y is not None and np.isnan(y["min"]).any():
|
|
280
|
+
missing.append("ymin")
|
|
281
|
+
if y is not None and np.isnan(y["max"]).any():
|
|
282
|
+
missing.append("ymax")
|
|
283
|
+
|
|
284
|
+
if missing:
|
|
285
|
+
tip: List[str] = []
|
|
286
|
+
if any(re.match("^x", m) for m in missing):
|
|
287
|
+
tip.append(
|
|
288
|
+
"Have you specified exactly two of xmin, xmax, x, or "
|
|
289
|
+
"width for every row?"
|
|
290
|
+
)
|
|
291
|
+
if any(re.match("^y", m) for m in missing):
|
|
292
|
+
tip.append(
|
|
293
|
+
"Have you specified exactly two of ymin, ymax, y, or "
|
|
294
|
+
"height for every row?"
|
|
295
|
+
)
|
|
296
|
+
msg = (
|
|
297
|
+
"Could not resolve the position of every "
|
|
298
|
+
+ ", ".join("`%s`" % m for m in missing)
|
|
299
|
+
+ (" aesthetic." if len(missing) == 1 else " aesthetics.")
|
|
300
|
+
)
|
|
301
|
+
if tip:
|
|
302
|
+
msg = msg + "\n" + "\n".join("i " + t for t in tip)
|
|
303
|
+
cli_warn(msg)
|
|
304
|
+
|
|
305
|
+
# R: data[c("xmin","xmax","ymin","ymax")] <- list(x$min, x$max, y$min,
|
|
306
|
+
# y$max). When an axis is absent, x/y is NULL and the NULL list entries
|
|
307
|
+
# are skipped -> those corner columns are NOT created (no NaN columns).
|
|
308
|
+
if x is not None:
|
|
309
|
+
data["xmin"] = x["min"]
|
|
310
|
+
data["xmax"] = x["max"]
|
|
311
|
+
if y is not None:
|
|
312
|
+
data["ymin"] = y["min"]
|
|
313
|
+
data["ymax"] = y["max"]
|
|
314
|
+
|
|
315
|
+
for drop in ("x", "width", "y", "height"):
|
|
316
|
+
if drop in data.columns:
|
|
317
|
+
data = data.drop(columns=drop)
|
|
318
|
+
|
|
319
|
+
return data
|
|
320
|
+
|
|
321
|
+
def draw_panel(
|
|
322
|
+
self,
|
|
323
|
+
data: pd.DataFrame,
|
|
324
|
+
panel_params: Any,
|
|
325
|
+
coord: Any,
|
|
326
|
+
lineend: str = "butt",
|
|
327
|
+
linejoin: str = "mitre",
|
|
328
|
+
radius: Any = None,
|
|
329
|
+
**params: Any,
|
|
330
|
+
) -> Any:
|
|
331
|
+
"""Build a rect/roundrect/polygon grob for the resolved rectangles.
|
|
332
|
+
|
|
333
|
+
Port of R ``GeomBox$draw_panel`` (``geom_box.R:141-220``).
|
|
334
|
+
|
|
335
|
+
* Non-linear coord: each rectangle is expanded into a five-vertex
|
|
336
|
+
polygon (winding ``xmin, xmax, xmax, xmin, xmin`` /
|
|
337
|
+
``ymax, ymax, ymin, ymin, ymin``) and drawn by
|
|
338
|
+
:meth:`ggplot2_py.GeomPolygon.draw_panel` (``radius`` ignored).
|
|
339
|
+
* Linear coord, ``radius is None``: a single :func:`rect_grob`.
|
|
340
|
+
* Linear coord, ``radius`` given: one :func:`roundrect_grob` per row
|
|
341
|
+
combined with :func:`grob_tree`.
|
|
342
|
+
|
|
343
|
+
Parameters
|
|
344
|
+
----------
|
|
345
|
+
data : pandas.DataFrame
|
|
346
|
+
Resolved layer data (``xmin``/``xmax``/``ymin``/``ymax`` present).
|
|
347
|
+
panel_params : Any
|
|
348
|
+
Panel scales / ranges.
|
|
349
|
+
coord : Any
|
|
350
|
+
Active coordinate system.
|
|
351
|
+
lineend : str, default ``"butt"``
|
|
352
|
+
Line end style.
|
|
353
|
+
linejoin : str, default ``"mitre"``
|
|
354
|
+
Line join style.
|
|
355
|
+
radius : grid unit, numeric, or None, default ``None``
|
|
356
|
+
Corner radius. ``numeric`` is interpreted as millimetres; any
|
|
357
|
+
non-unit value falls back to ``0pt``. Ignored under non-linear
|
|
358
|
+
coords.
|
|
359
|
+
|
|
360
|
+
Returns
|
|
361
|
+
-------
|
|
362
|
+
grid_py.Grob
|
|
363
|
+
The assembled grob.
|
|
364
|
+
"""
|
|
365
|
+
if not coord.is_linear():
|
|
366
|
+
aesthetics = [
|
|
367
|
+
c
|
|
368
|
+
for c in data.columns
|
|
369
|
+
if c not in ("x", "y", "xmin", "xmax", "ymin", "ymax")
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
# Rectangle to polygon: replicate each row 5 times.
|
|
373
|
+
idx = np.repeat(np.arange(len(data)), 5)
|
|
374
|
+
new_data = data.iloc[idx].reset_index(drop=True).copy()
|
|
375
|
+
new_data["x"] = vec_interleave(
|
|
376
|
+
data["xmin"].to_numpy(),
|
|
377
|
+
data["xmax"].to_numpy(),
|
|
378
|
+
data["xmax"].to_numpy(),
|
|
379
|
+
data["xmin"].to_numpy(),
|
|
380
|
+
data["xmin"].to_numpy(),
|
|
381
|
+
)
|
|
382
|
+
new_data["y"] = vec_interleave(
|
|
383
|
+
data["ymax"].to_numpy(),
|
|
384
|
+
data["ymax"].to_numpy(),
|
|
385
|
+
data["ymin"].to_numpy(),
|
|
386
|
+
data["ymin"].to_numpy(),
|
|
387
|
+
data["ymin"].to_numpy(),
|
|
388
|
+
)
|
|
389
|
+
for drop in ("xmin", "xmax", "ymin", "ymax"):
|
|
390
|
+
if drop in new_data.columns:
|
|
391
|
+
new_data = new_data.drop(columns=drop)
|
|
392
|
+
|
|
393
|
+
return ggproto_parent(GeomPolygon, self).draw_panel(
|
|
394
|
+
new_data, panel_params, coord, lineend=lineend, linejoin=linejoin
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
coords = _coord_transform(coord, data, panel_params)
|
|
398
|
+
coords = coords.copy()
|
|
399
|
+
coords["fill"] = _fill_alpha(
|
|
400
|
+
coords["fill"].to_numpy() if "fill" in coords.columns else "grey35",
|
|
401
|
+
coords["alpha"].to_numpy() if "alpha" in coords.columns else None,
|
|
402
|
+
)
|
|
403
|
+
coords["linewidth"] = (
|
|
404
|
+
coords["linewidth"].to_numpy() * PT
|
|
405
|
+
if "linewidth" in coords.columns
|
|
406
|
+
else 0.5 * PT
|
|
407
|
+
)
|
|
408
|
+
coords["width"] = coords["xmax"].to_numpy() - coords["xmin"].to_numpy()
|
|
409
|
+
coords["height"] = coords["ymax"].to_numpy() - coords["ymin"].to_numpy()
|
|
410
|
+
|
|
411
|
+
col = coords["colour"].to_numpy() if "colour" in coords.columns else None
|
|
412
|
+
lty = coords["linetype"].to_numpy() if "linetype" in coords.columns else 1
|
|
413
|
+
|
|
414
|
+
if radius is None:
|
|
415
|
+
return _ggname(
|
|
416
|
+
"geom_box",
|
|
417
|
+
rect_grob(
|
|
418
|
+
coords["xmin"].to_numpy(),
|
|
419
|
+
coords["ymax"].to_numpy(),
|
|
420
|
+
width=coords["width"].to_numpy(),
|
|
421
|
+
height=coords["height"].to_numpy(),
|
|
422
|
+
default_units="native",
|
|
423
|
+
just=("left", "top"),
|
|
424
|
+
gp=Gpar(
|
|
425
|
+
col=col,
|
|
426
|
+
fill=coords["fill"].to_numpy(),
|
|
427
|
+
lwd=coords["linewidth"].to_numpy(),
|
|
428
|
+
lty=lty,
|
|
429
|
+
linejoin=linejoin,
|
|
430
|
+
lineend=lineend,
|
|
431
|
+
),
|
|
432
|
+
),
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
if isinstance(radius, (int, float)) and not is_unit(radius):
|
|
436
|
+
radius = Unit(radius, "mm")
|
|
437
|
+
if not is_unit(radius):
|
|
438
|
+
radius = Unit(0, "pt")
|
|
439
|
+
|
|
440
|
+
fill = coords["fill"].to_numpy()
|
|
441
|
+
lwd = coords["linewidth"].to_numpy()
|
|
442
|
+
col_arr = np.asarray(col) if col is not None else None
|
|
443
|
+
lty_arr = np.asarray(lty) if not np.isscalar(lty) else None
|
|
444
|
+
xmin = coords["xmin"].to_numpy()
|
|
445
|
+
ymax = coords["ymax"].to_numpy()
|
|
446
|
+
w = coords["width"].to_numpy()
|
|
447
|
+
h = coords["height"].to_numpy()
|
|
448
|
+
|
|
449
|
+
grobs: List[Any] = []
|
|
450
|
+
for i in range(len(coords)):
|
|
451
|
+
grobs.append(
|
|
452
|
+
roundrect_grob(
|
|
453
|
+
xmin[i],
|
|
454
|
+
ymax[i],
|
|
455
|
+
width=w[i],
|
|
456
|
+
height=h[i],
|
|
457
|
+
r=radius,
|
|
458
|
+
default_units="native",
|
|
459
|
+
just=("left", "top"),
|
|
460
|
+
gp=Gpar(
|
|
461
|
+
col=(col_arr[i] if col_arr is not None else None),
|
|
462
|
+
fill=fill[i],
|
|
463
|
+
lwd=lwd[i],
|
|
464
|
+
lty=(lty_arr[i] if lty_arr is not None else lty),
|
|
465
|
+
linejoin=linejoin,
|
|
466
|
+
lineend=lineend,
|
|
467
|
+
),
|
|
468
|
+
)
|
|
469
|
+
)
|
|
470
|
+
return _ggname("geom_box", grob_tree(*grobs))
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def geom_box(
|
|
474
|
+
mapping: Optional[Mapping] = None,
|
|
475
|
+
data: Any = None,
|
|
476
|
+
stat: str = "identity",
|
|
477
|
+
position: str = "identity",
|
|
478
|
+
linejoin: str = "mitre",
|
|
479
|
+
na_rm: bool = False,
|
|
480
|
+
show_legend: Any = None,
|
|
481
|
+
inherit_aes: bool = True,
|
|
482
|
+
radius: Any = None,
|
|
483
|
+
**kwargs: Any,
|
|
484
|
+
) -> Any:
|
|
485
|
+
"""Create a flexible rectangle layer.
|
|
486
|
+
|
|
487
|
+
Port of R ``geom_box()`` (``geom_box.R:45-72``). A more flexible variant of
|
|
488
|
+
:func:`ggplot2_py.geom_rect` / ``geom_tile`` that accepts any two of the
|
|
489
|
+
``(x/y)min``/``(x/y)max``/``(x/y)``/``(width/height)`` aesthetics per axis.
|
|
490
|
+
|
|
491
|
+
Parameters
|
|
492
|
+
----------
|
|
493
|
+
mapping : Mapping, optional
|
|
494
|
+
Aesthetic mapping created by :func:`ggplot2_py.aes`.
|
|
495
|
+
data : Any, optional
|
|
496
|
+
Layer data.
|
|
497
|
+
stat : str, default ``"identity"``
|
|
498
|
+
Statistical transformation.
|
|
499
|
+
position : str, default ``"identity"``
|
|
500
|
+
Position adjustment.
|
|
501
|
+
linejoin : str, default ``"mitre"``
|
|
502
|
+
Line join style for the rectangle borders.
|
|
503
|
+
na_rm : bool, default ``False``
|
|
504
|
+
If ``True``, silently remove missing values.
|
|
505
|
+
show_legend : bool or None, default ``None``
|
|
506
|
+
Whether to show a legend for this layer.
|
|
507
|
+
inherit_aes : bool, default ``True``
|
|
508
|
+
Whether to inherit the plot's default aesthetics.
|
|
509
|
+
radius : grid unit, numeric, or None, default ``None``
|
|
510
|
+
Rounded-corner radius. ``numeric`` is interpreted as millimetres. Does
|
|
511
|
+
not work under non-linear coordinates.
|
|
512
|
+
**kwargs : Any
|
|
513
|
+
Additional aesthetic parameters passed to the layer.
|
|
514
|
+
|
|
515
|
+
Returns
|
|
516
|
+
-------
|
|
517
|
+
ggplot2_py.Layer
|
|
518
|
+
A layer object that can be added to a plot.
|
|
519
|
+
"""
|
|
520
|
+
from ggplot2_py.layer import layer
|
|
521
|
+
|
|
522
|
+
return layer(
|
|
523
|
+
data=data,
|
|
524
|
+
mapping=mapping,
|
|
525
|
+
stat=stat,
|
|
526
|
+
geom=GeomBox,
|
|
527
|
+
position=position,
|
|
528
|
+
show_legend=show_legend,
|
|
529
|
+
inherit_aes=inherit_aes,
|
|
530
|
+
params={
|
|
531
|
+
"linejoin": linejoin,
|
|
532
|
+
"na_rm": na_rm,
|
|
533
|
+
"radius": radius,
|
|
534
|
+
**kwargs,
|
|
535
|
+
},
|
|
536
|
+
)
|