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/_state.py
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
"""Global state management for grid_py (port of R's grid C-level state).
|
|
2
|
+
|
|
3
|
+
This module provides the :class:`GridState` singleton that manages the
|
|
4
|
+
viewport tree, display list, graphical-parameter inheritance stack, and
|
|
5
|
+
the binding to a rendering backend (:class:`GridRenderer` subclass).
|
|
6
|
+
It replaces the C-level ``GridState`` struct found in R's *grid* package.
|
|
7
|
+
|
|
8
|
+
.. note::
|
|
9
|
+
Viewport classes are **not** imported here to avoid circular
|
|
10
|
+
dependencies. Viewport references are stored and manipulated via
|
|
11
|
+
duck typing (any object with ``name``, ``parent``, ``children``, and
|
|
12
|
+
``layout_pos`` attributes is accepted).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import copy
|
|
18
|
+
from collections import deque
|
|
19
|
+
from typing import Any, Deque, Dict, List, Optional, Sequence, Tuple, Union
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
|
|
23
|
+
from ._gpar import Gpar
|
|
24
|
+
from ._display_list import DisplayList
|
|
25
|
+
|
|
26
|
+
__all__ = ["GridState", "get_state"]
|
|
27
|
+
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
# Default device dimensions (≈ 7 in ≈ 17.78 cm)
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
_DEFAULT_DEVICE_WIDTH_CM: float = 17.78
|
|
33
|
+
_DEFAULT_DEVICE_HEIGHT_CM: float = 17.78
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# Helpers
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
def _make_root_viewport() -> Any:
|
|
41
|
+
"""Create a minimal root viewport dict (duck-typed).
|
|
42
|
+
|
|
43
|
+
A "real" viewport object will replace this once
|
|
44
|
+
:meth:`GridState.reset` or :meth:`GridState.push_viewport` is
|
|
45
|
+
called with an actual viewport instance. The dict is used only as
|
|
46
|
+
the initial sentinel so that the tree is never ``None``.
|
|
47
|
+
|
|
48
|
+
Returns
|
|
49
|
+
-------
|
|
50
|
+
dict
|
|
51
|
+
A mapping that quacks like a viewport for bootstrap purposes.
|
|
52
|
+
"""
|
|
53
|
+
return {
|
|
54
|
+
"name": "ROOT",
|
|
55
|
+
"parent": None,
|
|
56
|
+
"children": [],
|
|
57
|
+
"layout_pos": None,
|
|
58
|
+
"gpar": Gpar(),
|
|
59
|
+
"rotation": 0.0,
|
|
60
|
+
"transform": np.eye(3, dtype=np.float64),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _vp_attr(vp: Any, attr: str, default: Any = None) -> Any:
|
|
65
|
+
"""Retrieve an attribute from a viewport, supporting both objects and dicts.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
vp : Any
|
|
70
|
+
Viewport object or dict.
|
|
71
|
+
attr : str
|
|
72
|
+
Attribute / key name.
|
|
73
|
+
default : Any, optional
|
|
74
|
+
Fallback value.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
Any
|
|
79
|
+
"""
|
|
80
|
+
if isinstance(vp, dict):
|
|
81
|
+
return vp.get(attr, default)
|
|
82
|
+
return getattr(vp, attr, default)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _vp_set_attr(vp: Any, attr: str, value: Any) -> None:
|
|
86
|
+
"""Set an attribute on a viewport, supporting both objects and dicts.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
vp : Any
|
|
91
|
+
Viewport object or dict.
|
|
92
|
+
attr : str
|
|
93
|
+
Attribute / key name.
|
|
94
|
+
value : Any
|
|
95
|
+
Value to assign.
|
|
96
|
+
"""
|
|
97
|
+
if isinstance(vp, dict):
|
|
98
|
+
vp[attr] = value
|
|
99
|
+
else:
|
|
100
|
+
setattr(vp, attr, value)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _vp_children(vp: Any) -> list:
|
|
104
|
+
"""Return the children list of a viewport.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
vp : Any
|
|
109
|
+
Viewport object or dict.
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
list
|
|
114
|
+
The children list (never ``None``). If the attribute is absent
|
|
115
|
+
or ``None``, an empty list is returned.
|
|
116
|
+
"""
|
|
117
|
+
result = _vp_attr(vp, "children", None)
|
|
118
|
+
if result is None:
|
|
119
|
+
# Initialise an empty list on the viewport so future appends persist.
|
|
120
|
+
result = []
|
|
121
|
+
_vp_set_attr(vp, "children", result)
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _vp_name(vp: Any) -> str:
|
|
126
|
+
"""Return the name of a viewport.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
vp : Any
|
|
131
|
+
Viewport object or dict.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
str
|
|
136
|
+
"""
|
|
137
|
+
return _vp_attr(vp, "name", "")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _vp_parent(vp: Any) -> Any:
|
|
141
|
+
"""Return the parent of a viewport.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
vp : Any
|
|
146
|
+
Viewport object or dict.
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
Any
|
|
151
|
+
"""
|
|
152
|
+
return _vp_attr(vp, "parent", None)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
# ---------------------------------------------------------------------------
|
|
156
|
+
# GridState
|
|
157
|
+
# ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
class GridState:
|
|
160
|
+
"""Singleton holding the global grid graphics state.
|
|
161
|
+
|
|
162
|
+
Manages the viewport tree, display list, graphical-parameter
|
|
163
|
+
inheritance stack, and the connection to a rendering backend
|
|
164
|
+
(``Figure`` / ``Axes``).
|
|
165
|
+
|
|
166
|
+
Attributes
|
|
167
|
+
----------
|
|
168
|
+
_vp_tree : Any
|
|
169
|
+
The root viewport (pushed viewport representing the device).
|
|
170
|
+
_current_vp : Any
|
|
171
|
+
Reference to the currently active viewport.
|
|
172
|
+
_display_list : list[Any]
|
|
173
|
+
Recorded drawing operations.
|
|
174
|
+
_dl_on : bool
|
|
175
|
+
Whether display-list recording is enabled.
|
|
176
|
+
_gpar_stack : list[Gpar]
|
|
177
|
+
Stack of graphical parameter objects for inheritance.
|
|
178
|
+
_device_width_cm : float
|
|
179
|
+
Device width in centimetres.
|
|
180
|
+
_device_height_cm : float
|
|
181
|
+
Device height in centimetres.
|
|
182
|
+
_renderer : Optional[Any]
|
|
183
|
+
:class:`GridRenderer` subclass instance (or ``None``).
|
|
184
|
+
|
|
185
|
+
Examples
|
|
186
|
+
--------
|
|
187
|
+
>>> state = GridState()
|
|
188
|
+
>>> state.current_viewport()["name"]
|
|
189
|
+
'ROOT'
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
# ---- class-level singleton bookkeeping --------------------------------
|
|
193
|
+
|
|
194
|
+
_instance: Optional["GridState"] = None
|
|
195
|
+
|
|
196
|
+
def __new__(cls) -> "GridState":
|
|
197
|
+
if cls._instance is None:
|
|
198
|
+
inst = super().__new__(cls)
|
|
199
|
+
inst._initialized = False
|
|
200
|
+
cls._instance = inst
|
|
201
|
+
return cls._instance
|
|
202
|
+
|
|
203
|
+
# ---- initialisation ---------------------------------------------------
|
|
204
|
+
|
|
205
|
+
def __init__(self) -> None:
|
|
206
|
+
if self._initialized:
|
|
207
|
+
return
|
|
208
|
+
self._initialized: bool = True
|
|
209
|
+
self._init_defaults()
|
|
210
|
+
|
|
211
|
+
def _init_defaults(self) -> None:
|
|
212
|
+
"""Set every slot to its default value."""
|
|
213
|
+
self._vp_tree: Any = _make_root_viewport()
|
|
214
|
+
self._current_vp: Any = self._vp_tree
|
|
215
|
+
self._display_list: DisplayList = DisplayList()
|
|
216
|
+
self._dl_on: bool = True
|
|
217
|
+
self._gpar_stack: List[Gpar] = [Gpar()]
|
|
218
|
+
self._device_width_cm: float = _DEFAULT_DEVICE_WIDTH_CM
|
|
219
|
+
self._device_height_cm: float = _DEFAULT_DEVICE_HEIGHT_CM
|
|
220
|
+
self._renderer: Optional[Any] = None
|
|
221
|
+
# GSS_SCALE: zoom factor for physical units (R unit.c:804-814)
|
|
222
|
+
# R grid state slot 15. Default 1.0, set by grid.newpage(zoom=).
|
|
223
|
+
self._scale: float = 1.0
|
|
224
|
+
# GSS_GROUPS: group registry for define/use (R grid.h:63, state.c:51)
|
|
225
|
+
# Maps group name → dict with keys: ref, xy, xyin, wh, r, etc.
|
|
226
|
+
self._groups: Dict[str, Any] = {}
|
|
227
|
+
|
|
228
|
+
# ---- reset ------------------------------------------------------------
|
|
229
|
+
|
|
230
|
+
def reset(self) -> None:
|
|
231
|
+
"""Clear all state and reinitialise to the default root viewport.
|
|
232
|
+
|
|
233
|
+
This is the equivalent of ``grid.newpage()`` in R.
|
|
234
|
+
"""
|
|
235
|
+
self._init_defaults()
|
|
236
|
+
|
|
237
|
+
# ---- viewport tree manipulation ---------------------------------------
|
|
238
|
+
|
|
239
|
+
def push_viewport(self, vp: Any) -> None:
|
|
240
|
+
"""Push a viewport onto the tree as a child of the current viewport.
|
|
241
|
+
|
|
242
|
+
Parameters
|
|
243
|
+
----------
|
|
244
|
+
vp : Any
|
|
245
|
+
A viewport-like object. Must expose ``name``, ``parent``,
|
|
246
|
+
and ``children`` attributes (or dict keys).
|
|
247
|
+
"""
|
|
248
|
+
_vp_set_attr(vp, "parent", self._current_vp)
|
|
249
|
+
children = _vp_children(self._current_vp)
|
|
250
|
+
if children is None:
|
|
251
|
+
children = []
|
|
252
|
+
_vp_set_attr(self._current_vp, "children", children)
|
|
253
|
+
children.append(vp)
|
|
254
|
+
self._current_vp = vp
|
|
255
|
+
|
|
256
|
+
def pop_viewport(self, n: int = 1) -> None:
|
|
257
|
+
"""Pop *n* viewports, navigating back toward the root.
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
n : int, optional
|
|
262
|
+
Number of levels to pop (default ``1``). If *n* equals ``0``
|
|
263
|
+
the navigation returns to the root viewport.
|
|
264
|
+
|
|
265
|
+
Raises
|
|
266
|
+
------
|
|
267
|
+
ValueError
|
|
268
|
+
If *n* is negative or greater than the current depth.
|
|
269
|
+
"""
|
|
270
|
+
if n < 0:
|
|
271
|
+
raise ValueError(f"'n' must be non-negative, got {n}")
|
|
272
|
+
if n == 0:
|
|
273
|
+
# Pop to root.
|
|
274
|
+
self._current_vp = self._vp_tree
|
|
275
|
+
return
|
|
276
|
+
for _ in range(n):
|
|
277
|
+
parent = _vp_parent(self._current_vp)
|
|
278
|
+
if parent is None:
|
|
279
|
+
raise ValueError(
|
|
280
|
+
"Cannot pop past the root viewport."
|
|
281
|
+
)
|
|
282
|
+
# Remove the current viewport from its parent's children.
|
|
283
|
+
parent_children = _vp_children(parent)
|
|
284
|
+
try:
|
|
285
|
+
parent_children.remove(self._current_vp)
|
|
286
|
+
except ValueError:
|
|
287
|
+
pass
|
|
288
|
+
self._current_vp = parent
|
|
289
|
+
|
|
290
|
+
def up_viewport(self, n: int = 1) -> None:
|
|
291
|
+
"""Navigate up *n* levels without removing viewports from the tree.
|
|
292
|
+
|
|
293
|
+
Parameters
|
|
294
|
+
----------
|
|
295
|
+
n : int, optional
|
|
296
|
+
Number of levels to ascend (default ``1``). ``0`` navigates
|
|
297
|
+
to the root.
|
|
298
|
+
|
|
299
|
+
Raises
|
|
300
|
+
------
|
|
301
|
+
ValueError
|
|
302
|
+
If *n* is negative or exceeds the current depth.
|
|
303
|
+
"""
|
|
304
|
+
if n < 0:
|
|
305
|
+
raise ValueError(f"'n' must be non-negative, got {n}")
|
|
306
|
+
if n == 0:
|
|
307
|
+
self._current_vp = self._vp_tree
|
|
308
|
+
return
|
|
309
|
+
for _ in range(n):
|
|
310
|
+
parent = _vp_parent(self._current_vp)
|
|
311
|
+
if parent is None:
|
|
312
|
+
raise ValueError(
|
|
313
|
+
"Cannot navigate above the root viewport."
|
|
314
|
+
)
|
|
315
|
+
self._current_vp = parent
|
|
316
|
+
|
|
317
|
+
def down_viewport(self, name: str, strict: bool = False) -> int:
|
|
318
|
+
"""Navigate down to a named viewport (breadth-first search).
|
|
319
|
+
|
|
320
|
+
The search starts from the **children** of the current viewport.
|
|
321
|
+
|
|
322
|
+
Parameters
|
|
323
|
+
----------
|
|
324
|
+
name : str
|
|
325
|
+
Name of the target viewport.
|
|
326
|
+
strict : bool, optional
|
|
327
|
+
If ``True`` the name must match exactly; otherwise a
|
|
328
|
+
case-insensitive match is attempted after an exact match
|
|
329
|
+
fails.
|
|
330
|
+
|
|
331
|
+
Returns
|
|
332
|
+
-------
|
|
333
|
+
int
|
|
334
|
+
The depth (number of levels descended) to reach the target.
|
|
335
|
+
|
|
336
|
+
Raises
|
|
337
|
+
------
|
|
338
|
+
LookupError
|
|
339
|
+
If no matching viewport is found.
|
|
340
|
+
"""
|
|
341
|
+
depth = self._search_down(self._current_vp, name, strict)
|
|
342
|
+
if depth is None:
|
|
343
|
+
raise LookupError(
|
|
344
|
+
f"Viewport '{name}' not found below the current viewport."
|
|
345
|
+
)
|
|
346
|
+
return depth
|
|
347
|
+
|
|
348
|
+
def _search_down(
|
|
349
|
+
self, start: Any, name: str, strict: bool
|
|
350
|
+
) -> Optional[int]:
|
|
351
|
+
"""BFS helper for :meth:`down_viewport`.
|
|
352
|
+
|
|
353
|
+
Returns the depth on success, or ``None`` on failure. As a
|
|
354
|
+
side-effect, ``_current_vp`` is updated to point to the found
|
|
355
|
+
viewport.
|
|
356
|
+
"""
|
|
357
|
+
queue: Deque[Tuple[Any, int]] = deque()
|
|
358
|
+
for child in _vp_children(start):
|
|
359
|
+
queue.append((child, 1))
|
|
360
|
+
|
|
361
|
+
while queue:
|
|
362
|
+
vp, d = queue.popleft()
|
|
363
|
+
vp_n = _vp_name(vp)
|
|
364
|
+
if vp_n == name or (not strict and vp_n.lower() == name.lower()):
|
|
365
|
+
self._current_vp = vp
|
|
366
|
+
return d
|
|
367
|
+
for child in _vp_children(vp):
|
|
368
|
+
queue.append((child, d + 1))
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
def seek_viewport(self, name: str) -> int:
|
|
372
|
+
"""Global search for a named viewport starting from the root.
|
|
373
|
+
|
|
374
|
+
If found, ``_current_vp`` is set to the matching viewport and
|
|
375
|
+
the absolute depth from the root is returned.
|
|
376
|
+
|
|
377
|
+
Parameters
|
|
378
|
+
----------
|
|
379
|
+
name : str
|
|
380
|
+
Viewport name to search for.
|
|
381
|
+
|
|
382
|
+
Returns
|
|
383
|
+
-------
|
|
384
|
+
int
|
|
385
|
+
Depth from the root to the found viewport.
|
|
386
|
+
|
|
387
|
+
Raises
|
|
388
|
+
------
|
|
389
|
+
LookupError
|
|
390
|
+
If no matching viewport is found anywhere in the tree.
|
|
391
|
+
"""
|
|
392
|
+
result = self._search_down(self._vp_tree, name, strict=False)
|
|
393
|
+
if result is None:
|
|
394
|
+
raise LookupError(
|
|
395
|
+
f"Viewport '{name}' not found in the viewport tree."
|
|
396
|
+
)
|
|
397
|
+
return result
|
|
398
|
+
|
|
399
|
+
# ---- viewport queries -------------------------------------------------
|
|
400
|
+
|
|
401
|
+
def current_viewport(self) -> Any:
|
|
402
|
+
"""Return the currently active viewport.
|
|
403
|
+
|
|
404
|
+
Returns
|
|
405
|
+
-------
|
|
406
|
+
Any
|
|
407
|
+
The active viewport object (or dict).
|
|
408
|
+
"""
|
|
409
|
+
return self._current_vp
|
|
410
|
+
|
|
411
|
+
def current_vp_path(self) -> str:
|
|
412
|
+
"""Return the ``/``-separated path from root to the current viewport.
|
|
413
|
+
|
|
414
|
+
Returns
|
|
415
|
+
-------
|
|
416
|
+
str
|
|
417
|
+
E.g. ``"ROOT/panel/strip"``.
|
|
418
|
+
"""
|
|
419
|
+
parts: List[str] = []
|
|
420
|
+
vp: Any = self._current_vp
|
|
421
|
+
while vp is not None:
|
|
422
|
+
parts.append(_vp_name(vp))
|
|
423
|
+
vp = _vp_parent(vp)
|
|
424
|
+
parts.reverse()
|
|
425
|
+
return "/".join(parts)
|
|
426
|
+
|
|
427
|
+
def current_vp_tree(self) -> Any:
|
|
428
|
+
"""Return the root of the entire viewport tree.
|
|
429
|
+
|
|
430
|
+
Returns
|
|
431
|
+
-------
|
|
432
|
+
Any
|
|
433
|
+
"""
|
|
434
|
+
return self._vp_tree
|
|
435
|
+
|
|
436
|
+
def current_transform(self) -> np.ndarray:
|
|
437
|
+
"""Return the cumulative 3x3 transformation matrix for the current viewport.
|
|
438
|
+
|
|
439
|
+
The matrix is accumulated by multiplying transforms from the
|
|
440
|
+
root down to the current viewport.
|
|
441
|
+
|
|
442
|
+
Returns
|
|
443
|
+
-------
|
|
444
|
+
numpy.ndarray
|
|
445
|
+
A 3x3 ``float64`` transformation matrix.
|
|
446
|
+
"""
|
|
447
|
+
matrices: List[np.ndarray] = []
|
|
448
|
+
vp: Any = self._current_vp
|
|
449
|
+
while vp is not None:
|
|
450
|
+
t = _vp_attr(vp, "transform", None)
|
|
451
|
+
if t is not None:
|
|
452
|
+
matrices.append(np.asarray(t, dtype=np.float64))
|
|
453
|
+
vp = _vp_parent(vp)
|
|
454
|
+
matrices.reverse()
|
|
455
|
+
|
|
456
|
+
result = np.eye(3, dtype=np.float64)
|
|
457
|
+
for m in matrices:
|
|
458
|
+
result = result @ m
|
|
459
|
+
return result
|
|
460
|
+
|
|
461
|
+
def current_rotation(self) -> float:
|
|
462
|
+
"""Return the cumulative rotation angle (degrees) at the current viewport.
|
|
463
|
+
|
|
464
|
+
Returns
|
|
465
|
+
-------
|
|
466
|
+
float
|
|
467
|
+
The sum of ``rotation`` attributes from root to current viewport.
|
|
468
|
+
"""
|
|
469
|
+
total: float = 0.0
|
|
470
|
+
vp: Any = self._current_vp
|
|
471
|
+
while vp is not None:
|
|
472
|
+
total += float(_vp_attr(vp, "rotation", 0.0))
|
|
473
|
+
vp = _vp_parent(vp)
|
|
474
|
+
return total
|
|
475
|
+
|
|
476
|
+
def current_parent(self) -> Any:
|
|
477
|
+
"""Return the parent of the current viewport.
|
|
478
|
+
|
|
479
|
+
Returns
|
|
480
|
+
-------
|
|
481
|
+
Any
|
|
482
|
+
Parent viewport, or ``None`` if at the root.
|
|
483
|
+
"""
|
|
484
|
+
return _vp_parent(self._current_vp)
|
|
485
|
+
|
|
486
|
+
# ---- gpar management --------------------------------------------------
|
|
487
|
+
|
|
488
|
+
def get_gpar(self) -> Gpar:
|
|
489
|
+
"""Return the current (top-of-stack) graphical parameters.
|
|
490
|
+
|
|
491
|
+
Returns
|
|
492
|
+
-------
|
|
493
|
+
Gpar
|
|
494
|
+
"""
|
|
495
|
+
return self._gpar_stack[-1]
|
|
496
|
+
|
|
497
|
+
def set_gpar(self, gp: Gpar) -> None:
|
|
498
|
+
"""Push a :class:`Gpar` onto the parameter stack.
|
|
499
|
+
|
|
500
|
+
Parameters
|
|
501
|
+
----------
|
|
502
|
+
gp : Gpar
|
|
503
|
+
Graphical parameters to make current.
|
|
504
|
+
"""
|
|
505
|
+
self._gpar_stack.append(gp)
|
|
506
|
+
|
|
507
|
+
def replace_gpar(self, gp: Gpar) -> None:
|
|
508
|
+
"""Replace the current (top-of-stack) graphical parameters.
|
|
509
|
+
|
|
510
|
+
Unlike :meth:`set_gpar`, this does **not** grow the stack; it
|
|
511
|
+
overwrites the most-recent entry. This mirrors R's
|
|
512
|
+
``C_setGPar`` which is a simple slot replacement on the device
|
|
513
|
+
state, used by viewport push/pop/up/down to update gpar without
|
|
514
|
+
creating a new stack frame.
|
|
515
|
+
"""
|
|
516
|
+
self._gpar_stack[-1] = gp
|
|
517
|
+
|
|
518
|
+
# ---- display list -----------------------------------------------------
|
|
519
|
+
|
|
520
|
+
@property
|
|
521
|
+
def display_list(self) -> DisplayList:
|
|
522
|
+
"""Return the current :class:`DisplayList` object.
|
|
523
|
+
|
|
524
|
+
Returns
|
|
525
|
+
-------
|
|
526
|
+
DisplayList
|
|
527
|
+
"""
|
|
528
|
+
return self._display_list
|
|
529
|
+
|
|
530
|
+
@display_list.setter
|
|
531
|
+
def display_list(self, value: DisplayList) -> None:
|
|
532
|
+
"""Replace the current display list.
|
|
533
|
+
|
|
534
|
+
Parameters
|
|
535
|
+
----------
|
|
536
|
+
value : DisplayList
|
|
537
|
+
The new display list.
|
|
538
|
+
"""
|
|
539
|
+
self._display_list = value
|
|
540
|
+
|
|
541
|
+
def record(self, op: Any) -> None:
|
|
542
|
+
"""Append an operation to the display list (if recording is on).
|
|
543
|
+
|
|
544
|
+
Parameters
|
|
545
|
+
----------
|
|
546
|
+
op : Any
|
|
547
|
+
A drawable / grob operation.
|
|
548
|
+
"""
|
|
549
|
+
if self._dl_on:
|
|
550
|
+
self._display_list.record(op)
|
|
551
|
+
|
|
552
|
+
def get_display_list(self) -> DisplayList:
|
|
553
|
+
"""Return the current display list.
|
|
554
|
+
|
|
555
|
+
Returns
|
|
556
|
+
-------
|
|
557
|
+
DisplayList
|
|
558
|
+
"""
|
|
559
|
+
return self._display_list
|
|
560
|
+
|
|
561
|
+
def set_display_list_on(self, on: bool) -> None:
|
|
562
|
+
"""Enable or disable display-list recording.
|
|
563
|
+
|
|
564
|
+
Parameters
|
|
565
|
+
----------
|
|
566
|
+
on : bool
|
|
567
|
+
``True`` to enable, ``False`` to disable.
|
|
568
|
+
"""
|
|
569
|
+
self._dl_on = bool(on)
|
|
570
|
+
|
|
571
|
+
# ---- group registry (R GSS_GROUPS, grid.h:63) -------------------------
|
|
572
|
+
|
|
573
|
+
def record_group(self, name: str, group_data: Dict[str, Any]) -> None:
|
|
574
|
+
"""Register a group definition for later reuse.
|
|
575
|
+
|
|
576
|
+
Port of R ``recordGroup()`` (group.R:65-104).
|
|
577
|
+
|
|
578
|
+
Parameters
|
|
579
|
+
----------
|
|
580
|
+
name : str
|
|
581
|
+
Group name (must match the DefineGrob name).
|
|
582
|
+
group_data : dict
|
|
583
|
+
Group metadata including ``ref`` (renderer-specific handle),
|
|
584
|
+
``xy``, ``xyin``, ``wh``, ``r`` (rotation).
|
|
585
|
+
"""
|
|
586
|
+
self._groups[name] = group_data
|
|
587
|
+
|
|
588
|
+
def lookup_group(self, name: str) -> Optional[Dict[str, Any]]:
|
|
589
|
+
"""Look up a previously defined group.
|
|
590
|
+
|
|
591
|
+
Port of R ``lookupGroup()`` (group.R:106-110).
|
|
592
|
+
|
|
593
|
+
Parameters
|
|
594
|
+
----------
|
|
595
|
+
name : str
|
|
596
|
+
Group name.
|
|
597
|
+
|
|
598
|
+
Returns
|
|
599
|
+
-------
|
|
600
|
+
dict or None
|
|
601
|
+
Group metadata, or ``None`` if not found.
|
|
602
|
+
"""
|
|
603
|
+
return self._groups.get(name)
|
|
604
|
+
|
|
605
|
+
def clear_groups(self) -> None:
|
|
606
|
+
"""Remove all group definitions."""
|
|
607
|
+
self._groups.clear()
|
|
608
|
+
|
|
609
|
+
# ---- device binding ---------------------------------------------------
|
|
610
|
+
|
|
611
|
+
def init_device(
|
|
612
|
+
self,
|
|
613
|
+
renderer: Any,
|
|
614
|
+
width_cm: Optional[float] = None,
|
|
615
|
+
height_cm: Optional[float] = None,
|
|
616
|
+
) -> None:
|
|
617
|
+
"""Bind the state to a rendering backend.
|
|
618
|
+
|
|
619
|
+
If ``width_cm`` / ``height_cm`` are not supplied, they are read
|
|
620
|
+
from the renderer's own ``width_in`` / ``height_in`` fields so the
|
|
621
|
+
unit system uses the same canvas the renderer is drawing on. This
|
|
622
|
+
avoids a systematic mismatch where the state defaulted to a 7-inch
|
|
623
|
+
square canvas while the renderer was actually 6×4 inches, causing
|
|
624
|
+
``strwidth`` / ``strheight`` to be converted to native coordinates
|
|
625
|
+
against the wrong reference width.
|
|
626
|
+
|
|
627
|
+
Parameters
|
|
628
|
+
----------
|
|
629
|
+
renderer : GridRenderer
|
|
630
|
+
A :class:`GridRenderer` subclass instance (e.g.
|
|
631
|
+
``CairoRenderer`` or ``WebRenderer``).
|
|
632
|
+
width_cm, height_cm : float, optional
|
|
633
|
+
Device dimensions in centimetres. If ``None``, pulled from
|
|
634
|
+
the renderer's own ``width_in`` / ``height_in`` when
|
|
635
|
+
available, otherwise defaulted to ``_DEFAULT_DEVICE_*_CM``.
|
|
636
|
+
"""
|
|
637
|
+
self._renderer = renderer
|
|
638
|
+
if width_cm is None:
|
|
639
|
+
w_in = float(getattr(renderer, "width_in", 0.0) or 0.0)
|
|
640
|
+
width_cm = w_in * 2.54 if w_in > 0 else _DEFAULT_DEVICE_WIDTH_CM
|
|
641
|
+
if height_cm is None:
|
|
642
|
+
h_in = float(getattr(renderer, "height_in", 0.0) or 0.0)
|
|
643
|
+
height_cm = h_in * 2.54 if h_in > 0 else _DEFAULT_DEVICE_HEIGHT_CM
|
|
644
|
+
self._device_width_cm = float(width_cm)
|
|
645
|
+
self._device_height_cm = float(height_cm)
|
|
646
|
+
|
|
647
|
+
def get_renderer(self) -> Any:
|
|
648
|
+
"""Return the current rendering backend.
|
|
649
|
+
|
|
650
|
+
Returns
|
|
651
|
+
-------
|
|
652
|
+
GridRenderer or None
|
|
653
|
+
The renderer, or ``None`` if :meth:`init_device` has not
|
|
654
|
+
been called.
|
|
655
|
+
"""
|
|
656
|
+
return self._renderer
|
|
657
|
+
|
|
658
|
+
def get_device(self) -> Tuple[Any, Any]:
|
|
659
|
+
"""Backward-compatible accessor.
|
|
660
|
+
|
|
661
|
+
Returns ``(renderer, renderer)`` so that code using
|
|
662
|
+
``fig, ax = state.get_device()`` still works during the
|
|
663
|
+
transition. Both elements are the renderer (or ``None``).
|
|
664
|
+
"""
|
|
665
|
+
return (self._renderer, self._renderer)
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
# ---------------------------------------------------------------------------
|
|
669
|
+
# Module-level singleton & accessor
|
|
670
|
+
# ---------------------------------------------------------------------------
|
|
671
|
+
|
|
672
|
+
_state: GridState = GridState()
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def get_state() -> GridState:
|
|
676
|
+
"""Return the module-level :class:`GridState` singleton.
|
|
677
|
+
|
|
678
|
+
Returns
|
|
679
|
+
-------
|
|
680
|
+
GridState
|
|
681
|
+
The global state instance.
|
|
682
|
+
"""
|
|
683
|
+
return _state
|