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/_group.py
ADDED
|
@@ -0,0 +1,798 @@
|
|
|
1
|
+
"""Group, define, and use grob system for grid_py.
|
|
2
|
+
|
|
3
|
+
Port of R's ``grid/R/group.R``. This module provides grob classes and
|
|
4
|
+
factory functions for compositing groups:
|
|
5
|
+
|
|
6
|
+
* :class:`GroupGrob` -- a grob that groups its children with a compositing
|
|
7
|
+
operator (``"over"``, ``"source"``, ``"xor"``, etc.).
|
|
8
|
+
* :class:`DefineGrob` -- a grob for deferred definition (define once, use
|
|
9
|
+
later via :class:`UseGrob`).
|
|
10
|
+
* :class:`UseGrob` -- a grob that references a previously defined group and
|
|
11
|
+
optionally applies an affine transform.
|
|
12
|
+
|
|
13
|
+
Factory functions mirror the R API:
|
|
14
|
+
|
|
15
|
+
* :func:`group_grob` / :func:`grid_group`
|
|
16
|
+
* :func:`define_grob` / :func:`grid_define`
|
|
17
|
+
* :func:`use_grob` / :func:`grid_use`
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import warnings
|
|
23
|
+
from typing import Any, Optional, Union
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
from numpy.typing import NDArray
|
|
27
|
+
|
|
28
|
+
from ._gpar import Gpar
|
|
29
|
+
from ._grob import Grob, GList, GTree
|
|
30
|
+
from ._transforms import (
|
|
31
|
+
viewport_transform,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# Classes
|
|
36
|
+
"GroupGrob",
|
|
37
|
+
"DefineGrob",
|
|
38
|
+
"UseGrob",
|
|
39
|
+
# Factory / convenience functions
|
|
40
|
+
"group_grob",
|
|
41
|
+
"grid_group",
|
|
42
|
+
"define_grob",
|
|
43
|
+
"grid_define",
|
|
44
|
+
"use_grob",
|
|
45
|
+
"grid_use",
|
|
46
|
+
# Constants
|
|
47
|
+
"COMPOSITING_OPERATORS",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# Valid compositing operators (mirrors R's .opIndex validation)
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
COMPOSITING_OPERATORS: tuple[str, ...] = (
|
|
55
|
+
"clear",
|
|
56
|
+
"source",
|
|
57
|
+
"over",
|
|
58
|
+
"in",
|
|
59
|
+
"out",
|
|
60
|
+
"atop",
|
|
61
|
+
"dest",
|
|
62
|
+
"dest.over",
|
|
63
|
+
"dest.in",
|
|
64
|
+
"dest.out",
|
|
65
|
+
"dest.atop",
|
|
66
|
+
"xor",
|
|
67
|
+
"add",
|
|
68
|
+
"saturate",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _validate_op(op: str) -> str:
|
|
73
|
+
"""Validate a compositing operator string.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
op : str
|
|
78
|
+
Compositing operator name.
|
|
79
|
+
|
|
80
|
+
Returns
|
|
81
|
+
-------
|
|
82
|
+
str
|
|
83
|
+
The validated (lower-cased) operator.
|
|
84
|
+
|
|
85
|
+
Raises
|
|
86
|
+
------
|
|
87
|
+
ValueError
|
|
88
|
+
If *op* is not a recognised compositing operator.
|
|
89
|
+
"""
|
|
90
|
+
op_lower = op.lower()
|
|
91
|
+
if op_lower not in COMPOSITING_OPERATORS:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"Invalid compositing operator {op!r}. "
|
|
94
|
+
f"Must be one of {COMPOSITING_OPERATORS!r}"
|
|
95
|
+
)
|
|
96
|
+
return op_lower
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _validate_transform(transform: Optional[NDArray[np.float64]]) -> None:
|
|
100
|
+
"""Validate that *transform* is a legal 3x3 affine matrix.
|
|
101
|
+
|
|
102
|
+
The bottom-right element must be 1 and the right column (indices
|
|
103
|
+
``[0, 2]`` and ``[1, 2]``) must be 0, matching R's convention.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
transform : ndarray or None
|
|
108
|
+
A 3x3 numeric matrix, or ``None``.
|
|
109
|
+
|
|
110
|
+
Raises
|
|
111
|
+
------
|
|
112
|
+
ValueError
|
|
113
|
+
If the matrix does not satisfy the constraints.
|
|
114
|
+
TypeError
|
|
115
|
+
If *transform* is not a numpy array.
|
|
116
|
+
"""
|
|
117
|
+
if transform is None:
|
|
118
|
+
return
|
|
119
|
+
if not isinstance(transform, np.ndarray):
|
|
120
|
+
raise TypeError(
|
|
121
|
+
f"'transform' must be a numpy ndarray, got {type(transform).__name__}"
|
|
122
|
+
)
|
|
123
|
+
if transform.shape != (3, 3):
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"'transform' must be a 3x3 matrix, got shape {transform.shape}"
|
|
126
|
+
)
|
|
127
|
+
if not np.issubdtype(transform.dtype, np.number):
|
|
128
|
+
raise ValueError("'transform' must contain numeric values")
|
|
129
|
+
if transform[0, 2] != 0 or transform[1, 2] != 0 or transform[2, 2] != 1:
|
|
130
|
+
raise ValueError(
|
|
131
|
+
"Invalid transform: requires transform[0,2]==0, "
|
|
132
|
+
"transform[1,2]==0, and transform[2,2]==1"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ============================================================================
|
|
137
|
+
# GroupGrob
|
|
138
|
+
# ============================================================================
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class GroupGrob(GTree):
|
|
142
|
+
"""A grob that groups its children with a compositing operator.
|
|
143
|
+
|
|
144
|
+
This is the Python equivalent of R's ``GridGroup`` S3 class produced by
|
|
145
|
+
``groupGrob()``. When drawn, the *src* grob is composited onto the
|
|
146
|
+
optional *dst* grob using the specified Porter-Duff compositing
|
|
147
|
+
*op* (defaulting to ``"over"``).
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
src : Grob or None
|
|
152
|
+
Source grob to composite.
|
|
153
|
+
op : str
|
|
154
|
+
Compositing operator (default ``"over"``). Must be one of
|
|
155
|
+
:data:`COMPOSITING_OPERATORS`.
|
|
156
|
+
dst : Grob or None
|
|
157
|
+
Destination grob. When ``None`` the destination is transparent.
|
|
158
|
+
name : str or None
|
|
159
|
+
Unique grob name. Auto-generated when ``None``.
|
|
160
|
+
gp : Gpar or None
|
|
161
|
+
Graphical parameters.
|
|
162
|
+
vp : object or None
|
|
163
|
+
Viewport.
|
|
164
|
+
|
|
165
|
+
Raises
|
|
166
|
+
------
|
|
167
|
+
TypeError
|
|
168
|
+
If *src* is not a :class:`Grob` (when provided) or *dst* is neither
|
|
169
|
+
a :class:`Grob` nor ``None``.
|
|
170
|
+
ValueError
|
|
171
|
+
If *op* is not a recognised compositing operator.
|
|
172
|
+
|
|
173
|
+
Examples
|
|
174
|
+
--------
|
|
175
|
+
>>> from grid_py._grob import Grob
|
|
176
|
+
>>> src = Grob(name="circle1")
|
|
177
|
+
>>> g = GroupGrob(src=src, op="xor")
|
|
178
|
+
>>> g.op
|
|
179
|
+
'xor'
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
_grid_class: str = "GridGroup" # type: ignore[assignment]
|
|
183
|
+
|
|
184
|
+
def __init__(
|
|
185
|
+
self,
|
|
186
|
+
src: Optional[Grob] = None,
|
|
187
|
+
op: str = "over",
|
|
188
|
+
dst: Optional[Grob] = None,
|
|
189
|
+
name: Optional[str] = None,
|
|
190
|
+
gp: Optional[Gpar] = None,
|
|
191
|
+
vp: Optional[Any] = None,
|
|
192
|
+
) -> None:
|
|
193
|
+
self.src: Optional[Grob] = src
|
|
194
|
+
self.op: str = _validate_op(op)
|
|
195
|
+
self.dst: Optional[Grob] = dst
|
|
196
|
+
# Build a children GList from src and dst for the GTree machinery
|
|
197
|
+
children = self._build_children()
|
|
198
|
+
super().__init__(
|
|
199
|
+
children=children,
|
|
200
|
+
name=name,
|
|
201
|
+
gp=gp,
|
|
202
|
+
vp=vp,
|
|
203
|
+
_grid_class="GridGroup",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# -- helpers -----------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
def _build_children(self) -> Optional[GList]:
|
|
209
|
+
"""Build a :class:`GList` from *src* and *dst*."""
|
|
210
|
+
parts: list[Grob] = []
|
|
211
|
+
if self.src is not None:
|
|
212
|
+
parts.append(self.src)
|
|
213
|
+
if self.dst is not None:
|
|
214
|
+
parts.append(self.dst)
|
|
215
|
+
return GList(*parts) if parts else None
|
|
216
|
+
|
|
217
|
+
# -- validation --------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
def valid_details(self) -> None:
|
|
220
|
+
"""Validate GroupGrob-specific slots.
|
|
221
|
+
|
|
222
|
+
Raises
|
|
223
|
+
------
|
|
224
|
+
TypeError
|
|
225
|
+
If *src* or *dst* have incorrect types.
|
|
226
|
+
ValueError
|
|
227
|
+
If *op* is invalid.
|
|
228
|
+
"""
|
|
229
|
+
if hasattr(self, "src"):
|
|
230
|
+
if self.src is not None and not isinstance(self.src, Grob):
|
|
231
|
+
raise TypeError("Invalid source: must be a Grob or None")
|
|
232
|
+
if hasattr(self, "dst"):
|
|
233
|
+
if self.dst is not None and not isinstance(self.dst, Grob):
|
|
234
|
+
raise TypeError("Invalid destination: must be a Grob or None")
|
|
235
|
+
if hasattr(self, "op"):
|
|
236
|
+
self.op = _validate_op(self.op)
|
|
237
|
+
|
|
238
|
+
# -- drawing -----------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
def draw_details(self, recording: bool = True) -> None:
|
|
241
|
+
"""Draw the composited group.
|
|
242
|
+
|
|
243
|
+
Port of R ``drawDetails.GridGroup`` (group.R:261-270):
|
|
244
|
+
1. finaliseGroup(x) → source/destination closures
|
|
245
|
+
2. .defineGroup(src, op, dst) → ref
|
|
246
|
+
3. recordGroup(x, ref)
|
|
247
|
+
4. .useGroup(ref, NULL)
|
|
248
|
+
"""
|
|
249
|
+
_draw_group_grob(self, use_immediately=True)
|
|
250
|
+
|
|
251
|
+
# -- repr --------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
def __repr__(self) -> str:
|
|
254
|
+
return (
|
|
255
|
+
f"GroupGrob[{self.name}](op={self.op!r}, "
|
|
256
|
+
f"src={self.src!r}, dst={self.dst!r})"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ============================================================================
|
|
261
|
+
# DefineGrob
|
|
262
|
+
# ============================================================================
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class DefineGrob(GTree):
|
|
266
|
+
"""A grob for deferred group definition.
|
|
267
|
+
|
|
268
|
+
This is the Python equivalent of R's ``GridDefine`` S3 class produced by
|
|
269
|
+
``defineGrob()``. The group is defined (but not drawn) so that it can
|
|
270
|
+
later be referenced by :class:`UseGrob`.
|
|
271
|
+
|
|
272
|
+
Parameters
|
|
273
|
+
----------
|
|
274
|
+
src : Grob
|
|
275
|
+
Source grob to define.
|
|
276
|
+
op : str
|
|
277
|
+
Compositing operator (default ``"over"``).
|
|
278
|
+
dst : Grob or None
|
|
279
|
+
Destination grob (default ``None``).
|
|
280
|
+
name : str or None
|
|
281
|
+
Unique grob name. Auto-generated when ``None``.
|
|
282
|
+
gp : Gpar or None
|
|
283
|
+
Graphical parameters.
|
|
284
|
+
vp : object or None
|
|
285
|
+
Viewport.
|
|
286
|
+
|
|
287
|
+
Raises
|
|
288
|
+
------
|
|
289
|
+
TypeError
|
|
290
|
+
If *src* is not a :class:`Grob`.
|
|
291
|
+
ValueError
|
|
292
|
+
If *op* is not a recognised compositing operator.
|
|
293
|
+
|
|
294
|
+
Examples
|
|
295
|
+
--------
|
|
296
|
+
>>> from grid_py._grob import Grob
|
|
297
|
+
>>> src = Grob(name="rect1")
|
|
298
|
+
>>> d = DefineGrob(src=src)
|
|
299
|
+
>>> d.src.name
|
|
300
|
+
'rect1'
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
_grid_class: str = "GridDefine" # type: ignore[assignment]
|
|
304
|
+
|
|
305
|
+
def __init__(
|
|
306
|
+
self,
|
|
307
|
+
src: Grob = None, # type: ignore[assignment]
|
|
308
|
+
op: str = "over",
|
|
309
|
+
dst: Optional[Grob] = None,
|
|
310
|
+
name: Optional[str] = None,
|
|
311
|
+
gp: Optional[Gpar] = None,
|
|
312
|
+
vp: Optional[Any] = None,
|
|
313
|
+
) -> None:
|
|
314
|
+
self.src: Grob = src # type: ignore[assignment]
|
|
315
|
+
self.op: str = _validate_op(op)
|
|
316
|
+
self.dst: Optional[Grob] = dst
|
|
317
|
+
# Build children
|
|
318
|
+
parts: list[Grob] = []
|
|
319
|
+
if self.src is not None:
|
|
320
|
+
parts.append(self.src)
|
|
321
|
+
if self.dst is not None:
|
|
322
|
+
parts.append(self.dst)
|
|
323
|
+
children = GList(*parts) if parts else None
|
|
324
|
+
super().__init__(
|
|
325
|
+
children=children,
|
|
326
|
+
name=name,
|
|
327
|
+
gp=gp,
|
|
328
|
+
vp=vp,
|
|
329
|
+
_grid_class="GridDefine",
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# -- validation --------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
def valid_details(self) -> None:
|
|
335
|
+
"""Validate DefineGrob-specific slots.
|
|
336
|
+
|
|
337
|
+
Raises
|
|
338
|
+
------
|
|
339
|
+
TypeError
|
|
340
|
+
If *src* is not a :class:`Grob`.
|
|
341
|
+
"""
|
|
342
|
+
if hasattr(self, "src") and self.src is not None:
|
|
343
|
+
if not isinstance(self.src, Grob):
|
|
344
|
+
raise TypeError("Invalid source: must be a Grob")
|
|
345
|
+
if hasattr(self, "dst"):
|
|
346
|
+
if self.dst is not None and not isinstance(self.dst, Grob):
|
|
347
|
+
raise TypeError("Invalid destination: must be a Grob or None")
|
|
348
|
+
if hasattr(self, "op"):
|
|
349
|
+
self.op = _validate_op(self.op)
|
|
350
|
+
|
|
351
|
+
# -- drawing -----------------------------------------------------------
|
|
352
|
+
|
|
353
|
+
def draw_details(self, recording: bool = True) -> None:
|
|
354
|
+
"""Define the group without drawing.
|
|
355
|
+
|
|
356
|
+
Port of R ``drawDetails.GridDefine`` (group.R:300-304):
|
|
357
|
+
1. finaliseGroup(x) → source/destination closures
|
|
358
|
+
2. .defineGroup(src, op, dst) → ref
|
|
359
|
+
3. recordGroup(x, ref) — store for later UseGrob
|
|
360
|
+
No visible output is produced.
|
|
361
|
+
"""
|
|
362
|
+
_draw_group_grob(self, use_immediately=False)
|
|
363
|
+
|
|
364
|
+
# -- repr --------------------------------------------------------------
|
|
365
|
+
|
|
366
|
+
def __repr__(self) -> str:
|
|
367
|
+
return (
|
|
368
|
+
f"DefineGrob[{self.name}](src={self.src!r}, "
|
|
369
|
+
f"op={self.op!r}, dst={self.dst!r})"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ============================================================================
|
|
374
|
+
# UseGrob
|
|
375
|
+
# ============================================================================
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
class UseGrob(Grob):
|
|
379
|
+
"""A grob that references a previously defined group.
|
|
380
|
+
|
|
381
|
+
This is the Python equivalent of R's ``GridUse`` S3 class produced by
|
|
382
|
+
``useGrob()``. It draws a group that was previously registered via
|
|
383
|
+
:class:`DefineGrob`, optionally applying an affine *transform*.
|
|
384
|
+
|
|
385
|
+
Parameters
|
|
386
|
+
----------
|
|
387
|
+
group : str
|
|
388
|
+
Name of the previously defined group to use.
|
|
389
|
+
transform : ndarray or None
|
|
390
|
+
A 3x3 affine transformation matrix (NumPy array, dtype float64).
|
|
391
|
+
When ``None``, the default viewport transform is used.
|
|
392
|
+
name : str or None
|
|
393
|
+
Unique grob name. Auto-generated when ``None``.
|
|
394
|
+
gp : Gpar or None
|
|
395
|
+
Graphical parameters.
|
|
396
|
+
vp : object or None
|
|
397
|
+
Viewport.
|
|
398
|
+
|
|
399
|
+
Raises
|
|
400
|
+
------
|
|
401
|
+
TypeError
|
|
402
|
+
If *group* is not a string or *transform* is not a numpy array.
|
|
403
|
+
ValueError
|
|
404
|
+
If *transform* does not satisfy the affine-matrix constraints.
|
|
405
|
+
|
|
406
|
+
Examples
|
|
407
|
+
--------
|
|
408
|
+
>>> u = UseGrob(group="rect1")
|
|
409
|
+
>>> u.group
|
|
410
|
+
'rect1'
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
_grid_class: str = "GridUse" # type: ignore[assignment]
|
|
414
|
+
|
|
415
|
+
def __init__(
|
|
416
|
+
self,
|
|
417
|
+
group: str = "",
|
|
418
|
+
transform: Optional[NDArray[np.float64]] = None,
|
|
419
|
+
name: Optional[str] = None,
|
|
420
|
+
gp: Optional[Gpar] = None,
|
|
421
|
+
vp: Optional[Any] = None,
|
|
422
|
+
) -> None:
|
|
423
|
+
self.group: str = str(group)
|
|
424
|
+
self.transform: Optional[NDArray[np.float64]] = transform
|
|
425
|
+
_validate_transform(self.transform)
|
|
426
|
+
super().__init__(
|
|
427
|
+
name=name,
|
|
428
|
+
gp=gp,
|
|
429
|
+
vp=vp,
|
|
430
|
+
_grid_class="GridUse",
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# -- validation --------------------------------------------------------
|
|
434
|
+
|
|
435
|
+
def valid_details(self) -> None:
|
|
436
|
+
"""Validate UseGrob-specific slots.
|
|
437
|
+
|
|
438
|
+
Raises
|
|
439
|
+
------
|
|
440
|
+
TypeError
|
|
441
|
+
If *group* is not a string.
|
|
442
|
+
ValueError
|
|
443
|
+
If *transform* is invalid.
|
|
444
|
+
"""
|
|
445
|
+
if hasattr(self, "group"):
|
|
446
|
+
if not isinstance(self.group, str):
|
|
447
|
+
raise TypeError("'group' must be a string")
|
|
448
|
+
if hasattr(self, "transform"):
|
|
449
|
+
_validate_transform(self.transform)
|
|
450
|
+
|
|
451
|
+
# -- drawing -----------------------------------------------------------
|
|
452
|
+
|
|
453
|
+
def draw_details(self, recording: bool = True) -> None:
|
|
454
|
+
"""Draw the referenced group with the optional transform.
|
|
455
|
+
|
|
456
|
+
Port of R ``drawDetails.GridUse`` (group.R:330-347):
|
|
457
|
+
1. lookupGroup(x$group) → group metadata
|
|
458
|
+
2. Compute transform via x$transform(group, device=TRUE)
|
|
459
|
+
3. Validate 3x3 affine matrix
|
|
460
|
+
4. .useGroup(group$ref, transform)
|
|
461
|
+
"""
|
|
462
|
+
from ._state import get_state
|
|
463
|
+
|
|
464
|
+
state = get_state()
|
|
465
|
+
group_data = state.lookup_group(self.group)
|
|
466
|
+
|
|
467
|
+
if group_data is None:
|
|
468
|
+
warnings.warn(f"Unknown group: {self.group}")
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
ref = group_data.get("ref")
|
|
472
|
+
if ref is None:
|
|
473
|
+
warnings.warn(f"Group '{self.group}' has no ref")
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
# Compute transform (R group.R:335)
|
|
477
|
+
transform = self.transform
|
|
478
|
+
if callable(transform):
|
|
479
|
+
# R passes a function: x$transform(group, device=TRUE)
|
|
480
|
+
transform = transform(group_data, device=True)
|
|
481
|
+
|
|
482
|
+
# Validate transform (R group.R:336-344)
|
|
483
|
+
if transform is not None:
|
|
484
|
+
m = np.asarray(transform, dtype=float)
|
|
485
|
+
if m.shape != (3, 3):
|
|
486
|
+
warnings.warn("Invalid transform (nothing drawn)")
|
|
487
|
+
return
|
|
488
|
+
if m[0, 2] != 0 or m[1, 2] != 0 or m[2, 2] != 1:
|
|
489
|
+
warnings.warn("Invalid transform (nothing drawn)")
|
|
490
|
+
return
|
|
491
|
+
|
|
492
|
+
renderer = state.get_renderer()
|
|
493
|
+
if renderer is not None and hasattr(renderer, "use_group"):
|
|
494
|
+
renderer.use_group(ref, transform)
|
|
495
|
+
|
|
496
|
+
# -- repr --------------------------------------------------------------
|
|
497
|
+
|
|
498
|
+
def __repr__(self) -> str:
|
|
499
|
+
return (
|
|
500
|
+
f"UseGrob[{self.name}](group={self.group!r}, "
|
|
501
|
+
f"transform={'set' if self.transform is not None else 'None'})"
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
# ============================================================================
|
|
506
|
+
# Internal: shared group drawing logic
|
|
507
|
+
# ============================================================================
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _draw_group_grob(grob: Union[GroupGrob, DefineGrob],
|
|
511
|
+
use_immediately: bool) -> None:
|
|
512
|
+
"""Shared logic for GroupGrob.draw_details and DefineGrob.draw_details.
|
|
513
|
+
|
|
514
|
+
Port of R's ``drawDetails.GridGroup`` (group.R:261-270) and
|
|
515
|
+
``drawDetails.GridDefine`` (group.R:300-304).
|
|
516
|
+
|
|
517
|
+
1. Build source/destination draw closures (``finaliseGroup``, group.R:9-58)
|
|
518
|
+
2. Call renderer.define_group(src, op, dst) → ref
|
|
519
|
+
3. Record group in state for later UseGrob access
|
|
520
|
+
4. If *use_immediately*, call renderer.use_group(ref, None)
|
|
521
|
+
"""
|
|
522
|
+
from ._state import get_state
|
|
523
|
+
from ._draw import grid_draw
|
|
524
|
+
|
|
525
|
+
state = get_state()
|
|
526
|
+
renderer = state.get_renderer()
|
|
527
|
+
|
|
528
|
+
if renderer is None:
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
src_grob = getattr(grob, "src", None)
|
|
532
|
+
dst_grob = getattr(grob, "dst", None)
|
|
533
|
+
op = getattr(grob, "op", "over")
|
|
534
|
+
|
|
535
|
+
# Build source closure (R group.R:10-38)
|
|
536
|
+
# R pushes a viewport with mask="none" to ensure clean group context.
|
|
537
|
+
# We simply draw the source grob.
|
|
538
|
+
def source_fn():
|
|
539
|
+
if src_grob is not None:
|
|
540
|
+
grid_draw(src_grob, recording=False)
|
|
541
|
+
|
|
542
|
+
# Build destination closure (R group.R:40-56)
|
|
543
|
+
dst_fn = None
|
|
544
|
+
if dst_grob is not None:
|
|
545
|
+
def dst_fn():
|
|
546
|
+
grid_draw(dst_grob, recording=False)
|
|
547
|
+
|
|
548
|
+
# Define group on renderer (R group.R:263/302)
|
|
549
|
+
ref = renderer.define_group(source_fn, op, dst_fn)
|
|
550
|
+
|
|
551
|
+
# Record group for later UseGrob access (R group.R:265/303)
|
|
552
|
+
# Port of recordGroup (group.R:65-104)
|
|
553
|
+
from ._units import convert_x, convert_y, Unit
|
|
554
|
+
group_data = {
|
|
555
|
+
"ref": ref,
|
|
556
|
+
"name": grob.name,
|
|
557
|
+
}
|
|
558
|
+
# Store viewport location/size for viewportTransform
|
|
559
|
+
try:
|
|
560
|
+
vtr = renderer._vp_transform_stack[-1]
|
|
561
|
+
group_data["wh"] = (vtr.width_cm / 2.54, vtr.height_cm / 2.54)
|
|
562
|
+
group_data["r"] = vtr.rotation_angle
|
|
563
|
+
group_data["transform"] = vtr.transform.copy()
|
|
564
|
+
except Exception:
|
|
565
|
+
group_data["wh"] = (1.0, 1.0)
|
|
566
|
+
group_data["r"] = 0.0
|
|
567
|
+
|
|
568
|
+
state.record_group(grob.name, group_data)
|
|
569
|
+
|
|
570
|
+
if ref is None:
|
|
571
|
+
warnings.warn("Group definition failed")
|
|
572
|
+
return
|
|
573
|
+
|
|
574
|
+
# Use immediately for GroupGrob (R group.R:269)
|
|
575
|
+
if use_immediately:
|
|
576
|
+
renderer.use_group(ref, None)
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
# ============================================================================
|
|
580
|
+
# Factory functions
|
|
581
|
+
# ============================================================================
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def group_grob(
|
|
585
|
+
src: Optional[Grob] = None,
|
|
586
|
+
op: str = "over",
|
|
587
|
+
dst: Optional[Grob] = None,
|
|
588
|
+
name: Optional[str] = None,
|
|
589
|
+
gp: Optional[Gpar] = None,
|
|
590
|
+
vp: Optional[Any] = None,
|
|
591
|
+
) -> GroupGrob:
|
|
592
|
+
"""Create a :class:`GroupGrob`.
|
|
593
|
+
|
|
594
|
+
This is the functional equivalent of R's ``groupGrob()``.
|
|
595
|
+
|
|
596
|
+
Parameters
|
|
597
|
+
----------
|
|
598
|
+
src : Grob or None
|
|
599
|
+
Source grob.
|
|
600
|
+
op : str
|
|
601
|
+
Compositing operator (default ``"over"``).
|
|
602
|
+
dst : Grob or None
|
|
603
|
+
Destination grob.
|
|
604
|
+
name : str or None
|
|
605
|
+
Grob name.
|
|
606
|
+
gp : Gpar or None
|
|
607
|
+
Graphical parameters.
|
|
608
|
+
vp : object or None
|
|
609
|
+
Viewport.
|
|
610
|
+
|
|
611
|
+
Returns
|
|
612
|
+
-------
|
|
613
|
+
GroupGrob
|
|
614
|
+
"""
|
|
615
|
+
return GroupGrob(src=src, op=op, dst=dst, name=name, gp=gp, vp=vp)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def grid_group(
|
|
619
|
+
src: Optional[Grob] = None,
|
|
620
|
+
op: str = "over",
|
|
621
|
+
dst: Optional[Grob] = None,
|
|
622
|
+
name: Optional[str] = None,
|
|
623
|
+
gp: Optional[Gpar] = None,
|
|
624
|
+
vp: Optional[Any] = None,
|
|
625
|
+
draw: bool = True,
|
|
626
|
+
) -> GroupGrob:
|
|
627
|
+
"""Create and optionally draw a :class:`GroupGrob`.
|
|
628
|
+
|
|
629
|
+
This is the functional equivalent of R's ``grid.group()``.
|
|
630
|
+
|
|
631
|
+
Parameters
|
|
632
|
+
----------
|
|
633
|
+
src : Grob or None
|
|
634
|
+
Source grob.
|
|
635
|
+
op : str
|
|
636
|
+
Compositing operator (default ``"over"``).
|
|
637
|
+
dst : Grob or None
|
|
638
|
+
Destination grob.
|
|
639
|
+
name : str or None
|
|
640
|
+
Grob name.
|
|
641
|
+
gp : Gpar or None
|
|
642
|
+
Graphical parameters.
|
|
643
|
+
vp : object or None
|
|
644
|
+
Viewport.
|
|
645
|
+
draw : bool
|
|
646
|
+
If ``True`` (default), the grob is drawn immediately via
|
|
647
|
+
``draw_details``.
|
|
648
|
+
|
|
649
|
+
Returns
|
|
650
|
+
-------
|
|
651
|
+
GroupGrob
|
|
652
|
+
"""
|
|
653
|
+
grb = GroupGrob(src=src, op=op, dst=dst, name=name, gp=gp, vp=vp)
|
|
654
|
+
if draw:
|
|
655
|
+
grb.draw_details()
|
|
656
|
+
return grb
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def define_grob(
|
|
660
|
+
src: Grob,
|
|
661
|
+
op: str = "over",
|
|
662
|
+
dst: Optional[Grob] = None,
|
|
663
|
+
name: Optional[str] = None,
|
|
664
|
+
gp: Optional[Gpar] = None,
|
|
665
|
+
vp: Optional[Any] = None,
|
|
666
|
+
) -> DefineGrob:
|
|
667
|
+
"""Create a :class:`DefineGrob`.
|
|
668
|
+
|
|
669
|
+
This is the functional equivalent of R's ``defineGrob()``.
|
|
670
|
+
|
|
671
|
+
Parameters
|
|
672
|
+
----------
|
|
673
|
+
src : Grob
|
|
674
|
+
Source grob.
|
|
675
|
+
op : str
|
|
676
|
+
Compositing operator (default ``"over"``).
|
|
677
|
+
dst : Grob or None
|
|
678
|
+
Destination grob.
|
|
679
|
+
name : str or None
|
|
680
|
+
Grob name.
|
|
681
|
+
gp : Gpar or None
|
|
682
|
+
Graphical parameters.
|
|
683
|
+
vp : object or None
|
|
684
|
+
Viewport.
|
|
685
|
+
|
|
686
|
+
Returns
|
|
687
|
+
-------
|
|
688
|
+
DefineGrob
|
|
689
|
+
"""
|
|
690
|
+
return DefineGrob(src=src, op=op, dst=dst, name=name, gp=gp, vp=vp)
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def grid_define(
|
|
694
|
+
src: Grob,
|
|
695
|
+
op: str = "over",
|
|
696
|
+
dst: Optional[Grob] = None,
|
|
697
|
+
name: Optional[str] = None,
|
|
698
|
+
gp: Optional[Gpar] = None,
|
|
699
|
+
vp: Optional[Any] = None,
|
|
700
|
+
draw: bool = True,
|
|
701
|
+
) -> DefineGrob:
|
|
702
|
+
"""Create and optionally draw a :class:`DefineGrob`.
|
|
703
|
+
|
|
704
|
+
This is the functional equivalent of R's ``grid.define()``.
|
|
705
|
+
|
|
706
|
+
Parameters
|
|
707
|
+
----------
|
|
708
|
+
src : Grob
|
|
709
|
+
Source grob.
|
|
710
|
+
op : str
|
|
711
|
+
Compositing operator (default ``"over"``).
|
|
712
|
+
dst : Grob or None
|
|
713
|
+
Destination grob.
|
|
714
|
+
name : str or None
|
|
715
|
+
Grob name.
|
|
716
|
+
gp : Gpar or None
|
|
717
|
+
Graphical parameters.
|
|
718
|
+
vp : object or None
|
|
719
|
+
Viewport.
|
|
720
|
+
draw : bool
|
|
721
|
+
If ``True`` (default), the grob is drawn (defined) immediately.
|
|
722
|
+
|
|
723
|
+
Returns
|
|
724
|
+
-------
|
|
725
|
+
DefineGrob
|
|
726
|
+
"""
|
|
727
|
+
grb = DefineGrob(src=src, op=op, dst=dst, name=name, gp=gp, vp=vp)
|
|
728
|
+
if draw:
|
|
729
|
+
grb.draw_details()
|
|
730
|
+
return grb
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def use_grob(
|
|
734
|
+
group: str,
|
|
735
|
+
transform: Optional[NDArray[np.float64]] = None,
|
|
736
|
+
name: Optional[str] = None,
|
|
737
|
+
gp: Optional[Gpar] = None,
|
|
738
|
+
vp: Optional[Any] = None,
|
|
739
|
+
) -> UseGrob:
|
|
740
|
+
"""Create a :class:`UseGrob`.
|
|
741
|
+
|
|
742
|
+
This is the functional equivalent of R's ``useGrob()``.
|
|
743
|
+
|
|
744
|
+
Parameters
|
|
745
|
+
----------
|
|
746
|
+
group : str
|
|
747
|
+
Name of the previously defined group.
|
|
748
|
+
transform : ndarray or None
|
|
749
|
+
3x3 affine transformation matrix.
|
|
750
|
+
name : str or None
|
|
751
|
+
Grob name.
|
|
752
|
+
gp : Gpar or None
|
|
753
|
+
Graphical parameters.
|
|
754
|
+
vp : object or None
|
|
755
|
+
Viewport.
|
|
756
|
+
|
|
757
|
+
Returns
|
|
758
|
+
-------
|
|
759
|
+
UseGrob
|
|
760
|
+
"""
|
|
761
|
+
return UseGrob(group=group, transform=transform, name=name, gp=gp, vp=vp)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def grid_use(
|
|
765
|
+
group: str,
|
|
766
|
+
transform: Optional[NDArray[np.float64]] = None,
|
|
767
|
+
name: Optional[str] = None,
|
|
768
|
+
gp: Optional[Gpar] = None,
|
|
769
|
+
vp: Optional[Any] = None,
|
|
770
|
+
draw: bool = True,
|
|
771
|
+
) -> UseGrob:
|
|
772
|
+
"""Create and optionally draw a :class:`UseGrob`.
|
|
773
|
+
|
|
774
|
+
This is the functional equivalent of R's ``grid.use()``.
|
|
775
|
+
|
|
776
|
+
Parameters
|
|
777
|
+
----------
|
|
778
|
+
group : str
|
|
779
|
+
Name of the previously defined group.
|
|
780
|
+
transform : ndarray or None
|
|
781
|
+
3x3 affine transformation matrix.
|
|
782
|
+
name : str or None
|
|
783
|
+
Grob name.
|
|
784
|
+
gp : Gpar or None
|
|
785
|
+
Graphical parameters.
|
|
786
|
+
vp : object or None
|
|
787
|
+
Viewport.
|
|
788
|
+
draw : bool
|
|
789
|
+
If ``True`` (default), the grob is drawn immediately.
|
|
790
|
+
|
|
791
|
+
Returns
|
|
792
|
+
-------
|
|
793
|
+
UseGrob
|
|
794
|
+
"""
|
|
795
|
+
grb = UseGrob(group=group, transform=transform, name=name, gp=gp, vp=vp)
|
|
796
|
+
if draw:
|
|
797
|
+
grb.draw_details()
|
|
798
|
+
return grb
|