ggplot2-python 4.0.2.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 (54) hide show
  1. ggplot2_py/__init__.py +852 -0
  2. ggplot2_py/_compat.py +475 -0
  3. ggplot2_py/_plugins.py +129 -0
  4. ggplot2_py/_utils.py +544 -0
  5. ggplot2_py/aes.py +586 -0
  6. ggplot2_py/annotation.py +540 -0
  7. ggplot2_py/coord.py +2108 -0
  8. ggplot2_py/coords/__init__.py +49 -0
  9. ggplot2_py/datasets.py +265 -0
  10. ggplot2_py/draw_key.py +454 -0
  11. ggplot2_py/facet.py +1456 -0
  12. ggplot2_py/fortify.py +95 -0
  13. ggplot2_py/geom.py +4516 -0
  14. ggplot2_py/geoms/__init__.py +12 -0
  15. ggplot2_py/ggproto.py +279 -0
  16. ggplot2_py/guide.py +2925 -0
  17. ggplot2_py/guide_axis.py +615 -0
  18. ggplot2_py/guide_colourbar.py +657 -0
  19. ggplot2_py/guide_legend.py +1061 -0
  20. ggplot2_py/guides/__init__.py +8 -0
  21. ggplot2_py/labeller.py +296 -0
  22. ggplot2_py/labels.py +309 -0
  23. ggplot2_py/layer.py +954 -0
  24. ggplot2_py/layout.py +754 -0
  25. ggplot2_py/limits.py +314 -0
  26. ggplot2_py/plot.py +1401 -0
  27. ggplot2_py/plot_render.py +866 -0
  28. ggplot2_py/position.py +1269 -0
  29. ggplot2_py/protocols.py +171 -0
  30. ggplot2_py/py.typed +0 -0
  31. ggplot2_py/qplot.py +233 -0
  32. ggplot2_py/resources/diamonds.csv +53941 -0
  33. ggplot2_py/resources/economics.csv +575 -0
  34. ggplot2_py/resources/economics_long.csv +2871 -0
  35. ggplot2_py/resources/faithfuld.csv +5626 -0
  36. ggplot2_py/resources/luv_colours.csv +658 -0
  37. ggplot2_py/resources/midwest.csv +438 -0
  38. ggplot2_py/resources/mpg.csv +235 -0
  39. ggplot2_py/resources/msleep.csv +84 -0
  40. ggplot2_py/resources/presidential.csv +13 -0
  41. ggplot2_py/resources/seals.csv +1156 -0
  42. ggplot2_py/resources/txhousing.csv +8603 -0
  43. ggplot2_py/save.py +316 -0
  44. ggplot2_py/scale.py +2727 -0
  45. ggplot2_py/scales/__init__.py +4252 -0
  46. ggplot2_py/stat.py +6071 -0
  47. ggplot2_py/stats/__init__.py +9 -0
  48. ggplot2_py/theme.py +490 -0
  49. ggplot2_py/theme_defaults.py +1350 -0
  50. ggplot2_py/theme_elements.py +2052 -0
  51. ggplot2_python-4.0.2.9000.dist-info/METADATA +179 -0
  52. ggplot2_python-4.0.2.9000.dist-info/RECORD +54 -0
  53. ggplot2_python-4.0.2.9000.dist-info/WHEEL +4 -0
  54. ggplot2_python-4.0.2.9000.dist-info/licenses/LICENSE +3 -0
