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/_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)