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/_utils.py
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility helpers for the grid_py package.
|
|
3
|
+
|
|
4
|
+
This module provides internal helpers and public utility functions ported
|
|
5
|
+
from the R *grid* package's ``util.R`` and related sources.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from typing import Any, Generator, List, Optional, Sequence, Union
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
__all__: list[str] = [
|
|
16
|
+
"depth",
|
|
17
|
+
"explode",
|
|
18
|
+
"grid_pretty",
|
|
19
|
+
"n2mfrow",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
# Internal constants
|
|
24
|
+
# ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
_grid_path_sep: str = "::"
|
|
27
|
+
"""Path separator used by viewport paths and grob paths (mirrors ``.grid.pathSep`` in R)."""
|
|
28
|
+
|
|
29
|
+
# ---------------------------------------------------------------------------
|
|
30
|
+
# Internal helpers
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _auto_name_counter(prefix: str = "GRID") -> Generator[str, None, None]:
|
|
35
|
+
"""Yield sequential auto-generated names.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
prefix : str, optional
|
|
40
|
+
Prefix for each name. Default is ``"GRID"``.
|
|
41
|
+
|
|
42
|
+
Yields
|
|
43
|
+
------
|
|
44
|
+
str
|
|
45
|
+
Names of the form ``"GRID.1"``, ``"GRID.2"``, ...
|
|
46
|
+
"""
|
|
47
|
+
n = 0
|
|
48
|
+
while True:
|
|
49
|
+
n += 1
|
|
50
|
+
yield f"{prefix}.{n}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _recycle(x: Union[np.ndarray, Sequence[Any]], length: int) -> np.ndarray:
|
|
54
|
+
"""Recycle *x* to the requested *length* (R-style recycling).
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
x : array_like
|
|
59
|
+
Input values.
|
|
60
|
+
length : int
|
|
61
|
+
Desired output length. Must be >= 0.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
numpy.ndarray
|
|
66
|
+
Array of *length* elements obtained by repeating *x* cyclically.
|
|
67
|
+
|
|
68
|
+
Examples
|
|
69
|
+
--------
|
|
70
|
+
>>> _recycle([1, 2, 3], 7)
|
|
71
|
+
array([1, 2, 3, 1, 2, 3, 1])
|
|
72
|
+
"""
|
|
73
|
+
arr = np.asarray(x).ravel()
|
|
74
|
+
if len(arr) == 0:
|
|
75
|
+
return np.empty(0, dtype=arr.dtype)
|
|
76
|
+
if length == 0:
|
|
77
|
+
return np.empty(0, dtype=arr.dtype)
|
|
78
|
+
# np.resize already recycles, but it silently returns empty for 0-length
|
|
79
|
+
return np.resize(arr, length)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _is_finite(x: Any) -> Union[bool, np.ndarray]:
|
|
83
|
+
"""Check for finite values, treating ``None`` as non-finite.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
x : scalar or array_like
|
|
88
|
+
Value(s) to test. ``None`` is treated as non-finite.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
bool or numpy.ndarray of bool
|
|
93
|
+
``True`` where values are finite, ``False`` otherwise.
|
|
94
|
+
"""
|
|
95
|
+
if x is None:
|
|
96
|
+
return False
|
|
97
|
+
arr = np.asarray(x)
|
|
98
|
+
# For non-numeric dtypes every element is "not finite"
|
|
99
|
+
if not np.issubdtype(arr.dtype, np.number):
|
|
100
|
+
return np.zeros(arr.shape, dtype=bool) if arr.ndim else False
|
|
101
|
+
return np.isfinite(arr)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# Public functions
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def depth(x: Any) -> int:
|
|
110
|
+
"""Return the depth of a path-like object.
|
|
111
|
+
|
|
112
|
+
For plain strings the depth equals the number of components separated
|
|
113
|
+
by ``"::"``. For path objects (dicts with an ``"n"`` key) the stored
|
|
114
|
+
depth is returned directly.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
x : str or path-like
|
|
119
|
+
A viewport-path or grob-path object. Strings are split on the
|
|
120
|
+
``"::"`` separator to determine their depth.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
int
|
|
125
|
+
Number of components in the path.
|
|
126
|
+
|
|
127
|
+
Examples
|
|
128
|
+
--------
|
|
129
|
+
>>> depth("A::B::C")
|
|
130
|
+
3
|
|
131
|
+
>>> depth("ROOT")
|
|
132
|
+
1
|
|
133
|
+
"""
|
|
134
|
+
# Path-like dict (vpPath / gPath style)
|
|
135
|
+
if isinstance(x, dict) and "n" in x:
|
|
136
|
+
return int(x["n"])
|
|
137
|
+
|
|
138
|
+
# Plain string
|
|
139
|
+
if isinstance(x, str):
|
|
140
|
+
parts = x.split(_grid_path_sep)
|
|
141
|
+
return len(parts)
|
|
142
|
+
|
|
143
|
+
# Objects that expose a `.depth()` method or `.n` attribute
|
|
144
|
+
if hasattr(x, "depth"):
|
|
145
|
+
return x.depth()
|
|
146
|
+
if hasattr(x, "n"):
|
|
147
|
+
return int(x.n)
|
|
148
|
+
|
|
149
|
+
raise TypeError(f"Cannot compute depth of {type(x).__name__!r}")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def explode(x: Any) -> List[str]:
|
|
153
|
+
"""Split a path into its individual components.
|
|
154
|
+
|
|
155
|
+
This is the Python equivalent of the S3 generic ``explode()`` in R's
|
|
156
|
+
grid package.
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
x : str or path-like
|
|
161
|
+
A viewport-path or grob-path object. Strings are split on the
|
|
162
|
+
``"::"`` separator.
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
-------
|
|
166
|
+
list of str
|
|
167
|
+
The individual path components.
|
|
168
|
+
|
|
169
|
+
Examples
|
|
170
|
+
--------
|
|
171
|
+
>>> explode("A::B::C")
|
|
172
|
+
['A', 'B', 'C']
|
|
173
|
+
>>> explode("ROOT")
|
|
174
|
+
['ROOT']
|
|
175
|
+
"""
|
|
176
|
+
# Path-like dict (vpPath / gPath style)
|
|
177
|
+
if isinstance(x, dict):
|
|
178
|
+
if x.get("n", 0) == 1:
|
|
179
|
+
return [x["name"]]
|
|
180
|
+
parts = explode(x["path"])
|
|
181
|
+
parts.append(x["name"])
|
|
182
|
+
return parts
|
|
183
|
+
|
|
184
|
+
if isinstance(x, str):
|
|
185
|
+
return x.split(_grid_path_sep)
|
|
186
|
+
|
|
187
|
+
# Objects that expose an `explode()` method
|
|
188
|
+
if hasattr(x, "explode"):
|
|
189
|
+
return list(x.explode())
|
|
190
|
+
|
|
191
|
+
raise TypeError(f"Cannot explode {type(x).__name__!r}")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def grid_pretty(range_val: Sequence[float], n: int = 5) -> np.ndarray:
|
|
195
|
+
"""Return *pretty* tick mark positions for a numeric range.
|
|
196
|
+
|
|
197
|
+
This mirrors R's ``grid.pretty(range, n)`` which internally calls
|
|
198
|
+
``pretty()``. The algorithm chooses "nice" numbers that cover the
|
|
199
|
+
given range.
|
|
200
|
+
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
range_val : sequence of float
|
|
204
|
+
A two-element sequence ``[lo, hi]`` giving the data range.
|
|
205
|
+
n : int, optional
|
|
206
|
+
Target number of intervals (the result may have slightly more or
|
|
207
|
+
fewer tick marks). Default is 5.
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
numpy.ndarray
|
|
212
|
+
Array of pretty tick positions.
|
|
213
|
+
|
|
214
|
+
Raises
|
|
215
|
+
------
|
|
216
|
+
ValueError
|
|
217
|
+
If *range_val* is not numeric or does not have two elements.
|
|
218
|
+
|
|
219
|
+
Examples
|
|
220
|
+
--------
|
|
221
|
+
>>> grid_pretty([0.0, 1.0])
|
|
222
|
+
array([0. , 0.2, 0.4, 0.6, 0.8, 1. ])
|
|
223
|
+
"""
|
|
224
|
+
rng = np.asarray(range_val, dtype=float)
|
|
225
|
+
if rng.size != 2:
|
|
226
|
+
raise ValueError("'range_val' must have exactly two elements")
|
|
227
|
+
if not np.all(np.isfinite(rng)):
|
|
228
|
+
raise ValueError("'range_val' must be finite numeric")
|
|
229
|
+
|
|
230
|
+
lo, hi = float(rng[0]), float(rng[1])
|
|
231
|
+
|
|
232
|
+
if lo == hi:
|
|
233
|
+
return np.array([lo])
|
|
234
|
+
|
|
235
|
+
# ---- Port of R's pretty (src/appl/pretty.c R_pretty) ------------------
|
|
236
|
+
# R's grid.pretty(range, n) is `pretty(range, n)` filtered to within
|
|
237
|
+
# range; we follow the same two-step recipe.
|
|
238
|
+
diff = hi - lo
|
|
239
|
+
cell = max(abs(diff) / max(n, 1), 1e-10)
|
|
240
|
+
|
|
241
|
+
base = 10.0 ** math.floor(math.log10(cell))
|
|
242
|
+
# GEPretty (R src/main/engine.c) calls R_pretty0 with the
|
|
243
|
+
# `high_u_fact = {0.8, 1.7}` bias, NOT R's user-facing pretty()
|
|
244
|
+
# default of {1.5, 2.75}. The smaller bias prefers denser ticks,
|
|
245
|
+
# which is why grid.pretty(c(-7.49, 7.49)) returns step=2 (giving
|
|
246
|
+
# 7 ticks) while pretty(c(-7.49, 7.49)) returns step=5 (5 ticks).
|
|
247
|
+
h = 0.8
|
|
248
|
+
h5 = 1.7
|
|
249
|
+
unit = base
|
|
250
|
+
if 2.0 * base - cell < h * (cell - unit):
|
|
251
|
+
unit = 2.0 * base
|
|
252
|
+
if 5.0 * base - cell < h5 * (cell - unit):
|
|
253
|
+
unit = 5.0 * base
|
|
254
|
+
if 10.0 * base - cell < h * (cell - unit):
|
|
255
|
+
unit = 10.0 * base
|
|
256
|
+
|
|
257
|
+
ns = math.floor(lo / unit + 1e-7)
|
|
258
|
+
nu = math.ceil(hi / unit - 1e-7)
|
|
259
|
+
|
|
260
|
+
lo_tick = ns * unit
|
|
261
|
+
hi_tick = nu * unit
|
|
262
|
+
|
|
263
|
+
ticks = np.arange(lo_tick, hi_tick + unit * 0.5, unit)
|
|
264
|
+
# Clip floating-point noise at the boundaries
|
|
265
|
+
ticks = np.round(ticks / unit) * unit
|
|
266
|
+
|
|
267
|
+
# grid.pretty restricts to within the requested range
|
|
268
|
+
# (grid R-3.6 src/library/grid/R/util.R: `res[res >= range[1] & res <= range[2]]`).
|
|
269
|
+
ticks = ticks[(ticks >= lo) & (ticks <= hi)]
|
|
270
|
+
|
|
271
|
+
return ticks
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def n2mfrow(n: int) -> tuple[int, int]:
|
|
275
|
+
"""Compute a ``(nrow, ncol)`` layout to display *n* plots.
|
|
276
|
+
|
|
277
|
+
This is a Python port of R's ``grDevices::n2mfrow``.
|
|
278
|
+
|
|
279
|
+
Parameters
|
|
280
|
+
----------
|
|
281
|
+
n : int
|
|
282
|
+
Total number of plots.
|
|
283
|
+
|
|
284
|
+
Returns
|
|
285
|
+
-------
|
|
286
|
+
tuple of (int, int)
|
|
287
|
+
``(nrow, ncol)`` suitable for passing to a layout function.
|
|
288
|
+
|
|
289
|
+
Examples
|
|
290
|
+
--------
|
|
291
|
+
>>> n2mfrow(5)
|
|
292
|
+
(3, 2)
|
|
293
|
+
>>> n2mfrow(1)
|
|
294
|
+
(1, 1)
|
|
295
|
+
"""
|
|
296
|
+
if n <= 0:
|
|
297
|
+
return (0, 0)
|
|
298
|
+
if n <= 3:
|
|
299
|
+
return (n, 1)
|
|
300
|
+
if n <= 6:
|
|
301
|
+
return (3, 2) if n > 4 else (2, 2)
|
|
302
|
+
if n <= 12:
|
|
303
|
+
ncol = 3
|
|
304
|
+
nrow = math.ceil(n / ncol)
|
|
305
|
+
return (nrow, ncol)
|
|
306
|
+
|
|
307
|
+
# General case: roughly square
|
|
308
|
+
ncol = math.ceil(math.sqrt(n))
|
|
309
|
+
nrow = math.ceil(n / ncol)
|
|
310
|
+
return (nrow, ncol)
|