rgrid-python 4.5.3__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.
- grid_py/__init__.py +340 -0
- grid_py/_arrow.py +331 -0
- grid_py/_clippath.py +170 -0
- grid_py/_colour.py +815 -0
- grid_py/_coords.py +1534 -0
- grid_py/_curve.py +1668 -0
- grid_py/_display_list.py +507 -0
- grid_py/_draw.py +1397 -0
- grid_py/_edit.py +756 -0
- grid_py/_font_metrics.py +319 -0
- grid_py/_gpar.py +572 -0
- grid_py/_grab.py +501 -0
- grid_py/_grob.py +1377 -0
- grid_py/_group.py +798 -0
- grid_py/_highlevel.py +2176 -0
- grid_py/_just.py +361 -0
- grid_py/_layout.py +593 -0
- grid_py/_ls.py +895 -0
- grid_py/_mask.py +196 -0
- grid_py/_path.py +414 -0
- grid_py/_patterns.py +1049 -0
- grid_py/_primitives.py +2198 -0
- grid_py/_renderer_base.py +1184 -0
- grid_py/_scene_graph.py +248 -0
- grid_py/_size.py +1352 -0
- grid_py/_state.py +683 -0
- grid_py/_transforms.py +448 -0
- grid_py/_typeset.py +384 -0
- grid_py/_units.py +1924 -0
- grid_py/_utils.py +310 -0
- grid_py/_viewport.py +1649 -0
- grid_py/_vp_calc.py +970 -0
- grid_py/py.typed +0 -0
- grid_py/renderer.py +1762 -0
- grid_py/renderer_web.py +764 -0
- grid_py/resources/d3.v7.min.js +2 -0
- grid_py/resources/gridpy.css +80 -0
- grid_py/resources/gridpy.js +813 -0
- rgrid_python-4.5.3.dist-info/METADATA +489 -0
- rgrid_python-4.5.3.dist-info/RECORD +42 -0
- rgrid_python-4.5.3.dist-info/WHEEL +4 -0
- rgrid_python-4.5.3.dist-info/licenses/LICENSE +3 -0
grid_py/_gpar.py
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
"""Graphical parameters for grid_py (port of R's grid ``gpar`` system).
|
|
2
|
+
|
|
3
|
+
This module provides the :class:`Gpar` class, which encapsulates a set of
|
|
4
|
+
graphical parameters analogous to R's ``gpar()`` objects. Individual
|
|
5
|
+
parameters may be scalars **or** vectors (lists); when vectorised the
|
|
6
|
+
recycling / subscripting semantics of R are preserved.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import copy
|
|
12
|
+
import math
|
|
13
|
+
from typing import Any, Dict, List, Optional, Sequence, Union
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
__all__ = ["Gpar", "get_gpar"]
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Constants
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
_VALID_LTY: set[str] = {
|
|
24
|
+
"solid",
|
|
25
|
+
"dashed",
|
|
26
|
+
"dotted",
|
|
27
|
+
"dotdash",
|
|
28
|
+
"longdash",
|
|
29
|
+
"twodash",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_VALID_LINEEND: set[str] = {"round", "butt", "square"}
|
|
33
|
+
|
|
34
|
+
_VALID_LINEJOIN: set[str] = {"round", "mitre", "bevel"}
|
|
35
|
+
|
|
36
|
+
_FONTFACE_MAP: dict[str, int] = {
|
|
37
|
+
"plain": 1,
|
|
38
|
+
"bold": 2,
|
|
39
|
+
"italic": 3,
|
|
40
|
+
"oblique": 3,
|
|
41
|
+
"bold.italic": 4,
|
|
42
|
+
"symbol": 5,
|
|
43
|
+
"cyrillic": 5,
|
|
44
|
+
"cyrillic.oblique": 6,
|
|
45
|
+
"EUC": 7,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# The set of parameter names that the Gpar constructor accepts.
|
|
49
|
+
_GPAR_NAMES: set[str] = {
|
|
50
|
+
"col",
|
|
51
|
+
"fill",
|
|
52
|
+
"alpha",
|
|
53
|
+
"lty",
|
|
54
|
+
"lwd",
|
|
55
|
+
"lex",
|
|
56
|
+
"lineend",
|
|
57
|
+
"linejoin",
|
|
58
|
+
"linemitre",
|
|
59
|
+
"fontsize",
|
|
60
|
+
"cex",
|
|
61
|
+
"fontfamily",
|
|
62
|
+
"fontface",
|
|
63
|
+
"lineheight",
|
|
64
|
+
"font",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Helpers
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _as_list(value: Any) -> list:
|
|
74
|
+
"""Wrap scalars in a list; pass through sequences unchanged."""
|
|
75
|
+
if isinstance(value, (list, tuple, np.ndarray)):
|
|
76
|
+
return list(value)
|
|
77
|
+
return [value]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _is_hex_lty(s: str) -> bool:
|
|
81
|
+
"""Return True when *s* looks like a valid hex-string line-type spec."""
|
|
82
|
+
return all(c in "0123456789abcdefABCDEF" for c in s) and len(s) > 0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _resolve_fontface(value: Any) -> int:
|
|
86
|
+
"""Convert a fontface specification to an integer code.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
value : int, str, or float
|
|
91
|
+
A fontface specification. Strings are mapped through
|
|
92
|
+
``_FONTFACE_MAP``; numeric values are cast to ``int``.
|
|
93
|
+
|
|
94
|
+
Returns
|
|
95
|
+
-------
|
|
96
|
+
int
|
|
97
|
+
The integer font-face code.
|
|
98
|
+
|
|
99
|
+
Raises
|
|
100
|
+
------
|
|
101
|
+
ValueError
|
|
102
|
+
If the string is not a recognised face name.
|
|
103
|
+
"""
|
|
104
|
+
if isinstance(value, (int, float, np.integer, np.floating)):
|
|
105
|
+
return int(value)
|
|
106
|
+
if isinstance(value, str):
|
|
107
|
+
if value in _FONTFACE_MAP:
|
|
108
|
+
return _FONTFACE_MAP[value]
|
|
109
|
+
raise ValueError(f"invalid fontface '{value}'")
|
|
110
|
+
raise TypeError(f"fontface must be int or str, got {type(value).__name__}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Gpar class
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Gpar:
|
|
119
|
+
"""A set of graphical parameters (port of R's ``gpar``).
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
col : str or list of str, optional
|
|
124
|
+
Line / border colour(s).
|
|
125
|
+
fill : str or list of str, optional
|
|
126
|
+
Fill colour(s) or pattern(s).
|
|
127
|
+
alpha : float or list of float, optional
|
|
128
|
+
Transparency value(s) in the range ``[0, 1]``.
|
|
129
|
+
lty : str, int, or list, optional
|
|
130
|
+
Line type. One of ``"solid"``, ``"dashed"``, ``"dotted"``,
|
|
131
|
+
``"dotdash"``, ``"longdash"``, ``"twodash"``, a hex string, or an
|
|
132
|
+
integer code.
|
|
133
|
+
lwd : float or list of float, optional
|
|
134
|
+
Line width(s).
|
|
135
|
+
lex : float or list of float, optional
|
|
136
|
+
Line-width expansion multiplier(s).
|
|
137
|
+
lineend : str or list of str, optional
|
|
138
|
+
Line end style: ``"round"``, ``"butt"``, or ``"square"``.
|
|
139
|
+
linejoin : str or list of str, optional
|
|
140
|
+
Line join style: ``"round"``, ``"mitre"``, or ``"bevel"``.
|
|
141
|
+
linemitre : float or list of float, optional
|
|
142
|
+
Mitre limit (must be >= 1).
|
|
143
|
+
fontsize : float or list of float, optional
|
|
144
|
+
Font size in points.
|
|
145
|
+
cex : float or list of float, optional
|
|
146
|
+
Character expansion factor.
|
|
147
|
+
fontfamily : str or list of str, optional
|
|
148
|
+
Font family name(s).
|
|
149
|
+
fontface : int, str, or list, optional
|
|
150
|
+
Font face specification. Mapped to an integer code internally.
|
|
151
|
+
Cannot be specified together with *font*.
|
|
152
|
+
lineheight : float or list of float, optional
|
|
153
|
+
Line-height multiplier.
|
|
154
|
+
font : int or list of int, optional
|
|
155
|
+
Integer font-face code (alias for *fontface*). Cannot be specified
|
|
156
|
+
together with *fontface*.
|
|
157
|
+
|
|
158
|
+
Raises
|
|
159
|
+
------
|
|
160
|
+
TypeError
|
|
161
|
+
If a parameter has an inappropriate type.
|
|
162
|
+
ValueError
|
|
163
|
+
If a parameter value is out of range or not among valid choices.
|
|
164
|
+
|
|
165
|
+
Examples
|
|
166
|
+
--------
|
|
167
|
+
>>> gp = Gpar(col="red", lwd=2, alpha=0.5)
|
|
168
|
+
>>> gp
|
|
169
|
+
Gpar(col='red', lwd=2, alpha=0.5)
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
# Slots keep instances lightweight.
|
|
173
|
+
__slots__ = ("_params",)
|
|
174
|
+
|
|
175
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
176
|
+
# Reject unknown parameter names early.
|
|
177
|
+
unknown = set(kwargs) - _GPAR_NAMES
|
|
178
|
+
if unknown:
|
|
179
|
+
raise TypeError(
|
|
180
|
+
f"unknown graphical parameter(s): {', '.join(sorted(unknown))}"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
params: Dict[str, Any] = {}
|
|
184
|
+
|
|
185
|
+
# -- fontface / font mutual exclusion (mirrors R) ------------------
|
|
186
|
+
if "fontface" in kwargs and "font" in kwargs:
|
|
187
|
+
raise ValueError("must specify only one of 'font' and 'fontface'")
|
|
188
|
+
|
|
189
|
+
if "fontface" in kwargs:
|
|
190
|
+
ff = kwargs.pop("fontface")
|
|
191
|
+
if ff is not None:
|
|
192
|
+
ff_list = _as_list(ff)
|
|
193
|
+
if len(ff_list) == 0:
|
|
194
|
+
raise ValueError("'gpar' element 'fontface' must not be length 0")
|
|
195
|
+
resolved = [_resolve_fontface(v) for v in ff_list]
|
|
196
|
+
params["font"] = resolved[0] if len(resolved) == 1 else resolved
|
|
197
|
+
# fontface is consumed; do not store it directly.
|
|
198
|
+
|
|
199
|
+
# -- process remaining parameters ----------------------------------
|
|
200
|
+
for name, value in kwargs.items():
|
|
201
|
+
if value is None:
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
vals = _as_list(value)
|
|
205
|
+
|
|
206
|
+
if len(vals) == 0:
|
|
207
|
+
raise ValueError(
|
|
208
|
+
f"'gpar' element '{name}' must not be length 0"
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# --- per-parameter validation ---------------------------------
|
|
212
|
+
if name in ("fontsize", "lineheight", "cex", "lwd", "lex"):
|
|
213
|
+
try:
|
|
214
|
+
vals = [float(v) for v in vals]
|
|
215
|
+
except (TypeError, ValueError) as exc:
|
|
216
|
+
raise TypeError(
|
|
217
|
+
f"'{name}' must be numeric, got {type(value).__name__}"
|
|
218
|
+
) from exc
|
|
219
|
+
|
|
220
|
+
elif name == "alpha":
|
|
221
|
+
try:
|
|
222
|
+
vals = [float(v) for v in vals]
|
|
223
|
+
except (TypeError, ValueError) as exc:
|
|
224
|
+
raise TypeError(
|
|
225
|
+
f"'alpha' must be numeric, got {type(value).__name__}"
|
|
226
|
+
) from exc
|
|
227
|
+
if any(v < 0 or v > 1 for v in vals):
|
|
228
|
+
raise ValueError("invalid 'alpha' value (must be 0-1)")
|
|
229
|
+
|
|
230
|
+
elif name == "linemitre":
|
|
231
|
+
try:
|
|
232
|
+
vals = [float(v) for v in vals]
|
|
233
|
+
except (TypeError, ValueError) as exc:
|
|
234
|
+
raise TypeError(
|
|
235
|
+
f"'linemitre' must be numeric, got {type(value).__name__}"
|
|
236
|
+
) from exc
|
|
237
|
+
if any(v < 1 for v in vals):
|
|
238
|
+
raise ValueError("invalid 'linemitre' value (must be >= 1)")
|
|
239
|
+
|
|
240
|
+
elif name == "font":
|
|
241
|
+
try:
|
|
242
|
+
vals = [int(v) for v in vals]
|
|
243
|
+
except (TypeError, ValueError) as exc:
|
|
244
|
+
raise TypeError(
|
|
245
|
+
f"'font' must be integer, got {type(value).__name__}"
|
|
246
|
+
) from exc
|
|
247
|
+
|
|
248
|
+
elif name == "lty":
|
|
249
|
+
for v in vals:
|
|
250
|
+
if isinstance(v, str):
|
|
251
|
+
if v not in _VALID_LTY and not _is_hex_lty(v):
|
|
252
|
+
raise ValueError(
|
|
253
|
+
f"invalid line type '{v}'; must be one of "
|
|
254
|
+
f"{sorted(_VALID_LTY)} or a hex string"
|
|
255
|
+
)
|
|
256
|
+
elif not isinstance(v, (int, float, np.integer, np.floating)):
|
|
257
|
+
raise TypeError(
|
|
258
|
+
f"'lty' must be str or numeric, got {type(v).__name__}"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
elif name == "lineend":
|
|
262
|
+
for v in vals:
|
|
263
|
+
if v not in _VALID_LINEEND:
|
|
264
|
+
raise ValueError(
|
|
265
|
+
f"invalid 'lineend' value '{v}'; "
|
|
266
|
+
f"must be one of {sorted(_VALID_LINEEND)}"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
elif name == "linejoin":
|
|
270
|
+
for v in vals:
|
|
271
|
+
if v not in _VALID_LINEJOIN:
|
|
272
|
+
raise ValueError(
|
|
273
|
+
f"invalid 'linejoin' value '{v}'; "
|
|
274
|
+
f"must be one of {sorted(_VALID_LINEJOIN)}"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
elif name == "fontfamily":
|
|
278
|
+
vals = [str(v) for v in vals]
|
|
279
|
+
|
|
280
|
+
elif name in ("col", "fill"):
|
|
281
|
+
# Accept strings or lists of strings; no further validation
|
|
282
|
+
# here (colour resolution is deferred to the rendering
|
|
283
|
+
# backend, matching R's behaviour).
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
# Store single-element lists as scalars for cleaner repr.
|
|
287
|
+
params[name] = vals[0] if len(vals) == 1 else vals
|
|
288
|
+
|
|
289
|
+
self._params = params
|
|
290
|
+
|
|
291
|
+
# -- dict-like access --------------------------------------------------
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def params(self) -> Dict[str, Any]:
|
|
295
|
+
"""Return a **copy** of the underlying parameter dictionary.
|
|
296
|
+
|
|
297
|
+
Returns
|
|
298
|
+
-------
|
|
299
|
+
dict
|
|
300
|
+
Mapping of parameter names to their values.
|
|
301
|
+
"""
|
|
302
|
+
return dict(self._params)
|
|
303
|
+
|
|
304
|
+
def get(self, name: str, default: Any = None) -> Any:
|
|
305
|
+
"""Retrieve a single parameter value.
|
|
306
|
+
|
|
307
|
+
Parameters
|
|
308
|
+
----------
|
|
309
|
+
name : str
|
|
310
|
+
Parameter name.
|
|
311
|
+
default : object, optional
|
|
312
|
+
Value returned when *name* is not set.
|
|
313
|
+
|
|
314
|
+
Returns
|
|
315
|
+
-------
|
|
316
|
+
object
|
|
317
|
+
The parameter value, or *default*.
|
|
318
|
+
"""
|
|
319
|
+
return self._params.get(name, default)
|
|
320
|
+
|
|
321
|
+
def set(self, name: str, value: Any) -> None:
|
|
322
|
+
"""Set a single parameter value.
|
|
323
|
+
|
|
324
|
+
Parameters
|
|
325
|
+
----------
|
|
326
|
+
name : str
|
|
327
|
+
Parameter name.
|
|
328
|
+
value : object
|
|
329
|
+
The value to set.
|
|
330
|
+
"""
|
|
331
|
+
self._params[name] = value
|
|
332
|
+
|
|
333
|
+
def __contains__(self, name: str) -> bool:
|
|
334
|
+
return name in self._params
|
|
335
|
+
|
|
336
|
+
def names(self) -> List[str]:
|
|
337
|
+
"""Return the names of parameters currently set.
|
|
338
|
+
|
|
339
|
+
Returns
|
|
340
|
+
-------
|
|
341
|
+
list of str
|
|
342
|
+
"""
|
|
343
|
+
return list(self._params.keys())
|
|
344
|
+
|
|
345
|
+
# -- length & subscripting ---------------------------------------------
|
|
346
|
+
|
|
347
|
+
def __len__(self) -> int:
|
|
348
|
+
"""Return the maximum length across all vectorised parameters.
|
|
349
|
+
|
|
350
|
+
Returns
|
|
351
|
+
-------
|
|
352
|
+
int
|
|
353
|
+
0 if no parameters are set; otherwise ``max(len(v))`` over all
|
|
354
|
+
parameters (scalars count as length 1).
|
|
355
|
+
"""
|
|
356
|
+
if not self._params:
|
|
357
|
+
return 0
|
|
358
|
+
return max(
|
|
359
|
+
len(v) if isinstance(v, list) else 1
|
|
360
|
+
for v in self._params.values()
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def __getitem__(self, index: int) -> "Gpar":
|
|
364
|
+
"""Subscript the Gpar, returning a new Gpar with the *index*-th element.
|
|
365
|
+
|
|
366
|
+
Vector parameters are recycled to the maximum length (matching R's
|
|
367
|
+
``[.gpar`` method) before the element is selected.
|
|
368
|
+
|
|
369
|
+
Parameters
|
|
370
|
+
----------
|
|
371
|
+
index : int
|
|
372
|
+
Zero-based index into the (recycled) parameter vectors.
|
|
373
|
+
|
|
374
|
+
Returns
|
|
375
|
+
-------
|
|
376
|
+
Gpar
|
|
377
|
+
A new ``Gpar`` containing scalar values for each parameter.
|
|
378
|
+
|
|
379
|
+
Raises
|
|
380
|
+
------
|
|
381
|
+
IndexError
|
|
382
|
+
If *index* is out of range after recycling.
|
|
383
|
+
"""
|
|
384
|
+
n = len(self)
|
|
385
|
+
if n == 0:
|
|
386
|
+
return Gpar()
|
|
387
|
+
|
|
388
|
+
if index < 0:
|
|
389
|
+
index += n
|
|
390
|
+
if index < 0 or index >= n:
|
|
391
|
+
raise IndexError(f"Gpar index {index} out of range [0, {n})")
|
|
392
|
+
|
|
393
|
+
new_params: Dict[str, Any] = {}
|
|
394
|
+
for name, value in self._params.items():
|
|
395
|
+
if isinstance(value, list):
|
|
396
|
+
# Recycle to length n, then pick element.
|
|
397
|
+
recycled = (value * math.ceil(n / len(value)))[:n]
|
|
398
|
+
new_params[name] = recycled[index]
|
|
399
|
+
else:
|
|
400
|
+
new_params[name] = value
|
|
401
|
+
# Build via internal path to skip re-validation.
|
|
402
|
+
gp = object.__new__(Gpar)
|
|
403
|
+
gp._params = new_params
|
|
404
|
+
return gp
|
|
405
|
+
|
|
406
|
+
# -- merge -------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
def _merge(self, parent: "Gpar") -> "Gpar":
|
|
409
|
+
"""Merge with a *parent* ``Gpar`` (child overrides parent).
|
|
410
|
+
|
|
411
|
+
Parameters that are present in *self* take precedence. Parameters
|
|
412
|
+
only present in *parent* are inherited. ``cex``, ``alpha``, and
|
|
413
|
+
``lex`` are **cumulative** — the child value is multiplied by the
|
|
414
|
+
parent value, matching R's ``set.gpar`` semantics.
|
|
415
|
+
|
|
416
|
+
Parameters
|
|
417
|
+
----------
|
|
418
|
+
parent : Gpar
|
|
419
|
+
The parent graphical parameters to merge with.
|
|
420
|
+
|
|
421
|
+
Returns
|
|
422
|
+
-------
|
|
423
|
+
Gpar
|
|
424
|
+
A new ``Gpar`` containing the merged parameters.
|
|
425
|
+
"""
|
|
426
|
+
merged = copy.deepcopy(parent._params)
|
|
427
|
+
merged.update(copy.deepcopy(self._params))
|
|
428
|
+
|
|
429
|
+
# Cumulative parameters
|
|
430
|
+
for cum_name in ("cex", "alpha", "lex"):
|
|
431
|
+
if cum_name in self._params and cum_name in parent._params:
|
|
432
|
+
child_val = self._params[cum_name]
|
|
433
|
+
parent_val = parent._params[cum_name]
|
|
434
|
+
if isinstance(child_val, list) or isinstance(parent_val, list):
|
|
435
|
+
c_list = _as_list(child_val)
|
|
436
|
+
p_list = _as_list(parent_val)
|
|
437
|
+
maxn = max(len(c_list), len(p_list))
|
|
438
|
+
c_cyc = (c_list * math.ceil(maxn / len(c_list)))[:maxn]
|
|
439
|
+
p_cyc = (p_list * math.ceil(maxn / len(p_list)))[:maxn]
|
|
440
|
+
result = [c * p for c, p in zip(c_cyc, p_cyc)]
|
|
441
|
+
merged[cum_name] = result[0] if len(result) == 1 else result
|
|
442
|
+
else:
|
|
443
|
+
merged[cum_name] = child_val * parent_val
|
|
444
|
+
|
|
445
|
+
gp = object.__new__(Gpar)
|
|
446
|
+
gp._params = merged
|
|
447
|
+
return gp
|
|
448
|
+
|
|
449
|
+
# -- display -----------------------------------------------------------
|
|
450
|
+
|
|
451
|
+
def __repr__(self) -> str:
|
|
452
|
+
if not self._params:
|
|
453
|
+
return "Gpar()"
|
|
454
|
+
items = ", ".join(f"{k}={v!r}" for k, v in self._params.items())
|
|
455
|
+
return f"Gpar({items})"
|
|
456
|
+
|
|
457
|
+
def __str__(self) -> str:
|
|
458
|
+
return self.__repr__()
|
|
459
|
+
|
|
460
|
+
# -- equality (useful for testing) -------------------------------------
|
|
461
|
+
|
|
462
|
+
def __eq__(self, other: object) -> bool:
|
|
463
|
+
if not isinstance(other, Gpar):
|
|
464
|
+
return NotImplemented
|
|
465
|
+
return self._params == other._params
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
# ---------------------------------------------------------------------------
|
|
469
|
+
# Module-level helpers
|
|
470
|
+
# ---------------------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _default_gpar() -> Gpar:
|
|
474
|
+
"""Return a ``Gpar`` populated with R's default graphical parameters.
|
|
475
|
+
|
|
476
|
+
Returns
|
|
477
|
+
-------
|
|
478
|
+
Gpar
|
|
479
|
+
Default graphical parameters matching R's internal defaults:
|
|
480
|
+
``fontsize=12``, ``cex=1``, ``fontfamily=""``, ``fontface=1``
|
|
481
|
+
(plain), ``lineheight=1.2``, ``col="black"``, ``fill="transparent"``,
|
|
482
|
+
``alpha=1``, ``lwd=1``, ``lex=1``, ``lty="solid"``,
|
|
483
|
+
``lineend="round"``, ``linejoin="round"``, ``linemitre=10``.
|
|
484
|
+
"""
|
|
485
|
+
return Gpar(
|
|
486
|
+
fontsize=12,
|
|
487
|
+
cex=1,
|
|
488
|
+
fontfamily="",
|
|
489
|
+
fontface=1,
|
|
490
|
+
lineheight=1.2,
|
|
491
|
+
col="black",
|
|
492
|
+
fill="transparent",
|
|
493
|
+
alpha=1.0,
|
|
494
|
+
lwd=1.0,
|
|
495
|
+
lex=1.0,
|
|
496
|
+
lty="solid",
|
|
497
|
+
lineend="round",
|
|
498
|
+
linejoin="round",
|
|
499
|
+
linemitre=10.0,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def get_gpar(names: Optional[Sequence[str]] = None) -> Gpar:
|
|
504
|
+
"""Return the current graphical parameters.
|
|
505
|
+
|
|
506
|
+
Port of R's ``get.gpar()`` (``gpar.R:275-293``). Reads the live
|
|
507
|
+
gpar state from the GridState singleton (equivalent to R's
|
|
508
|
+
``grid.Call(C_getGPar)`` which reads from ``GSS_GPAR``).
|
|
509
|
+
Falls back to defaults if no state is initialised.
|
|
510
|
+
|
|
511
|
+
Parameters
|
|
512
|
+
----------
|
|
513
|
+
names : sequence of str, optional
|
|
514
|
+
If provided, only the listed parameter names are returned. All
|
|
515
|
+
names must be valid ``Gpar`` parameter names.
|
|
516
|
+
|
|
517
|
+
Returns
|
|
518
|
+
-------
|
|
519
|
+
Gpar
|
|
520
|
+
A ``Gpar`` instance with the requested (or all current) parameters.
|
|
521
|
+
|
|
522
|
+
Raises
|
|
523
|
+
------
|
|
524
|
+
ValueError
|
|
525
|
+
If any element of *names* is not a valid gpar name.
|
|
526
|
+
|
|
527
|
+
Examples
|
|
528
|
+
--------
|
|
529
|
+
>>> gp = get_gpar()
|
|
530
|
+
>>> gp.get("fontsize")
|
|
531
|
+
12.0
|
|
532
|
+
>>> get_gpar(names=["col", "lwd"])
|
|
533
|
+
Gpar(col='black', lwd=1.0)
|
|
534
|
+
"""
|
|
535
|
+
# R: result <- grid.Call(C_getGPar) — read from current device state
|
|
536
|
+
# R's C_getGPar returns the fully-resolved gpar from GSS_GPAR which
|
|
537
|
+
# already contains all default values. We emulate this by merging
|
|
538
|
+
# the defaults with whatever the state stack currently holds.
|
|
539
|
+
defaults = _default_gpar()
|
|
540
|
+
try:
|
|
541
|
+
from ._state import get_state
|
|
542
|
+
state = get_state()
|
|
543
|
+
state_gp = state.get_gpar()
|
|
544
|
+
except Exception:
|
|
545
|
+
state_gp = None
|
|
546
|
+
|
|
547
|
+
# Build merged result: defaults overridden by state
|
|
548
|
+
merged = Gpar(**defaults._params)
|
|
549
|
+
if state_gp is not None:
|
|
550
|
+
for k, v in state_gp._params.items():
|
|
551
|
+
if v is not None:
|
|
552
|
+
merged._params[k] = v
|
|
553
|
+
|
|
554
|
+
if names is None:
|
|
555
|
+
return merged
|
|
556
|
+
|
|
557
|
+
# R: if (!is.character(names) || !all(names %in% .grid.gpar.names))
|
|
558
|
+
# stop("must specify only valid 'gpar' names")
|
|
559
|
+
invalid = set(names) - _GPAR_NAMES
|
|
560
|
+
if invalid:
|
|
561
|
+
raise ValueError(
|
|
562
|
+
f"invalid gpar name(s): {', '.join(sorted(invalid))}"
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
subset: Dict[str, Any] = {}
|
|
566
|
+
for n in names:
|
|
567
|
+
val = merged.get(n, None)
|
|
568
|
+
if val is not None:
|
|
569
|
+
subset[n] = val
|
|
570
|
+
gp = object.__new__(Gpar)
|
|
571
|
+
gp._params = subset
|
|
572
|
+
return gp
|