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.
Files changed (64) hide show
  1. ggh4x/__init__.py +140 -0
  2. ggh4x/_aimed_text_grob.py +432 -0
  3. ggh4x/_borrowed_ggplot2.py +273 -0
  4. ggh4x/_cli.py +84 -0
  5. ggh4x/_datasets.py +106 -0
  6. ggh4x/_download.py +111 -0
  7. ggh4x/_facet_helpers.py +313 -0
  8. ggh4x/_facet_utils.py +649 -0
  9. ggh4x/_gap_grobs.py +606 -0
  10. ggh4x/_registry.py +10 -0
  11. ggh4x/_rlang.py +93 -0
  12. ggh4x/_utils.py +150 -0
  13. ggh4x/_vctrs.py +233 -0
  14. ggh4x/conveniences.py +601 -0
  15. ggh4x/coord_axes_inside.py +380 -0
  16. ggh4x/element_part_rect.py +545 -0
  17. ggh4x/facet_grid2.py +1018 -0
  18. ggh4x/facet_manual.py +901 -0
  19. ggh4x/facet_nested.py +776 -0
  20. ggh4x/facet_nested_wrap.py +193 -0
  21. ggh4x/facet_wrap2.py +896 -0
  22. ggh4x/geom_box.py +536 -0
  23. ggh4x/geom_outline_point.py +444 -0
  24. ggh4x/geom_pointpath.py +259 -0
  25. ggh4x/geom_polygonraster.py +252 -0
  26. ggh4x/geom_rectrug.py +489 -0
  27. ggh4x/geom_text_aimed.py +279 -0
  28. ggh4x/guide_stringlegend.py +354 -0
  29. ggh4x/help_secondary.py +549 -0
  30. ggh4x/multiscale/__init__.py +51 -0
  31. ggh4x/multiscale/_multiscale_add.py +207 -0
  32. ggh4x/multiscale/scale_listed.py +167 -0
  33. ggh4x/multiscale/scale_manual.py +478 -0
  34. ggh4x/multiscale/scale_multi.py +393 -0
  35. ggh4x/panel_scales/__init__.py +58 -0
  36. ggh4x/panel_scales/at_panel.py +115 -0
  37. ggh4x/panel_scales/facetted_pos_scales.py +647 -0
  38. ggh4x/panel_scales/force_panelsize.py +411 -0
  39. ggh4x/panel_scales/scale_facet.py +222 -0
  40. ggh4x/position_disjoint_ranges.py +229 -0
  41. ggh4x/position_lineartrans.py +242 -0
  42. ggh4x/py.typed +0 -0
  43. ggh4x/resources/faithful.csv +273 -0
  44. ggh4x/resources/iris.csv +151 -0
  45. ggh4x/resources/mtcars.csv +33 -0
  46. ggh4x/resources/pressure.csv +20 -0
  47. ggh4x/resources/volcano.csv +87 -0
  48. ggh4x/save.py +255 -0
  49. ggh4x/stat_difference.py +388 -0
  50. ggh4x/stat_funxy.py +436 -0
  51. ggh4x/stat_rle.py +290 -0
  52. ggh4x/stat_rollingkernel.py +369 -0
  53. ggh4x/stat_theodensity.py +681 -0
  54. ggh4x/strip_nested.py +448 -0
  55. ggh4x/strip_split.py +687 -0
  56. ggh4x/strip_tag.py +636 -0
  57. ggh4x/strip_themed.py +232 -0
  58. ggh4x/strip_vanilla.py +1464 -0
  59. ggh4x/themes.py +31 -0
  60. ggh4x/themes_ggh4x.py +67 -0
  61. ggh4x_python-0.3.1.9000.dist-info/METADATA +40 -0
  62. ggh4x_python-0.3.1.9000.dist-info/RECORD +64 -0
  63. ggh4x_python-0.3.1.9000.dist-info/WHEEL +4 -0
  64. 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
+ )