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/_just.py ADDED
@@ -0,0 +1,361 @@
1
+ """Justification utilities for grid_py.
2
+
3
+ This module ports the justification logic from R's *grid* package
4
+ (``src/library/grid/R/just.R``) to Python. Justification values
5
+ control where a graphical object is placed relative to its anchor
6
+ point.
7
+
8
+ Numeric convention (matching R):
9
+
10
+ * 0 -- left / bottom
11
+ * 0.5 -- centre
12
+ * 1 -- right / top
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import List, Optional, Sequence, Tuple, Union
18
+
19
+ import numpy as np
20
+
21
+ __all__ = [
22
+ "valid_just",
23
+ "resolve_hjust",
24
+ "resolve_vjust",
25
+ "resolve_raster_size",
26
+ ]
27
+
28
+ # ------------------------------------------------------------------ #
29
+ # Internal look-up tables #
30
+ # ------------------------------------------------------------------ #
31
+
32
+ # The order matches the R enum in lattice.h:
33
+ # "left"=0, "right"=1, "bottom"=2, "top"=3, "centre"=4, "center"=5
34
+ _JUST_STRINGS: Tuple[str, ...] = (
35
+ "left",
36
+ "right",
37
+ "bottom",
38
+ "top",
39
+ "centre",
40
+ "center",
41
+ )
42
+
43
+ # Mapping from enum index to numeric justification
44
+ _HJUST_MAP = {0: 0.0, 1: 1.0, 4: 0.5, 5: 0.5} # left # right # centre # center
45
+ _VJUST_MAP = {2: 0.0, 3: 1.0, 4: 0.5, 5: 0.5} # bottom # top # centre # center
46
+
47
+ # When a single string is provided, R expands it to (hjust, vjust) as:
48
+ # left -> (left, centre)
49
+ # right -> (right, centre)
50
+ # bottom -> (centre, bottom)
51
+ # top -> (centre, top)
52
+ # centre -> (centre, centre)
53
+ # center -> (centre, centre)
54
+ _SINGLE_EXPAND = {
55
+ 0: (0, 4), # left -> (left, centre)
56
+ 1: (1, 4), # right -> (right, centre)
57
+ 2: (4, 2), # bottom -> (centre, bottom)
58
+ 3: (4, 3), # top -> (centre, top)
59
+ 4: (4, 4), # centre -> (centre, centre)
60
+ 5: (4, 4), # center -> (centre, centre)
61
+ }
62
+
63
+ # Valid enum indices for each axis
64
+ _VALID_HJUST_INDICES = {0, 1, 4, 5} # left, right, centre, center
65
+ _VALID_VJUST_INDICES = {2, 3, 4, 5} # bottom, top, centre, center
66
+
67
+ # Convenient string-to-float shortcuts
68
+ _STR_TO_HJUST = {"left": 0.0, "right": 1.0, "centre": 0.5, "center": 0.5}
69
+ _STR_TO_VJUST = {"bottom": 0.0, "top": 1.0, "centre": 0.5, "center": 0.5}
70
+
71
+ # Type aliases
72
+ JustSpec = Union[str, float, int, Sequence[Union[str, float, int]]]
73
+
74
+
75
+ # ------------------------------------------------------------------ #
76
+ # Internal helpers #
77
+ # ------------------------------------------------------------------ #
78
+
79
+
80
+ def _match_just_string(s: str) -> int:
81
+ """Return the enum index for a justification string, or raise."""
82
+ try:
83
+ return _JUST_STRINGS.index(s)
84
+ except ValueError:
85
+ raise ValueError(
86
+ f"invalid justification string {s!r}; "
87
+ f"must be one of {_JUST_STRINGS}"
88
+ ) from None
89
+
90
+
91
+ def _valid_charjust(just: Sequence[str]) -> Tuple[float, float]:
92
+ """Validate and convert character justification (mirrors ``valid.charjust`` in R).
93
+
94
+ Parameters
95
+ ----------
96
+ just : sequence of str
97
+ One or two justification strings.
98
+
99
+ Returns
100
+ -------
101
+ tuple of float
102
+ ``(hjust, vjust)`` each in [0, 1].
103
+ """
104
+ n = len(just)
105
+ if n == 0:
106
+ return (0.5, 0.5)
107
+
108
+ if n == 1:
109
+ idx = _match_just_string(just[0])
110
+ h_idx, v_idx = _SINGLE_EXPAND[idx]
111
+ else:
112
+ h_idx = _match_just_string(just[0])
113
+ if h_idx not in _VALID_HJUST_INDICES:
114
+ raise ValueError(
115
+ f"invalid horizontal justification {just[0]!r}; "
116
+ "must be 'left', 'right', 'centre', or 'center'"
117
+ )
118
+ v_idx = _match_just_string(just[1])
119
+ if v_idx not in _VALID_VJUST_INDICES:
120
+ raise ValueError(
121
+ f"invalid vertical justification {just[1]!r}; "
122
+ "must be 'bottom', 'top', 'centre', or 'center'"
123
+ )
124
+
125
+ hjust = _HJUST_MAP.get(h_idx)
126
+ vjust = _VJUST_MAP.get(v_idx)
127
+ if hjust is None or vjust is None:
128
+ raise ValueError("invalid justification")
129
+ return (hjust, vjust)
130
+
131
+
132
+ def _valid_numjust(
133
+ just: Sequence[Union[int, float]],
134
+ ) -> Tuple[float, float]:
135
+ """Validate and convert numeric justification (mirrors ``valid.numjust`` in R).
136
+
137
+ Parameters
138
+ ----------
139
+ just : sequence of int or float
140
+ Zero, one, or two numeric justification values.
141
+
142
+ Returns
143
+ -------
144
+ tuple of float
145
+ ``(hjust, vjust)``.
146
+ """
147
+ n = len(just)
148
+ if n == 0:
149
+ return (0.5, 0.5)
150
+ if n == 1:
151
+ return (float(just[0]), 0.5)
152
+ return (float(just[0]), float(just[1]))
153
+
154
+
155
+ # ------------------------------------------------------------------ #
156
+ # Public API #
157
+ # ------------------------------------------------------------------ #
158
+
159
+
160
+ def valid_just(just: JustSpec) -> Tuple[float, float]:
161
+ """Validate and normalise a justification specification.
162
+
163
+ This mirrors R's ``valid.just`` function. Justification may be
164
+ given as:
165
+
166
+ * a single string (``"left"``, ``"right"``, ``"bottom"``, ``"top"``,
167
+ ``"centre"``, ``"center"``),
168
+ * a two-element sequence ``[hjust, vjust]`` where each element is a
169
+ string *or* a number, or
170
+ * a single number (treated as *hjust* with *vjust* defaulting to 0.5).
171
+
172
+ Parameters
173
+ ----------
174
+ just : str, float, int, or sequence thereof
175
+ Justification specification.
176
+
177
+ Returns
178
+ -------
179
+ tuple of float
180
+ ``(hjust, vjust)`` where 0 is left/bottom, 0.5 is centre, and
181
+ 1 is right/top.
182
+
183
+ Raises
184
+ ------
185
+ ValueError
186
+ If *just* contains an unrecognised string or an invalid
187
+ combination of horizontal/vertical strings.
188
+
189
+ Examples
190
+ --------
191
+ >>> valid_just("centre")
192
+ (0.5, 0.5)
193
+ >>> valid_just("left")
194
+ (0.0, 0.5)
195
+ >>> valid_just(["right", "top"])
196
+ (1.0, 1.0)
197
+ >>> valid_just(0.25)
198
+ (0.25, 0.5)
199
+ >>> valid_just([0.1, 0.9])
200
+ (0.1, 0.9)
201
+ """
202
+ # Scalar string
203
+ if isinstance(just, str):
204
+ return _valid_charjust([just])
205
+
206
+ # Scalar numeric
207
+ if isinstance(just, (int, float, np.integer, np.floating)):
208
+ return (float(just), 0.5)
209
+
210
+ # Sequence -- coerce to list for uniform handling
211
+ just_list: List[Union[str, float, int]] = list(just)
212
+ if len(just_list) == 0:
213
+ return (0.5, 0.5)
214
+
215
+ # All-string path
216
+ if all(isinstance(j, str) for j in just_list):
217
+ return _valid_charjust(just_list) # type: ignore[arg-type]
218
+
219
+ # All-numeric path
220
+ if all(isinstance(j, (int, float, np.integer, np.floating)) for j in just_list):
221
+ return _valid_numjust(just_list) # type: ignore[arg-type]
222
+
223
+ # Mixed: try to convert everything to float (matches R's as.numeric)
224
+ try:
225
+ nums = [float(j) for j in just_list]
226
+ except (TypeError, ValueError):
227
+ raise ValueError(
228
+ f"invalid justification specification: {just!r}"
229
+ ) from None
230
+ return _valid_numjust(nums)
231
+
232
+
233
+ def resolve_hjust(
234
+ just: JustSpec,
235
+ hjust: Optional[float] = None,
236
+ ) -> float:
237
+ """Resolve horizontal justification to a numeric value.
238
+
239
+ If *hjust* is provided it is returned directly; otherwise the
240
+ horizontal component is extracted from *just* via `valid_just`.
241
+ This mirrors R's ``resolveHJust``.
242
+
243
+ Parameters
244
+ ----------
245
+ just : str, float, int, or sequence thereof
246
+ General justification specification (see `valid_just`).
247
+ hjust : float, optional
248
+ Explicit horizontal justification override. When not *None*
249
+ this value is returned as-is.
250
+
251
+ Returns
252
+ -------
253
+ float
254
+ Horizontal justification in [0, 1].
255
+
256
+ Examples
257
+ --------
258
+ >>> resolve_hjust("left")
259
+ 0.0
260
+ >>> resolve_hjust("centre", hjust=0.3)
261
+ 0.3
262
+ """
263
+ if hjust is not None:
264
+ return float(hjust)
265
+ return valid_just(just)[0]
266
+
267
+
268
+ def resolve_vjust(
269
+ just: JustSpec,
270
+ vjust: Optional[float] = None,
271
+ ) -> float:
272
+ """Resolve vertical justification to a numeric value.
273
+
274
+ If *vjust* is provided it is returned directly; otherwise the
275
+ vertical component is extracted from *just* via `valid_just`.
276
+ This mirrors R's ``resolveVJust``.
277
+
278
+ Parameters
279
+ ----------
280
+ just : str, float, int, or sequence thereof
281
+ General justification specification (see `valid_just`).
282
+ vjust : float, optional
283
+ Explicit vertical justification override. When not *None*
284
+ this value is returned as-is.
285
+
286
+ Returns
287
+ -------
288
+ float
289
+ Vertical justification in [0, 1].
290
+
291
+ Examples
292
+ --------
293
+ >>> resolve_vjust("top")
294
+ 1.0
295
+ >>> resolve_vjust("centre", vjust=0.8)
296
+ 0.8
297
+ """
298
+ if vjust is not None:
299
+ return float(vjust)
300
+ return valid_just(just)[1]
301
+
302
+
303
+ def resolve_raster_size(
304
+ x: np.ndarray,
305
+ default_size: Tuple[float, float],
306
+ target_size: Tuple[Optional[float], Optional[float]],
307
+ ) -> Tuple[float, float]:
308
+ """Compute width and height for a raster grob.
309
+
310
+ When either the target width or height is *None* the missing
311
+ dimension is derived from the raster's aspect ratio and the
312
+ available viewport size, matching R's ``resolveRasterSize``.
313
+
314
+ Parameters
315
+ ----------
316
+ x : numpy.ndarray
317
+ The raster data array. Its shape ``(nrow, ncol, ...)`` is used
318
+ to compute the aspect ratio (``nrow / ncol``).
319
+ default_size : tuple of float
320
+ ``(viewport_width, viewport_height)`` in inches, used when both
321
+ *width* and *height* are *None*.
322
+ target_size : tuple of float or None
323
+ ``(width, height)`` requested by the user. Either or both may
324
+ be *None* to indicate "auto".
325
+
326
+ Returns
327
+ -------
328
+ tuple of float
329
+ ``(width, height)`` in the same units as *default_size* /
330
+ *target_size*.
331
+
332
+ Examples
333
+ --------
334
+ >>> import numpy as np
335
+ >>> img = np.zeros((200, 400, 3))
336
+ >>> resolve_raster_size(img, (6.0, 4.0), (None, None))
337
+ (6.0, 3.0)
338
+ """
339
+ nrow, ncol = x.shape[0], x.shape[1]
340
+ raster_ratio = nrow / ncol # height / width in pixels
341
+
342
+ width, height = target_size
343
+
344
+ if width is None and height is None:
345
+ vp_width, vp_height = default_size
346
+ vp_ratio = vp_height / vp_width
347
+ if raster_ratio > vp_ratio:
348
+ # raster is taller (relative) than viewport
349
+ height = vp_height
350
+ width = vp_height * ncol / nrow
351
+ else:
352
+ width = vp_width
353
+ height = vp_width * nrow / ncol
354
+ elif width is None:
355
+ # height is known; derive width
356
+ width = height * ncol / nrow
357
+ elif height is None:
358
+ # width is known; derive height
359
+ height = width * nrow / ncol
360
+
361
+ return (float(width), float(height))