@@ -0,0 +1,2052 @@
1
+ """
2
+ Theme element classes for ggplot2.
3
+
4
+ Provides the element hierarchy (blank, line, rect, text, point, polygon, geom),
5
+ the ``Rel`` and ``Margin`` helper types, factory functions such as
6
+ ``element_blank()``, ``element_line()``, etc., and the element-tree machinery
7
+ used to resolve theme inheritance via ``calc_element()``.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import copy
13
+ from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
14
+
15
+ from grid_py import (
16
+ Unit,
17
+ Gpar,
18
+ unit_c,
19
+ Grob,
20
+ GTree,
21
+ rect_grob,
22
+ lines_grob,
23
+ polyline_grob,
24
+ text_grob,
25
+ polygon_grob,
26
+ null_grob,
27
+ Viewport,
28
+ edit_grob,
29
+ grob_width,
30
+ grob_height,
31
+ )
32
+
33
+ from ggplot2_py._compat import Waiver, is_waiver, waiver, cli_abort, cli_warn
34
+
35
+ __all__ = [
36
+ "Element",
37
+ "ElementBlank",
38
+ "ElementLine",
39
+ "ElementRect",
40
+ "ElementText",
41
+ "ElementPoint",
42
+ "ElementPolygon",
43
+ "ElementGeom",
44
+ "element_blank",
45
+ "element_line",
46
+ "element_rect",
47
+ "element_text",
48
+ "element_point",
49
+ "element_polygon",
50
+ "element_geom",
51
+ "element_grob",
52
+ "element_render",
53
+ "el_def",
54
+ "merge_element",
55
+ "combine_elements",
56
+ "is_theme_element",
57
+ "Margin",
58
+ "margin",
59
+ "margin_auto",
60
+ "margin_part",
61
+ "is_margin",
62
+ "Rel",
63
+ "rel",
64
+ "is_rel",
65
+ "calc_element",
66
+ "get_element_tree",
67
+ "register_theme_elements",
68
+ "reset_theme_settings",
69
+ ]
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # Graphical-unit conversion constants (R: ggplot2/R/geom-.R, lines 513-517)
74
+ # ---------------------------------------------------------------------------
75
+ # Multiply a size in mm by these to convert to the units that grid uses
76
+ # internally for ``lwd`` and ``fontsize``.
77
+ # .pt = 72.27 / 25.4 — mm → points (for lwd & fontsize)
78
+ # .stroke = 96 / 25.4 — mm → stroke units (for point border widths)
79
+ _PT: float = 72.27 / 25.4
80
+ _STROKE: float = 96 / 25.4
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Rel — relative size multiplier
85
+ # ---------------------------------------------------------------------------
86
+
87
+ class Rel:
88
+ """A relative-size wrapper.
89
+
90
+ Parameters
91
+ ----------
92
+ x : float
93
+ The multiplier applied relative to the parent element's value.
94
+ """
95
+
96
+ __slots__ = ("_x",)
97
+
98
+ def __init__(self, x: float) -> None:
99
+ self._x = float(x)
100
+
101
+ @property
102
+ def value(self) -> float:
103
+ """The numeric multiplier."""
104
+ return self._x
105
+
106
+ # Arithmetic so that ``Rel(0.8) * 11`` works transparently.
107
+ def __mul__(self, other: Any) -> Any:
108
+ if isinstance(other, Rel):
109
+ return Rel(self._x * other._x)
110
+ if isinstance(other, (int, float)):
111
+ return self._x * other
112
+ if isinstance(other, Unit):
113
+ return self._x * other
114
+ return NotImplemented
115
+
116
+ def __rmul__(self, other: Any) -> Any:
117
+ return self.__mul__(other)
118
+
119
+ def __float__(self) -> float:
120
+ return self._x
121
+
122
+ def __repr__(self) -> str:
123
+ return f"rel({self._x})"
124
+
125
+
126
+ def rel(x: float) -> Rel:
127
+ """Create a ``Rel`` (relative-size) object.
128
+
129
+ Parameters
130
+ ----------
131
+ x : float
132
+ Numeric multiplier specifying size relative to the parent element.
133
+
134
+ Returns
135
+ -------
136
+ Rel
137
+ A relative-size wrapper.
138
+ """
139
+ return Rel(x)
140
+
141
+
142
+ def is_rel(x: Any) -> bool:
143
+ """Test whether *x* is a ``Rel`` object.
144
+
145
+ Parameters
146
+ ----------
147
+ x : Any
148
+ Object to test.
149
+
150
+ Returns
151
+ -------
152
+ bool
153
+ """
154
+ return isinstance(x, Rel)
155
+
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # Margin
159
+ # ---------------------------------------------------------------------------
160
+
161
+ class Margin:
162
+ """A four-sided margin (top, right, bottom, left) stored as a ``Unit``.
163
+
164
+ Parameters
165
+ ----------
166
+ t : float
167
+ Top margin value.
168
+ r : float
169
+ Right margin value.
170
+ b : float
171
+ Bottom margin value.
172
+ l : float
173
+ Left margin value.
174
+ unit : str
175
+ Unit string (default ``"pt"``).
176
+ """
177
+
178
+ __slots__ = ("_values", "_unit_str", "_unit")
179
+
180
+ def __init__(
181
+ self,
182
+ t: float = 0.0,
183
+ r: float = 0.0,
184
+ b: float = 0.0,
185
+ l: float = 0.0,
186
+ unit: str = "pt",
187
+ ) -> None:
188
+ self._values: Tuple[float, float, float, float] = (
189
+ float(t),
190
+ float(r),
191
+ float(b),
192
+ float(l),
193
+ )
194
+ self._unit_str = unit
195
+ self._unit = Unit(list(self._values), unit)
196
+
197
+ # Named accessors
198
+ @property
199
+ def t(self) -> float:
200
+ """Top margin."""
201
+ return self._values[0]
202
+
203
+ @property
204
+ def r(self) -> float:
205
+ """Right margin."""
206
+ return self._values[1]
207
+
208
+ @property
209
+ def b(self) -> float:
210
+ """Bottom margin."""
211
+ return self._values[2]
212
+
213
+ @property
214
+ def l(self) -> float:
215
+ """Left margin."""
216
+ return self._values[3]
217
+
218
+ @property
219
+ def unit_str(self) -> str:
220
+ """The unit string."""
221
+ return self._unit_str
222
+
223
+ @property
224
+ def unit(self) -> Unit:
225
+ """The underlying ``grid_py.Unit`` object."""
226
+ return self._unit
227
+
228
+ def __getitem__(self, idx: int) -> float:
229
+ return self._values[idx]
230
+
231
+ def __len__(self) -> int:
232
+ return 4
233
+
234
+ def __iter__(self):
235
+ return iter(self._values)
236
+
237
+ def __repr__(self) -> str:
238
+ return (
239
+ f"margin(t={self.t}, r={self.r}, b={self.b}, l={self.l}, "
240
+ f"unit={self._unit_str!r})"
241
+ )
242
+
243
+ def __eq__(self, other: object) -> bool:
244
+ if isinstance(other, Margin):
245
+ return self._values == other._values and self._unit_str == other._unit_str
246
+ return NotImplemented
247
+
248
+
249
+ def margin(
250
+ t: float = 0.0,
251
+ r: float = 0.0,
252
+ b: float = 0.0,
253
+ l: float = 0.0,
254
+ unit: str = "pt",
255
+ ) -> Margin:
256
+ """Create a ``Margin`` object.
257
+
258
+ Parameters
259
+ ----------
260
+ t : float
261
+ Top margin.
262
+ r : float
263
+ Right margin.
264
+ b : float
265
+ Bottom margin.
266
+ l : float
267
+ Left margin.
268
+ unit : str
269
+ Measurement unit (default ``"pt"``).
270
+
271
+ Returns
272
+ -------
273
+ Margin
274
+ A four-sided margin.
275
+ """
276
+ return Margin(t=t, r=r, b=b, l=l, unit=unit)
277
+
278
+
279
+ def margin_auto(
280
+ t: float = 0.0,
281
+ r: Optional[float] = None,
282
+ b: Optional[float] = None,
283
+ l: Optional[float] = None,
284
+ unit: str = "pt",
285
+ ) -> Margin:
286
+ """Create a ``Margin`` with auto-recycling (CSS-like shorthand).
287
+
288
+ Parameters
289
+ ----------
290
+ t : float
291
+ Top margin.
292
+ r : float, optional
293
+ Right margin. Defaults to *t*.
294
+ b : float, optional
295
+ Bottom margin. Defaults to *t*.
296
+ l : float, optional
297
+ Left margin. Defaults to *r*.
298
+ unit : str
299
+ Measurement unit (default ``"pt"``).
300
+
301
+ Returns
302
+ -------
303
+ Margin
304
+ """
305
+ if r is None:
306
+ r = t
307
+ if b is None:
308
+ b = t
309
+ if l is None:
310
+ l = r
311
+ return Margin(t=t, r=r, b=b, l=l, unit=unit)
312
+
313
+
314
+ def margin_part(
315
+ t: float = float("nan"),
316
+ r: float = float("nan"),
317
+ b: float = float("nan"),
318
+ l: float = float("nan"),
319
+ unit: str = "pt",
320
+ ) -> Margin:
321
+ """Create a partial ``Margin`` (unset sides are ``NaN``).
322
+
323
+ Parameters
324
+ ----------
325
+ t, r, b, l : float
326
+ Margin values; NaN means "inherit from parent".
327
+ unit : str
328
+ Measurement unit.
329
+
330
+ Returns
331
+ -------
332
+ Margin
333
+ """
334
+ return Margin(t=t, r=r, b=b, l=l, unit=unit)
335
+
336
+
337
+ def is_margin(x: Any) -> bool:
338
+ """Test whether *x* is a ``Margin`` object.
339
+
340
+ Parameters
341
+ ----------
342
+ x : Any
343
+ Object to test.
344
+
345
+ Returns
346
+ -------
347
+ bool
348
+ """
349
+ return isinstance(x, Margin)
350
+
351
+
352
+ # ---------------------------------------------------------------------------
353
+ # Element base & subclasses
354
+ # ---------------------------------------------------------------------------
355
+
356
+ class Element:
357
+ """Abstract base class for theme elements.
358
+
359
+ All concrete element classes inherit from this.
360
+ """
361
+
362
+ @property
363
+ def blank(self) -> bool:
364
+ """Whether this element draws nothing."""
365
+ return False
366
+
367
+ def merge(self, other: "Element") -> "Element":
368
+ """Merge *other* (parent) into this element, filling ``None`` slots.
369
+
370
+ Parameters
371
+ ----------
372
+ other : Element
373
+ The parent element to inherit from.
374
+
375
+ Returns
376
+ -------
377
+ Element
378
+ A new element with ``None`` properties filled from *other*.
379
+ """
380
+ return merge_element(self, other)
381
+
382
+
383
+ class ElementBlank(Element):
384
+ """An element that draws nothing and allocates no space.
385
+
386
+ Parameters
387
+ ----------
388
+ inherit_blank : bool
389
+ Kept for interface consistency; always ``True`` conceptually.
390
+ """
391
+
392
+ def __init__(self, inherit_blank: bool = True) -> None:
393
+ self.inherit_blank = inherit_blank
394
+
395
+ @property
396
+ def blank(self) -> bool:
397
+ return True
398
+
399
+ def __repr__(self) -> str:
400
+ return "element_blank()"
401
+
402
+
403
+ class ElementLine(Element):
404
+ """Theme element for lines.
405
+
406
+ Parameters
407
+ ----------
408
+ colour : str or None
409
+ Line colour.
410
+ linewidth : float or None
411
+ Line width in mm.
412
+ linetype : int, str, or None
413
+ Line type.
414
+ lineend : str or None
415
+ Line end style (``"round"``, ``"butt"``, ``"square"``).
416
+ linejoin : str or None
417
+ Line join style (``"round"``, ``"mitre"``, ``"bevel"``).
418
+ arrow : object or None
419
+ Arrow specification (from ``grid_py.arrow``).
420
+ arrow_fill : str or None
421
+ Fill colour for closed arrow heads.
422
+ inherit_blank : bool
423
+ Whether to inherit ``element_blank`` from parents.
424
+ """
425
+
426
+ def __init__(
427
+ self,
428
+ colour: Optional[str] = None,
429
+ linewidth: Optional[float] = None,
430
+ linetype: Optional[Union[int, str]] = None,
431
+ lineend: Optional[str] = None,
432
+ linejoin: Optional[str] = None,
433
+ arrow: Optional[Any] = None,
434
+ arrow_fill: Optional[str] = None,
435
+ inherit_blank: bool = False,
436
+ ) -> None:
437
+ self.colour = colour
438
+ self.linewidth = linewidth
439
+ self.linetype = linetype
440
+ self.lineend = lineend
441
+ self.linejoin = linejoin
442
+ self.arrow = arrow
443
+ self.arrow_fill = arrow_fill
444
+ self.inherit_blank = inherit_blank
445
+
446
+ def __repr__(self) -> str:
447
+ parts = []
448
+ for attr in (
449
+ "colour",
450
+ "linewidth",
451
+ "linetype",
452
+ "lineend",
453
+ "linejoin",
454
+ "arrow",
455
+ "arrow_fill",
456
+ "inherit_blank",
457
+ ):
458
+ val = getattr(self, attr)
459
+ if val is not None and val is not False:
460
+ parts.append(f"{attr}={val!r}")
461
+ return f"element_line({', '.join(parts)})"
462
+
463
+
464
+ class ElementRect(Element):
465
+ """Theme element for rectangles (borders and backgrounds).
466
+
467
+ Parameters
468
+ ----------
469
+ fill : str or None
470
+ Fill colour.
471
+ colour : str or None
472
+ Border colour.
473
+ linewidth : float or None
474
+ Border width in mm.
475
+ linetype : int, str, or None
476
+ Border line type.
477
+ linejoin : str or None
478
+ Line join style.
479
+ inherit_blank : bool
480
+ Whether to inherit ``element_blank`` from parents.
481
+ """
482
+
483
+ def __init__(
484
+ self,
485
+ fill: Optional[str] = None,
486
+ colour: Optional[str] = None,
487
+ linewidth: Optional[float] = None,
488
+ linetype: Optional[Union[int, str]] = None,
489
+ linejoin: Optional[str] = None,
490
+ inherit_blank: bool = False,
491
+ ) -> None:
492
+ self.fill = fill
493
+ self.colour = colour
494
+ self.linewidth = linewidth
495
+ self.linetype = linetype
496
+ self.linejoin = linejoin
497
+ self.inherit_blank = inherit_blank
498
+
499
+ def __repr__(self) -> str:
500
+ parts = []
501
+ for attr in ("fill", "colour", "linewidth", "linetype", "linejoin", "inherit_blank"):
502
+ val = getattr(self, attr)
503
+ if val is not None and val is not False:
504
+ parts.append(f"{attr}={val!r}")
505
+ return f"element_rect({', '.join(parts)})"
506
+
507
+
508
+ class ElementText(Element):
509
+ """Theme element for text.
510
+
511
+ Parameters
512
+ ----------
513
+ family : str or None
514
+ Font family.
515
+ face : str or None
516
+ Font face (``"plain"``, ``"italic"``, ``"bold"``, ``"bold.italic"``).
517
+ colour : str or None
518
+ Text colour.
519
+ size : float, Rel, or None
520
+ Font size in points (or ``Rel`` for relative sizing).
521
+ hjust : float or None
522
+ Horizontal justification (0--1).
523
+ vjust : float or None
524
+ Vertical justification (0--1).
525
+ angle : float or None
526
+ Rotation angle in degrees.
527
+ lineheight : float or None
528
+ Line height multiplier.
529
+ margin : Margin or None
530
+ Margins around the text.
531
+ debug : bool or None
532
+ If ``True``, draw debugging annotations.
533
+ inherit_blank : bool
534
+ Whether to inherit ``element_blank`` from parents.
535
+ """
536
+
537
+ def __init__(
538
+ self,
539
+ family: Optional[str] = None,
540
+ face: Optional[str] = None,
541
+ colour: Optional[str] = None,
542
+ size: Optional[Union[float, Rel]] = None,
543
+ hjust: Optional[float] = None,
544
+ vjust: Optional[float] = None,
545
+ angle: Optional[float] = None,
546
+ lineheight: Optional[float] = None,
547
+ margin: Optional[Margin] = None,
548
+ debug: Optional[bool] = None,
549
+ inherit_blank: bool = False,
550
+ ) -> None:
551
+ self.family = family
552
+ self.face = face
553
+ self.colour = colour
554
+ self.size = size
555
+ self.hjust = hjust
556
+ self.vjust = vjust
557
+ self.angle = angle
558
+ self.lineheight = lineheight
559
+ self.margin = margin
560
+ self.debug = debug
561
+ self.inherit_blank = inherit_blank
562
+
563
+ def __repr__(self) -> str:
564
+ parts = []
565
+ for attr in (
566
+ "family",
567
+ "face",
568
+ "colour",
569
+ "size",
570
+ "hjust",
571
+ "vjust",
572
+ "angle",
573
+ "lineheight",
574
+ "margin",
575
+ "debug",
576
+ "inherit_blank",
577
+ ):
578
+ val = getattr(self, attr)
579
+ if val is not None and val is not False:
580
+ parts.append(f"{attr}={val!r}")
581
+ return f"element_text({', '.join(parts)})"
582
+
583
+
584
+ class ElementPoint(Element):
585
+ """Theme element for points.
586
+
587
+ Parameters
588
+ ----------
589
+ shape : int, str, or None
590
+ Point shape.
591
+ colour : str or None
592
+ Point colour.
593
+ fill : str or None
594
+ Point fill colour.
595
+ size : float or None
596
+ Point size in mm.
597
+ stroke : float or None
598
+ Stroke width.
599
+ inherit_blank : bool
600
+ Whether to inherit ``element_blank`` from parents.
601
+ """
602
+
603
+ def __init__(
604
+ self,
605
+ shape: Optional[Union[int, str]] = None,
606
+ colour: Optional[str] = None,
607
+ fill: Optional[str] = None,
608
+ size: Optional[float] = None,
609
+ stroke: Optional[float] = None,
610
+ inherit_blank: bool = False,
611
+ ) -> None:
612
+ self.shape = shape
613
+ self.colour = colour
614
+ self.fill = fill
615
+ self.size = size
616
+ self.stroke = stroke
617
+ self.inherit_blank = inherit_blank
618
+
619
+ def __repr__(self) -> str:
620
+ parts = []
621
+ for attr in ("shape", "colour", "fill", "size", "stroke", "inherit_blank"):
622
+ val = getattr(self, attr)
623
+ if val is not None and val is not False:
624
+ parts.append(f"{attr}={val!r}")
625
+ return f"element_point({', '.join(parts)})"
626
+
627
+
628
+ class ElementPolygon(Element):
629
+ """Theme element for polygons.
630
+
631
+ Parameters
632
+ ----------
633
+ colour : str or None
634
+ Border colour.
635
+ fill : str or None
636
+ Fill colour.
637
+ linewidth : float or None
638
+ Border width in mm.
639
+ linetype : int, str, or None
640
+ Border line type.
641
+ linejoin : str or None
642
+ Line join style.
643
+ inherit_blank : bool
644
+ Whether to inherit ``element_blank`` from parents.
645
+ """
646
+
647
+ def __init__(
648
+ self,
649
+ colour: Optional[str] = None,
650
+ fill: Optional[str] = None,
651
+ linewidth: Optional[float] = None,
652
+ linetype: Optional[Union[int, str]] = None,
653
+ linejoin: Optional[str] = None,
654
+ inherit_blank: bool = False,
655
+ ) -> None:
656
+ self.colour = colour
657
+ self.fill = fill
658
+ self.linewidth = linewidth
659
+ self.linetype = linetype
660
+ self.linejoin = linejoin
661
+ self.inherit_blank = inherit_blank
662
+
663
+ def __repr__(self) -> str:
664
+ parts = []
665
+ for attr in ("colour", "fill", "linewidth", "linetype", "linejoin", "inherit_blank"):
666
+ val = getattr(self, attr)
667
+ if val is not None and val is not False:
668
+ parts.append(f"{attr}={val!r}")
669
+ return f"element_polygon({', '.join(parts)})"
670
+
671
+
672
+ class ElementGeom(Element):
673
+ """Theme element for global geom defaults.
674
+
675
+ Parameters
676
+ ----------
677
+ ink : str or None
678
+ Foreground colour.
679
+ paper : str or None
680
+ Background colour.
681
+ accent : str or None
682
+ Accent colour.
683
+ linewidth : float or None
684
+ Default line width in mm.
685
+ borderwidth : float or None
686
+ Default border width in mm.
687
+ linetype : int, str, or None
688
+ Default line type.
689
+ bordertype : int, str, or None
690
+ Default border type.
691
+ family : str or None
692
+ Default font family.
693
+ fontsize : float or None
694
+ Default font size in points.
695
+ pointsize : float or None
696
+ Default point size in mm.
697
+ pointshape : int or None
698
+ Default point shape.
699
+ colour : str or None
700
+ Explicit colour override.
701
+ fill : str or None
702
+ Explicit fill override.
703
+ """
704
+
705
+ def __init__(
706
+ self,
707
+ ink: Optional[str] = None,
708
+ paper: Optional[str] = None,
709
+ accent: Optional[str] = None,
710
+ linewidth: Optional[float] = None,
711
+ borderwidth: Optional[float] = None,
712
+ linetype: Optional[Union[int, str]] = None,
713
+ bordertype: Optional[Union[int, str]] = None,
714
+ family: Optional[str] = None,
715
+ fontsize: Optional[float] = None,
716
+ pointsize: Optional[float] = None,
717
+ pointshape: Optional[int] = None,
718
+ colour: Optional[str] = None,
719
+ fill: Optional[str] = None,
720
+ ) -> None:
721
+ self.ink = ink
722
+ self.paper = paper
723
+ self.accent = accent
724
+ self.linewidth = linewidth
725
+ self.borderwidth = borderwidth
726
+ self.linetype = linetype
727
+ self.bordertype = bordertype
728
+ self.family = family
729
+ self.fontsize = fontsize
730
+ self.pointsize = pointsize
731
+ self.pointshape = pointshape
732
+ self.colour = colour
733
+ self.fill = fill
734
+
735
+ def __repr__(self) -> str:
736
+ parts = []
737
+ for attr in (
738
+ "ink",
739
+ "paper",
740
+ "accent",
741
+ "linewidth",
742
+ "borderwidth",
743
+ "linetype",
744
+ "bordertype",
745
+ "family",
746
+ "fontsize",
747
+ "pointsize",
748
+ "pointshape",
749
+ "colour",
750
+ "fill",
751
+ ):
752
+ val = getattr(self, attr)
753
+ if val is not None:
754
+ parts.append(f"{attr}={val!r}")
755
+ return f"element_geom({', '.join(parts)})"
756
+
757
+
758
+ # ---------------------------------------------------------------------------
759
+ # Factory functions
760
+ # ---------------------------------------------------------------------------
761
+
762
+ def element_blank() -> ElementBlank:
763
+ """Create a blank element that draws nothing.
764
+
765
+ Returns
766
+ -------
767
+ ElementBlank
768
+ """
769
+ return ElementBlank()
770
+
771
+
772
+ def element_line(
773
+ colour: Optional[str] = None,
774
+ linewidth: Optional[float] = None,
775
+ linetype: Optional[Union[int, str]] = None,
776
+ lineend: Optional[str] = None,
777
+ linejoin: Optional[str] = None,
778
+ arrow: Optional[Any] = None,
779
+ arrow_fill: Optional[str] = None,
780
+ color: Optional[str] = None,
781
+ inherit_blank: bool = False,
782
+ ) -> ElementLine:
783
+ """Create a line theme element.
784
+
785
+ Parameters
786
+ ----------
787
+ colour : str, optional
788
+ Line colour.
789
+ linewidth : float, optional
790
+ Line width in mm.
791
+ linetype : int or str, optional
792
+ Line type.
793
+ lineend : str, optional
794
+ Line end style.
795
+ linejoin : str, optional
796
+ Line join style.
797
+ arrow : object, optional
798
+ Arrow specification.
799
+ arrow_fill : str, optional
800
+ Arrow fill colour.
801
+ color : str, optional
802
+ Alias for *colour*.
803
+ inherit_blank : bool
804
+ Inherit blank from parents (default ``False``).
805
+
806
+ Returns
807
+ -------
808
+ ElementLine
809
+ """
810
+ colour = color if colour is None else colour
811
+ return ElementLine(
812
+ colour=colour,
813
+ linewidth=linewidth,
814
+ linetype=linetype,
815
+ lineend=lineend,
816
+ linejoin=linejoin,
817
+ arrow=arrow,
818
+ arrow_fill=arrow_fill,
819
+ inherit_blank=inherit_blank,
820
+ )
821
+
822
+
823
+ def element_rect(
824
+ fill: Optional[str] = None,
825
+ colour: Optional[str] = None,
826
+ linewidth: Optional[float] = None,
827
+ linetype: Optional[Union[int, str]] = None,
828
+ color: Optional[str] = None,
829
+ linejoin: Optional[str] = None,
830
+ inherit_blank: bool = False,
831
+ ) -> ElementRect:
832
+ """Create a rectangle theme element.
833
+
834
+ Parameters
835
+ ----------
836
+ fill : str, optional
837
+ Fill colour.
838
+ colour : str, optional
839
+ Border colour.
840
+ linewidth : float, optional
841
+ Border width in mm.
842
+ linetype : int or str, optional
843
+ Border line type.
844
+ color : str, optional
845
+ Alias for *colour*.
846
+ linejoin : str, optional
847
+ Line join style.
848
+ inherit_blank : bool
849
+ Inherit blank from parents (default ``False``).
850
+
851
+ Returns
852
+ -------
853
+ ElementRect
854
+ """
855
+ colour = color if colour is None else colour
856
+ return ElementRect(
857
+ fill=fill,
858
+ colour=colour,
859
+ linewidth=linewidth,
860
+ linetype=linetype,
861
+ linejoin=linejoin,
862
+ inherit_blank=inherit_blank,
863
+ )
864
+
865
+
866
+ def element_text(
867
+ family: Optional[str] = None,
868
+ face: Optional[str] = None,
869
+ colour: Optional[str] = None,
870
+ size: Optional[Union[float, Rel]] = None,
871
+ hjust: Optional[float] = None,
872
+ vjust: Optional[float] = None,
873
+ angle: Optional[float] = None,
874
+ lineheight: Optional[float] = None,
875
+ color: Optional[str] = None,
876
+ margin: Optional[Margin] = None,
877
+ debug: Optional[bool] = None,
878
+ inherit_blank: bool = False,
879
+ ) -> ElementText:
880
+ """Create a text theme element.
881
+
882
+ Parameters
883
+ ----------
884
+ family : str, optional
885
+ Font family.
886
+ face : str, optional
887
+ Font face.
888
+ colour : str, optional
889
+ Text colour.
890
+ size : float or Rel, optional
891
+ Font size in points, or a ``Rel`` for relative sizing.
892
+ hjust : float, optional
893
+ Horizontal justification (0--1).
894
+ vjust : float, optional
895
+ Vertical justification (0--1).
896
+ angle : float, optional
897
+ Text rotation angle in degrees.
898
+ lineheight : float, optional
899
+ Line height multiplier.
900
+ color : str, optional
901
+ Alias for *colour*.
902
+ margin : Margin, optional
903
+ Margins around the text.
904
+ debug : bool, optional
905
+ Draw debug annotations.
906
+ inherit_blank : bool
907
+ Inherit blank from parents (default ``False``).
908
+
909
+ Returns
910
+ -------
911
+ ElementText
912
+ """
913
+ colour = color if colour is None else colour
914
+ return ElementText(
915
+ family=family,
916
+ face=face,
917
+ colour=colour,
918
+ size=size,
919
+ hjust=hjust,
920
+ vjust=vjust,
921
+ angle=angle,
922
+ lineheight=lineheight,
923
+ margin=margin,
924
+ debug=debug,
925
+ inherit_blank=inherit_blank,
926
+ )
927
+
928
+
929
+ def element_point(
930
+ shape: Optional[Union[int, str]] = None,
931
+ colour: Optional[str] = None,
932
+ fill: Optional[str] = None,
933
+ size: Optional[float] = None,
934
+ stroke: Optional[float] = None,
935
+ color: Optional[str] = None,
936
+ inherit_blank: bool = False,
937
+ ) -> ElementPoint:
938
+ """Create a point theme element.
939
+
940
+ Parameters
941
+ ----------
942
+ shape : int or str, optional
943
+ Point shape.
944
+ colour : str, optional
945
+ Point colour.
946
+ fill : str, optional
947
+ Point fill colour.
948
+ size : float, optional
949
+ Point size in mm.
950
+ stroke : float, optional
951
+ Stroke width.
952
+ color : str, optional
953
+ Alias for *colour*.
954
+ inherit_blank : bool
955
+ Inherit blank from parents (default ``False``).
956
+
957
+ Returns
958
+ -------
959
+ ElementPoint
960
+ """
961
+ colour = color if colour is None else colour
962
+ return ElementPoint(
963
+ shape=shape,
964
+ colour=colour,
965
+ fill=fill,
966
+ size=size,
967
+ stroke=stroke,
968
+ inherit_blank=inherit_blank,
969
+ )
970
+
971
+
972
+ def element_polygon(
973
+ colour: Optional[str] = None,
974
+ fill: Optional[str] = None,
975
+ linewidth: Optional[float] = None,
976
+ linetype: Optional[Union[int, str]] = None,
977
+ color: Optional[str] = None,
978
+ linejoin: Optional[str] = None,
979
+ inherit_blank: bool = False,
980
+ ) -> ElementPolygon:
981
+ """Create a polygon theme element.
982
+
983
+ Parameters
984
+ ----------
985
+ colour : str, optional
986
+ Border colour.
987
+ fill : str, optional
988
+ Fill colour.
989
+ linewidth : float, optional
990
+ Border width in mm.
991
+ linetype : int or str, optional
992
+ Border line type.
993
+ color : str, optional
994
+ Alias for *colour*.
995
+ linejoin : str, optional
996
+ Line join style.
997
+ inherit_blank : bool
998
+ Inherit blank from parents (default ``False``).
999
+
1000
+ Returns
1001
+ -------
1002
+ ElementPolygon
1003
+ """
1004
+ colour = color if colour is None else colour
1005
+ return ElementPolygon(
1006
+ colour=colour,
1007
+ fill=fill,
1008
+ linewidth=linewidth,
1009
+ linetype=linetype,
1010
+ linejoin=linejoin,
1011
+ inherit_blank=inherit_blank,
1012
+ )
1013
+
1014
+
1015
+ def element_geom(
1016
+ ink: Optional[str] = None,
1017
+ paper: Optional[str] = None,
1018
+ accent: Optional[str] = None,
1019
+ linewidth: Optional[float] = None,
1020
+ borderwidth: Optional[float] = None,
1021
+ linetype: Optional[Union[int, str]] = None,
1022
+ bordertype: Optional[Union[int, str]] = None,
1023
+ family: Optional[str] = None,
1024
+ fontsize: Optional[float] = None,
1025
+ pointsize: Optional[float] = None,
1026
+ pointshape: Optional[int] = None,
1027
+ colour: Optional[str] = None,
1028
+ color: Optional[str] = None,
1029
+ fill: Optional[str] = None,
1030
+ ) -> ElementGeom:
1031
+ """Create a geom defaults theme element.
1032
+
1033
+ Parameters
1034
+ ----------
1035
+ ink : str, optional
1036
+ Foreground colour.
1037
+ paper : str, optional
1038
+ Background colour.
1039
+ accent : str, optional
1040
+ Accent colour.
1041
+ linewidth : float, optional
1042
+ Default line width in mm.
1043
+ borderwidth : float, optional
1044
+ Default border width in mm.
1045
+ linetype : int or str, optional
1046
+ Default line type.
1047
+ bordertype : int or str, optional
1048
+ Default border type.
1049
+ family : str, optional
1050
+ Default font family.
1051
+ fontsize : float, optional
1052
+ Default font size in points.
1053
+ pointsize : float, optional
1054
+ Default point size in mm.
1055
+ pointshape : int, optional
1056
+ Default point shape.
1057
+ colour : str, optional
1058
+ Explicit colour override.
1059
+ color : str, optional
1060
+ Alias for *colour*.
1061
+ fill : str, optional
1062
+ Explicit fill override.
1063
+
1064
+ Returns
1065
+ -------
1066
+ ElementGeom
1067
+ """
1068
+ colour = color if colour is None else colour
1069
+ return ElementGeom(
1070
+ ink=ink,
1071
+ paper=paper,
1072
+ accent=accent,
1073
+ linewidth=linewidth,
1074
+ borderwidth=borderwidth,
1075
+ linetype=linetype,
1076
+ bordertype=bordertype,
1077
+ family=family,
1078
+ fontsize=fontsize,
1079
+ pointsize=pointsize,
1080
+ pointshape=pointshape,
1081
+ colour=colour,
1082
+ fill=fill,
1083
+ )
1084
+
1085
+
1086
+ # ---------------------------------------------------------------------------
1087
+ # Type predicates
1088
+ # ---------------------------------------------------------------------------
1089
+
1090
+ _TYPE_MAP: Dict[str, type] = {
1091
+ "any": Element,
1092
+ "blank": ElementBlank,
1093
+ "line": ElementLine,
1094
+ "rect": ElementRect,
1095
+ "text": ElementText,
1096
+ "point": ElementPoint,
1097
+ "polygon": ElementPolygon,
1098
+ "geom": ElementGeom,
1099
+ }
1100
+
1101
+
1102
+ def is_theme_element(x: Any, type_: str = "any") -> bool:
1103
+ """Test whether *x* is a theme element, optionally of a specific type.
1104
+
1105
+ Parameters
1106
+ ----------
1107
+ x : Any
1108
+ Object to test.
1109
+ type_ : str
1110
+ One of ``"any"``, ``"blank"``, ``"rect"``, ``"line"``, ``"text"``,
1111
+ ``"polygon"``, ``"point"``, ``"geom"``.
1112
+
1113
+ Returns
1114
+ -------
1115
+ bool
1116
+ """
1117
+ cls = _TYPE_MAP.get(type_, None)
1118
+ if cls is None:
1119
+ return False
1120
+ return isinstance(x, cls)
1121
+
1122
+
1123
+ # ---------------------------------------------------------------------------
1124
+ # Helper to get "properties" dict from an element (for merging)
1125
+ # ---------------------------------------------------------------------------
1126
+
1127
+ def _element_props(el: Element) -> Dict[str, Any]:
1128
+ """Return a dict of the element's settable properties."""
1129
+ if isinstance(el, ElementBlank):
1130
+ return {"inherit_blank": el.inherit_blank}
1131
+ return {k: v for k, v in el.__dict__.items()}
1132
+
1133
+
1134
+ def _element_prop_names(el: Element) -> List[str]:
1135
+ """Return the property names of an element (excluding inherit_blank for checks)."""
1136
+ return list(el.__dict__.keys())
1137
+
1138
+
1139
+ # ---------------------------------------------------------------------------
1140
+ # Merge & combine
1141
+ # ---------------------------------------------------------------------------
1142
+
1143
+ def merge_element(new: Any, old: Any) -> Any:
1144
+ """Merge a child element (*new*) with a parent element (*old*).
1145
+
1146
+ Properties that are ``None`` in *new* are filled from *old*.
1147
+
1148
+ Parameters
1149
+ ----------
1150
+ new : Element or other
1151
+ The child element.
1152
+ old : Element or other
1153
+ The parent element.
1154
+
1155
+ Returns
1156
+ -------
1157
+ Element or other
1158
+ A copy of *new* with ``None`` properties filled from *old*.
1159
+ """
1160
+ if old is None or isinstance(old, ElementBlank):
1161
+ return new
1162
+ if new is None or isinstance(new, (str, int, float, bool)):
1163
+ return new
1164
+ if isinstance(new, ElementBlank):
1165
+ return new
1166
+ if isinstance(new, Unit):
1167
+ return new
1168
+ if isinstance(new, Margin):
1169
+ return new
1170
+ if not isinstance(new, Element) or not isinstance(old, Element):
1171
+ return new
1172
+
1173
+ # Classes must be compatible for merging
1174
+ if type(new) is not type(old):
1175
+ # Allow merging if new's class is a subclass of old's
1176
+ if not isinstance(new, type(old)):
1177
+ cli_abort(
1178
+ f"Only elements of the same class can be merged, "
1179
+ f"got {type(new).__name__} and {type(old).__name__}."
1180
+ )
1181
+
1182
+ result = copy.copy(new)
1183
+ for attr in old.__dict__:
1184
+ if attr in result.__dict__ and getattr(result, attr) is None:
1185
+ setattr(result, attr, getattr(old, attr))
1186
+ return result
1187
+
1188
+
1189
+ def combine_elements(e1: Any, e2: Any) -> Any:
1190
+ """Combine element *e1* with its parent *e2* (full inheritance resolution).
1191
+
1192
+ Unlike ``merge_element``, this also resolves ``Rel`` sizes and
1193
+ handles ``element_blank`` inheritance.
1194
+
1195
+ Parameters
1196
+ ----------
1197
+ e1 : Any
1198
+ The child element (or value).
1199
+ e2 : Any
1200
+ The parent element (or value) from which *e1* inherits.
1201
+
1202
+ Returns
1203
+ -------
1204
+ Any
1205
+ The resolved element.
1206
+ """
1207
+ # If e2 is None, nothing to inherit
1208
+ if e2 is None or isinstance(e1, ElementBlank):
1209
+ return e1
1210
+
1211
+ # If e1 is None, inherit everything from e2
1212
+ if e1 is None:
1213
+ return e2
1214
+
1215
+ # Rel handling
1216
+ if isinstance(e1, Rel):
1217
+ if isinstance(e2, Rel):
1218
+ return Rel(e1.value * e2.value)
1219
+ if isinstance(e2, (int, float)):
1220
+ return e1.value * e2
1221
+ if isinstance(e2, Unit):
1222
+ return e1.value * e2
1223
+ return e1
1224
+
1225
+ # Margin merging
1226
+ if isinstance(e1, Margin) and isinstance(e2, Margin):
1227
+ import math
1228
+
1229
+ t = e2.t if math.isnan(e1.t) else e1.t
1230
+ r = e2.r if math.isnan(e1.r) else e1.r
1231
+ b = e2.b if math.isnan(e1.b) else e1.b
1232
+ l = e2.l if math.isnan(e1.l) else e1.l
1233
+ return Margin(t=t, r=r, b=b, l=l, unit=e1.unit_str)
1234
+
1235
+ # If neither is an Element, return e1
1236
+ if not isinstance(e1, Element) and not isinstance(e2, Element):
1237
+ return e1
1238
+
1239
+ # If e2 is blank and e1 inherits blank, return e2
1240
+ if isinstance(e2, ElementBlank):
1241
+ if isinstance(e1, Element) and getattr(e1, "inherit_blank", False):
1242
+ return e2
1243
+ return e1
1244
+
1245
+ # Fill None properties of e1 from e2
1246
+ if isinstance(e1, Element) and isinstance(e2, Element):
1247
+ result = copy.copy(e1)
1248
+ for attr in e2.__dict__:
1249
+ if attr in result.__dict__ and getattr(result, attr) is None:
1250
+ setattr(result, attr, getattr(e2, attr))
1251
+
1252
+ # Resolve relative sizes
1253
+ if hasattr(result, "size") and isinstance(result.size, Rel):
1254
+ parent_size = getattr(e2, "size", None)
1255
+ if parent_size is not None and not isinstance(parent_size, Rel):
1256
+ result.size = result.size.value * parent_size
1257
+
1258
+ # Resolve relative linewidth
1259
+ if hasattr(result, "linewidth") and isinstance(result.linewidth, Rel):
1260
+ parent_lw = getattr(e2, "linewidth", None)
1261
+ if parent_lw is not None and not isinstance(parent_lw, Rel):
1262
+ result.linewidth = result.linewidth.value * parent_lw
1263
+
1264
+ # Resolve margin inheritance for text elements
1265
+ if isinstance(result, ElementText) and result.margin is not None:
1266
+ parent_margin = getattr(e2, "margin", None)
1267
+ if parent_margin is not None:
1268
+ result.margin = combine_elements(result.margin, parent_margin)
1269
+
1270
+ return result
1271
+
1272
+ return e1
1273
+
1274
+
1275
+ # ---------------------------------------------------------------------------
1276
+ # Element grob rendering
1277
+ # ---------------------------------------------------------------------------
1278
+
1279
+ def element_grob(element: Element, **kwargs: Any) -> Any:
1280
+ """Generate a grid grob from a theme element.
1281
+
1282
+ Parameters
1283
+ ----------
1284
+ element : Element
1285
+ A theme element (``ElementLine``, ``ElementRect``, ``ElementText``,
1286
+ ``ElementBlank``, etc.).
1287
+ **kwargs
1288
+ Additional arguments controlling rendering (e.g. position, labels).
1289
+
1290
+ Returns
1291
+ -------
1292
+ Grob
1293
+ A grid grob.
1294
+ """
1295
+ if isinstance(element, ElementBlank):
1296
+ return null_grob()
1297
+
1298
+ if isinstance(element, ElementRect):
1299
+ return _grob_from_rect(element, **kwargs)
1300
+
1301
+ if isinstance(element, ElementLine):
1302
+ return _grob_from_line(element, **kwargs)
1303
+
1304
+ if isinstance(element, ElementText):
1305
+ return _grob_from_text(element, **kwargs)
1306
+
1307
+ if isinstance(element, ElementPoint):
1308
+ return _grob_from_point(element, **kwargs)
1309
+
1310
+ if isinstance(element, ElementPolygon):
1311
+ return _grob_from_polygon(element, **kwargs)
1312
+
1313
+ if isinstance(element, ElementGeom):
1314
+ # ElementGeom defines global defaults; not directly rendered.
1315
+ return null_grob()
1316
+
1317
+ # Fallback
1318
+ return null_grob()
1319
+
1320
+
1321
+ def _grob_from_rect(
1322
+ element: ElementRect,
1323
+ x: float = 0.5,
1324
+ y: float = 0.5,
1325
+ width: float = 1.0,
1326
+ height: float = 1.0,
1327
+ fill: Optional[str] = None,
1328
+ colour: Optional[str] = None,
1329
+ linewidth: Optional[float] = None,
1330
+ linetype: Optional[Union[int, str]] = None,
1331
+ **kwargs: Any,
1332
+ ) -> Any:
1333
+ """Render an ``ElementRect`` as a rect grob.
1334
+
1335
+ Mirrors R's ``element_grob(element_rect, ...)`` which converts
1336
+ linewidth (mm) → lwd (points) via ``gg_par(lwd = linewidth)``.
1337
+ """
1338
+ lwd_mm = linewidth if linewidth is not None else element.linewidth
1339
+ gp = Gpar(
1340
+ fill=fill if fill is not None else element.fill,
1341
+ col=colour if colour is not None else element.colour,
1342
+ lwd=float(lwd_mm) * _PT if lwd_mm is not None else None,
1343
+ lty=linetype if linetype is not None else element.linetype,
1344
+ )
1345
+ return rect_grob(x=x, y=y, width=width, height=height, gp=gp, **kwargs)
1346
+
1347
+
1348
+ def _grob_from_line(
1349
+ element: ElementLine,
1350
+ x: Any = None,
1351
+ y: Any = None,
1352
+ colour: Optional[str] = None,
1353
+ linewidth: Optional[float] = None,
1354
+ linetype: Optional[Union[int, str]] = None,
1355
+ lineend: Optional[str] = None,
1356
+ id: Optional[Any] = None,
1357
+ id_lengths: Optional[Any] = None,
1358
+ default_units: str = "npc",
1359
+ **kwargs: Any,
1360
+ ) -> Any:
1361
+ """Render an ``ElementLine`` as a polyline grob.
1362
+
1363
+ Mirrors R's ``element_grob.element_line`` (theme-elements.R:558-595):
1364
+ always emits a ``polylineGrob`` (accepts ``id.lengths`` for
1365
+ multi-segment lines — used for panel gridlines); converts
1366
+ ``linewidth`` (mm) → ``lwd`` (points) via ``gg_par(lwd = linewidth)``.
1367
+ """
1368
+ if x is None:
1369
+ x = [0, 1]
1370
+ if y is None:
1371
+ y = [0, 1]
1372
+ lwd_mm = linewidth if linewidth is not None else element.linewidth
1373
+ gp = Gpar(
1374
+ col=colour if colour is not None else element.colour,
1375
+ lwd=float(lwd_mm) * _PT if lwd_mm is not None else None,
1376
+ lty=linetype if linetype is not None else element.linetype,
1377
+ lineend=lineend if lineend is not None else element.lineend,
1378
+ )
1379
+ return polyline_grob(
1380
+ x=x, y=y,
1381
+ id=id, id_lengths=id_lengths,
1382
+ default_units=default_units,
1383
+ gp=gp, **kwargs,
1384
+ )
1385
+
1386
+
1387
+ def _rotate_just(angle: float, hjust: float, vjust: float) -> Tuple[float, float]:
1388
+ """Rotate (hjust, vjust) counter-clockwise into the rotated text frame.
1389
+
1390
+ Mirrors R's ``rotate_just()`` (margins.R:216-276). Used by
1391
+ ``titleGrob`` to compute default x/y anchors so rotated text
1392
+ sits at the correct point within its parent viewport.
1393
+ """
1394
+ import bisect
1395
+ a = (float(angle) if angle is not None else 0.0) % 360
1396
+ hj = 0.5 if hjust is None else float(hjust)
1397
+ vj = 0.5 if vjust is None else float(vjust)
1398
+ # R: case <- findInterval(angle, c(0, 90, 180, 270, 360))
1399
+ # findInterval is right-open: case=1 for [0,90), 2 for [90,180), etc.
1400
+ case = bisect.bisect_right([0.0, 90.0, 180.0, 270.0, 360.0], a)
1401
+ if case == 2: # 90 <= a < 180
1402
+ return (1 - vj, hj)
1403
+ if case == 3: # 180 <= a < 270
1404
+ return (1 - hj, 1 - vj)
1405
+ if case == 4: # 270 <= a < 360
1406
+ return (vj, 1 - hj)
1407
+ return (hj, vj) # 0 <= a < 90
1408
+
1409
+
1410
+ class _TitleGrob(GTree):
1411
+ """A text grob wrapped with its margin — simplified port of R's titleGrob.
1412
+
1413
+ When ``margin_x`` or ``margin_y`` is ``True``, the grob carries
1414
+ ``_widths`` and ``_heights`` vectors that include the surrounding
1415
+ margin, so ``grob_height()`` / ``grob_width()`` return the full
1416
+ extent including padding.
1417
+
1418
+ R reference: ``ggplot2/R/margins.R`` lines 88-206.
1419
+ """
1420
+
1421
+ def __init__(self, text_grob_child: Any, widths: Any, heights: Any,
1422
+ name: str = "title") -> None:
1423
+ from grid_py import GList
1424
+ super().__init__(children=GList(text_grob_child), name=name)
1425
+ self._title_widths = widths
1426
+ self._title_heights = heights
1427
+
1428
+ # R: widthDetails.titleGrob → sum(x$widths)
1429
+ def width_details(self) -> Any:
1430
+ if self._title_widths is not None:
1431
+ return sum(self._title_widths)
1432
+ return Unit(0, "cm")
1433
+
1434
+ # R: heightDetails.titleGrob → sum(x$heights)
1435
+ def height_details(self) -> Any:
1436
+ if self._title_heights is not None:
1437
+ return sum(self._title_heights)
1438
+ return Unit(0, "cm")
1439
+
1440
+
1441
+ def _grob_from_text(
1442
+ element: ElementText,
1443
+ label: Optional[str] = None,
1444
+ x: Any = None,
1445
+ y: Any = None,
1446
+ family: Optional[str] = None,
1447
+ face: Optional[str] = None,
1448
+ colour: Optional[str] = None,
1449
+ size: Optional[float] = None,
1450
+ hjust: Optional[float] = None,
1451
+ vjust: Optional[float] = None,
1452
+ angle: Optional[float] = None,
1453
+ lineheight: Optional[float] = None,
1454
+ margin: Any = None,
1455
+ margin_x: bool = False,
1456
+ margin_y: bool = False,
1457
+ **kwargs: Any,
1458
+ ) -> Any:
1459
+ """Render an ``ElementText`` as a text grob.
1460
+
1461
+ When *margin_x* or *margin_y* is ``True``, wraps the result in a
1462
+ ``_TitleGrob`` whose ``width_details``/``height_details`` include
1463
+ the element's margin — matching R's ``titleGrob()`` (margins.R:88-206).
1464
+ """
1465
+ if label is None:
1466
+ return null_grob()
1467
+ gp = Gpar(
1468
+ fontfamily=family if family is not None else element.family,
1469
+ fontface=face if face is not None else element.face,
1470
+ fontsize=size if size is not None else element.size,
1471
+ col=colour if colour is not None else element.colour,
1472
+ lineheight=lineheight if lineheight is not None else element.lineheight,
1473
+ )
1474
+ hj = hjust if hjust is not None else (element.hjust if element.hjust is not None else 0.5)
1475
+ vj = vjust if vjust is not None else (element.vjust if element.vjust is not None else 0.5)
1476
+ ang = angle if angle is not None else (element.angle if element.angle is not None else 0)
1477
+
1478
+ # R titleGrob (margins.R:95-107): when x/y are NULL, default them to the
1479
+ # ROTATED justification anchor inside the parent viewport.
1480
+ just_hj, just_vj = _rotate_just(ang, hj, vj)
1481
+ if x is None:
1482
+ x = Unit(just_hj, "npc")
1483
+ if y is None:
1484
+ y = Unit(just_vj, "npc")
1485
+
1486
+ # If no margin wrapping requested, emit a plain text grob.
1487
+ if not margin_x and not margin_y:
1488
+ return text_grob(label=label, x=x, y=y, hjust=hj, vjust=vj, rot=ang,
1489
+ gp=gp, **kwargs)
1490
+
1491
+ # --- titleGrob (R: margins.R:88-196) ---------------------------------
1492
+ # Resolve the element's margin (R: element_text has a `margin` slot).
1493
+ el_margin = margin if margin is not None else getattr(element, "margin", None)
1494
+ if el_margin is None:
1495
+ m_t = m_r = m_b = m_l = Unit(0, "pt")
1496
+ elif isinstance(el_margin, Margin):
1497
+ m_t = Unit(el_margin.t, el_margin.unit_str)
1498
+ m_r = Unit(el_margin.r, el_margin.unit_str)
1499
+ m_b = Unit(el_margin.b, el_margin.unit_str)
1500
+ m_l = Unit(el_margin.l, el_margin.unit_str)
1501
+ else:
1502
+ m_t = m_r = m_b = m_l = Unit(0, "pt")
1503
+
1504
+ # Shift x/y inward by margin before constructing the text grob
1505
+ # (R: margins.R:150, 156):
1506
+ # new_x = x - margin[2] * just$hjust + margin[4] * (1 - just$hjust)
1507
+ # new_y = y - margin[1] * just$vjust + margin[3] * (1 - just$vjust)
1508
+ # This is what creates the actual visual gap between the text and the
1509
+ # edge of its cell. Without it the margin only affects cell sizing.
1510
+ if margin_x:
1511
+ if just_hj != 0:
1512
+ x = x - m_r * just_hj
1513
+ if just_hj != 1:
1514
+ x = x + m_l * (1 - just_hj)
1515
+ if margin_y:
1516
+ if just_vj != 0:
1517
+ y = y - m_t * just_vj
1518
+ if just_vj != 1:
1519
+ y = y + m_b * (1 - just_vj)
1520
+
1521
+ grob = text_grob(label=label, x=x, y=y, hjust=hj, vjust=vj, rot=ang,
1522
+ gp=gp, **kwargs)
1523
+
1524
+ # --- Compute widths/heights for width_details/height_details ---------
1525
+ # R emits *lazy* grobwidth/grobheight units referencing the rotated
1526
+ # textGrob. grid resolves them at draw time using the rotated
1527
+ # bounding box (tested: grid_py handles this correctly).
1528
+ #
1529
+ # R: width = unit(1, "grobwidth", grob) + x_descent
1530
+ # R: height = unit(1, "grobheight", grob) + y_descent
1531
+ # where x_descent = abs(sin(rad)) * font_descent
1532
+ # y_descent = abs(cos(rad)) * font_descent
1533
+ import math
1534
+ from grid_py._size import calc_string_metric
1535
+
1536
+ fontsize_val = size if size is not None else (element.size if element.size is not None else 12)
1537
+ # Use a canonical descender probe like R (margins.R:115-120: it replaces
1538
+ # the label with descender letters to guarantee consistent height).
1539
+ metrics = calc_string_metric("gjpqy", Gpar(
1540
+ fontsize=fontsize_val,
1541
+ fontfamily=family if family is not None else element.family,
1542
+ fontface=face if face is not None else element.face,
1543
+ ))
1544
+ descent_in = metrics["descent"]
1545
+
1546
+ ang_val = float(ang) if ang is not None else 0.0
1547
+ rad = math.radians(ang_val % 360)
1548
+ x_descent_unit = Unit(abs(math.sin(rad)) * descent_in, "inches")
1549
+ y_descent_unit = Unit(abs(math.cos(rad)) * descent_in, "inches")
1550
+
1551
+ width = grob_width(grob) + x_descent_unit
1552
+ height = grob_height(grob) + y_descent_unit
1553
+
1554
+ # Build widths/heights vectors including margins.
1555
+ # R: new_width = unit.c(margin[4], width, margin[2]) # left, w, right
1556
+ # R: new_height = unit.c(margin[1], height, margin[3]) # top, h, bottom
1557
+ widths = unit_c(m_l, width, m_r) if margin_x else width
1558
+ heights = unit_c(m_t, height, m_b) if margin_y else height
1559
+
1560
+ return _TitleGrob(grob, widths=widths, heights=heights,
1561
+ name=kwargs.get("name", "title"))
1562
+
1563
+
1564
+ def _grob_from_point(
1565
+ element: "ElementPoint",
1566
+ x: float = 0.5,
1567
+ y: float = 0.5,
1568
+ colour: Optional[str] = None,
1569
+ shape: Optional[int] = None,
1570
+ fill: Optional[str] = None,
1571
+ size: Optional[float] = None,
1572
+ stroke: Optional[float] = None,
1573
+ **kwargs: Any,
1574
+ ) -> Any:
1575
+ """Render an ``ElementPoint`` as a points grob.
1576
+
1577
+ Mirrors R's ``element_grob(element_point, ...)`` which converts
1578
+ pointsize (mm) and stroke (mm) via ``gg_par(pointsize=size, stroke=stroke)``:
1579
+ fontsize = pointsize_mm * .pt + stroke_mm * .stroke / 2
1580
+ """
1581
+ from grid_py import points_grob, Gpar
1582
+ col = colour or element.colour or "black"
1583
+ sh = shape if shape is not None else (element.shape if element.shape is not None else 19)
1584
+ fl = fill or element.fill
1585
+ sz = size if size is not None else (element.size if element.size is not None else 1.5)
1586
+ st = stroke if stroke is not None else (element.stroke if element.stroke is not None else 0.5)
1587
+ # R: gg_par(pointsize=sz, stroke=st) → fontsize = sz * .pt + st * .stroke / 2
1588
+ fontsize = float(sz) * _PT + float(st) * _STROKE / 2
1589
+ gp = Gpar(col=col, fill=fl, fontsize=fontsize)
1590
+ try:
1591
+ return points_grob(x=x, y=y, pch=int(sh), gp=gp, **kwargs)
1592
+ except Exception:
1593
+ from grid_py import null_grob
1594
+ return null_grob()
1595
+
1596
+
1597
+ def _grob_from_polygon(
1598
+ element: "ElementPolygon",
1599
+ x=None, y=None,
1600
+ fill: Optional[str] = None,
1601
+ colour: Optional[str] = None,
1602
+ linewidth: Optional[float] = None,
1603
+ linetype: Optional[int] = None,
1604
+ **kwargs: Any,
1605
+ ) -> Any:
1606
+ """Render an ``ElementPolygon`` as a path grob.
1607
+
1608
+ Mirrors R's ``element_grob(element_polygon, ...)`` which converts
1609
+ linewidth (mm) → lwd (points) via ``gg_par(lwd = linewidth)``.
1610
+ """
1611
+ from grid_py import polygon_grob, Gpar
1612
+ if x is None:
1613
+ x = [0, 0.5, 1, 0.5]
1614
+ if y is None:
1615
+ y = [0.5, 1, 0.5, 0]
1616
+ fl = fill or element.fill or "grey20"
1617
+ col = colour or element.colour
1618
+ lwd_mm = linewidth if linewidth is not None else (element.linewidth if element.linewidth is not None else 0.5)
1619
+ lty = linetype if linetype is not None else (element.linetype if element.linetype is not None else 1)
1620
+ gp = Gpar(fill=fl, col=col, lwd=float(lwd_mm) * _PT, lty=lty)
1621
+ return polygon_grob(x=x, y=y, gp=gp, **kwargs)
1622
+
1623
+
1624
+ def element_render(theme: Any, element_name: str, name: Optional[str] = None, **kwargs: Any) -> Any:
1625
+ """Render a named theme element into a grob.
1626
+
1627
+ Parameters
1628
+ ----------
1629
+ theme : Theme
1630
+ The theme object.
1631
+ element_name : str
1632
+ The element name (e.g., ``"axis.line.x"``).
1633
+ name : str, optional
1634
+ Additional name component for the grob.
1635
+ **kwargs
1636
+ Passed through to ``element_grob()``.
1637
+
1638
+ Returns
1639
+ -------
1640
+ Grob
1641
+ A grid grob for the element.
1642
+ """
1643
+ el = calc_element(element_name, theme)
1644
+ if el is None:
1645
+ return null_grob()
1646
+ grob = element_grob(el, **kwargs)
1647
+ return grob
1648
+
1649
+
1650
+ # ---------------------------------------------------------------------------
1651
+ # Element tree definition (el_def)
1652
+ # ---------------------------------------------------------------------------
1653
+
1654
+ def el_def(
1655
+ class_: Any = None,
1656
+ inherit: Optional[Union[str, List[str]]] = None,
1657
+ description: Optional[str] = None,
1658
+ ) -> Dict[str, Any]:
1659
+ """Define an entry in the element tree.
1660
+
1661
+ Parameters
1662
+ ----------
1663
+ class_ : type or str or list of str, optional
1664
+ The expected element class (e.g. ``ElementLine``, ``"character"``).
1665
+ inherit : str or list of str, optional
1666
+ Name(s) of the parent element(s) from which this element inherits.
1667
+ description : str, optional
1668
+ Human-readable description.
1669
+
1670
+ Returns
1671
+ -------
1672
+ dict
1673
+ A dictionary with keys ``"class"``, ``"inherit"``, ``"description"``.
1674
+ """
1675
+ if isinstance(inherit, str):
1676
+ inherit = [inherit]
1677
+ return {"class": class_, "inherit": inherit, "description": description}
1678
+
1679
+
1680
+ # ---------------------------------------------------------------------------
1681
+ # The default element tree
1682
+ # ---------------------------------------------------------------------------
1683
+
1684
+ _ELEMENT_TREE: Dict[str, Dict[str, Any]] = {
1685
+ "line": el_def(ElementLine),
1686
+ "rect": el_def(ElementRect),
1687
+ "text": el_def(ElementText),
1688
+ "point": el_def(ElementPoint),
1689
+ "polygon": el_def(ElementPolygon),
1690
+ "geom": el_def(ElementGeom),
1691
+ "title": el_def(ElementText, "text"),
1692
+ "spacing": el_def("unit"),
1693
+ "margins": el_def("margin"),
1694
+
1695
+ # Axis lines
1696
+ "axis.line": el_def(ElementLine, "line"),
1697
+ "axis.line.x": el_def(ElementLine, "axis.line"),
1698
+ "axis.line.x.top": el_def(ElementLine, "axis.line.x"),
1699
+ "axis.line.x.bottom": el_def(ElementLine, "axis.line.x"),
1700
+ "axis.line.y": el_def(ElementLine, "axis.line"),
1701
+ "axis.line.y.left": el_def(ElementLine, "axis.line.y"),
1702
+ "axis.line.y.right": el_def(ElementLine, "axis.line.y"),
1703
+ "axis.line.theta": el_def(ElementLine, "axis.line.x"),
1704
+ "axis.line.r": el_def(ElementLine, "axis.line.y"),
1705
+
1706
+ # Axis text
1707
+ "axis.text": el_def(ElementText, "text"),
1708
+ "axis.text.x": el_def(ElementText, "axis.text"),
1709
+ "axis.text.x.top": el_def(ElementText, "axis.text.x"),
1710
+ "axis.text.x.bottom": el_def(ElementText, "axis.text.x"),
1711
+ "axis.text.y": el_def(ElementText, "axis.text"),
1712
+ "axis.text.y.left": el_def(ElementText, "axis.text.y"),
1713
+ "axis.text.y.right": el_def(ElementText, "axis.text.y"),
1714
+ "axis.text.theta": el_def(ElementText, "axis.text.x"),
1715
+ "axis.text.r": el_def(ElementText, "axis.text.y"),
1716
+
1717
+ # Axis ticks
1718
+ "axis.ticks": el_def(ElementLine, "line"),
1719
+ "axis.ticks.x": el_def(ElementLine, "axis.ticks"),
1720
+ "axis.ticks.x.top": el_def(ElementLine, "axis.ticks.x"),
1721
+ "axis.ticks.x.bottom": el_def(ElementLine, "axis.ticks.x"),
1722
+ "axis.ticks.y": el_def(ElementLine, "axis.ticks"),
1723
+ "axis.ticks.y.left": el_def(ElementLine, "axis.ticks.y"),
1724
+ "axis.ticks.y.right": el_def(ElementLine, "axis.ticks.y"),
1725
+ "axis.ticks.theta": el_def(ElementLine, "axis.ticks.x"),
1726
+ "axis.ticks.r": el_def(ElementLine, "axis.ticks.y"),
1727
+
1728
+ # Axis tick lengths
1729
+ "axis.ticks.length": el_def("unit_or_rel", "spacing"),
1730
+ "axis.ticks.length.x": el_def("unit_or_rel", "axis.ticks.length"),
1731
+ "axis.ticks.length.x.top": el_def("unit_or_rel", "axis.ticks.length.x"),
1732
+ "axis.ticks.length.x.bottom": el_def("unit_or_rel", "axis.ticks.length.x"),
1733
+ "axis.ticks.length.y": el_def("unit_or_rel", "axis.ticks.length"),
1734
+ "axis.ticks.length.y.left": el_def("unit_or_rel", "axis.ticks.length.y"),
1735
+ "axis.ticks.length.y.right": el_def("unit_or_rel", "axis.ticks.length.y"),
1736
+ "axis.ticks.length.theta": el_def("unit_or_rel", "axis.ticks.length.x"),
1737
+ "axis.ticks.length.r": el_def("unit_or_rel", "axis.ticks.length.y"),
1738
+
1739
+ # Axis minor ticks
1740
+ "axis.minor.ticks.x.top": el_def(ElementLine, "axis.ticks.x.top"),
1741
+ "axis.minor.ticks.x.bottom": el_def(ElementLine, "axis.ticks.x.bottom"),
1742
+ "axis.minor.ticks.y.left": el_def(ElementLine, "axis.ticks.y.left"),
1743
+ "axis.minor.ticks.y.right": el_def(ElementLine, "axis.ticks.y.right"),
1744
+ "axis.minor.ticks.theta": el_def(ElementLine, "axis.ticks.theta"),
1745
+ "axis.minor.ticks.r": el_def(ElementLine, "axis.ticks.r"),
1746
+
1747
+ # Axis minor tick lengths
1748
+ "axis.minor.ticks.length": el_def("unit_or_rel"),
1749
+ "axis.minor.ticks.length.x": el_def("unit_or_rel", "axis.minor.ticks.length"),
1750
+ "axis.minor.ticks.length.x.top": el_def(
1751
+ "unit_or_rel", ["axis.minor.ticks.length.x", "axis.ticks.length.x.top"]
1752
+ ),
1753
+ "axis.minor.ticks.length.x.bottom": el_def(
1754
+ "unit_or_rel", ["axis.minor.ticks.length.x", "axis.ticks.length.x.bottom"]
1755
+ ),
1756
+ "axis.minor.ticks.length.y": el_def("unit_or_rel", "axis.minor.ticks.length"),
1757
+ "axis.minor.ticks.length.y.left": el_def(
1758
+ "unit_or_rel", ["axis.minor.ticks.length.y", "axis.ticks.length.y.left"]
1759
+ ),
1760
+ "axis.minor.ticks.length.y.right": el_def(
1761
+ "unit_or_rel", ["axis.minor.ticks.length.y", "axis.ticks.length.y.right"]
1762
+ ),
1763
+ "axis.minor.ticks.length.theta": el_def(
1764
+ "unit_or_rel", ["axis.minor.ticks.length.x", "axis.ticks.length.theta"]
1765
+ ),
1766
+ "axis.minor.ticks.length.r": el_def(
1767
+ "unit_or_rel", ["axis.minor.ticks.length.y", "axis.ticks.length.r"]
1768
+ ),
1769
+
1770
+ # Axis titles
1771
+ "axis.title": el_def(ElementText, "title"),
1772
+ "axis.title.x": el_def(ElementText, "axis.title"),
1773
+ "axis.title.x.top": el_def(ElementText, "axis.title.x"),
1774
+ "axis.title.x.bottom": el_def(ElementText, "axis.title.x"),
1775
+ "axis.title.y": el_def(ElementText, "axis.title"),
1776
+ "axis.title.y.left": el_def(ElementText, "axis.title.y"),
1777
+ "axis.title.y.right": el_def(ElementText, "axis.title.y"),
1778
+
1779
+ # Legend
1780
+ "legend.background": el_def(ElementRect, "rect"),
1781
+ "legend.margin": el_def("margin", "margins"),
1782
+ "legend.spacing": el_def("unit_or_rel", "spacing"),
1783
+ "legend.spacing.x": el_def("unit_or_rel", "legend.spacing"),
1784
+ "legend.spacing.y": el_def("unit_or_rel", "legend.spacing"),
1785
+ "legend.key": el_def(ElementRect, "panel.background"),
1786
+ "legend.key.size": el_def("unit_or_rel", "spacing"),
1787
+ "legend.key.height": el_def("unit_or_rel", "legend.key.size"),
1788
+ "legend.key.width": el_def("unit_or_rel", "legend.key.size"),
1789
+ "legend.key.spacing": el_def("unit_or_rel", "spacing"),
1790
+ "legend.key.spacing.x": el_def("unit_or_rel", "legend.key.spacing"),
1791
+ "legend.key.spacing.y": el_def("unit_or_rel", "legend.key.spacing"),
1792
+ "legend.key.justification": el_def("character"),
1793
+ "legend.frame": el_def(ElementRect, "rect"),
1794
+ "legend.axis.line": el_def(ElementLine, "line"),
1795
+ "legend.ticks": el_def(ElementLine, "legend.axis.line"),
1796
+ "legend.ticks.length": el_def("unit_or_rel", "legend.key.size"),
1797
+ "legend.text": el_def(ElementText, "text"),
1798
+ "legend.text.position": el_def("character"),
1799
+ "legend.title": el_def(ElementText, "title"),
1800
+ "legend.title.position": el_def("character"),
1801
+ "legend.byrow": el_def("logical"),
1802
+ "legend.position": el_def("character"),
1803
+ "legend.position.inside": el_def("numeric"),
1804
+ "legend.direction": el_def("character"),
1805
+ "legend.justification": el_def("character"),
1806
+ "legend.justification.top": el_def("character", "legend.justification"),
1807
+ "legend.justification.bottom": el_def("character", "legend.justification"),
1808
+ "legend.justification.left": el_def("character", "legend.justification"),
1809
+ "legend.justification.right": el_def("character", "legend.justification"),
1810
+ "legend.justification.inside": el_def("character", "legend.justification"),
1811
+ "legend.location": el_def("character"),
1812
+ "legend.box": el_def("character"),
1813
+ "legend.box.just": el_def("character"),
1814
+ "legend.box.margin": el_def("margin", "margins"),
1815
+ "legend.box.background": el_def(ElementRect, "rect"),
1816
+ "legend.box.spacing": el_def("unit_or_rel", "spacing"),
1817
+
1818
+ # Panel
1819
+ "panel.background": el_def(ElementRect, "rect"),
1820
+ "panel.border": el_def(ElementRect, "rect"),
1821
+ "panel.spacing": el_def("unit_or_rel", "spacing"),
1822
+ "panel.spacing.x": el_def("unit_or_rel", "panel.spacing"),
1823
+ "panel.spacing.y": el_def("unit_or_rel", "panel.spacing"),
1824
+ "panel.grid": el_def(ElementLine, "line"),
1825
+ "panel.grid.major": el_def(ElementLine, "panel.grid"),
1826
+ "panel.grid.minor": el_def(ElementLine, "panel.grid"),
1827
+ "panel.grid.major.x": el_def(ElementLine, "panel.grid.major"),
1828
+ "panel.grid.major.y": el_def(ElementLine, "panel.grid.major"),
1829
+ "panel.grid.minor.x": el_def(ElementLine, "panel.grid.minor"),
1830
+ "panel.grid.minor.y": el_def(ElementLine, "panel.grid.minor"),
1831
+ "panel.ontop": el_def("logical"),
1832
+ "panel.widths": el_def("unit"),
1833
+ "panel.heights": el_def("unit"),
1834
+
1835
+ # Strip
1836
+ "strip.background": el_def(ElementRect, "rect"),
1837
+ "strip.background.x": el_def(ElementRect, "strip.background"),
1838
+ "strip.background.y": el_def(ElementRect, "strip.background"),
1839
+ "strip.clip": el_def("character"),
1840
+ "strip.text": el_def(ElementText, "text"),
1841
+ "strip.text.x": el_def(ElementText, "strip.text"),
1842
+ "strip.text.x.top": el_def(ElementText, "strip.text.x"),
1843
+ "strip.text.x.bottom": el_def(ElementText, "strip.text.x"),
1844
+ "strip.text.y": el_def(ElementText, "strip.text"),
1845
+ "strip.text.y.left": el_def(ElementText, "strip.text.y"),
1846
+ "strip.text.y.right": el_def(ElementText, "strip.text.y"),
1847
+ "strip.placement": el_def("character"),
1848
+ "strip.placement.x": el_def("character", "strip.placement"),
1849
+ "strip.placement.y": el_def("character", "strip.placement"),
1850
+ "strip.switch.pad.grid": el_def("unit_or_rel", "spacing"),
1851
+ "strip.switch.pad.wrap": el_def("unit_or_rel", "spacing"),
1852
+
1853
+ # Plot
1854
+ "plot.background": el_def(ElementRect, "rect"),
1855
+ "plot.title": el_def(ElementText, "title"),
1856
+ "plot.title.position": el_def("character"),
1857
+ "plot.subtitle": el_def(ElementText, "text"),
1858
+ "plot.caption": el_def(ElementText, "text"),
1859
+ "plot.caption.position": el_def("character"),
1860
+ "plot.tag": el_def(ElementText, "text"),
1861
+ "plot.tag.position": el_def("character"),
1862
+ "plot.tag.location": el_def("character"),
1863
+ "plot.margin": el_def("margin", "margins"),
1864
+
1865
+ # Aspect ratio
1866
+ "aspect.ratio": el_def("numeric"),
1867
+ }
1868
+
1869
+
1870
+ # ---------------------------------------------------------------------------
1871
+ # Global element-tree state
1872
+ # ---------------------------------------------------------------------------
1873
+
1874
+ class _ThemeGlobal:
1875
+ """Module-level singleton holding current theme and element tree state."""
1876
+
1877
+ def __init__(self) -> None:
1878
+ self.element_tree: Dict[str, Dict[str, Any]] = dict(_ELEMENT_TREE)
1879
+ self.theme_default: Any = None
1880
+ self.theme_current: Any = None
1881
+
1882
+
1883
+ _ggplot_global = _ThemeGlobal()
1884
+
1885
+
1886
+ def get_element_tree() -> Dict[str, Dict[str, Any]]:
1887
+ """Return the currently active element tree.
1888
+
1889
+ Returns
1890
+ -------
1891
+ dict
1892
+ A mapping of element names to their definitions (created by ``el_def``).
1893
+ """
1894
+ return _ggplot_global.element_tree
1895
+
1896
+
1897
+ def register_theme_elements(
1898
+ element_tree: Optional[Dict[str, Dict[str, Any]]] = None,
1899
+ **kwargs: Any,
1900
+ ) -> None:
1901
+ """Register new theme elements globally.
1902
+
1903
+ Parameters
1904
+ ----------
1905
+ element_tree : dict, optional
1906
+ Additional element tree entries (name -> ``el_def(...)``).
1907
+ **kwargs
1908
+ Element default values to merge into the default theme.
1909
+ """
1910
+ if element_tree is not None:
1911
+ _ggplot_global.element_tree.update(element_tree)
1912
+ # Defaults are handled by the theme module once it is imported.
1913
+
1914
+
1915
+ def reset_theme_settings(reset_current: bool = True) -> None:
1916
+ """Reset the element tree and default theme to built-in defaults.
1917
+
1918
+ Mirrors R's ``reset_theme_settings()`` (theme-elements.R:714-723):
1919
+ restores the element tree, sets ``theme_default = theme_grey()``,
1920
+ and (unless disabled) sets ``theme_current = theme_default``.
1921
+
1922
+ Parameters
1923
+ ----------
1924
+ reset_current : bool
1925
+ If ``True`` (default), also reset the currently active theme.
1926
+ """
1927
+ _ggplot_global.element_tree = dict(_ELEMENT_TREE)
1928
+ # Local import: theme_defaults depends on this module (circular at top level).
1929
+ from ggplot2_py.theme_defaults import theme_grey
1930
+ _ggplot_global.theme_default = theme_grey()
1931
+ if reset_current:
1932
+ _ggplot_global.theme_current = _ggplot_global.theme_default
1933
+
1934
+
1935
+ # ---------------------------------------------------------------------------
1936
+ # calc_element — element inheritance resolution
1937
+ # ---------------------------------------------------------------------------
1938
+
1939
+ def calc_element(
1940
+ element: str,
1941
+ theme: Any,
1942
+ verbose: bool = False,
1943
+ skip_blank: bool = False,
1944
+ ) -> Any:
1945
+ """Resolve a theme element by walking the inheritance tree.
1946
+
1947
+ Parameters
1948
+ ----------
1949
+ element : str
1950
+ Name of the element to resolve (e.g. ``"axis.text.x"``).
1951
+ theme : Theme
1952
+ The theme object.
1953
+ verbose : bool
1954
+ If ``True``, print inheritance chain.
1955
+ skip_blank : bool
1956
+ If ``True``, skip ``element_blank`` ancestors.
1957
+
1958
+ Returns
1959
+ -------
1960
+ Element or other
1961
+ The fully resolved element, or ``None`` if not found.
1962
+ """
1963
+ if verbose:
1964
+ print(f"{element} --> ", end="")
1965
+
1966
+ # Look up the element value in the theme
1967
+ el_out = theme.get(element) if hasattr(theme, "get") else getattr(theme, element, None)
1968
+
1969
+ # If blank, decide whether to skip
1970
+ if isinstance(el_out, ElementBlank):
1971
+ if skip_blank:
1972
+ el_out = None
1973
+ else:
1974
+ if verbose:
1975
+ print("element_blank (no inheritance)")
1976
+ return el_out
1977
+
1978
+ # Get element tree
1979
+ element_tree = get_element_tree()
1980
+
1981
+ # Validate element class against tree definition (R: check_element)
1982
+ tree_entry = element_tree.get(element)
1983
+ if tree_entry is None:
1984
+ if verbose:
1985
+ print("(not in element tree)")
1986
+ return el_out
1987
+
1988
+ if el_out is not None and not isinstance(el_out, ElementBlank):
1989
+ expected_class = tree_entry.get("class")
1990
+ if expected_class is not None and isinstance(expected_class, type):
1991
+ if not isinstance(el_out, (expected_class, ElementBlank)):
1992
+ import warnings
1993
+ warnings.warn(
1994
+ f"Theme element '{element}' must be a "
1995
+ f"{expected_class.__name__} object, "
1996
+ f"got {type(el_out).__name__}.",
1997
+ stacklevel=3,
1998
+ )
1999
+
2000
+ # Get parent names
2001
+ pnames = tree_entry.get("inherit")
2002
+
2003
+ # If no parents, this is a root node
2004
+ if pnames is None:
2005
+ if verbose:
2006
+ print("(top level)")
2007
+
2008
+ if el_out is not None:
2009
+ # Check for None properties
2010
+ if isinstance(el_out, Element):
2011
+ null_props = [k for k, v in el_out.__dict__.items() if v is None]
2012
+ else:
2013
+ null_props = []
2014
+ if not null_props:
2015
+ return el_out
2016
+
2017
+ # Try to fill from default theme
2018
+ default_theme = _ggplot_global.theme_default
2019
+ if default_theme is not None:
2020
+ default_el = (
2021
+ default_theme.get(element)
2022
+ if hasattr(default_theme, "get")
2023
+ else getattr(default_theme, element, None)
2024
+ )
2025
+ el_out = combine_elements(el_out, default_el)
2026
+
2027
+ return el_out
2028
+
2029
+ if verbose:
2030
+ print(f"{pnames}")
2031
+
2032
+ # If el_out has inherit_blank=False, start skipping blanks
2033
+ if (
2034
+ not skip_blank
2035
+ and el_out is not None
2036
+ and isinstance(el_out, Element)
2037
+ and not getattr(el_out, "inherit_blank", True)
2038
+ ):
2039
+ skip_blank = True
2040
+
2041
+ # Recursively calculate parents
2042
+ parents = [
2043
+ calc_element(pname, theme, verbose=verbose, skip_blank=skip_blank)
2044
+ for pname in pnames
2045
+ ]
2046
+
2047
+ # Combine with parents using reduce
2048
+ result = el_out
2049
+ for parent in parents:
2050
+ result = combine_elements(result, parent)
2051
+
2052
+ return result