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
@@ -0,0 +1,545 @@
1
+ """Partial rectangle theme element.
2
+
3
+ Port of ggh4x R source ``element_part_rect.R``.
4
+
5
+ The :func:`element_part_rect` factory draws individual sides of a rectangle as a
6
+ theme element, substituting :func:`ggplot2_py.theme_elements.element_rect`. A
7
+ ``side`` string built from the letters ``"t"`` (top), ``"l"`` (left), ``"b"``
8
+ (bottom) and ``"r"`` (right) selects which borders are drawn.
9
+
10
+ Components ported from the R file
11
+ ---------------------------------
12
+ * :class:`ElementPartRect` -- subclass of ``ElementRect`` carrying a ``side`` slot
13
+ (the R S3 class vector ``c("element_part_rect", "element_rect", "element")``).
14
+ * :func:`element_part_rect` -- polymorphic factory (R L29-78). All four sides ->
15
+ plain ``element_rect``; no sides -> ``element_rect(colour=NA)``; otherwise an
16
+ ``ElementPartRect``.
17
+ * :func:`_grob_from_part_rect` -- the ``element_grob.element_part_rect`` S3 method
18
+ (R L82-109): merges caller graphical parameters over the element's own and
19
+ delegates to :func:`part_rect_grob`.
20
+ * :func:`part_rect_grob` -- ``partrectGrob`` (R L111-243): a grob tree of a
21
+ fill-only rectangle plus a segments grob drawing the requested sides.
22
+ * :func:`element_grob` -- a ggh4x-local wrapper around
23
+ ``ggplot2_py.theme_elements.element_grob`` that intercepts ``ElementPartRect``
24
+ (which is-a ``ElementRect``) *before* the upstream ``ElementRect`` branch.
25
+
26
+ Notes
27
+ -----
28
+ ``ggplot2_py.theme_elements.element_grob`` is a hard ``isinstance`` chain rather
29
+ than R-style S3 dispatch, so the wrapper here adds the missing
30
+ ``ElementPartRect`` branch. Importing this module also monkeypatches the
31
+ upstream ``element_grob`` so any code path that dispatches an ``ElementPartRect``
32
+ through the original function still renders partial borders.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import re
38
+ from typing import Any, List, Optional, Union
39
+
40
+ from grid_py import (
41
+ Gpar,
42
+ Unit,
43
+ grob_tree,
44
+ is_unit,
45
+ rect_grob,
46
+ segments_grob,
47
+ unit_c,
48
+ unit_rep,
49
+ )
50
+
51
+ import ggplot2_py.theme_elements as _te
52
+ from ggplot2_py._compat import NA, is_na
53
+ from ggplot2_py.theme_elements import ElementRect, element_rect
54
+ from ggplot2_py.theme_elements import _PT # 72.27 / 25.4 == R's .pt
55
+
56
+ __all__ = [
57
+ "ElementPartRect",
58
+ "element_part_rect",
59
+ "part_rect_grob",
60
+ "element_grob",
61
+ ]
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # Class
66
+ # ---------------------------------------------------------------------------
67
+
68
+
69
+ class ElementPartRect(ElementRect):
70
+ """Theme element drawing partial rectangle borders.
71
+
72
+ Subclasses :class:`ggplot2_py.theme_elements.ElementRect` so existing theme
73
+ inheritance (``combine_elements`` / ``merge_element``) treats it like a rect
74
+ for ``strip.background`` / ``panel.background``. Adds a ``side`` slot
75
+ selecting which borders to draw.
76
+
77
+ Parameters
78
+ ----------
79
+ fill : str or None
80
+ Fill colour.
81
+ colour : str or None
82
+ Border colour.
83
+ linewidth : float or None
84
+ Border width in millimetres.
85
+ linetype : int, str, or None
86
+ Border line type.
87
+ inherit_blank : bool
88
+ Whether to inherit ``element_blank`` from parents.
89
+ side : str
90
+ Any combination of ``"t"``, ``"l"``, ``"b"``, ``"r"`` selecting the
91
+ top, left, bottom and right borders respectively.
92
+ """
93
+
94
+ def __init__(
95
+ self,
96
+ fill: Optional[str] = None,
97
+ colour: Optional[str] = None,
98
+ linewidth: Optional[float] = None,
99
+ linetype: Optional[Union[int, str]] = None,
100
+ inherit_blank: bool = False,
101
+ side: str = "tlbr",
102
+ ) -> None:
103
+ super().__init__(
104
+ fill=fill,
105
+ colour=colour,
106
+ linewidth=linewidth,
107
+ linetype=linetype,
108
+ inherit_blank=inherit_blank,
109
+ )
110
+ self.side = side
111
+
112
+ def __repr__(self) -> str: # pragma: no cover - cosmetic
113
+ parts = []
114
+ for attr in ("fill", "colour", "linewidth", "linetype", "side", "inherit_blank"):
115
+ val = getattr(self, attr)
116
+ if val is not None and val is not False:
117
+ parts.append(f"{attr}={val!r}")
118
+ return f"element_part_rect({', '.join(parts)})"
119
+
120
+
121
+ # ---------------------------------------------------------------------------
122
+ # Factory (R L29-78)
123
+ # ---------------------------------------------------------------------------
124
+
125
+
126
+ def element_part_rect(
127
+ side: str = "tlbr",
128
+ fill: Optional[str] = None,
129
+ colour: Optional[str] = None,
130
+ linewidth: Optional[float] = None,
131
+ linetype: Optional[Union[int, str]] = None,
132
+ color: Optional[str] = None,
133
+ inherit_blank: bool = False,
134
+ ) -> ElementRect:
135
+ """Construct a partial-rectangle theme element.
136
+
137
+ Polymorphic factory mirroring R ``element_part_rect`` (L29-78):
138
+
139
+ * If ``side`` contains all of ``t``, ``l``, ``b`` and ``r`` the element
140
+ simplifies to a plain :func:`ggplot2_py.theme_elements.element_rect`.
141
+ * If ``side`` contains none of those letters it simplifies to
142
+ ``element_rect(colour=NA, ...)`` (a borderless rectangle).
143
+ * Otherwise an :class:`ElementPartRect` is returned.
144
+
145
+ Parameters
146
+ ----------
147
+ side : str
148
+ Any combination of ``"t"``, ``"l"``, ``"b"``, ``"r"``. Including all
149
+ or none of these letters defaults to a regular ``element_rect()``.
150
+ fill : str or None
151
+ Fill colour.
152
+ colour : str or None
153
+ Border colour.
154
+ linewidth : float or None
155
+ Line/border size in millimetres.
156
+ linetype : int, str, or None
157
+ Line type: an integer (0--8), a name, or a hex dash string.
158
+ color : str or None
159
+ American-spelling alias for ``colour``; overrides ``colour`` when given.
160
+ inherit_blank : bool
161
+ Whether to inherit ``element_blank`` from parents.
162
+
163
+ Returns
164
+ -------
165
+ ElementRect or ElementPartRect
166
+ A plain ``ElementRect`` for the all-sides / no-sides cases, otherwise
167
+ an ``ElementPartRect``.
168
+ """
169
+ if color is not None:
170
+ colour = color
171
+
172
+ # R: grepl("(?=.*t)(?=.*l)(?=.*r)(?=.*b)", side, perl = TRUE)
173
+ all_sides = re.search(r"(?=.*t)(?=.*l)(?=.*r)(?=.*b)", side) is not None
174
+ if all_sides:
175
+ # Simplifies to regular rectangle.
176
+ return element_rect(
177
+ fill=fill,
178
+ colour=colour,
179
+ linewidth=linewidth,
180
+ linetype=linetype,
181
+ inherit_blank=inherit_blank,
182
+ )
183
+
184
+ # R: !grepl("t|l|r|b", side, perl = TRUE)
185
+ no_sides = re.search(r"[tlrb]", side) is None
186
+ if no_sides:
187
+ # Also simplifies to a regular rectangle, but with no colour.
188
+ return element_rect(
189
+ fill=fill,
190
+ colour=NA,
191
+ linewidth=linewidth,
192
+ linetype=linetype,
193
+ inherit_blank=inherit_blank,
194
+ )
195
+
196
+ return ElementPartRect(
197
+ fill=fill,
198
+ colour=colour,
199
+ linewidth=linewidth,
200
+ linetype=linetype,
201
+ inherit_blank=inherit_blank,
202
+ side=side,
203
+ )
204
+
205
+
206
+ # ---------------------------------------------------------------------------
207
+ # gpar helpers
208
+ # ---------------------------------------------------------------------------
209
+
210
+
211
+ def _gpar_from_fields(
212
+ lwd: Any = None,
213
+ col: Any = None,
214
+ fill: Any = None,
215
+ lty: Any = None,
216
+ ) -> dict:
217
+ """Build a gpar-parameter dict applying R ``gpar(...)`` NULL-dropping.
218
+
219
+ R ``gpar(lwd = NULL, col = colour, ...)`` silently drops any ``NULL``
220
+ argument. ``NA`` is retained (e.g. ``gpar(col = NA)``) and forwarded as
221
+ grid_py's ``None`` NA sentinel. This helper returns a plain ``dict`` so
222
+ callers can further merge/strip fields before constructing a
223
+ :class:`grid_py.Gpar`.
224
+
225
+ Parameters
226
+ ----------
227
+ lwd, col, fill, lty : Any
228
+ Candidate graphical-parameter values. ``None`` mirrors R ``NULL``
229
+ (drop the field); :data:`ggplot2_py._compat.NA` maps to grid_py's
230
+ ``None`` NA sentinel (retain the field, draw nothing).
231
+
232
+ Returns
233
+ -------
234
+ dict
235
+ Parameter dict with ``None`` (NULL) entries omitted. ``NA`` entries
236
+ are stored as Python ``None`` (grid_py NA sentinel).
237
+ """
238
+ out: dict = {}
239
+ for key, value in (("lwd", lwd), ("col", col), ("fill", fill), ("lty", lty)):
240
+ if value is None:
241
+ continue # R NULL -> drop entirely
242
+ if is_na(value):
243
+ out[key] = None # R NA -> grid_py NA sentinel
244
+ else:
245
+ out[key] = value
246
+ return out
247
+
248
+
249
+ def _strip_field(params: dict, field: str) -> Gpar:
250
+ """Return a :class:`grid_py.Gpar` with ``field`` forced to the NA sentinel.
251
+
252
+ Mirrors R ``do.call(gpar, within(gp, col <- NA))`` (and the ``fill`` twin):
253
+ the named field is *set* to ``NA`` (kept, value NA) while every other field
254
+ is preserved unchanged.
255
+
256
+ Parameters
257
+ ----------
258
+ params : dict
259
+ Source parameter dict (already NULL-dropped).
260
+ field : str
261
+ Field to force to ``NA`` -- ``"col"`` or ``"fill"``.
262
+
263
+ Returns
264
+ -------
265
+ grid_py.Gpar
266
+ The graphical parameters with ``field`` set to grid_py's ``None`` NA
267
+ sentinel (renders nothing for that aesthetic).
268
+ """
269
+ merged = dict(params)
270
+ merged[field] = None # grid_py NA sentinel (== R NA)
271
+ return Gpar(**merged)
272
+
273
+
274
+ # ---------------------------------------------------------------------------
275
+ # element_grob.element_part_rect (R L82-109)
276
+ # ---------------------------------------------------------------------------
277
+
278
+
279
+ def _grob_from_part_rect(
280
+ element: ElementPartRect,
281
+ x: Any = 0.5,
282
+ y: Any = 0.5,
283
+ width: Any = 1,
284
+ height: Any = 1,
285
+ fill: Optional[str] = None,
286
+ colour: Optional[str] = None,
287
+ linewidth: Optional[float] = None,
288
+ linetype: Optional[Union[int, str]] = None,
289
+ **kwargs: Any,
290
+ ) -> Any:
291
+ """Render an :class:`ElementPartRect` as a grob.
292
+
293
+ Port of R ``element_grob.element_part_rect`` (L82-109). Builds a caller
294
+ graphical-parameter set and an element graphical-parameter set, then lets
295
+ the caller values override the element values field-by-field before calling
296
+ :func:`part_rect_grob`.
297
+
298
+ Parameters
299
+ ----------
300
+ element : ElementPartRect
301
+ The element being rendered (supplies ``side`` plus default
302
+ fill/colour/linewidth/linetype).
303
+ x, y : Unit or numeric
304
+ Rectangle centre. Default ``0.5`` npc.
305
+ width, height : Unit or numeric
306
+ Rectangle size. Default ``1`` npc.
307
+ fill : str or None
308
+ Caller fill override (millimetre-independent).
309
+ colour : str or None
310
+ Caller border-colour override.
311
+ linewidth : float or None
312
+ Caller border width in millimetres (converted to ``lwd`` via ``_PT``).
313
+ linetype : int, str, or None
314
+ Caller line-type override.
315
+ **kwargs
316
+ Forwarded to :func:`part_rect_grob` (e.g. ``name``, ``vp``,
317
+ ``default_units``).
318
+
319
+ Returns
320
+ -------
321
+ grid_py.GTree
322
+ A grob tree drawing the requested borders.
323
+ """
324
+ # R: caller gp = gpar(lwd = linewidth * .pt or NULL, col, fill, lty)
325
+ caller = _gpar_from_fields(
326
+ lwd=(linewidth * _PT) if linewidth is not None else None,
327
+ col=colour,
328
+ fill=fill,
329
+ lty=linetype,
330
+ )
331
+
332
+ # R: element_gp = gpar(lwd = element$linewidth * .pt or NULL, ...)
333
+ el_lw = element.linewidth
334
+ element_gp = _gpar_from_fields(
335
+ lwd=(el_lw * _PT) if el_lw is not None else None,
336
+ col=element.colour,
337
+ fill=element.fill,
338
+ lty=element.linetype,
339
+ )
340
+
341
+ # R: for (i in names(gp)) element_gp[[i]] <- gp[[i]] (caller overrides)
342
+ for key, value in caller.items():
343
+ element_gp[key] = value
344
+
345
+ return part_rect_grob(
346
+ x,
347
+ y,
348
+ width,
349
+ height,
350
+ gp=element_gp,
351
+ sides=element.side,
352
+ **kwargs,
353
+ )
354
+
355
+
356
+ # ---------------------------------------------------------------------------
357
+ # partrectGrob (R L111-243)
358
+ # ---------------------------------------------------------------------------
359
+
360
+
361
+ def _as_unit(value: Any, default_units: str) -> Unit:
362
+ """Coerce *value* to a :class:`grid_py.Unit` (R ``if (!is.unit(x)) unit(...)``)."""
363
+ if is_unit(value):
364
+ return value
365
+ return Unit(value, default_units)
366
+
367
+
368
+ # Side geometry: factors of (x, width) for x0/x1 and (y, height) for y0/y1.
369
+ # Each tuple is (wx0, wx1, hy0, hy1) where the endpoint is
370
+ # x0 = x + wx0 * width, x1 = x + wx1 * width,
371
+ # y0 = y + hy0 * height, y1 = y + hy1 * height.
372
+ # Order of evaluation is fixed t -> b -> l -> r regardless of the input string
373
+ # (mirrors R's sequence of grepl blocks, L144-230).
374
+ _SIDE_SPECS = (
375
+ ("t", (-0.5, 0.5, 0.5, 0.5)),
376
+ ("b", (-0.5, 0.5, -0.5, -0.5)),
377
+ ("l", (-0.5, -0.5, -0.5, 0.5)),
378
+ ("r", (0.5, 0.5, -0.5, 0.5)),
379
+ )
380
+
381
+
382
+ def part_rect_grob(
383
+ x: Any = None,
384
+ y: Any = None,
385
+ width: Any = None,
386
+ height: Any = None,
387
+ default_units: str = "npc",
388
+ name: Optional[str] = None,
389
+ gp: Optional[Union[Gpar, dict]] = None,
390
+ vp: Optional[Any] = None,
391
+ sides: str = "tlbr",
392
+ ) -> Any:
393
+ """Build a grob tree drawing selected rectangle borders.
394
+
395
+ Port of R ``partrectGrob`` (L111-243). Produces a :class:`grid_py.GTree`
396
+ containing (1) a fill-only rectangle (border stripped) drawn first, and (2)
397
+ a segments grob (fill stripped) drawing only the requested sides, drawn on
398
+ top.
399
+
400
+ Parameters
401
+ ----------
402
+ x, y : Unit or numeric, optional
403
+ Rectangle centre. Defaults to ``0.5`` npc.
404
+ width, height : Unit or numeric, optional
405
+ Rectangle size. Defaults to ``1`` npc.
406
+ default_units : str
407
+ Unit type applied to bare numerics. Default ``"npc"``.
408
+ name : str or None
409
+ Name for the returned grob tree.
410
+ gp : grid_py.Gpar or dict or None
411
+ Graphical parameters. A dict is accepted and treated like R's
412
+ unclassed gpar list.
413
+ vp : object or None
414
+ Optional viewport (applied to the children and the tree, matching R).
415
+ sides : str
416
+ Any combination of ``"t"``, ``"l"``, ``"b"``, ``"r"`` selecting which
417
+ borders to draw.
418
+
419
+ Returns
420
+ -------
421
+ grid_py.GTree
422
+ ``grob_tree(fillgrob, sidegrob, name=name, vp=vp)``.
423
+ """
424
+ if x is None:
425
+ x = Unit(0.5, "npc")
426
+ if y is None:
427
+ y = Unit(0.5, "npc")
428
+ if width is None:
429
+ width = Unit(1, "npc")
430
+ if height is None:
431
+ height = Unit(1, "npc")
432
+
433
+ x = _as_unit(x, default_units)
434
+ y = _as_unit(y, default_units)
435
+ width = _as_unit(width, default_units)
436
+ height = _as_unit(height, default_units)
437
+
438
+ # R: gp <- unclass(gp) -- normalise to a plain param dict.
439
+ if gp is None:
440
+ params: dict = {}
441
+ elif isinstance(gp, Gpar):
442
+ params = gp.params
443
+ elif isinstance(gp, dict):
444
+ params = dict(gp)
445
+ else: # pragma: no cover - defensive
446
+ raise TypeError(f"gp must be a Gpar, dict, or None, got {type(gp)!r}")
447
+
448
+ # R: rectfill = rectGrob(..., gp = do.call(gpar, within(gp, col <- NA)))
449
+ rectfill = rect_grob(
450
+ x=x,
451
+ y=y,
452
+ width=width,
453
+ height=height,
454
+ default_units=default_units,
455
+ name="fillgrob",
456
+ vp=vp,
457
+ gp=_strip_field(params, "col"),
458
+ )
459
+
460
+ # n = max(length(x), length(width), length(y), length(height))
461
+ n = max(len(x), len(width), len(y), len(height))
462
+
463
+ x0: Optional[Unit] = None
464
+ x1: Optional[Unit] = None
465
+ y0: Optional[Unit] = None
466
+ y1: Optional[Unit] = None
467
+
468
+ for letter, (wx0, wx1, hy0, hy1) in _SIDE_SPECS:
469
+ # R: grepl("(?=.*t)", sides, perl = TRUE) etc. -- letter present?
470
+ if letter not in sides:
471
+ continue
472
+ seg_x0 = unit_rep(x + width * wx0, length_out=n)
473
+ seg_x1 = unit_rep(x + width * wx1, length_out=n)
474
+ seg_y0 = unit_rep(y + height * hy0, length_out=n)
475
+ seg_y1 = unit_rep(y + height * hy1, length_out=n)
476
+ if x0 is None:
477
+ x0, x1, y0, y1 = seg_x0, seg_x1, seg_y0, seg_y1
478
+ else:
479
+ x0 = unit_c(x0, seg_x0)
480
+ x1 = unit_c(x1, seg_x1)
481
+ y0 = unit_c(y0, seg_y0)
482
+ y1 = unit_c(y1, seg_y1)
483
+
484
+ # R: sidegrob = segmentsGrob(..., gp = do.call(gpar, within(gp, fill <- NA)))
485
+ sidegrob = segments_grob(
486
+ x0=x0,
487
+ y0=y0,
488
+ x1=x1,
489
+ y1=y1,
490
+ name="sidegrob",
491
+ gp=_strip_field(params, "fill"),
492
+ vp=vp,
493
+ )
494
+
495
+ # R: grobTree(rectfill, sidegrob, name = name, vp = vp) -- fill first.
496
+ return grob_tree(rectfill, sidegrob, name=name, vp=vp)
497
+
498
+
499
+ # ---------------------------------------------------------------------------
500
+ # element_grob dispatch wrapper
501
+ # ---------------------------------------------------------------------------
502
+
503
+ # Capture the upstream (possibly already-patched) implementation exactly once.
504
+ _gg_element_grob = getattr(_te, "_ggh4x_orig_element_grob", _te.element_grob)
505
+
506
+
507
+ def element_grob(element: Any, **kwargs: Any) -> Any:
508
+ """Dispatch a theme element to its grob, handling :class:`ElementPartRect`.
509
+
510
+ ``ggplot2_py.theme_elements.element_grob`` is a hard ``isinstance`` chain,
511
+ not S3 dispatch. Because :class:`ElementPartRect` is-a ``ElementRect``, the
512
+ upstream ``ElementRect`` branch would wrongly draw a full rectangle, so this
513
+ wrapper intercepts ``ElementPartRect`` *first* and otherwise defers to the
514
+ upstream function.
515
+
516
+ Parameters
517
+ ----------
518
+ element : Element
519
+ Any theme element.
520
+ **kwargs
521
+ Forwarded to the appropriate renderer.
522
+
523
+ Returns
524
+ -------
525
+ Grob
526
+ A grid grob.
527
+ """
528
+ if isinstance(element, ElementPartRect):
529
+ return _grob_from_part_rect(element, **kwargs)
530
+ return _gg_element_grob(element, **kwargs)
531
+
532
+
533
+ def _install_element_grob_patch() -> None:
534
+ """Monkeypatch ``ggplot2_py.theme_elements.element_grob`` with the wrapper.
535
+
536
+ Stores the original under ``_ggh4x_orig_element_grob`` so re-imports stay
537
+ idempotent, then replaces the module attribute so any code dispatching an
538
+ ``ElementPartRect`` through the upstream function renders partial borders.
539
+ """
540
+ if getattr(_te, "_ggh4x_orig_element_grob", None) is None:
541
+ _te._ggh4x_orig_element_grob = _te.element_grob # type: ignore[attr-defined]
542
+ _te.element_grob = element_grob # type: ignore[assignment]
543
+
544
+
545
+ _install_element_grob_patch()