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.
- ggplot2_py/__init__.py +852 -0
- ggplot2_py/_compat.py +475 -0
- ggplot2_py/_plugins.py +129 -0
- ggplot2_py/_utils.py +544 -0
- ggplot2_py/aes.py +586 -0
- ggplot2_py/annotation.py +540 -0
- ggplot2_py/coord.py +2108 -0
- ggplot2_py/coords/__init__.py +49 -0
- ggplot2_py/datasets.py +265 -0
- ggplot2_py/draw_key.py +454 -0
- ggplot2_py/facet.py +1456 -0
- ggplot2_py/fortify.py +95 -0
- ggplot2_py/geom.py +4516 -0
- ggplot2_py/geoms/__init__.py +12 -0
- ggplot2_py/ggproto.py +279 -0
- ggplot2_py/guide.py +2925 -0
- ggplot2_py/guide_axis.py +615 -0
- ggplot2_py/guide_colourbar.py +657 -0
- ggplot2_py/guide_legend.py +1061 -0
- ggplot2_py/guides/__init__.py +8 -0
- ggplot2_py/labeller.py +296 -0
- ggplot2_py/labels.py +309 -0
- ggplot2_py/layer.py +954 -0
- ggplot2_py/layout.py +754 -0
- ggplot2_py/limits.py +314 -0
- ggplot2_py/plot.py +1401 -0
- ggplot2_py/plot_render.py +866 -0
- ggplot2_py/position.py +1269 -0
- ggplot2_py/protocols.py +171 -0
- ggplot2_py/py.typed +0 -0
- ggplot2_py/qplot.py +233 -0
- ggplot2_py/resources/diamonds.csv +53941 -0
- ggplot2_py/resources/economics.csv +575 -0
- ggplot2_py/resources/economics_long.csv +2871 -0
- ggplot2_py/resources/faithfuld.csv +5626 -0
- ggplot2_py/resources/luv_colours.csv +658 -0
- ggplot2_py/resources/midwest.csv +438 -0
- ggplot2_py/resources/mpg.csv +235 -0
- ggplot2_py/resources/msleep.csv +84 -0
- ggplot2_py/resources/presidential.csv +13 -0
- ggplot2_py/resources/seals.csv +1156 -0
- ggplot2_py/resources/txhousing.csv +8603 -0
- ggplot2_py/save.py +316 -0
- ggplot2_py/scale.py +2727 -0
- ggplot2_py/scales/__init__.py +4252 -0
- ggplot2_py/stat.py +6071 -0
- ggplot2_py/stats/__init__.py +9 -0
- ggplot2_py/theme.py +490 -0
- ggplot2_py/theme_defaults.py +1350 -0
- ggplot2_py/theme_elements.py +2052 -0
- ggplot2_python-4.0.2.9000.dist-info/METADATA +179 -0
- ggplot2_python-4.0.2.9000.dist-info/RECORD +54 -0
- ggplot2_python-4.0.2.9000.dist-info/WHEEL +4 -0
- 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
|