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/_viewport.py
ADDED
|
@@ -0,0 +1,1649 @@
|
|
|
1
|
+
"""Viewport system for grid_py -- Python port of R's ``grid::viewport``.
|
|
2
|
+
|
|
3
|
+
This module provides the :class:`Viewport` class and associated container
|
|
4
|
+
classes (:class:`VpList`, :class:`VpStack`, :class:`VpTree`) that mirror
|
|
5
|
+
the viewport infrastructure in R's *grid* package. It also exposes the
|
|
6
|
+
navigation functions (``push_viewport``, ``pop_viewport``, ``up_viewport``,
|
|
7
|
+
``down_viewport``, ``seek_viewport``) and query helpers
|
|
8
|
+
(``current_viewport``, ``current_vp_path``, etc.).
|
|
9
|
+
|
|
10
|
+
Viewports define nested rectangular sub-regions of the graphics device,
|
|
11
|
+
each with its own coordinate system, clipping behaviour, and graphical
|
|
12
|
+
parameter settings.
|
|
13
|
+
|
|
14
|
+
References
|
|
15
|
+
----------
|
|
16
|
+
R source: ``src/library/grid/R/viewport.R``, ``src/library/grid/R/grid.R``
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import copy
|
|
22
|
+
import math
|
|
23
|
+
import threading
|
|
24
|
+
from typing import (
|
|
25
|
+
Any,
|
|
26
|
+
Iterator,
|
|
27
|
+
List,
|
|
28
|
+
Optional,
|
|
29
|
+
Sequence,
|
|
30
|
+
Tuple,
|
|
31
|
+
Union,
|
|
32
|
+
overload,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
import numpy as np
|
|
36
|
+
|
|
37
|
+
from ._gpar import Gpar
|
|
38
|
+
from ._just import valid_just
|
|
39
|
+
from ._layout import GridLayout
|
|
40
|
+
from ._path import VpPath
|
|
41
|
+
from ._units import Unit, is_unit
|
|
42
|
+
|
|
43
|
+
__all__ = [
|
|
44
|
+
"Viewport",
|
|
45
|
+
"VpList",
|
|
46
|
+
"VpStack",
|
|
47
|
+
"VpTree",
|
|
48
|
+
"push_viewport",
|
|
49
|
+
"pop_viewport",
|
|
50
|
+
"down_viewport",
|
|
51
|
+
"up_viewport",
|
|
52
|
+
"seek_viewport",
|
|
53
|
+
"current_viewport",
|
|
54
|
+
"current_vp_path",
|
|
55
|
+
"current_vp_tree",
|
|
56
|
+
"current_transform",
|
|
57
|
+
"current_rotation",
|
|
58
|
+
"current_parent",
|
|
59
|
+
"data_viewport",
|
|
60
|
+
"plot_viewport",
|
|
61
|
+
"edit_viewport",
|
|
62
|
+
"show_viewport",
|
|
63
|
+
"depth",
|
|
64
|
+
"is_viewport",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Module-level auto-name counter (thread-safe)
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
_vp_name_lock = threading.Lock()
|
|
72
|
+
_vp_name_index: int = 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _vp_auto_name() -> str:
|
|
76
|
+
"""Generate a unique viewport name of the form ``GRID.VP.<n>``.
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
str
|
|
81
|
+
The generated name.
|
|
82
|
+
|
|
83
|
+
Notes
|
|
84
|
+
-----
|
|
85
|
+
This mirrors R's ``vpAutoName()`` closure. A module-level counter is
|
|
86
|
+
used instead of a closure so that it can be reset for testing. Access
|
|
87
|
+
is serialised with a lock for thread safety.
|
|
88
|
+
"""
|
|
89
|
+
global _vp_name_index
|
|
90
|
+
with _vp_name_lock:
|
|
91
|
+
_vp_name_index += 1
|
|
92
|
+
return f"GRID.VP.{_vp_name_index}"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _reset_vp_auto_name() -> None:
|
|
96
|
+
"""Reset the auto-name counter to zero (for testing)."""
|
|
97
|
+
global _vp_name_index
|
|
98
|
+
with _vp_name_lock:
|
|
99
|
+
_vp_name_index = 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
# Clip value normalisation
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
_CLIP_MAP = {
|
|
107
|
+
"on": True,
|
|
108
|
+
"off": None, # R uses NA; we use None
|
|
109
|
+
"inherit": False,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _valid_clip(clip: Any) -> Any:
|
|
114
|
+
"""Normalise *clip* to its internal representation.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
clip : bool, str, or other
|
|
119
|
+
``"on"`` -> ``True``, ``"off"`` -> ``None``, ``"inherit"`` -> ``False``.
|
|
120
|
+
Booleans and ``None`` pass through unchanged.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
bool or None
|
|
125
|
+
|
|
126
|
+
Raises
|
|
127
|
+
------
|
|
128
|
+
ValueError
|
|
129
|
+
If *clip* is a string that is not one of the accepted values.
|
|
130
|
+
"""
|
|
131
|
+
if isinstance(clip, bool) or clip is None:
|
|
132
|
+
return clip
|
|
133
|
+
if isinstance(clip, str):
|
|
134
|
+
val = _CLIP_MAP.get(clip.lower())
|
|
135
|
+
if val is None and clip.lower() != "off":
|
|
136
|
+
raise ValueError(
|
|
137
|
+
f"invalid 'clip' value {clip!r}; "
|
|
138
|
+
"must be 'on', 'off', 'inherit', or a boolean"
|
|
139
|
+
)
|
|
140
|
+
# "off" -> None is correct from the map
|
|
141
|
+
return _CLIP_MAP.get(clip.lower(), None)
|
|
142
|
+
raise ValueError(
|
|
143
|
+
f"invalid 'clip' value {clip!r}; "
|
|
144
|
+
"must be 'on', 'off', 'inherit', or a boolean"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# Mask value normalisation
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
_MASK_MAP = {
|
|
153
|
+
"inherit": True,
|
|
154
|
+
"none": False,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _valid_mask(mask: Any) -> Any:
|
|
159
|
+
"""Normalise *mask* to its internal representation.
|
|
160
|
+
|
|
161
|
+
Parameters
|
|
162
|
+
----------
|
|
163
|
+
mask : bool, str, or other
|
|
164
|
+
``"inherit"`` -> ``True``, ``"none"`` -> ``False``.
|
|
165
|
+
Booleans pass through unchanged. Other objects (e.g. mask grobs)
|
|
166
|
+
are returned as-is.
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
bool or object
|
|
171
|
+
|
|
172
|
+
Raises
|
|
173
|
+
------
|
|
174
|
+
ValueError
|
|
175
|
+
If *mask* is a string that is not one of the accepted values.
|
|
176
|
+
"""
|
|
177
|
+
if isinstance(mask, bool):
|
|
178
|
+
return mask
|
|
179
|
+
if isinstance(mask, str):
|
|
180
|
+
val = _MASK_MAP.get(mask.lower())
|
|
181
|
+
if val is None:
|
|
182
|
+
raise ValueError(
|
|
183
|
+
f"invalid 'mask' value {mask!r}; "
|
|
184
|
+
"must be 'inherit', 'none', or a boolean"
|
|
185
|
+
)
|
|
186
|
+
return val
|
|
187
|
+
# Arbitrary mask objects (grobs, etc.) pass through
|
|
188
|
+
return mask
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
# Viewport class
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class Viewport:
|
|
197
|
+
"""A viewport specification -- a rectangular sub-region of a device.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
x : Unit or float or None
|
|
202
|
+
Horizontal position. Defaults to ``Unit(0.5, "npc")``.
|
|
203
|
+
y : Unit or float or None
|
|
204
|
+
Vertical position. Defaults to ``Unit(0.5, "npc")``.
|
|
205
|
+
width : Unit or float or None
|
|
206
|
+
Width. Defaults to ``Unit(1, "npc")``.
|
|
207
|
+
height : Unit or float or None
|
|
208
|
+
Height. Defaults to ``Unit(1, "npc")``.
|
|
209
|
+
default_units : str
|
|
210
|
+
Unit type applied when *x*, *y*, *width*, or *height* are given
|
|
211
|
+
as plain numbers rather than :class:`Unit` objects.
|
|
212
|
+
just : str or sequence
|
|
213
|
+
Justification specification (see :func:`valid_just`).
|
|
214
|
+
gp : Gpar or None
|
|
215
|
+
Graphical parameter settings.
|
|
216
|
+
clip : str or bool
|
|
217
|
+
Clipping mode: ``"inherit"`` (default), ``"on"``, or ``"off"``.
|
|
218
|
+
mask : bool or str
|
|
219
|
+
Masking mode: ``"inherit"`` (default, mapped to ``True``),
|
|
220
|
+
``"none"`` (mapped to ``False``), or a mask grob.
|
|
221
|
+
xscale : sequence of float or None
|
|
222
|
+
Two-element ``[min, max]`` giving the native x-coordinate range.
|
|
223
|
+
Defaults to ``[0, 1]``.
|
|
224
|
+
yscale : sequence of float or None
|
|
225
|
+
Two-element ``[min, max]`` giving the native y-coordinate range.
|
|
226
|
+
Defaults to ``[0, 1]``.
|
|
227
|
+
angle : float
|
|
228
|
+
Rotation angle in degrees.
|
|
229
|
+
layout : GridLayout or None
|
|
230
|
+
Layout for arranging children of this viewport.
|
|
231
|
+
layout_pos_row : int, sequence of int, or None
|
|
232
|
+
Row position(s) of this viewport in the parent's layout.
|
|
233
|
+
layout_pos_col : int, sequence of int, or None
|
|
234
|
+
Column position(s) of this viewport in the parent's layout.
|
|
235
|
+
name : str or None
|
|
236
|
+
Viewport name. Auto-generated (``"GRID.VP.<n>"``) if ``None``.
|
|
237
|
+
|
|
238
|
+
Raises
|
|
239
|
+
------
|
|
240
|
+
ValueError
|
|
241
|
+
If any argument fails validation.
|
|
242
|
+
TypeError
|
|
243
|
+
If *gp* is not a :class:`Gpar` (or ``None``).
|
|
244
|
+
|
|
245
|
+
Examples
|
|
246
|
+
--------
|
|
247
|
+
>>> vp = Viewport(width=Unit(0.8, "npc"), height=Unit(0.8, "npc"))
|
|
248
|
+
>>> str(vp)
|
|
249
|
+
'viewport[GRID.VP.1]'
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
# We store everything on the instance rather than using ``__slots__``
|
|
253
|
+
# because pushed-viewport copies add additional runtime attributes
|
|
254
|
+
# (``parentgpar``, ``trans``, ``children``, etc.) and slots would
|
|
255
|
+
# prevent that.
|
|
256
|
+
|
|
257
|
+
def __init__(
|
|
258
|
+
self,
|
|
259
|
+
x: Union[Unit, float, int, None] = None,
|
|
260
|
+
y: Union[Unit, float, int, None] = None,
|
|
261
|
+
width: Union[Unit, float, int, None] = None,
|
|
262
|
+
height: Union[Unit, float, int, None] = None,
|
|
263
|
+
default_units: str = "npc",
|
|
264
|
+
just: Any = "centre",
|
|
265
|
+
gp: Optional[Gpar] = None,
|
|
266
|
+
clip: Any = "inherit",
|
|
267
|
+
mask: Any = "inherit",
|
|
268
|
+
xscale: Optional[Sequence[float]] = None,
|
|
269
|
+
yscale: Optional[Sequence[float]] = None,
|
|
270
|
+
angle: float = 0,
|
|
271
|
+
layout: Optional[GridLayout] = None,
|
|
272
|
+
layout_pos_row: Optional[Union[int, Sequence[int]]] = None,
|
|
273
|
+
layout_pos_col: Optional[Union[int, Sequence[int]]] = None,
|
|
274
|
+
name: Optional[str] = None,
|
|
275
|
+
) -> None:
|
|
276
|
+
# -- position / size defaults ----------------------------------------
|
|
277
|
+
if x is None:
|
|
278
|
+
x = Unit(0.5, "npc")
|
|
279
|
+
if y is None:
|
|
280
|
+
y = Unit(0.5, "npc")
|
|
281
|
+
if width is None:
|
|
282
|
+
width = Unit(1, "npc")
|
|
283
|
+
if height is None:
|
|
284
|
+
height = Unit(1, "npc")
|
|
285
|
+
|
|
286
|
+
# Coerce plain numerics to Unit with *default_units*
|
|
287
|
+
if not is_unit(x):
|
|
288
|
+
x = Unit(x, default_units)
|
|
289
|
+
if not is_unit(y):
|
|
290
|
+
y = Unit(y, default_units)
|
|
291
|
+
if not is_unit(width):
|
|
292
|
+
width = Unit(width, default_units)
|
|
293
|
+
if not is_unit(height):
|
|
294
|
+
height = Unit(height, default_units)
|
|
295
|
+
|
|
296
|
+
# -- validate scalar unit length -------------------------------------
|
|
297
|
+
for arg_name, arg_val in [
|
|
298
|
+
("x", x), ("y", y), ("width", width), ("height", height),
|
|
299
|
+
]:
|
|
300
|
+
if len(arg_val) != 1:
|
|
301
|
+
raise ValueError(
|
|
302
|
+
f"'{arg_name}' must be a unit of length 1, "
|
|
303
|
+
f"got length {len(arg_val)}"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# -- gp ---------------------------------------------------------------
|
|
307
|
+
if gp is None:
|
|
308
|
+
gp = Gpar()
|
|
309
|
+
if not isinstance(gp, Gpar):
|
|
310
|
+
raise TypeError(
|
|
311
|
+
f"invalid 'gp' value: expected Gpar, got {type(gp).__name__}"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# -- clip / mask ------------------------------------------------------
|
|
315
|
+
clip = _valid_clip(clip)
|
|
316
|
+
mask = _valid_mask(mask)
|
|
317
|
+
|
|
318
|
+
# -- scales -----------------------------------------------------------
|
|
319
|
+
if xscale is None:
|
|
320
|
+
xscale = [0.0, 1.0]
|
|
321
|
+
if yscale is None:
|
|
322
|
+
yscale = [0.0, 1.0]
|
|
323
|
+
|
|
324
|
+
xscale = [float(v) for v in xscale]
|
|
325
|
+
yscale = [float(v) for v in yscale]
|
|
326
|
+
|
|
327
|
+
if len(xscale) != 2 or not all(math.isfinite(v) for v in xscale):
|
|
328
|
+
raise ValueError("invalid 'xscale' in viewport")
|
|
329
|
+
if xscale[1] == xscale[0]:
|
|
330
|
+
raise ValueError(
|
|
331
|
+
"invalid 'xscale' in viewport: range must be non-zero"
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
if len(yscale) != 2 or not all(math.isfinite(v) for v in yscale):
|
|
335
|
+
raise ValueError("invalid 'yscale' in viewport")
|
|
336
|
+
if yscale[1] == yscale[0]:
|
|
337
|
+
raise ValueError(
|
|
338
|
+
"invalid 'yscale' in viewport: range must be non-zero"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# -- angle ------------------------------------------------------------
|
|
342
|
+
angle = float(angle)
|
|
343
|
+
if not math.isfinite(angle):
|
|
344
|
+
raise ValueError("invalid 'angle' in viewport")
|
|
345
|
+
|
|
346
|
+
# -- layout -----------------------------------------------------------
|
|
347
|
+
if layout is not None and not isinstance(layout, GridLayout):
|
|
348
|
+
raise ValueError("invalid 'layout' in viewport")
|
|
349
|
+
|
|
350
|
+
# -- layout position ---------------------------------------------------
|
|
351
|
+
if layout_pos_row is not None:
|
|
352
|
+
if isinstance(layout_pos_row, (int, np.integer)):
|
|
353
|
+
layout_pos_row = [int(layout_pos_row), int(layout_pos_row)]
|
|
354
|
+
else:
|
|
355
|
+
vals = [int(v) for v in layout_pos_row]
|
|
356
|
+
layout_pos_row = [min(vals), max(vals)]
|
|
357
|
+
if not all(math.isfinite(v) for v in layout_pos_row):
|
|
358
|
+
raise ValueError("invalid 'layout_pos_row' in viewport")
|
|
359
|
+
|
|
360
|
+
if layout_pos_col is not None:
|
|
361
|
+
if isinstance(layout_pos_col, (int, np.integer)):
|
|
362
|
+
layout_pos_col = [int(layout_pos_col), int(layout_pos_col)]
|
|
363
|
+
else:
|
|
364
|
+
vals = [int(v) for v in layout_pos_col]
|
|
365
|
+
layout_pos_col = [min(vals), max(vals)]
|
|
366
|
+
if not all(math.isfinite(v) for v in layout_pos_col):
|
|
367
|
+
raise ValueError("invalid 'layout_pos_col' in viewport")
|
|
368
|
+
|
|
369
|
+
# -- justification ----------------------------------------------------
|
|
370
|
+
just_pair = valid_just(just)
|
|
371
|
+
|
|
372
|
+
# -- name --------------------------------------------------------------
|
|
373
|
+
if name is None:
|
|
374
|
+
name = _vp_auto_name()
|
|
375
|
+
if not isinstance(name, str) or not name:
|
|
376
|
+
raise ValueError(
|
|
377
|
+
f"invalid viewport name: {name!r}"
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# -- store validated fields -------------------------------------------
|
|
381
|
+
self._x: Unit = x
|
|
382
|
+
self._y: Unit = y
|
|
383
|
+
self._width: Unit = width
|
|
384
|
+
self._height: Unit = height
|
|
385
|
+
self._default_units: str = default_units
|
|
386
|
+
self._just: Tuple[float, float] = just_pair
|
|
387
|
+
self._gp: Gpar = gp
|
|
388
|
+
self._clip: Any = clip
|
|
389
|
+
self._mask: Any = mask
|
|
390
|
+
self._xscale: List[float] = xscale
|
|
391
|
+
self._yscale: List[float] = yscale
|
|
392
|
+
self._angle: float = angle
|
|
393
|
+
self._layout: Optional[GridLayout] = layout
|
|
394
|
+
self._layout_pos_row: Optional[List[int]] = layout_pos_row
|
|
395
|
+
self._layout_pos_col: Optional[List[int]] = layout_pos_col
|
|
396
|
+
self._name: str = name
|
|
397
|
+
|
|
398
|
+
# -- pushed-viewport slots (filled in when the vp is pushed) ----------
|
|
399
|
+
self.parentgpar: Optional[Gpar] = None
|
|
400
|
+
self.gpar: Optional[Gpar] = None
|
|
401
|
+
self.trans: Optional[np.ndarray] = None
|
|
402
|
+
self.widths: Optional[Any] = None
|
|
403
|
+
self.heights: Optional[Any] = None
|
|
404
|
+
self.width_cm: Optional[float] = None
|
|
405
|
+
self.height_cm: Optional[float] = None
|
|
406
|
+
self.rotation: Optional[float] = None
|
|
407
|
+
self.cliprect: Optional[Any] = None
|
|
408
|
+
self.parent: Optional["Viewport"] = None
|
|
409
|
+
self.children: Optional[dict] = None
|
|
410
|
+
self.devwidth: Optional[float] = None
|
|
411
|
+
self.devheight: Optional[float] = None
|
|
412
|
+
self.clippath: Optional[Any] = None
|
|
413
|
+
self.resolvedmask: Optional[Any] = None
|
|
414
|
+
|
|
415
|
+
# -----------------------------------------------------------------------
|
|
416
|
+
# Properties
|
|
417
|
+
# -----------------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
@property
|
|
420
|
+
def x(self) -> Unit:
|
|
421
|
+
"""Horizontal position of the viewport.
|
|
422
|
+
|
|
423
|
+
Returns
|
|
424
|
+
-------
|
|
425
|
+
Unit
|
|
426
|
+
"""
|
|
427
|
+
return self._x
|
|
428
|
+
|
|
429
|
+
@property
|
|
430
|
+
def y(self) -> Unit:
|
|
431
|
+
"""Vertical position of the viewport.
|
|
432
|
+
|
|
433
|
+
Returns
|
|
434
|
+
-------
|
|
435
|
+
Unit
|
|
436
|
+
"""
|
|
437
|
+
return self._y
|
|
438
|
+
|
|
439
|
+
@property
|
|
440
|
+
def width(self) -> Unit:
|
|
441
|
+
"""Width of the viewport.
|
|
442
|
+
|
|
443
|
+
Returns
|
|
444
|
+
-------
|
|
445
|
+
Unit
|
|
446
|
+
"""
|
|
447
|
+
return self._width
|
|
448
|
+
|
|
449
|
+
@property
|
|
450
|
+
def height(self) -> Unit:
|
|
451
|
+
"""Height of the viewport.
|
|
452
|
+
|
|
453
|
+
Returns
|
|
454
|
+
-------
|
|
455
|
+
Unit
|
|
456
|
+
"""
|
|
457
|
+
return self._height
|
|
458
|
+
|
|
459
|
+
@property
|
|
460
|
+
def default_units(self) -> str:
|
|
461
|
+
"""Default unit type for numeric position/size arguments.
|
|
462
|
+
|
|
463
|
+
Returns
|
|
464
|
+
-------
|
|
465
|
+
str
|
|
466
|
+
"""
|
|
467
|
+
return self._default_units
|
|
468
|
+
|
|
469
|
+
@property
|
|
470
|
+
def just(self) -> Tuple[float, float]:
|
|
471
|
+
"""Justification as a ``(hjust, vjust)`` pair.
|
|
472
|
+
|
|
473
|
+
Returns
|
|
474
|
+
-------
|
|
475
|
+
tuple of float
|
|
476
|
+
"""
|
|
477
|
+
return self._just
|
|
478
|
+
|
|
479
|
+
@property
|
|
480
|
+
def gp(self) -> Gpar:
|
|
481
|
+
"""Graphical parameters associated with this viewport.
|
|
482
|
+
|
|
483
|
+
Returns
|
|
484
|
+
-------
|
|
485
|
+
Gpar
|
|
486
|
+
"""
|
|
487
|
+
return self._gp
|
|
488
|
+
|
|
489
|
+
@property
|
|
490
|
+
def clip(self) -> Any:
|
|
491
|
+
"""Clipping mode.
|
|
492
|
+
|
|
493
|
+
Returns
|
|
494
|
+
-------
|
|
495
|
+
bool or None
|
|
496
|
+
``True`` for ``"on"``, ``None`` for ``"off"``, ``False`` for
|
|
497
|
+
``"inherit"``.
|
|
498
|
+
"""
|
|
499
|
+
return self._clip
|
|
500
|
+
|
|
501
|
+
@property
|
|
502
|
+
def mask(self) -> Any:
|
|
503
|
+
"""Masking mode or mask object.
|
|
504
|
+
|
|
505
|
+
Returns
|
|
506
|
+
-------
|
|
507
|
+
bool or object
|
|
508
|
+
"""
|
|
509
|
+
return self._mask
|
|
510
|
+
|
|
511
|
+
@property
|
|
512
|
+
def xscale(self) -> List[float]:
|
|
513
|
+
"""Native x-coordinate range ``[min, max]``.
|
|
514
|
+
|
|
515
|
+
Returns
|
|
516
|
+
-------
|
|
517
|
+
list of float
|
|
518
|
+
"""
|
|
519
|
+
return list(self._xscale)
|
|
520
|
+
|
|
521
|
+
@property
|
|
522
|
+
def yscale(self) -> List[float]:
|
|
523
|
+
"""Native y-coordinate range ``[min, max]``.
|
|
524
|
+
|
|
525
|
+
Returns
|
|
526
|
+
-------
|
|
527
|
+
list of float
|
|
528
|
+
"""
|
|
529
|
+
return list(self._yscale)
|
|
530
|
+
|
|
531
|
+
@property
|
|
532
|
+
def angle(self) -> float:
|
|
533
|
+
"""Rotation angle in degrees.
|
|
534
|
+
|
|
535
|
+
Returns
|
|
536
|
+
-------
|
|
537
|
+
float
|
|
538
|
+
"""
|
|
539
|
+
return self._angle
|
|
540
|
+
|
|
541
|
+
@property
|
|
542
|
+
def layout(self) -> Optional[GridLayout]:
|
|
543
|
+
"""Layout for child arrangement, or ``None``.
|
|
544
|
+
|
|
545
|
+
Returns
|
|
546
|
+
-------
|
|
547
|
+
GridLayout or None
|
|
548
|
+
"""
|
|
549
|
+
return self._layout
|
|
550
|
+
|
|
551
|
+
@property
|
|
552
|
+
def layout_pos_row(self) -> Optional[List[int]]:
|
|
553
|
+
"""Row position(s) in parent layout, or ``None``.
|
|
554
|
+
|
|
555
|
+
Returns
|
|
556
|
+
-------
|
|
557
|
+
list of int or None
|
|
558
|
+
"""
|
|
559
|
+
return self._layout_pos_row
|
|
560
|
+
|
|
561
|
+
@property
|
|
562
|
+
def layout_pos_col(self) -> Optional[List[int]]:
|
|
563
|
+
"""Column position(s) in parent layout, or ``None``.
|
|
564
|
+
|
|
565
|
+
Returns
|
|
566
|
+
-------
|
|
567
|
+
list of int or None
|
|
568
|
+
"""
|
|
569
|
+
return self._layout_pos_col
|
|
570
|
+
|
|
571
|
+
@property
|
|
572
|
+
def name(self) -> str:
|
|
573
|
+
"""Name of the viewport.
|
|
574
|
+
|
|
575
|
+
Returns
|
|
576
|
+
-------
|
|
577
|
+
str
|
|
578
|
+
"""
|
|
579
|
+
return self._name
|
|
580
|
+
|
|
581
|
+
# -----------------------------------------------------------------------
|
|
582
|
+
# String representations
|
|
583
|
+
# -----------------------------------------------------------------------
|
|
584
|
+
|
|
585
|
+
def __str__(self) -> str:
|
|
586
|
+
"""Return a short description (mirrors R's ``as.character.viewport``).
|
|
587
|
+
|
|
588
|
+
Returns
|
|
589
|
+
-------
|
|
590
|
+
str
|
|
591
|
+
"""
|
|
592
|
+
return f"viewport[{self._name}]"
|
|
593
|
+
|
|
594
|
+
def __repr__(self) -> str:
|
|
595
|
+
"""Return a detailed description for debugging.
|
|
596
|
+
|
|
597
|
+
Returns
|
|
598
|
+
-------
|
|
599
|
+
str
|
|
600
|
+
"""
|
|
601
|
+
parts = [
|
|
602
|
+
f"name={self._name!r}",
|
|
603
|
+
f"x={self._x!r}",
|
|
604
|
+
f"y={self._y!r}",
|
|
605
|
+
f"width={self._width!r}",
|
|
606
|
+
f"height={self._height!r}",
|
|
607
|
+
f"just={self._just!r}",
|
|
608
|
+
f"xscale={self._xscale!r}",
|
|
609
|
+
f"yscale={self._yscale!r}",
|
|
610
|
+
f"angle={self._angle!r}",
|
|
611
|
+
]
|
|
612
|
+
return f"Viewport({', '.join(parts)})"
|
|
613
|
+
|
|
614
|
+
# -----------------------------------------------------------------------
|
|
615
|
+
# Copying
|
|
616
|
+
# -----------------------------------------------------------------------
|
|
617
|
+
|
|
618
|
+
def _copy(self) -> "Viewport":
|
|
619
|
+
"""Return a shallow copy of this viewport.
|
|
620
|
+
|
|
621
|
+
Returns
|
|
622
|
+
-------
|
|
623
|
+
Viewport
|
|
624
|
+
"""
|
|
625
|
+
return copy.copy(self)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
# ---------------------------------------------------------------------------
|
|
629
|
+
# Type guard
|
|
630
|
+
# ---------------------------------------------------------------------------
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def is_viewport(obj: Any) -> bool:
|
|
634
|
+
"""Return ``True`` if *obj* is a :class:`Viewport` (or subclass).
|
|
635
|
+
|
|
636
|
+
Parameters
|
|
637
|
+
----------
|
|
638
|
+
obj : object
|
|
639
|
+
Object to test.
|
|
640
|
+
|
|
641
|
+
Returns
|
|
642
|
+
-------
|
|
643
|
+
bool
|
|
644
|
+
"""
|
|
645
|
+
return isinstance(obj, Viewport)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _viewport_or_path(obj: Any) -> bool:
|
|
649
|
+
"""Return ``True`` if *obj* is a Viewport or a VpPath."""
|
|
650
|
+
return isinstance(obj, (Viewport, VpPath))
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
# ---------------------------------------------------------------------------
|
|
654
|
+
# VpList -- parallel push
|
|
655
|
+
# ---------------------------------------------------------------------------
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
class VpList:
|
|
659
|
+
"""A list of viewports to be pushed in parallel.
|
|
660
|
+
|
|
661
|
+
When pushed, all viewports in the list are pushed as siblings of the
|
|
662
|
+
current viewport. For all but the last element the navigation returns
|
|
663
|
+
to the common parent before pushing the next viewport; the final
|
|
664
|
+
element's viewport becomes the current viewport after the push.
|
|
665
|
+
|
|
666
|
+
Parameters
|
|
667
|
+
----------
|
|
668
|
+
*vps : Viewport or VpPath
|
|
669
|
+
One or more viewports (or viewport paths).
|
|
670
|
+
|
|
671
|
+
Raises
|
|
672
|
+
------
|
|
673
|
+
TypeError
|
|
674
|
+
If any element is not a :class:`Viewport` or :class:`VpPath`.
|
|
675
|
+
|
|
676
|
+
Examples
|
|
677
|
+
--------
|
|
678
|
+
>>> vl = VpList(Viewport(name="a"), Viewport(name="b"))
|
|
679
|
+
>>> len(vl)
|
|
680
|
+
2
|
|
681
|
+
"""
|
|
682
|
+
|
|
683
|
+
def __init__(self, *vps: Union[Viewport, VpPath]) -> None:
|
|
684
|
+
for v in vps:
|
|
685
|
+
if not _viewport_or_path(v):
|
|
686
|
+
raise TypeError(
|
|
687
|
+
f"only viewports allowed in VpList, got {type(v).__name__}"
|
|
688
|
+
)
|
|
689
|
+
self._vps: Tuple[Union[Viewport, VpPath], ...] = tuple(vps)
|
|
690
|
+
|
|
691
|
+
# -- container protocol ---------------------------------------------------
|
|
692
|
+
|
|
693
|
+
def __len__(self) -> int:
|
|
694
|
+
return len(self._vps)
|
|
695
|
+
|
|
696
|
+
def __getitem__(self, index: int) -> Union[Viewport, VpPath]:
|
|
697
|
+
return self._vps[index]
|
|
698
|
+
|
|
699
|
+
def __iter__(self) -> Iterator[Union[Viewport, VpPath]]:
|
|
700
|
+
return iter(self._vps)
|
|
701
|
+
|
|
702
|
+
# -- string representation -----------------------------------------------
|
|
703
|
+
|
|
704
|
+
def __str__(self) -> str:
|
|
705
|
+
"""Mirrors R's ``as.character.vpList``.
|
|
706
|
+
|
|
707
|
+
Returns
|
|
708
|
+
-------
|
|
709
|
+
str
|
|
710
|
+
"""
|
|
711
|
+
inner = ", ".join(str(v) for v in self._vps)
|
|
712
|
+
return f"({inner})"
|
|
713
|
+
|
|
714
|
+
def __repr__(self) -> str:
|
|
715
|
+
inner = ", ".join(repr(v) for v in self._vps)
|
|
716
|
+
return f"VpList({inner})"
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
# ---------------------------------------------------------------------------
|
|
720
|
+
# VpStack -- sequential (nested) push
|
|
721
|
+
# ---------------------------------------------------------------------------
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
class VpStack:
|
|
725
|
+
"""A stack of viewports to be pushed sequentially (nested).
|
|
726
|
+
|
|
727
|
+
Each viewport in the stack is pushed inside the preceding one, producing
|
|
728
|
+
a chain of nested viewports.
|
|
729
|
+
|
|
730
|
+
Parameters
|
|
731
|
+
----------
|
|
732
|
+
*vps : Viewport or VpPath
|
|
733
|
+
One or more viewports (or viewport paths).
|
|
734
|
+
|
|
735
|
+
Raises
|
|
736
|
+
------
|
|
737
|
+
TypeError
|
|
738
|
+
If any element is not a :class:`Viewport` or :class:`VpPath`.
|
|
739
|
+
|
|
740
|
+
Examples
|
|
741
|
+
--------
|
|
742
|
+
>>> vs = VpStack(Viewport(name="outer"), Viewport(name="inner"))
|
|
743
|
+
>>> str(vs)
|
|
744
|
+
'viewport[outer]->viewport[inner]'
|
|
745
|
+
"""
|
|
746
|
+
|
|
747
|
+
def __init__(self, *vps: Union[Viewport, VpPath]) -> None:
|
|
748
|
+
for v in vps:
|
|
749
|
+
if not _viewport_or_path(v):
|
|
750
|
+
raise TypeError(
|
|
751
|
+
f"only viewports allowed in VpStack, got {type(v).__name__}"
|
|
752
|
+
)
|
|
753
|
+
self._vps: Tuple[Union[Viewport, VpPath], ...] = tuple(vps)
|
|
754
|
+
|
|
755
|
+
# -- container protocol ---------------------------------------------------
|
|
756
|
+
|
|
757
|
+
def __len__(self) -> int:
|
|
758
|
+
return len(self._vps)
|
|
759
|
+
|
|
760
|
+
def __getitem__(self, index: int) -> Union[Viewport, VpPath]:
|
|
761
|
+
return self._vps[index]
|
|
762
|
+
|
|
763
|
+
def __iter__(self) -> Iterator[Union[Viewport, VpPath]]:
|
|
764
|
+
return iter(self._vps)
|
|
765
|
+
|
|
766
|
+
# -- string representation -----------------------------------------------
|
|
767
|
+
|
|
768
|
+
def __str__(self) -> str:
|
|
769
|
+
"""Mirrors R's ``as.character.vpStack``.
|
|
770
|
+
|
|
771
|
+
Returns
|
|
772
|
+
-------
|
|
773
|
+
str
|
|
774
|
+
"""
|
|
775
|
+
return "->".join(str(v) for v in self._vps)
|
|
776
|
+
|
|
777
|
+
def __repr__(self) -> str:
|
|
778
|
+
inner = ", ".join(repr(v) for v in self._vps)
|
|
779
|
+
return f"VpStack({inner})"
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
# ---------------------------------------------------------------------------
|
|
783
|
+
# VpTree -- parent + children (VpList)
|
|
784
|
+
# ---------------------------------------------------------------------------
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
class VpTree:
|
|
788
|
+
"""A viewport tree consisting of a parent viewport and a list of children.
|
|
789
|
+
|
|
790
|
+
When pushed, the *parent* viewport is pushed first, then the *children*
|
|
791
|
+
(a :class:`VpList`) are pushed inside it.
|
|
792
|
+
|
|
793
|
+
Parameters
|
|
794
|
+
----------
|
|
795
|
+
parent : Viewport
|
|
796
|
+
The parent viewport.
|
|
797
|
+
children : VpList
|
|
798
|
+
The children to push inside *parent*.
|
|
799
|
+
|
|
800
|
+
Raises
|
|
801
|
+
------
|
|
802
|
+
TypeError
|
|
803
|
+
If *parent* is not a Viewport/VpPath or *children* is not a
|
|
804
|
+
:class:`VpList`.
|
|
805
|
+
|
|
806
|
+
Examples
|
|
807
|
+
--------
|
|
808
|
+
>>> tree = VpTree(Viewport(name="p"), VpList(Viewport(name="c1")))
|
|
809
|
+
>>> str(tree)
|
|
810
|
+
'viewport[p]->(viewport[c1])'
|
|
811
|
+
"""
|
|
812
|
+
|
|
813
|
+
def __init__(
|
|
814
|
+
self,
|
|
815
|
+
parent: Union[Viewport, VpPath],
|
|
816
|
+
children: VpList,
|
|
817
|
+
) -> None:
|
|
818
|
+
if not _viewport_or_path(parent):
|
|
819
|
+
raise TypeError(
|
|
820
|
+
"'parent' must be a Viewport or VpPath in VpTree"
|
|
821
|
+
)
|
|
822
|
+
if not isinstance(children, VpList):
|
|
823
|
+
raise TypeError(
|
|
824
|
+
"'children' must be a VpList in VpTree"
|
|
825
|
+
)
|
|
826
|
+
self._parent = parent
|
|
827
|
+
self._children = children
|
|
828
|
+
|
|
829
|
+
@property
|
|
830
|
+
def parent(self) -> Union[Viewport, VpPath]:
|
|
831
|
+
"""Parent viewport.
|
|
832
|
+
|
|
833
|
+
Returns
|
|
834
|
+
-------
|
|
835
|
+
Viewport or VpPath
|
|
836
|
+
"""
|
|
837
|
+
return self._parent
|
|
838
|
+
|
|
839
|
+
@property
|
|
840
|
+
def children(self) -> VpList:
|
|
841
|
+
"""Child viewport list.
|
|
842
|
+
|
|
843
|
+
Returns
|
|
844
|
+
-------
|
|
845
|
+
VpList
|
|
846
|
+
"""
|
|
847
|
+
return self._children
|
|
848
|
+
|
|
849
|
+
# -- string representation -----------------------------------------------
|
|
850
|
+
|
|
851
|
+
def __str__(self) -> str:
|
|
852
|
+
"""Mirrors R's ``as.character.vpTree``.
|
|
853
|
+
|
|
854
|
+
Returns
|
|
855
|
+
-------
|
|
856
|
+
str
|
|
857
|
+
"""
|
|
858
|
+
return f"{self._parent}->{self._children}"
|
|
859
|
+
|
|
860
|
+
def __repr__(self) -> str:
|
|
861
|
+
return f"VpTree(parent={self._parent!r}, children={self._children!r})"
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
# ---------------------------------------------------------------------------
|
|
865
|
+
# depth() generic
|
|
866
|
+
# ---------------------------------------------------------------------------
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def depth(x: Any) -> int:
|
|
870
|
+
"""Return the depth (number of nesting levels) of a viewport object.
|
|
871
|
+
|
|
872
|
+
Parameters
|
|
873
|
+
----------
|
|
874
|
+
x : Viewport, VpList, VpStack, VpTree, or VpPath
|
|
875
|
+
The viewport object whose depth to compute.
|
|
876
|
+
|
|
877
|
+
Returns
|
|
878
|
+
-------
|
|
879
|
+
int
|
|
880
|
+
Number of levels.
|
|
881
|
+
|
|
882
|
+
Raises
|
|
883
|
+
------
|
|
884
|
+
TypeError
|
|
885
|
+
If *x* is not a recognised viewport type.
|
|
886
|
+
|
|
887
|
+
Notes
|
|
888
|
+
-----
|
|
889
|
+
This mirrors R's ``depth()`` generic.
|
|
890
|
+
|
|
891
|
+
* ``Viewport``: always 1.
|
|
892
|
+
* ``VpList``: depth of the *last* element (since pushing a list leaves
|
|
893
|
+
you wherever the last element leaves you).
|
|
894
|
+
* ``VpStack``: sum of the depths of all elements.
|
|
895
|
+
* ``VpTree``: depth of the parent plus depth of the last child.
|
|
896
|
+
* ``VpPath``: number of path components.
|
|
897
|
+
"""
|
|
898
|
+
if isinstance(x, Viewport):
|
|
899
|
+
return 1
|
|
900
|
+
if isinstance(x, VpList):
|
|
901
|
+
if len(x) == 0:
|
|
902
|
+
return 0
|
|
903
|
+
return depth(x[len(x) - 1])
|
|
904
|
+
if isinstance(x, VpStack):
|
|
905
|
+
return sum(depth(v) for v in x)
|
|
906
|
+
if isinstance(x, VpTree):
|
|
907
|
+
if len(x.children) == 0:
|
|
908
|
+
return depth(x.parent)
|
|
909
|
+
return depth(x.parent) + depth(x.children[len(x.children) - 1])
|
|
910
|
+
if isinstance(x, VpPath):
|
|
911
|
+
return x.n
|
|
912
|
+
raise TypeError(
|
|
913
|
+
f"depth() does not support {type(x).__name__}"
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
# ---------------------------------------------------------------------------
|
|
918
|
+
# State-management helpers (late-import pattern)
|
|
919
|
+
# ---------------------------------------------------------------------------
|
|
920
|
+
|
|
921
|
+
def _get_state() -> Any:
|
|
922
|
+
"""Lazily import the state manager to avoid circular imports.
|
|
923
|
+
|
|
924
|
+
Returns
|
|
925
|
+
-------
|
|
926
|
+
module
|
|
927
|
+
The ``grid_py._state`` module.
|
|
928
|
+
"""
|
|
929
|
+
from ._state import get_state # noqa: WPS433 (late import to break circular dep)
|
|
930
|
+
return get_state()
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
# ---------------------------------------------------------------------------
|
|
934
|
+
# Gpar restoration helper for viewport navigation
|
|
935
|
+
# ---------------------------------------------------------------------------
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def _restore_gpar_for_up(state: Any, n: int) -> None:
|
|
939
|
+
"""Restore gpar when navigating up/popping *n* viewports.
|
|
940
|
+
|
|
941
|
+
Mirrors R's ``L_upviewport`` / ``L_unsetviewport``:
|
|
942
|
+
walk *n* parent links from the current viewport to find the
|
|
943
|
+
outermost viewport being left, then replace the global gpar
|
|
944
|
+
with that viewport's ``parentgpar`` (the gpar that was active
|
|
945
|
+
*before* that viewport was pushed).
|
|
946
|
+
|
|
947
|
+
If *n* is 0 (meaning "to root"), the actual depth is computed
|
|
948
|
+
first, matching R's ``popViewport(0)`` → ``n <- vpDepth()``.
|
|
949
|
+
|
|
950
|
+
Parameters
|
|
951
|
+
----------
|
|
952
|
+
state : GridState
|
|
953
|
+
The current grid state singleton.
|
|
954
|
+
n : int
|
|
955
|
+
Number of levels to navigate up (0 = to root).
|
|
956
|
+
"""
|
|
957
|
+
from ._state import _vp_parent, _vp_attr # noqa: WPS433
|
|
958
|
+
|
|
959
|
+
vp = state.current_viewport()
|
|
960
|
+
if vp is None:
|
|
961
|
+
return
|
|
962
|
+
|
|
963
|
+
# n == 0 means "all the way to root". Compute actual depth.
|
|
964
|
+
if n == 0:
|
|
965
|
+
actual_n = 0
|
|
966
|
+
walk = vp
|
|
967
|
+
while _vp_parent(walk) is not None:
|
|
968
|
+
actual_n += 1
|
|
969
|
+
walk = _vp_parent(walk)
|
|
970
|
+
n = actual_n
|
|
971
|
+
|
|
972
|
+
if n <= 0:
|
|
973
|
+
return # already at root
|
|
974
|
+
|
|
975
|
+
# Walk n-1 parents from current viewport.
|
|
976
|
+
# After the loop, *vp* is the outermost viewport being left.
|
|
977
|
+
# R's C code: for (i = 1; i < n; i++) gvp = parent;
|
|
978
|
+
for _ in range(n - 1):
|
|
979
|
+
parent = _vp_parent(vp)
|
|
980
|
+
if parent is None:
|
|
981
|
+
break
|
|
982
|
+
vp = parent
|
|
983
|
+
|
|
984
|
+
# R: C_setGPar(VECTOR_ELT(gvp, PVP_PARENTGPAR))
|
|
985
|
+
pgp = _vp_attr(vp, "parentgpar", None)
|
|
986
|
+
if pgp is not None:
|
|
987
|
+
state.replace_gpar(pgp)
|
|
988
|
+
|
|
989
|
+
|
|
990
|
+
# ---------------------------------------------------------------------------
|
|
991
|
+
# Renderer-stack synchronisation helper
|
|
992
|
+
# ---------------------------------------------------------------------------
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
def _rebuild_renderer_stack(state: Any, renderer: Any) -> None:
|
|
996
|
+
"""Reset the renderer's coordinate stack and rebuild it from root to current vp.
|
|
997
|
+
|
|
998
|
+
Walks from ``state.current_viewport()`` up to the root to collect the
|
|
999
|
+
path, then pushes each viewport onto the renderer in order.
|
|
1000
|
+
"""
|
|
1001
|
+
from ._state import _vp_parent # noqa: WPS433
|
|
1002
|
+
|
|
1003
|
+
# Collect viewports from current → root (excluding the root sentinel).
|
|
1004
|
+
path: list = []
|
|
1005
|
+
vp = state.current_viewport()
|
|
1006
|
+
while vp is not None:
|
|
1007
|
+
parent = _vp_parent(vp)
|
|
1008
|
+
if parent is None:
|
|
1009
|
+
break # vp is the root — don't include it
|
|
1010
|
+
path.append(vp)
|
|
1011
|
+
vp = parent
|
|
1012
|
+
path.reverse()
|
|
1013
|
+
|
|
1014
|
+
# Reset renderer to root, then re-push the path.
|
|
1015
|
+
renderer.pop_viewport_to_root()
|
|
1016
|
+
for vp in path:
|
|
1017
|
+
renderer.push_viewport(vp)
|
|
1018
|
+
|
|
1019
|
+
|
|
1020
|
+
# ---------------------------------------------------------------------------
|
|
1021
|
+
# Navigation functions
|
|
1022
|
+
# ---------------------------------------------------------------------------
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def _push_single_vp(vp: Viewport, state: Any, renderer: Any) -> None:
|
|
1026
|
+
"""Push a single :class:`Viewport` onto the stack with full gpar handling.
|
|
1027
|
+
|
|
1028
|
+
Mirrors R's ``push.vp.viewport`` (grid.R:31-53):
|
|
1029
|
+
|
|
1030
|
+
1. ``vp.parentgpar ← current gpar`` — snapshot before push.
|
|
1031
|
+
2. Merge ``vp._gp`` into current gpar (``set.gpar`` semantics:
|
|
1032
|
+
cex/alpha/lex are multiplicatively cumulative).
|
|
1033
|
+
3. ``vp.gpar ← merged gpar`` — snapshot after merge.
|
|
1034
|
+
4. Replace the global gpar with the merged result.
|
|
1035
|
+
5. Push the viewport onto the state tree and renderer stack.
|
|
1036
|
+
"""
|
|
1037
|
+
from ._gpar import Gpar
|
|
1038
|
+
|
|
1039
|
+
# 1. Store parent gpar on the viewport (R: vp$parentgpar <- C_getGPar)
|
|
1040
|
+
current_gpar = state.get_gpar()
|
|
1041
|
+
vp.parentgpar = copy.copy(current_gpar)
|
|
1042
|
+
|
|
1043
|
+
# 2-3. Merge vp._gp into current gpar → vp.gpar (R: set.gpar(vp$gp))
|
|
1044
|
+
vp_gp = getattr(vp, "_gp", None)
|
|
1045
|
+
if vp_gp is not None and len(vp_gp) > 0:
|
|
1046
|
+
merged = vp_gp._merge(current_gpar)
|
|
1047
|
+
else:
|
|
1048
|
+
merged = copy.copy(current_gpar)
|
|
1049
|
+
vp.gpar = merged
|
|
1050
|
+
|
|
1051
|
+
# 4. Replace global gpar (R: grid.Call.graphics(C_setGPar, temp))
|
|
1052
|
+
state.replace_gpar(merged)
|
|
1053
|
+
|
|
1054
|
+
# 5. Push viewport onto state tree and renderer
|
|
1055
|
+
state.push_viewport(vp)
|
|
1056
|
+
if renderer is not None and hasattr(renderer, "push_viewport"):
|
|
1057
|
+
renderer.push_viewport(vp)
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def push_viewport(
|
|
1061
|
+
*args: Union[Viewport, VpList, VpStack, VpTree, VpPath],
|
|
1062
|
+
recording: bool = True,
|
|
1063
|
+
) -> None:
|
|
1064
|
+
"""Push one or more viewports onto the viewport stack.
|
|
1065
|
+
|
|
1066
|
+
Each argument is pushed in order. A :class:`Viewport` is pushed
|
|
1067
|
+
directly; container types (:class:`VpList`, :class:`VpStack`,
|
|
1068
|
+
:class:`VpTree`) are traversed according to their semantics.
|
|
1069
|
+
|
|
1070
|
+
Mirrors R's ``pushViewport`` (grid.R:96-104) including gpar
|
|
1071
|
+
save/merge/restore on each viewport push.
|
|
1072
|
+
|
|
1073
|
+
Parameters
|
|
1074
|
+
----------
|
|
1075
|
+
*args : Viewport, VpList, VpStack, VpTree, or VpPath
|
|
1076
|
+
Viewports to push.
|
|
1077
|
+
recording : bool
|
|
1078
|
+
Whether to record the operation on the display list.
|
|
1079
|
+
|
|
1080
|
+
Raises
|
|
1081
|
+
------
|
|
1082
|
+
ValueError
|
|
1083
|
+
If no viewports are provided.
|
|
1084
|
+
"""
|
|
1085
|
+
if len(args) == 0:
|
|
1086
|
+
raise ValueError("must specify at least one viewport")
|
|
1087
|
+
state = _get_state()
|
|
1088
|
+
renderer = state.get_renderer()
|
|
1089
|
+
for vp in args:
|
|
1090
|
+
_push_vp(vp, state, renderer, recording)
|
|
1091
|
+
|
|
1092
|
+
|
|
1093
|
+
def _push_vp(
|
|
1094
|
+
vp: Any, state: Any, renderer: Any, recording: bool
|
|
1095
|
+
) -> None:
|
|
1096
|
+
"""Dispatch a single viewport-like object for pushing.
|
|
1097
|
+
|
|
1098
|
+
Mirrors R's ``push.vp`` S3 dispatch (grid.R:24-90).
|
|
1099
|
+
"""
|
|
1100
|
+
if isinstance(vp, Viewport):
|
|
1101
|
+
_push_single_vp(vp, state, renderer)
|
|
1102
|
+
elif isinstance(vp, VpPath):
|
|
1103
|
+
# R: push.vp.vpPath → downViewport(vp, strict=TRUE)
|
|
1104
|
+
down_viewport(vp, strict=True, recording=recording)
|
|
1105
|
+
elif isinstance(vp, VpStack):
|
|
1106
|
+
# R: push.vp.vpStack → lapply(vp, push.vp)
|
|
1107
|
+
for child in vp:
|
|
1108
|
+
_push_vp(child, state, renderer, recording)
|
|
1109
|
+
elif isinstance(vp, VpList):
|
|
1110
|
+
# R: push.vp.vpList → push all but last + upViewport, then push last
|
|
1111
|
+
n = len(vp)
|
|
1112
|
+
for i, child in enumerate(vp):
|
|
1113
|
+
_push_vp(child, state, renderer, recording)
|
|
1114
|
+
if i < n - 1:
|
|
1115
|
+
up_viewport(depth(child), recording=recording)
|
|
1116
|
+
elif isinstance(vp, VpTree):
|
|
1117
|
+
# R: push.vp.vpTree → push parent, then push children (VpList)
|
|
1118
|
+
parent = vp.parent
|
|
1119
|
+
if not (isinstance(parent, Viewport) and parent.name == "ROOT"):
|
|
1120
|
+
_push_vp(parent, state, renderer, recording)
|
|
1121
|
+
_push_vp(vp.children, state, renderer, recording)
|
|
1122
|
+
else:
|
|
1123
|
+
# Fallback: treat as a plain viewport
|
|
1124
|
+
_push_single_vp(vp, state, renderer)
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
def pop_viewport(n: int = 1, recording: bool = True) -> None:
|
|
1128
|
+
"""Pop *n* viewports from the viewport stack.
|
|
1129
|
+
|
|
1130
|
+
Mirrors R's ``popViewport`` (grid.R:211-225) + ``L_unsetviewport``
|
|
1131
|
+
(grid.c:885-1014): removes viewports from the tree and restores
|
|
1132
|
+
the ``parentgpar`` stored on the outermost popped viewport.
|
|
1133
|
+
|
|
1134
|
+
Parameters
|
|
1135
|
+
----------
|
|
1136
|
+
n : int
|
|
1137
|
+
Number of viewports to pop. If ``0``, pop all viewports down
|
|
1138
|
+
to the root.
|
|
1139
|
+
recording : bool
|
|
1140
|
+
Whether to record the operation on the display list.
|
|
1141
|
+
|
|
1142
|
+
Raises
|
|
1143
|
+
------
|
|
1144
|
+
ValueError
|
|
1145
|
+
If *n* < 0.
|
|
1146
|
+
"""
|
|
1147
|
+
if n < 0:
|
|
1148
|
+
raise ValueError("must pop at least one viewport")
|
|
1149
|
+
state = _get_state()
|
|
1150
|
+
renderer = state.get_renderer()
|
|
1151
|
+
|
|
1152
|
+
# Restore gpar: walk *n* parents to find the outermost popped vp,
|
|
1153
|
+
# then use its parentgpar. (R: C_setGPar(gvp$parentgpar))
|
|
1154
|
+
# Must read before state.pop_viewport removes the viewports.
|
|
1155
|
+
_restore_gpar_for_up(state, n)
|
|
1156
|
+
|
|
1157
|
+
state.pop_viewport(n)
|
|
1158
|
+
# Synchronise the renderer's coordinate transform
|
|
1159
|
+
if renderer is not None:
|
|
1160
|
+
if n == 0:
|
|
1161
|
+
if hasattr(renderer, "pop_viewport_to_root"):
|
|
1162
|
+
renderer.pop_viewport_to_root()
|
|
1163
|
+
elif hasattr(renderer, "pop_viewport"):
|
|
1164
|
+
for _ in range(n):
|
|
1165
|
+
renderer.pop_viewport()
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
def up_viewport(n: int = 1, recording: bool = True) -> Optional[VpPath]:
|
|
1169
|
+
"""Navigate up *n* levels in the viewport tree without removing them.
|
|
1170
|
+
|
|
1171
|
+
Parameters
|
|
1172
|
+
----------
|
|
1173
|
+
n : int
|
|
1174
|
+
Number of levels to navigate up. If ``0``, navigate to the
|
|
1175
|
+
root viewport.
|
|
1176
|
+
recording : bool
|
|
1177
|
+
Whether to record the operation on the display list.
|
|
1178
|
+
|
|
1179
|
+
Returns
|
|
1180
|
+
-------
|
|
1181
|
+
VpPath or None
|
|
1182
|
+
The path segment that was navigated.
|
|
1183
|
+
|
|
1184
|
+
Raises
|
|
1185
|
+
------
|
|
1186
|
+
ValueError
|
|
1187
|
+
If *n* < 0.
|
|
1188
|
+
"""
|
|
1189
|
+
if n < 0:
|
|
1190
|
+
raise ValueError("must navigate up at least one viewport")
|
|
1191
|
+
state = _get_state()
|
|
1192
|
+
|
|
1193
|
+
# Capture the path segment being navigated (mirrors R grid.R:234-238).
|
|
1194
|
+
# R returns the tail of the current path corresponding to the n levels
|
|
1195
|
+
# being navigated away from.
|
|
1196
|
+
up_path: Optional[VpPath] = None
|
|
1197
|
+
path_str = state.current_vp_path() # e.g. "ROOT/A/B"
|
|
1198
|
+
if path_str:
|
|
1199
|
+
parts = path_str.split("/")
|
|
1200
|
+
# Remove "ROOT" prefix for VpPath (R doesn't include ROOT)
|
|
1201
|
+
vp_parts = [p for p in parts if p != "ROOT"]
|
|
1202
|
+
if n == 0:
|
|
1203
|
+
# Navigate to root: return entire path
|
|
1204
|
+
if vp_parts:
|
|
1205
|
+
up_path = VpPath("/".join(vp_parts))
|
|
1206
|
+
elif len(vp_parts) >= n:
|
|
1207
|
+
tail = "/".join(vp_parts[-n:])
|
|
1208
|
+
if tail:
|
|
1209
|
+
up_path = VpPath(tail)
|
|
1210
|
+
|
|
1211
|
+
# Restore gpar before navigating (must read current vp first).
|
|
1212
|
+
# R's L_upviewport: C_setGPar(gvp$parentgpar)
|
|
1213
|
+
_restore_gpar_for_up(state, n)
|
|
1214
|
+
|
|
1215
|
+
state.up_viewport(n)
|
|
1216
|
+
# Synchronise the renderer's coordinate transform
|
|
1217
|
+
renderer = state.get_renderer()
|
|
1218
|
+
if renderer is not None:
|
|
1219
|
+
if n == 0:
|
|
1220
|
+
if hasattr(renderer, "pop_viewport_to_root"):
|
|
1221
|
+
renderer.pop_viewport_to_root()
|
|
1222
|
+
elif hasattr(renderer, "pop_viewport"):
|
|
1223
|
+
for _ in range(n):
|
|
1224
|
+
renderer.pop_viewport()
|
|
1225
|
+
return up_path
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
def down_viewport(
|
|
1229
|
+
name: Union[str, VpPath],
|
|
1230
|
+
strict: bool = False,
|
|
1231
|
+
recording: bool = True,
|
|
1232
|
+
) -> int:
|
|
1233
|
+
"""Navigate down to a named viewport that has already been pushed.
|
|
1234
|
+
|
|
1235
|
+
Parameters
|
|
1236
|
+
----------
|
|
1237
|
+
name : str or VpPath
|
|
1238
|
+
Name or path of the viewport to navigate to.
|
|
1239
|
+
strict : bool
|
|
1240
|
+
If ``True``, require an exact path match.
|
|
1241
|
+
recording : bool
|
|
1242
|
+
Whether to record the operation on the display list.
|
|
1243
|
+
|
|
1244
|
+
Returns
|
|
1245
|
+
-------
|
|
1246
|
+
int
|
|
1247
|
+
The depth navigated.
|
|
1248
|
+
"""
|
|
1249
|
+
if isinstance(name, str):
|
|
1250
|
+
name = VpPath(name)
|
|
1251
|
+
state = _get_state()
|
|
1252
|
+
depth = state.down_viewport(str(name), strict=strict)
|
|
1253
|
+
# Synchronise the renderer's coordinate transform: rebuild the stack
|
|
1254
|
+
# from root to the new current viewport.
|
|
1255
|
+
renderer = state.get_renderer()
|
|
1256
|
+
if renderer is not None and hasattr(renderer, "pop_viewport_to_root"):
|
|
1257
|
+
_rebuild_renderer_stack(state, renderer)
|
|
1258
|
+
# Restore gpar for the target viewport (R: grid.R:173-175).
|
|
1259
|
+
# R's downViewport.vpPath: grid.Call.graphics(C_setGPar, pvp$gpar)
|
|
1260
|
+
target_vp = state.current_viewport()
|
|
1261
|
+
target_gpar = getattr(target_vp, "gpar", None)
|
|
1262
|
+
if target_gpar is not None:
|
|
1263
|
+
state.replace_gpar(target_gpar)
|
|
1264
|
+
return depth
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
def seek_viewport(name: str, recording: bool = True) -> int:
|
|
1268
|
+
"""Navigate to a named viewport from anywhere in the tree.
|
|
1269
|
+
|
|
1270
|
+
This is equivalent to navigating up to the root and then searching
|
|
1271
|
+
downward.
|
|
1272
|
+
|
|
1273
|
+
Parameters
|
|
1274
|
+
----------
|
|
1275
|
+
name : str
|
|
1276
|
+
Name of the viewport to find.
|
|
1277
|
+
recording : bool
|
|
1278
|
+
Whether to record the operation on the display list.
|
|
1279
|
+
|
|
1280
|
+
Returns
|
|
1281
|
+
-------
|
|
1282
|
+
int
|
|
1283
|
+
The depth navigated from the root.
|
|
1284
|
+
"""
|
|
1285
|
+
up_viewport(0, recording=recording)
|
|
1286
|
+
return down_viewport(name, recording=recording)
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
# ---------------------------------------------------------------------------
|
|
1290
|
+
# Query functions
|
|
1291
|
+
# ---------------------------------------------------------------------------
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
def current_viewport() -> Viewport:
|
|
1295
|
+
"""Return the current viewport.
|
|
1296
|
+
|
|
1297
|
+
Returns
|
|
1298
|
+
-------
|
|
1299
|
+
Viewport
|
|
1300
|
+
"""
|
|
1301
|
+
state = _get_state()
|
|
1302
|
+
return state.current_viewport()
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def current_vp_path() -> Optional[VpPath]:
|
|
1306
|
+
"""Return the full path from the root to the current viewport.
|
|
1307
|
+
|
|
1308
|
+
Returns
|
|
1309
|
+
-------
|
|
1310
|
+
VpPath or None
|
|
1311
|
+
``None`` if the current viewport is the root.
|
|
1312
|
+
"""
|
|
1313
|
+
state = _get_state()
|
|
1314
|
+
return state.current_vp_path()
|
|
1315
|
+
|
|
1316
|
+
|
|
1317
|
+
def current_vp_tree() -> Union[Viewport, VpTree]:
|
|
1318
|
+
"""Return the full viewport tree starting from the root.
|
|
1319
|
+
|
|
1320
|
+
Returns
|
|
1321
|
+
-------
|
|
1322
|
+
Viewport or VpTree
|
|
1323
|
+
"""
|
|
1324
|
+
state = _get_state()
|
|
1325
|
+
return state.current_vp_tree()
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
def current_transform() -> np.ndarray:
|
|
1329
|
+
"""Return the 3x3 transformation matrix of the current viewport.
|
|
1330
|
+
|
|
1331
|
+
The matrix maps normalised parent coordinates (NPC) to device
|
|
1332
|
+
coordinates, incorporating position, size, justification, and
|
|
1333
|
+
rotation.
|
|
1334
|
+
|
|
1335
|
+
Returns
|
|
1336
|
+
-------
|
|
1337
|
+
numpy.ndarray
|
|
1338
|
+
A 3x3 float array.
|
|
1339
|
+
"""
|
|
1340
|
+
state = _get_state()
|
|
1341
|
+
return state.current_transform()
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def current_rotation() -> float:
|
|
1345
|
+
"""Return the cumulative rotation angle (degrees) of the current viewport.
|
|
1346
|
+
|
|
1347
|
+
Returns
|
|
1348
|
+
-------
|
|
1349
|
+
float
|
|
1350
|
+
"""
|
|
1351
|
+
state = _get_state()
|
|
1352
|
+
return state.current_rotation()
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
def current_parent(n: int = 1) -> Optional[Viewport]:
|
|
1356
|
+
"""Return the *n*-th generation ancestor of the current viewport.
|
|
1357
|
+
|
|
1358
|
+
Parameters
|
|
1359
|
+
----------
|
|
1360
|
+
n : int
|
|
1361
|
+
Number of generations to go up (default 1 = immediate parent).
|
|
1362
|
+
|
|
1363
|
+
Returns
|
|
1364
|
+
-------
|
|
1365
|
+
Viewport or None
|
|
1366
|
+
``None`` if the ancestor is the root (which has no parent).
|
|
1367
|
+
|
|
1368
|
+
Raises
|
|
1369
|
+
------
|
|
1370
|
+
ValueError
|
|
1371
|
+
If *n* < 1 or exceeds the depth of the viewport stack.
|
|
1372
|
+
"""
|
|
1373
|
+
if n < 1:
|
|
1374
|
+
raise ValueError("invalid number of generations")
|
|
1375
|
+
state = _get_state()
|
|
1376
|
+
return state.current_parent(n)
|
|
1377
|
+
|
|
1378
|
+
|
|
1379
|
+
# ---------------------------------------------------------------------------
|
|
1380
|
+
# Convenience viewport constructors
|
|
1381
|
+
# ---------------------------------------------------------------------------
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
def data_viewport(
|
|
1385
|
+
xData: Optional[Sequence[float]] = None,
|
|
1386
|
+
yData: Optional[Sequence[float]] = None,
|
|
1387
|
+
xscale: Optional[Sequence[float]] = None,
|
|
1388
|
+
yscale: Optional[Sequence[float]] = None,
|
|
1389
|
+
extension: Union[float, Sequence[float]] = 0.05,
|
|
1390
|
+
**kwargs: Any,
|
|
1391
|
+
) -> Viewport:
|
|
1392
|
+
"""Create a viewport with scales derived from data ranges.
|
|
1393
|
+
|
|
1394
|
+
If *xscale* is not supplied it is computed from *xData* (and similarly
|
|
1395
|
+
for *yscale* / *yData*). An *extension* factor is applied to expand
|
|
1396
|
+
the range slightly beyond the data limits.
|
|
1397
|
+
|
|
1398
|
+
Parameters
|
|
1399
|
+
----------
|
|
1400
|
+
xData : array-like or None
|
|
1401
|
+
Data for the x-axis.
|
|
1402
|
+
yData : array-like or None
|
|
1403
|
+
Data for the y-axis.
|
|
1404
|
+
xscale : sequence of float or None
|
|
1405
|
+
Explicit x-scale. Overrides *xData* if given.
|
|
1406
|
+
yscale : sequence of float or None
|
|
1407
|
+
Explicit y-scale. Overrides *yData* if given.
|
|
1408
|
+
extension : float
|
|
1409
|
+
Proportional extension of the data range on each side.
|
|
1410
|
+
**kwargs
|
|
1411
|
+
Additional keyword arguments passed to :class:`Viewport`.
|
|
1412
|
+
|
|
1413
|
+
Returns
|
|
1414
|
+
-------
|
|
1415
|
+
Viewport
|
|
1416
|
+
|
|
1417
|
+
Raises
|
|
1418
|
+
------
|
|
1419
|
+
ValueError
|
|
1420
|
+
If neither *xData* nor *xscale* (or *yData* nor *yscale*) is
|
|
1421
|
+
supplied.
|
|
1422
|
+
|
|
1423
|
+
Notes
|
|
1424
|
+
-----
|
|
1425
|
+
Mirrors R's ``dataViewport()``.
|
|
1426
|
+
"""
|
|
1427
|
+
# R: extension <- rep(extension, length.out = 2)
|
|
1428
|
+
if isinstance(extension, (list, tuple)):
|
|
1429
|
+
ext = [float(x) for x in extension]
|
|
1430
|
+
else:
|
|
1431
|
+
ext = [float(extension)]
|
|
1432
|
+
# Recycle to length 2 (R's rep(..., length.out=2))
|
|
1433
|
+
while len(ext) < 2:
|
|
1434
|
+
ext.append(ext[0])
|
|
1435
|
+
ext = ext[:2]
|
|
1436
|
+
|
|
1437
|
+
if xscale is None:
|
|
1438
|
+
if xData is None:
|
|
1439
|
+
raise ValueError(
|
|
1440
|
+
"must specify at least one of 'xData' or 'xscale'"
|
|
1441
|
+
)
|
|
1442
|
+
xarr = np.asarray(xData, dtype=float)
|
|
1443
|
+
rng = float(np.nanmax(xarr) - np.nanmin(xarr))
|
|
1444
|
+
xscale = [
|
|
1445
|
+
float(np.nanmin(xarr)) - ext[0] * rng,
|
|
1446
|
+
float(np.nanmax(xarr)) + ext[0] * rng,
|
|
1447
|
+
]
|
|
1448
|
+
|
|
1449
|
+
if yscale is None:
|
|
1450
|
+
if yData is None:
|
|
1451
|
+
raise ValueError(
|
|
1452
|
+
"must specify at least one of 'yData' or 'yscale'"
|
|
1453
|
+
)
|
|
1454
|
+
yarr = np.asarray(yData, dtype=float)
|
|
1455
|
+
rng = float(np.nanmax(yarr) - np.nanmin(yarr))
|
|
1456
|
+
yscale = [
|
|
1457
|
+
float(np.nanmin(yarr)) - ext[1] * rng,
|
|
1458
|
+
float(np.nanmax(yarr)) + ext[1] * rng,
|
|
1459
|
+
]
|
|
1460
|
+
|
|
1461
|
+
return Viewport(xscale=xscale, yscale=yscale, **kwargs)
|
|
1462
|
+
|
|
1463
|
+
|
|
1464
|
+
def plot_viewport(
|
|
1465
|
+
margins: Optional[Sequence[float]] = None,
|
|
1466
|
+
**kwargs: Any,
|
|
1467
|
+
) -> Viewport:
|
|
1468
|
+
"""Create a viewport with margins specified in lines.
|
|
1469
|
+
|
|
1470
|
+
This mirrors R's ``plotViewport()``. The four margins are given in the
|
|
1471
|
+
order ``[bottom, left, top, right]``.
|
|
1472
|
+
|
|
1473
|
+
Parameters
|
|
1474
|
+
----------
|
|
1475
|
+
margins : sequence of float or None
|
|
1476
|
+
Four margin sizes in ``"lines"`` units, ordered
|
|
1477
|
+
``[bottom, left, top, right]``. Defaults to
|
|
1478
|
+
``[5.1, 4.1, 4.1, 2.1]``.
|
|
1479
|
+
**kwargs
|
|
1480
|
+
Additional keyword arguments passed to :class:`Viewport`.
|
|
1481
|
+
|
|
1482
|
+
Returns
|
|
1483
|
+
-------
|
|
1484
|
+
Viewport
|
|
1485
|
+
"""
|
|
1486
|
+
if margins is None:
|
|
1487
|
+
margins = [5.1, 4.1, 4.1, 2.1]
|
|
1488
|
+
else:
|
|
1489
|
+
margins = list(margins)
|
|
1490
|
+
# Ensure exactly 4 values by recycling
|
|
1491
|
+
while len(margins) < 4:
|
|
1492
|
+
margins = margins * 2
|
|
1493
|
+
margins = [float(m) for m in margins[:4]]
|
|
1494
|
+
|
|
1495
|
+
bottom, left, top, right = margins
|
|
1496
|
+
|
|
1497
|
+
x = Unit(left, "lines")
|
|
1498
|
+
width = Unit(1, "npc") - Unit(left + right, "lines")
|
|
1499
|
+
y = Unit(bottom, "lines")
|
|
1500
|
+
height = Unit(1, "npc") - Unit(bottom + top, "lines")
|
|
1501
|
+
|
|
1502
|
+
return Viewport(
|
|
1503
|
+
x=x,
|
|
1504
|
+
width=width,
|
|
1505
|
+
y=y,
|
|
1506
|
+
height=height,
|
|
1507
|
+
just=["left", "bottom"],
|
|
1508
|
+
**kwargs,
|
|
1509
|
+
)
|
|
1510
|
+
|
|
1511
|
+
|
|
1512
|
+
# ---------------------------------------------------------------------------
|
|
1513
|
+
# edit_viewport
|
|
1514
|
+
# ---------------------------------------------------------------------------
|
|
1515
|
+
|
|
1516
|
+
|
|
1517
|
+
def edit_viewport(
|
|
1518
|
+
vp: Optional[Viewport] = None,
|
|
1519
|
+
**kwargs: Any,
|
|
1520
|
+
) -> Viewport:
|
|
1521
|
+
"""Return an edited copy of a viewport.
|
|
1522
|
+
|
|
1523
|
+
Creates a new :class:`Viewport` by taking the fields of *vp* and
|
|
1524
|
+
overriding any that are supplied via keyword arguments.
|
|
1525
|
+
|
|
1526
|
+
Parameters
|
|
1527
|
+
----------
|
|
1528
|
+
vp : Viewport or None
|
|
1529
|
+
The viewport to edit. If ``None``, uses :func:`current_viewport`.
|
|
1530
|
+
**kwargs
|
|
1531
|
+
Fields to override (same names as :class:`Viewport` constructor
|
|
1532
|
+
parameters).
|
|
1533
|
+
|
|
1534
|
+
Returns
|
|
1535
|
+
-------
|
|
1536
|
+
Viewport
|
|
1537
|
+
A new viewport with the edited fields.
|
|
1538
|
+
|
|
1539
|
+
Notes
|
|
1540
|
+
-----
|
|
1541
|
+
Mirrors R's ``editViewport()``.
|
|
1542
|
+
"""
|
|
1543
|
+
if vp is None:
|
|
1544
|
+
vp = current_viewport()
|
|
1545
|
+
|
|
1546
|
+
base_kwargs = {
|
|
1547
|
+
"x": vp.x,
|
|
1548
|
+
"y": vp.y,
|
|
1549
|
+
"width": vp.width,
|
|
1550
|
+
"height": vp.height,
|
|
1551
|
+
"default_units": vp.default_units,
|
|
1552
|
+
"just": vp.just,
|
|
1553
|
+
"gp": vp.gp,
|
|
1554
|
+
"clip": vp.clip,
|
|
1555
|
+
"mask": vp.mask,
|
|
1556
|
+
"xscale": vp.xscale,
|
|
1557
|
+
"yscale": vp.yscale,
|
|
1558
|
+
"angle": vp.angle,
|
|
1559
|
+
"layout": vp.layout,
|
|
1560
|
+
"layout_pos_row": vp.layout_pos_row,
|
|
1561
|
+
"layout_pos_col": vp.layout_pos_col,
|
|
1562
|
+
"name": vp.name,
|
|
1563
|
+
}
|
|
1564
|
+
# Remap clip from internal representation back to constructor-friendly form
|
|
1565
|
+
if "clip" not in kwargs:
|
|
1566
|
+
clip_val = base_kwargs["clip"]
|
|
1567
|
+
if clip_val is True:
|
|
1568
|
+
base_kwargs["clip"] = "on"
|
|
1569
|
+
elif clip_val is None:
|
|
1570
|
+
base_kwargs["clip"] = "off"
|
|
1571
|
+
elif clip_val is False:
|
|
1572
|
+
base_kwargs["clip"] = "inherit"
|
|
1573
|
+
# Similarly for mask
|
|
1574
|
+
if "mask" not in kwargs:
|
|
1575
|
+
mask_val = base_kwargs["mask"]
|
|
1576
|
+
if mask_val is True:
|
|
1577
|
+
base_kwargs["mask"] = "inherit"
|
|
1578
|
+
elif mask_val is False:
|
|
1579
|
+
base_kwargs["mask"] = "none"
|
|
1580
|
+
|
|
1581
|
+
base_kwargs.update(kwargs)
|
|
1582
|
+
return Viewport(**base_kwargs)
|
|
1583
|
+
|
|
1584
|
+
|
|
1585
|
+
# ---------------------------------------------------------------------------
|
|
1586
|
+
# show_viewport
|
|
1587
|
+
# ---------------------------------------------------------------------------
|
|
1588
|
+
|
|
1589
|
+
|
|
1590
|
+
def show_viewport(
|
|
1591
|
+
vp: Optional[Viewport] = None,
|
|
1592
|
+
recurse: bool = True,
|
|
1593
|
+
depth_val: int = 0,
|
|
1594
|
+
**kwargs: Any,
|
|
1595
|
+
) -> str:
|
|
1596
|
+
"""Return a human-readable summary of a viewport (tree).
|
|
1597
|
+
|
|
1598
|
+
Parameters
|
|
1599
|
+
----------
|
|
1600
|
+
vp : Viewport or None
|
|
1601
|
+
The viewport to display. If ``None``, uses
|
|
1602
|
+
:func:`current_viewport`.
|
|
1603
|
+
recurse : bool
|
|
1604
|
+
Whether to recurse into children.
|
|
1605
|
+
depth_val : int
|
|
1606
|
+
Current indentation depth (used internally for recursive calls).
|
|
1607
|
+
**kwargs
|
|
1608
|
+
Reserved for future use.
|
|
1609
|
+
|
|
1610
|
+
Returns
|
|
1611
|
+
-------
|
|
1612
|
+
str
|
|
1613
|
+
Multi-line summary string.
|
|
1614
|
+
|
|
1615
|
+
Notes
|
|
1616
|
+
-----
|
|
1617
|
+
Mirrors R's ``showViewport()`` output.
|
|
1618
|
+
"""
|
|
1619
|
+
if vp is None:
|
|
1620
|
+
vp = current_viewport()
|
|
1621
|
+
|
|
1622
|
+
indent = " " * depth_val
|
|
1623
|
+
lines: List[str] = []
|
|
1624
|
+
lines.append(f"{indent}{vp}")
|
|
1625
|
+
lines.append(f"{indent} x = {vp.x!r}")
|
|
1626
|
+
lines.append(f"{indent} y = {vp.y!r}")
|
|
1627
|
+
lines.append(f"{indent} width = {vp.width!r}")
|
|
1628
|
+
lines.append(f"{indent} height = {vp.height!r}")
|
|
1629
|
+
lines.append(f"{indent} just = {vp.just!r}")
|
|
1630
|
+
lines.append(f"{indent} xscale = {vp.xscale!r}")
|
|
1631
|
+
lines.append(f"{indent} yscale = {vp.yscale!r}")
|
|
1632
|
+
lines.append(f"{indent} angle = {vp.angle!r}")
|
|
1633
|
+
|
|
1634
|
+
if vp.layout is not None:
|
|
1635
|
+
lines.append(f"{indent} layout = {vp.layout!r}")
|
|
1636
|
+
if vp.layout_pos_row is not None:
|
|
1637
|
+
lines.append(f"{indent} layout.pos.row = {vp.layout_pos_row!r}")
|
|
1638
|
+
if vp.layout_pos_col is not None:
|
|
1639
|
+
lines.append(f"{indent} layout.pos.col = {vp.layout_pos_col!r}")
|
|
1640
|
+
|
|
1641
|
+
if recurse and vp.children:
|
|
1642
|
+
for child_name, child_vp in vp.children.items():
|
|
1643
|
+
lines.append(
|
|
1644
|
+
show_viewport(
|
|
1645
|
+
child_vp, recurse=True, depth_val=depth_val + 1
|
|
1646
|
+
)
|
|
1647
|
+
)
|
|
1648
|
+
|
|
1649
|
+
return "\n".join(lines)
|