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/_highlevel.py
ADDED
|
@@ -0,0 +1,2176 @@
|
|
|
1
|
+
"""High-level grid functions -- Python port of R's grid high-level API.
|
|
2
|
+
|
|
3
|
+
This module ports functionality from three R source files:
|
|
4
|
+
|
|
5
|
+
* ``grid/R/highlevel.R`` -- grid.grill, grid.show.layout, grid.show.viewport,
|
|
6
|
+
grid.plot.and.legend, grid.abline, layoutTorture, grid.multipanel, etc.
|
|
7
|
+
* ``grid/R/frames.R`` -- frameGrob, grid.frame, packGrob, grid.pack,
|
|
8
|
+
placeGrob, grid.place, and internal helpers.
|
|
9
|
+
* ``grid/R/components.R`` -- xaxisGrob, grid.xaxis, yaxisGrob, grid.yaxis,
|
|
10
|
+
legendGrob, grid.legend, and related helpers.
|
|
11
|
+
|
|
12
|
+
References
|
|
13
|
+
----------
|
|
14
|
+
R source: ``src/library/grid/R/highlevel.R``, ``src/library/grid/R/frames.R``,
|
|
15
|
+
``src/library/grid/R/components.R``
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import copy
|
|
21
|
+
import math
|
|
22
|
+
from typing import (
|
|
23
|
+
Any,
|
|
24
|
+
Dict,
|
|
25
|
+
List,
|
|
26
|
+
Optional,
|
|
27
|
+
Sequence,
|
|
28
|
+
Tuple,
|
|
29
|
+
Union,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
import numpy as np
|
|
33
|
+
|
|
34
|
+
from ._gpar import Gpar
|
|
35
|
+
from ._grob import (
|
|
36
|
+
GEdit,
|
|
37
|
+
GEditList,
|
|
38
|
+
GList,
|
|
39
|
+
GTree,
|
|
40
|
+
Grob,
|
|
41
|
+
add_grob,
|
|
42
|
+
apply_edits,
|
|
43
|
+
edit_grob,
|
|
44
|
+
get_grob,
|
|
45
|
+
grob_tree,
|
|
46
|
+
is_grob,
|
|
47
|
+
remove_grob,
|
|
48
|
+
set_grob,
|
|
49
|
+
)
|
|
50
|
+
from ._just import valid_just
|
|
51
|
+
from ._layout import (
|
|
52
|
+
GridLayout,
|
|
53
|
+
layout_heights,
|
|
54
|
+
layout_ncol,
|
|
55
|
+
layout_nrow,
|
|
56
|
+
layout_widths,
|
|
57
|
+
)
|
|
58
|
+
from ._primitives import (
|
|
59
|
+
grid_lines,
|
|
60
|
+
grid_points,
|
|
61
|
+
grid_rect,
|
|
62
|
+
grid_segments,
|
|
63
|
+
grid_text,
|
|
64
|
+
lines_grob,
|
|
65
|
+
null_grob,
|
|
66
|
+
points_grob,
|
|
67
|
+
rect_grob,
|
|
68
|
+
segments_grob,
|
|
69
|
+
text_grob,
|
|
70
|
+
)
|
|
71
|
+
from ._units import Unit, is_unit, unit_c, unit_pmax
|
|
72
|
+
from ._viewport import (
|
|
73
|
+
Viewport,
|
|
74
|
+
VpStack,
|
|
75
|
+
current_viewport,
|
|
76
|
+
pop_viewport,
|
|
77
|
+
push_viewport,
|
|
78
|
+
)
|
|
79
|
+
from ._draw import grid_draw, grid_newpage, grid_pretty
|
|
80
|
+
|
|
81
|
+
__all__ = [
|
|
82
|
+
# high-level (highlevel.R)
|
|
83
|
+
"grid_grill",
|
|
84
|
+
"grid_plot_and_legend",
|
|
85
|
+
"grid_show_layout",
|
|
86
|
+
"grid_show_viewport",
|
|
87
|
+
"grid_abline",
|
|
88
|
+
"layout_torture",
|
|
89
|
+
# frames (frames.R)
|
|
90
|
+
"frame_grob",
|
|
91
|
+
"grid_frame",
|
|
92
|
+
"pack_grob",
|
|
93
|
+
"grid_pack",
|
|
94
|
+
"place_grob",
|
|
95
|
+
"grid_place",
|
|
96
|
+
# components (components.R)
|
|
97
|
+
"xaxis_grob",
|
|
98
|
+
"yaxis_grob",
|
|
99
|
+
"grid_xaxis",
|
|
100
|
+
"grid_yaxis",
|
|
101
|
+
"legend_grob",
|
|
102
|
+
"grid_legend",
|
|
103
|
+
"grid_multipanel",
|
|
104
|
+
"grid_panel",
|
|
105
|
+
"grid_strip",
|
|
106
|
+
"grid_top_level_vp",
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# =========================================================================
|
|
111
|
+
# Internal helpers
|
|
112
|
+
# =========================================================================
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _ensure_unit(value: Any, default_units: str = "npc") -> Unit:
|
|
116
|
+
"""Coerce *value* to a :class:`Unit` if it is not one already."""
|
|
117
|
+
if is_unit(value):
|
|
118
|
+
return value
|
|
119
|
+
return Unit(value, default_units)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _is_even(n: int) -> bool:
|
|
123
|
+
"""Return ``True`` if *n* is even."""
|
|
124
|
+
return n % 2 == 0
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _is_odd(n: int) -> bool:
|
|
128
|
+
"""Return ``True`` if *n* is odd."""
|
|
129
|
+
return n % 2 == 1
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _extend_range(x: Sequence[float], f: float = 0.05) -> Tuple[float, float]:
|
|
133
|
+
"""Extend range of *x* by fraction *f* on each side (like R extendrange)."""
|
|
134
|
+
mn, mx = float(min(x)), float(max(x))
|
|
135
|
+
rng = mx - mn
|
|
136
|
+
if rng == 0:
|
|
137
|
+
rng = 1.0
|
|
138
|
+
return (mn - f * rng, mx + f * rng)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# =========================================================================
|
|
142
|
+
# Frame / Pack / Place (frames.R)
|
|
143
|
+
# =========================================================================
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def frame_grob(
|
|
147
|
+
layout: Optional[GridLayout] = None,
|
|
148
|
+
name: Optional[str] = None,
|
|
149
|
+
gp: Optional[Gpar] = None,
|
|
150
|
+
vp: Optional[Any] = None,
|
|
151
|
+
) -> GTree:
|
|
152
|
+
"""Create a frame grob -- a GTree intended for packing child grobs.
|
|
153
|
+
|
|
154
|
+
Parameters
|
|
155
|
+
----------
|
|
156
|
+
layout : GridLayout or None
|
|
157
|
+
Optional initial layout for the frame.
|
|
158
|
+
name : str or None
|
|
159
|
+
Grob name (auto-generated if ``None``).
|
|
160
|
+
gp : Gpar or None
|
|
161
|
+
Graphical parameters.
|
|
162
|
+
vp : object or None
|
|
163
|
+
Viewport.
|
|
164
|
+
|
|
165
|
+
Returns
|
|
166
|
+
-------
|
|
167
|
+
GTree
|
|
168
|
+
A GTree with ``_grid_class="frame"`` and a *framevp* attribute.
|
|
169
|
+
"""
|
|
170
|
+
framevp: Optional[Viewport] = None
|
|
171
|
+
if layout is not None:
|
|
172
|
+
framevp = Viewport(layout=layout)
|
|
173
|
+
return GTree(
|
|
174
|
+
name=name,
|
|
175
|
+
gp=gp,
|
|
176
|
+
vp=vp,
|
|
177
|
+
_grid_class="frame",
|
|
178
|
+
framevp=framevp,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def grid_frame(
|
|
183
|
+
layout: Optional[GridLayout] = None,
|
|
184
|
+
name: Optional[str] = None,
|
|
185
|
+
gp: Optional[Gpar] = None,
|
|
186
|
+
vp: Optional[Any] = None,
|
|
187
|
+
draw: bool = True,
|
|
188
|
+
) -> GTree:
|
|
189
|
+
"""Create (and optionally draw) a frame grob.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
----------
|
|
193
|
+
layout : GridLayout or None
|
|
194
|
+
Optional initial layout.
|
|
195
|
+
name : str or None
|
|
196
|
+
Grob name.
|
|
197
|
+
gp : Gpar or None
|
|
198
|
+
Graphical parameters.
|
|
199
|
+
vp : object or None
|
|
200
|
+
Viewport.
|
|
201
|
+
draw : bool
|
|
202
|
+
If ``True``, draw the frame immediately.
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
GTree
|
|
207
|
+
The frame grob.
|
|
208
|
+
"""
|
|
209
|
+
fg = frame_grob(layout=layout, name=name, gp=gp, vp=vp)
|
|
210
|
+
if draw:
|
|
211
|
+
grid_draw(fg)
|
|
212
|
+
return fg
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
# Internal helpers for cell grobs and packing
|
|
217
|
+
# ---------------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _cell_viewport(
|
|
221
|
+
col: Any,
|
|
222
|
+
row: Any,
|
|
223
|
+
border: Optional[Sequence[Unit]],
|
|
224
|
+
) -> Any:
|
|
225
|
+
"""Build a viewport (or VpStack) for a cell, optionally with border insets.
|
|
226
|
+
|
|
227
|
+
Parameters
|
|
228
|
+
----------
|
|
229
|
+
col : int or list
|
|
230
|
+
Column index or range.
|
|
231
|
+
row : int or list
|
|
232
|
+
Row index or range.
|
|
233
|
+
border : list of Unit or None
|
|
234
|
+
Four-element border ``[bottom, left, top, right]``.
|
|
235
|
+
|
|
236
|
+
Returns
|
|
237
|
+
-------
|
|
238
|
+
Viewport or VpStack
|
|
239
|
+
"""
|
|
240
|
+
vp = Viewport(layout_pos_col=col, layout_pos_row=row)
|
|
241
|
+
if border is not None:
|
|
242
|
+
inner = Viewport(
|
|
243
|
+
x=border[1],
|
|
244
|
+
y=border[0],
|
|
245
|
+
width=Unit(1, "npc") - (border[1] + border[3]),
|
|
246
|
+
height=Unit(1, "npc") - (border[0] + border[2]),
|
|
247
|
+
just=["left", "bottom"],
|
|
248
|
+
)
|
|
249
|
+
return VpStack(vp, inner)
|
|
250
|
+
return vp
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _cell_grob(
|
|
254
|
+
col: Any,
|
|
255
|
+
row: Any,
|
|
256
|
+
border: Optional[Sequence[Unit]],
|
|
257
|
+
grob: Grob,
|
|
258
|
+
dynamic: bool,
|
|
259
|
+
vp: Any,
|
|
260
|
+
) -> GTree:
|
|
261
|
+
"""Wrap a grob in a cellGrob container.
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
col : int or list
|
|
266
|
+
Column position.
|
|
267
|
+
row : int or list
|
|
268
|
+
Row position.
|
|
269
|
+
border : list of Unit or None
|
|
270
|
+
Border insets.
|
|
271
|
+
grob : Grob
|
|
272
|
+
The child grob.
|
|
273
|
+
dynamic : bool
|
|
274
|
+
Whether to use dynamic sizing.
|
|
275
|
+
vp : object
|
|
276
|
+
Cell viewport.
|
|
277
|
+
|
|
278
|
+
Returns
|
|
279
|
+
-------
|
|
280
|
+
GTree
|
|
281
|
+
A GTree with ``_grid_class="cellGrob"``.
|
|
282
|
+
"""
|
|
283
|
+
return GTree(
|
|
284
|
+
children=GList(grob),
|
|
285
|
+
_grid_class="cellGrob",
|
|
286
|
+
col=col,
|
|
287
|
+
row=row,
|
|
288
|
+
border=border,
|
|
289
|
+
dynamic=dynamic,
|
|
290
|
+
cellvp=vp,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _frame_dim(frame: GTree) -> Tuple[int, int]:
|
|
295
|
+
"""Return ``(nrow, ncol)`` of *frame*'s layout, or ``(0, 0)``."""
|
|
296
|
+
framevp = getattr(frame, "framevp", None)
|
|
297
|
+
if framevp is None:
|
|
298
|
+
return (0, 0)
|
|
299
|
+
lay = getattr(framevp, "layout", None)
|
|
300
|
+
if lay is None:
|
|
301
|
+
return (0, 0)
|
|
302
|
+
return (layout_nrow(lay), layout_ncol(lay))
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# -- column / row specification helpers ------------------------------------
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _num_col_specs(
|
|
309
|
+
side: Optional[str],
|
|
310
|
+
col: Optional[Any],
|
|
311
|
+
col_before: Optional[int],
|
|
312
|
+
col_after: Optional[int],
|
|
313
|
+
) -> int:
|
|
314
|
+
side_counts = 0 if (side is None or side in ("top", "bottom")) else 1
|
|
315
|
+
return side_counts + (col is not None) + (col_before is not None) + (col_after is not None)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _col_spec(
|
|
319
|
+
side: Optional[str],
|
|
320
|
+
col: Optional[int],
|
|
321
|
+
col_before: Optional[int],
|
|
322
|
+
col_after: Optional[int],
|
|
323
|
+
ncol: int,
|
|
324
|
+
) -> int:
|
|
325
|
+
if side is not None:
|
|
326
|
+
if side == "left":
|
|
327
|
+
return 1
|
|
328
|
+
if side == "right":
|
|
329
|
+
return ncol + 1
|
|
330
|
+
if col_before is not None:
|
|
331
|
+
return col_before
|
|
332
|
+
if col_after is not None:
|
|
333
|
+
return col_after + 1
|
|
334
|
+
return col # type: ignore[return-value]
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _new_col(
|
|
338
|
+
side: Optional[str],
|
|
339
|
+
col: Optional[Any],
|
|
340
|
+
col_before: Optional[int],
|
|
341
|
+
col_after: Optional[int],
|
|
342
|
+
ncol: int,
|
|
343
|
+
) -> bool:
|
|
344
|
+
result = True
|
|
345
|
+
if col is not None:
|
|
346
|
+
if isinstance(col, (list, tuple)) and len(col) == 2:
|
|
347
|
+
if col[0] < 1 or col[1] > ncol:
|
|
348
|
+
raise ValueError("'col' can only be a range of existing columns")
|
|
349
|
+
result = False
|
|
350
|
+
else:
|
|
351
|
+
c = col if isinstance(col, int) else col
|
|
352
|
+
if c < 1 or c > ncol + 1:
|
|
353
|
+
raise ValueError("invalid 'col' specification")
|
|
354
|
+
result = c == ncol + 1
|
|
355
|
+
return result
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def _num_row_specs(
|
|
359
|
+
side: Optional[str],
|
|
360
|
+
row: Optional[Any],
|
|
361
|
+
row_before: Optional[int],
|
|
362
|
+
row_after: Optional[int],
|
|
363
|
+
) -> int:
|
|
364
|
+
side_counts = 0 if (side is None or side in ("left", "right")) else 1
|
|
365
|
+
return side_counts + (row is not None) + (row_before is not None) + (row_after is not None)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _row_spec(
|
|
369
|
+
side: Optional[str],
|
|
370
|
+
row: Optional[int],
|
|
371
|
+
row_before: Optional[int],
|
|
372
|
+
row_after: Optional[int],
|
|
373
|
+
nrow: int,
|
|
374
|
+
) -> int:
|
|
375
|
+
if side is not None:
|
|
376
|
+
if side == "top":
|
|
377
|
+
return 1
|
|
378
|
+
if side == "bottom":
|
|
379
|
+
return nrow + 1
|
|
380
|
+
if row_before is not None:
|
|
381
|
+
return row_before
|
|
382
|
+
if row_after is not None:
|
|
383
|
+
return row_after + 1
|
|
384
|
+
return row # type: ignore[return-value]
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _new_row(
|
|
388
|
+
side: Optional[str],
|
|
389
|
+
row: Optional[Any],
|
|
390
|
+
row_before: Optional[int],
|
|
391
|
+
row_after: Optional[int],
|
|
392
|
+
nrow: int,
|
|
393
|
+
) -> bool:
|
|
394
|
+
result = True
|
|
395
|
+
if row is not None:
|
|
396
|
+
if isinstance(row, (list, tuple)) and len(row) == 2:
|
|
397
|
+
if row[0] < 1 or row[1] > nrow:
|
|
398
|
+
raise ValueError("'row' can only be a range of existing rows")
|
|
399
|
+
result = False
|
|
400
|
+
else:
|
|
401
|
+
r = row if isinstance(row, int) else row
|
|
402
|
+
if r < 1 or r > nrow + 1:
|
|
403
|
+
raise ValueError("invalid 'row' specification")
|
|
404
|
+
result = r == nrow + 1
|
|
405
|
+
return result
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _mod_dims(
|
|
409
|
+
dim: Unit,
|
|
410
|
+
dims: Unit,
|
|
411
|
+
index: int,
|
|
412
|
+
is_new: bool,
|
|
413
|
+
nindex: int,
|
|
414
|
+
force: bool,
|
|
415
|
+
) -> Unit:
|
|
416
|
+
"""Update dimension list when packing a new grob.
|
|
417
|
+
|
|
418
|
+
Parameters
|
|
419
|
+
----------
|
|
420
|
+
dim : Unit
|
|
421
|
+
Width or height of the new grob.
|
|
422
|
+
dims : Unit
|
|
423
|
+
Current list of widths or heights.
|
|
424
|
+
index : int
|
|
425
|
+
1-based index for the new grob's row/column.
|
|
426
|
+
is_new : bool
|
|
427
|
+
Whether a new row/column is being added.
|
|
428
|
+
nindex : int
|
|
429
|
+
Current number of rows/columns (before adding).
|
|
430
|
+
force : bool
|
|
431
|
+
If ``True``, override existing dimension; otherwise take the max.
|
|
432
|
+
|
|
433
|
+
Returns
|
|
434
|
+
-------
|
|
435
|
+
Unit
|
|
436
|
+
"""
|
|
437
|
+
if is_new:
|
|
438
|
+
if index == 1:
|
|
439
|
+
return unit_c(dim, dims)
|
|
440
|
+
elif index == nindex:
|
|
441
|
+
return unit_c(dims, dim)
|
|
442
|
+
else:
|
|
443
|
+
# Insert before the existing index
|
|
444
|
+
before = dims[0 : index - 1] if index > 1 else None
|
|
445
|
+
after = dims[index - 1 :] if index <= nindex else None
|
|
446
|
+
parts: list[Unit] = []
|
|
447
|
+
if before is not None:
|
|
448
|
+
parts.append(before)
|
|
449
|
+
parts.append(dim)
|
|
450
|
+
if after is not None:
|
|
451
|
+
parts.append(after)
|
|
452
|
+
result = parts[0]
|
|
453
|
+
for p in parts[1:]:
|
|
454
|
+
result = unit_c(result, p)
|
|
455
|
+
return result
|
|
456
|
+
else:
|
|
457
|
+
# Existing row/col: take max or force
|
|
458
|
+
# R frames.R:254-255: if (!force) dim <- max(dim, dims[index])
|
|
459
|
+
if not force:
|
|
460
|
+
existing = dims[index - 1 : index] # 1-based → 0-based slice
|
|
461
|
+
dim = unit_pmax(dim, existing)
|
|
462
|
+
# Replace the dimension at *index* (1-based)
|
|
463
|
+
idx0 = index - 1
|
|
464
|
+
parts_list: list[Unit] = []
|
|
465
|
+
if idx0 > 0:
|
|
466
|
+
parts_list.append(dims[0:idx0])
|
|
467
|
+
parts_list.append(dim)
|
|
468
|
+
if idx0 + 1 < nindex:
|
|
469
|
+
parts_list.append(dims[idx0 + 1 :])
|
|
470
|
+
if not parts_list:
|
|
471
|
+
return dim
|
|
472
|
+
result2 = parts_list[0]
|
|
473
|
+
for p in parts_list[1:]:
|
|
474
|
+
result2 = unit_c(result2, p)
|
|
475
|
+
return result2
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _update_col(col: Any, added_col: int) -> Any:
|
|
479
|
+
"""Shift *col* if a new column was inserted before or at it."""
|
|
480
|
+
if isinstance(col, (list, tuple)) and len(col) == 2:
|
|
481
|
+
if added_col <= col[1]:
|
|
482
|
+
return [col[0], col[1] + 1]
|
|
483
|
+
return col
|
|
484
|
+
if added_col <= col:
|
|
485
|
+
return col + 1
|
|
486
|
+
return col
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _update_row(row: Any, added_row: int) -> Any:
|
|
490
|
+
"""Shift *row* if a new row was inserted before or at it."""
|
|
491
|
+
if isinstance(row, (list, tuple)) and len(row) == 2:
|
|
492
|
+
if added_row <= row[1]:
|
|
493
|
+
return [row[0], row[1] + 1]
|
|
494
|
+
return row
|
|
495
|
+
if added_row <= row:
|
|
496
|
+
return row + 1
|
|
497
|
+
return row
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# ---------------------------------------------------------------------------
|
|
501
|
+
# pack_grob / grid_pack
|
|
502
|
+
# ---------------------------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def pack_grob(
|
|
506
|
+
frame: GTree,
|
|
507
|
+
grob: Grob,
|
|
508
|
+
side: Optional[str] = None,
|
|
509
|
+
row: Optional[Any] = None,
|
|
510
|
+
row_before: Optional[int] = None,
|
|
511
|
+
row_after: Optional[int] = None,
|
|
512
|
+
col: Optional[Any] = None,
|
|
513
|
+
col_before: Optional[int] = None,
|
|
514
|
+
col_after: Optional[int] = None,
|
|
515
|
+
width: Optional[Unit] = None,
|
|
516
|
+
height: Optional[Unit] = None,
|
|
517
|
+
force_width: bool = False,
|
|
518
|
+
force_height: bool = False,
|
|
519
|
+
border: Optional[Sequence[Unit]] = None,
|
|
520
|
+
dynamic: bool = False,
|
|
521
|
+
) -> GTree:
|
|
522
|
+
"""Pack a grob into a frame, returning a new frame.
|
|
523
|
+
|
|
524
|
+
This function is the Python equivalent of R's ``packGrob``. It manages
|
|
525
|
+
the frame's internal layout, adding rows/columns as needed, and wraps
|
|
526
|
+
the child grob in a ``cellGrob``.
|
|
527
|
+
|
|
528
|
+
Parameters
|
|
529
|
+
----------
|
|
530
|
+
frame : GTree
|
|
531
|
+
The frame grob (must have ``_grid_class="frame"``).
|
|
532
|
+
grob : Grob
|
|
533
|
+
The child grob to pack.
|
|
534
|
+
side : str or None
|
|
535
|
+
One of ``"left"``, ``"right"``, ``"top"``, ``"bottom"``.
|
|
536
|
+
row : int, list of int, or None
|
|
537
|
+
Row position or range.
|
|
538
|
+
row_before : int or None
|
|
539
|
+
Insert before this row.
|
|
540
|
+
row_after : int or None
|
|
541
|
+
Insert after this row.
|
|
542
|
+
col : int, list of int, or None
|
|
543
|
+
Column position or range.
|
|
544
|
+
col_before : int or None
|
|
545
|
+
Insert before this column.
|
|
546
|
+
col_after : int or None
|
|
547
|
+
Insert after this column.
|
|
548
|
+
width : Unit or None
|
|
549
|
+
Explicit width (derived from *grob* if ``None``).
|
|
550
|
+
height : Unit or None
|
|
551
|
+
Explicit height (derived from *grob* if ``None``).
|
|
552
|
+
force_width : bool
|
|
553
|
+
If ``True``, override the existing column width.
|
|
554
|
+
force_height : bool
|
|
555
|
+
If ``True``, override the existing row height.
|
|
556
|
+
border : list of Unit or None
|
|
557
|
+
Four-element border insets ``[bottom, left, top, right]``.
|
|
558
|
+
dynamic : bool
|
|
559
|
+
If ``True``, use a grob path for deferred sizing.
|
|
560
|
+
|
|
561
|
+
Returns
|
|
562
|
+
-------
|
|
563
|
+
GTree
|
|
564
|
+
A new frame with the child packed in.
|
|
565
|
+
|
|
566
|
+
Raises
|
|
567
|
+
------
|
|
568
|
+
TypeError
|
|
569
|
+
If *frame* is not a frame grob or *grob* is not a grob.
|
|
570
|
+
ValueError
|
|
571
|
+
If the row/column specification is invalid.
|
|
572
|
+
"""
|
|
573
|
+
if not isinstance(frame, GTree) or getattr(frame, "_grid_class", None) != "frame":
|
|
574
|
+
raise TypeError("invalid 'frame'")
|
|
575
|
+
if not is_grob(grob):
|
|
576
|
+
raise TypeError("invalid 'grob'")
|
|
577
|
+
|
|
578
|
+
# Normalise col/row ranges
|
|
579
|
+
col_range = False
|
|
580
|
+
row_range = False
|
|
581
|
+
if col is not None and isinstance(col, (list, tuple)) and len(col) > 1:
|
|
582
|
+
col = [min(col), max(col)]
|
|
583
|
+
col_range = True
|
|
584
|
+
if row is not None and isinstance(row, (list, tuple)) and len(row) > 1:
|
|
585
|
+
row = [min(row), max(row)]
|
|
586
|
+
row_range = True
|
|
587
|
+
|
|
588
|
+
# Get current layout dimensions
|
|
589
|
+
frame = copy.deepcopy(frame)
|
|
590
|
+
frame_vp = getattr(frame, "framevp", None)
|
|
591
|
+
if frame_vp is None:
|
|
592
|
+
frame_vp = Viewport()
|
|
593
|
+
lay = getattr(frame_vp, "layout", None)
|
|
594
|
+
if lay is None:
|
|
595
|
+
ncol = 0
|
|
596
|
+
nrow = 0
|
|
597
|
+
else:
|
|
598
|
+
ncol = layout_ncol(lay)
|
|
599
|
+
nrow = layout_nrow(lay)
|
|
600
|
+
|
|
601
|
+
# (i) Validate location specifications
|
|
602
|
+
ncs = _num_col_specs(side, col, col_before, col_after)
|
|
603
|
+
if ncs == 0:
|
|
604
|
+
if ncol > 0:
|
|
605
|
+
col = [1, ncol]
|
|
606
|
+
col_range = True
|
|
607
|
+
else:
|
|
608
|
+
col = 1
|
|
609
|
+
ncs = 1
|
|
610
|
+
if ncs != 1:
|
|
611
|
+
raise ValueError(
|
|
612
|
+
"cannot specify more than one of 'side=[left/right]', "
|
|
613
|
+
"'col', 'col_before', or 'col_after'"
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
nrs = _num_row_specs(side, row, row_before, row_after)
|
|
617
|
+
if nrs == 0:
|
|
618
|
+
if nrow > 0:
|
|
619
|
+
row = [1, nrow]
|
|
620
|
+
row_range = True
|
|
621
|
+
else:
|
|
622
|
+
row = 1
|
|
623
|
+
nrs = 1
|
|
624
|
+
if nrs != 1:
|
|
625
|
+
raise ValueError(
|
|
626
|
+
"must specify exactly one of 'side=[top/bottom]', "
|
|
627
|
+
"'row', 'row_before', or 'row_after'"
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
# (ii) Determine location
|
|
631
|
+
is_new_col = _new_col(side, col, col_before, col_after, ncol)
|
|
632
|
+
col = _col_spec(side, col, col_before, col_after, ncol)
|
|
633
|
+
is_new_row = _new_row(side, row, row_before, row_after, nrow)
|
|
634
|
+
row = _row_spec(side, row, row_before, row_after, nrow)
|
|
635
|
+
|
|
636
|
+
# Build cell grob
|
|
637
|
+
cgrob: Optional[GTree] = None
|
|
638
|
+
if grob is not None:
|
|
639
|
+
cgrob = _cell_grob(col, row, border, grob, dynamic,
|
|
640
|
+
_cell_viewport(col, row, border))
|
|
641
|
+
|
|
642
|
+
# (iii) Default width/height from the grob
|
|
643
|
+
# R frames.R:399-414: if grob is present, use grobwidth/grobheight;
|
|
644
|
+
# if grob is NULL, use unit(1, "null")
|
|
645
|
+
if width is None:
|
|
646
|
+
if grob is None:
|
|
647
|
+
width = Unit(1, "null")
|
|
648
|
+
else:
|
|
649
|
+
# R: unit(1, "grobwidth", cgrob) or gPath variant when dynamic
|
|
650
|
+
width = Unit(1, "grobwidth", data=cgrob)
|
|
651
|
+
if height is None:
|
|
652
|
+
if grob is None:
|
|
653
|
+
height = Unit(1, "null")
|
|
654
|
+
else:
|
|
655
|
+
height = Unit(1, "grobheight", data=cgrob)
|
|
656
|
+
|
|
657
|
+
# Include border in width/height
|
|
658
|
+
if border is not None:
|
|
659
|
+
width = border[1] + width + border[3]
|
|
660
|
+
height = border[0] + height + border[2]
|
|
661
|
+
|
|
662
|
+
# (iv) Update layout
|
|
663
|
+
if is_new_col:
|
|
664
|
+
ncol += 1
|
|
665
|
+
if is_new_row:
|
|
666
|
+
nrow += 1
|
|
667
|
+
|
|
668
|
+
if lay is None:
|
|
669
|
+
widths = width
|
|
670
|
+
heights = height
|
|
671
|
+
else:
|
|
672
|
+
if col_range:
|
|
673
|
+
widths = layout_widths(lay)
|
|
674
|
+
else:
|
|
675
|
+
widths = _mod_dims(width, layout_widths(lay), col, is_new_col,
|
|
676
|
+
ncol, force_width)
|
|
677
|
+
if row_range:
|
|
678
|
+
heights = layout_heights(lay)
|
|
679
|
+
else:
|
|
680
|
+
heights = _mod_dims(height, layout_heights(lay), row, is_new_row,
|
|
681
|
+
nrow, force_height)
|
|
682
|
+
|
|
683
|
+
frame_vp._layout = GridLayout(nrow=nrow, ncol=ncol,
|
|
684
|
+
widths=widths, heights=heights)
|
|
685
|
+
|
|
686
|
+
# Shift existing children if new row/col was added
|
|
687
|
+
if is_new_col or is_new_row:
|
|
688
|
+
for child_name in list(frame._children_order):
|
|
689
|
+
child = frame._children[child_name]
|
|
690
|
+
if is_new_col:
|
|
691
|
+
new_c = _update_col(getattr(child, "col", 1), col)
|
|
692
|
+
child.col = new_c
|
|
693
|
+
child.cellvp = _cell_viewport(new_c, getattr(child, "row", 1),
|
|
694
|
+
getattr(child, "border", None))
|
|
695
|
+
if is_new_row:
|
|
696
|
+
new_r = _update_row(getattr(child, "row", 1), row)
|
|
697
|
+
child.row = new_r
|
|
698
|
+
child.cellvp = _cell_viewport(getattr(child, "col", 1), new_r,
|
|
699
|
+
getattr(child, "border", None))
|
|
700
|
+
|
|
701
|
+
# Add the new grob
|
|
702
|
+
if cgrob is not None:
|
|
703
|
+
frame.add_child(cgrob)
|
|
704
|
+
|
|
705
|
+
frame.framevp = frame_vp
|
|
706
|
+
return frame
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def grid_pack(
|
|
710
|
+
frame: GTree,
|
|
711
|
+
grob: Grob,
|
|
712
|
+
redraw: bool = True,
|
|
713
|
+
side: Optional[str] = None,
|
|
714
|
+
row: Optional[Any] = None,
|
|
715
|
+
row_before: Optional[int] = None,
|
|
716
|
+
row_after: Optional[int] = None,
|
|
717
|
+
col: Optional[Any] = None,
|
|
718
|
+
col_before: Optional[int] = None,
|
|
719
|
+
col_after: Optional[int] = None,
|
|
720
|
+
width: Optional[Unit] = None,
|
|
721
|
+
height: Optional[Unit] = None,
|
|
722
|
+
force_width: bool = False,
|
|
723
|
+
force_height: bool = False,
|
|
724
|
+
border: Optional[Sequence[Unit]] = None,
|
|
725
|
+
dynamic: bool = False,
|
|
726
|
+
) -> GTree:
|
|
727
|
+
"""Pack a grob into a frame and optionally redraw.
|
|
728
|
+
|
|
729
|
+
This is the display-list-aware wrapper around :func:`pack_grob`.
|
|
730
|
+
|
|
731
|
+
Parameters
|
|
732
|
+
----------
|
|
733
|
+
frame : GTree
|
|
734
|
+
The frame grob.
|
|
735
|
+
grob : Grob
|
|
736
|
+
The child grob to pack.
|
|
737
|
+
redraw : bool
|
|
738
|
+
If ``True``, redraw after packing.
|
|
739
|
+
side : str or None
|
|
740
|
+
Side specification.
|
|
741
|
+
row, row_before, row_after : int or None
|
|
742
|
+
Row specification.
|
|
743
|
+
col, col_before, col_after : int or None
|
|
744
|
+
Column specification.
|
|
745
|
+
width : Unit or None
|
|
746
|
+
Explicit width.
|
|
747
|
+
height : Unit or None
|
|
748
|
+
Explicit height.
|
|
749
|
+
force_width : bool
|
|
750
|
+
Override existing column width.
|
|
751
|
+
force_height : bool
|
|
752
|
+
Override existing row height.
|
|
753
|
+
border : list of Unit or None
|
|
754
|
+
Border insets.
|
|
755
|
+
dynamic : bool
|
|
756
|
+
Use dynamic sizing.
|
|
757
|
+
|
|
758
|
+
Returns
|
|
759
|
+
-------
|
|
760
|
+
GTree
|
|
761
|
+
The updated frame.
|
|
762
|
+
"""
|
|
763
|
+
result = pack_grob(
|
|
764
|
+
frame, grob,
|
|
765
|
+
side=side,
|
|
766
|
+
row=row, row_before=row_before, row_after=row_after,
|
|
767
|
+
col=col, col_before=col_before, col_after=col_after,
|
|
768
|
+
width=width, height=height,
|
|
769
|
+
force_width=force_width, force_height=force_height,
|
|
770
|
+
border=border, dynamic=dynamic,
|
|
771
|
+
)
|
|
772
|
+
if redraw:
|
|
773
|
+
grid_draw(result)
|
|
774
|
+
return result
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
# ---------------------------------------------------------------------------
|
|
778
|
+
# place_grob / grid_place
|
|
779
|
+
# ---------------------------------------------------------------------------
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def place_grob(
|
|
783
|
+
frame: GTree,
|
|
784
|
+
grob: Grob,
|
|
785
|
+
row: Optional[Any] = None,
|
|
786
|
+
col: Optional[Any] = None,
|
|
787
|
+
) -> GTree:
|
|
788
|
+
"""Place a grob into an existing cell of a frame.
|
|
789
|
+
|
|
790
|
+
Unlike :func:`pack_grob`, this does **not** create new rows or columns;
|
|
791
|
+
it only places the grob into an already-existing cell.
|
|
792
|
+
|
|
793
|
+
Parameters
|
|
794
|
+
----------
|
|
795
|
+
frame : GTree
|
|
796
|
+
The frame grob (must have ``_grid_class="frame"``).
|
|
797
|
+
grob : Grob
|
|
798
|
+
The child grob.
|
|
799
|
+
row : int, list of int, or None
|
|
800
|
+
Row position (defaults to full range).
|
|
801
|
+
col : int, list of int, or None
|
|
802
|
+
Column position (defaults to full range).
|
|
803
|
+
|
|
804
|
+
Returns
|
|
805
|
+
-------
|
|
806
|
+
GTree
|
|
807
|
+
A new frame with the grob placed.
|
|
808
|
+
|
|
809
|
+
Raises
|
|
810
|
+
------
|
|
811
|
+
TypeError
|
|
812
|
+
If *frame* is not a frame grob or *grob* is not a grob.
|
|
813
|
+
ValueError
|
|
814
|
+
If *row*/*col* are out of range.
|
|
815
|
+
"""
|
|
816
|
+
if not isinstance(frame, GTree) or getattr(frame, "_grid_class", None) != "frame":
|
|
817
|
+
raise TypeError("invalid 'frame'")
|
|
818
|
+
if not is_grob(grob):
|
|
819
|
+
raise TypeError("invalid 'grob'")
|
|
820
|
+
|
|
821
|
+
dim = _frame_dim(frame)
|
|
822
|
+
if row is None:
|
|
823
|
+
row = [1, dim[0]]
|
|
824
|
+
if col is None:
|
|
825
|
+
col = [1, dim[1]]
|
|
826
|
+
|
|
827
|
+
# Validate
|
|
828
|
+
row_vals = row if isinstance(row, (list, tuple)) else [row]
|
|
829
|
+
col_vals = col if isinstance(col, (list, tuple)) else [col]
|
|
830
|
+
if min(row_vals) < 1 or max(row_vals) > dim[0]:
|
|
831
|
+
raise ValueError("invalid 'row' (no such row in frame layout)")
|
|
832
|
+
if min(col_vals) < 1 or max(col_vals) > dim[1]:
|
|
833
|
+
raise ValueError("invalid 'col' (no such col in frame layout)")
|
|
834
|
+
|
|
835
|
+
cgrob = _cell_grob(col, row, None, grob, False,
|
|
836
|
+
_cell_viewport(col, row, None))
|
|
837
|
+
return add_grob(frame, cgrob)
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
def grid_place(
|
|
841
|
+
frame: GTree,
|
|
842
|
+
grob: Grob,
|
|
843
|
+
row: int = 1,
|
|
844
|
+
col: int = 1,
|
|
845
|
+
redraw: bool = True,
|
|
846
|
+
) -> GTree:
|
|
847
|
+
"""Place a grob into an existing cell of a frame and optionally redraw.
|
|
848
|
+
|
|
849
|
+
Parameters
|
|
850
|
+
----------
|
|
851
|
+
frame : GTree
|
|
852
|
+
The frame grob.
|
|
853
|
+
grob : Grob
|
|
854
|
+
The child grob.
|
|
855
|
+
row : int
|
|
856
|
+
Row position.
|
|
857
|
+
col : int
|
|
858
|
+
Column position.
|
|
859
|
+
redraw : bool
|
|
860
|
+
If ``True``, redraw after placing.
|
|
861
|
+
|
|
862
|
+
Returns
|
|
863
|
+
-------
|
|
864
|
+
GTree
|
|
865
|
+
The updated frame.
|
|
866
|
+
"""
|
|
867
|
+
result = place_grob(frame, grob, row=row, col=col)
|
|
868
|
+
if redraw:
|
|
869
|
+
grid_draw(result)
|
|
870
|
+
return result
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
# =========================================================================
|
|
874
|
+
# Components (components.R)
|
|
875
|
+
# =========================================================================
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
# ---------------------------------------------------------------------------
|
|
879
|
+
# X-axis internals
|
|
880
|
+
# ---------------------------------------------------------------------------
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def _make_xaxis_major(at: Sequence[float], main: bool) -> Grob:
|
|
884
|
+
"""Create the major line for an x-axis."""
|
|
885
|
+
y = [0, 0] if main else [1, 1]
|
|
886
|
+
return lines_grob(
|
|
887
|
+
x=Unit([min(at), max(at)], "native"),
|
|
888
|
+
y=Unit(y, "npc"),
|
|
889
|
+
name="major",
|
|
890
|
+
)
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def _make_xaxis_ticks(at: Sequence[float], main: bool) -> Grob:
|
|
894
|
+
"""Create tick marks for an x-axis."""
|
|
895
|
+
if main:
|
|
896
|
+
tick_y0 = Unit(0, "npc")
|
|
897
|
+
tick_y1 = Unit(-0.5, "lines")
|
|
898
|
+
else:
|
|
899
|
+
tick_y0 = Unit(1, "npc")
|
|
900
|
+
tick_y1 = Unit(1, "npc") + Unit(0.5, "lines")
|
|
901
|
+
return segments_grob(
|
|
902
|
+
x0=Unit(list(at), "native"),
|
|
903
|
+
y0=tick_y0,
|
|
904
|
+
x1=Unit(list(at), "native"),
|
|
905
|
+
y1=tick_y1,
|
|
906
|
+
name="ticks",
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _make_xaxis_labels(
|
|
911
|
+
at: Sequence[float],
|
|
912
|
+
label: Any,
|
|
913
|
+
main: bool,
|
|
914
|
+
) -> Grob:
|
|
915
|
+
"""Create tick labels for an x-axis."""
|
|
916
|
+
label_y = Unit(-1.5, "lines") if main else Unit(1, "npc") + Unit(1.5, "lines")
|
|
917
|
+
labels = [str(a) for a in at] if isinstance(label, bool) else list(label)
|
|
918
|
+
return text_grob(
|
|
919
|
+
label=labels,
|
|
920
|
+
x=Unit(list(at), "native"),
|
|
921
|
+
y=label_y,
|
|
922
|
+
just="centre",
|
|
923
|
+
rot=0,
|
|
924
|
+
name="labels",
|
|
925
|
+
)
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def _update_xlabels(x: GTree) -> GTree:
|
|
929
|
+
"""Add or remove x-axis labels depending on label specification."""
|
|
930
|
+
lab = getattr(x, "label", True)
|
|
931
|
+
if isinstance(lab, bool) and not lab:
|
|
932
|
+
try:
|
|
933
|
+
return remove_grob(x, "labels")
|
|
934
|
+
except KeyError:
|
|
935
|
+
return x
|
|
936
|
+
return add_grob(x, _make_xaxis_labels(x.at, x.label, x.main))
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
# ---------------------------------------------------------------------------
|
|
940
|
+
# Y-axis internals
|
|
941
|
+
# ---------------------------------------------------------------------------
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _make_yaxis_major(at: Sequence[float], main: bool) -> Grob:
|
|
945
|
+
"""Create the major line for a y-axis."""
|
|
946
|
+
x = [0, 0] if main else [1, 1]
|
|
947
|
+
return lines_grob(
|
|
948
|
+
x=Unit(x, "npc"),
|
|
949
|
+
y=Unit([min(at), max(at)], "native"),
|
|
950
|
+
name="major",
|
|
951
|
+
)
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _make_yaxis_ticks(at: Sequence[float], main: bool) -> Grob:
|
|
955
|
+
"""Create tick marks for a y-axis."""
|
|
956
|
+
if main:
|
|
957
|
+
tick_x0 = Unit(0, "npc")
|
|
958
|
+
tick_x1 = Unit(-0.5, "lines")
|
|
959
|
+
else:
|
|
960
|
+
tick_x0 = Unit(1, "npc")
|
|
961
|
+
tick_x1 = Unit(1, "npc") + Unit(0.5, "lines")
|
|
962
|
+
return segments_grob(
|
|
963
|
+
x0=tick_x0,
|
|
964
|
+
y0=Unit(list(at), "native"),
|
|
965
|
+
x1=tick_x1,
|
|
966
|
+
y1=Unit(list(at), "native"),
|
|
967
|
+
name="ticks",
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
def _make_yaxis_labels(
|
|
972
|
+
at: Sequence[float],
|
|
973
|
+
label: Any,
|
|
974
|
+
main: bool,
|
|
975
|
+
) -> Grob:
|
|
976
|
+
"""Create tick labels for a y-axis."""
|
|
977
|
+
if main:
|
|
978
|
+
hjust = "right"
|
|
979
|
+
label_x = Unit(-1, "lines")
|
|
980
|
+
else:
|
|
981
|
+
hjust = "left"
|
|
982
|
+
label_x = Unit(1, "npc") + Unit(1, "lines")
|
|
983
|
+
labels = [str(a) for a in at] if isinstance(label, bool) else list(label)
|
|
984
|
+
return text_grob(
|
|
985
|
+
label=labels,
|
|
986
|
+
x=label_x,
|
|
987
|
+
y=Unit(list(at), "native"),
|
|
988
|
+
just=[hjust, "centre"],
|
|
989
|
+
rot=0,
|
|
990
|
+
name="labels",
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def _update_ylabels(x: GTree) -> GTree:
|
|
995
|
+
"""Add or remove y-axis labels depending on label specification."""
|
|
996
|
+
lab = getattr(x, "label", True)
|
|
997
|
+
if isinstance(lab, bool) and not lab:
|
|
998
|
+
try:
|
|
999
|
+
return remove_grob(x, "labels")
|
|
1000
|
+
except KeyError:
|
|
1001
|
+
return x
|
|
1002
|
+
return add_grob(x, _make_yaxis_labels(x.at, x.label, x.main))
|
|
1003
|
+
|
|
1004
|
+
|
|
1005
|
+
class _XAxisGTree(GTree):
|
|
1006
|
+
"""GTree subclass for x-axis with on-the-fly tick generation."""
|
|
1007
|
+
|
|
1008
|
+
def __init__(self, at, label, main, edits, **kwargs):
|
|
1009
|
+
super().__init__(_grid_class="xaxis", at=at, label=label,
|
|
1010
|
+
main=main, edits=edits, **kwargs)
|
|
1011
|
+
|
|
1012
|
+
def make_content(self):
|
|
1013
|
+
at = getattr(self, "at", None)
|
|
1014
|
+
if at is None:
|
|
1015
|
+
from ._viewport import current_viewport
|
|
1016
|
+
vp = current_viewport()
|
|
1017
|
+
xscale = getattr(vp, "_xscale", None) or getattr(vp, "xscale", [0, 1])
|
|
1018
|
+
at = grid_pretty(xscale)
|
|
1019
|
+
self.at = at
|
|
1020
|
+
main = getattr(self, "main", True)
|
|
1021
|
+
label = getattr(self, "label", True)
|
|
1022
|
+
self = add_grob(self, _make_xaxis_major(at, main))
|
|
1023
|
+
self = add_grob(self, _make_xaxis_ticks(at, main))
|
|
1024
|
+
self = _update_xlabels(self)
|
|
1025
|
+
edits = getattr(self, "edits", None)
|
|
1026
|
+
if edits is not None:
|
|
1027
|
+
self = apply_edits(self, edits)
|
|
1028
|
+
return self
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
class _YAxisGTree(GTree):
|
|
1032
|
+
"""GTree subclass for y-axis with on-the-fly tick generation."""
|
|
1033
|
+
|
|
1034
|
+
def __init__(self, at, label, main, edits, **kwargs):
|
|
1035
|
+
super().__init__(_grid_class="yaxis", at=at, label=label,
|
|
1036
|
+
main=main, edits=edits, **kwargs)
|
|
1037
|
+
|
|
1038
|
+
def make_content(self):
|
|
1039
|
+
at = getattr(self, "at", None)
|
|
1040
|
+
if at is None:
|
|
1041
|
+
from ._viewport import current_viewport
|
|
1042
|
+
vp = current_viewport()
|
|
1043
|
+
yscale = getattr(vp, "_yscale", None) or getattr(vp, "yscale", [0, 1])
|
|
1044
|
+
at = grid_pretty(yscale)
|
|
1045
|
+
self.at = at
|
|
1046
|
+
main = getattr(self, "main", True)
|
|
1047
|
+
label = getattr(self, "label", True)
|
|
1048
|
+
self = add_grob(self, _make_yaxis_major(at, main))
|
|
1049
|
+
self = add_grob(self, _make_yaxis_ticks(at, main))
|
|
1050
|
+
self = _update_ylabels(self)
|
|
1051
|
+
edits = getattr(self, "edits", None)
|
|
1052
|
+
if edits is not None:
|
|
1053
|
+
self = apply_edits(self, edits)
|
|
1054
|
+
return self
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
# ---------------------------------------------------------------------------
|
|
1058
|
+
# Public axis constructors
|
|
1059
|
+
# ---------------------------------------------------------------------------
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
def xaxis_grob(
|
|
1063
|
+
at: Optional[Sequence[float]] = None,
|
|
1064
|
+
label: Any = True,
|
|
1065
|
+
main: bool = True,
|
|
1066
|
+
edits: Optional[Any] = None,
|
|
1067
|
+
name: Optional[str] = None,
|
|
1068
|
+
gp: Optional[Gpar] = None,
|
|
1069
|
+
vp: Optional[Any] = None,
|
|
1070
|
+
) -> GTree:
|
|
1071
|
+
"""Create an x-axis grob.
|
|
1072
|
+
|
|
1073
|
+
Parameters
|
|
1074
|
+
----------
|
|
1075
|
+
at : sequence of float or None
|
|
1076
|
+
Tick positions in native coordinates. If ``None``, tick positions
|
|
1077
|
+
are calculated on-the-fly when the grob is drawn.
|
|
1078
|
+
label : bool or sequence of str
|
|
1079
|
+
If ``True`` (default), labels are derived from *at*. If ``False``,
|
|
1080
|
+
no labels are drawn. A sequence provides explicit labels.
|
|
1081
|
+
main : bool
|
|
1082
|
+
If ``True`` (default), the axis is drawn on the bottom.
|
|
1083
|
+
edits : GEdit, GEditList, or None
|
|
1084
|
+
Edits to apply to child grobs.
|
|
1085
|
+
name : str or None
|
|
1086
|
+
Grob name.
|
|
1087
|
+
gp : Gpar or None
|
|
1088
|
+
Graphical parameters.
|
|
1089
|
+
vp : object or None
|
|
1090
|
+
Viewport.
|
|
1091
|
+
|
|
1092
|
+
Returns
|
|
1093
|
+
-------
|
|
1094
|
+
GTree
|
|
1095
|
+
A GTree with ``_grid_class="xaxis"``.
|
|
1096
|
+
"""
|
|
1097
|
+
return grid_xaxis(at=at, label=label, main=main, edits=edits,
|
|
1098
|
+
name=name, gp=gp, draw=False, vp=vp)
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def grid_xaxis(
|
|
1102
|
+
at: Optional[Sequence[float]] = None,
|
|
1103
|
+
label: Any = True,
|
|
1104
|
+
main: bool = True,
|
|
1105
|
+
edits: Optional[Any] = None,
|
|
1106
|
+
name: Optional[str] = None,
|
|
1107
|
+
gp: Optional[Gpar] = None,
|
|
1108
|
+
draw: bool = True,
|
|
1109
|
+
vp: Optional[Any] = None,
|
|
1110
|
+
) -> GTree:
|
|
1111
|
+
"""Create and optionally draw an x-axis.
|
|
1112
|
+
|
|
1113
|
+
Parameters
|
|
1114
|
+
----------
|
|
1115
|
+
at : sequence of float or None
|
|
1116
|
+
Tick positions in native coordinates.
|
|
1117
|
+
label : bool or sequence of str
|
|
1118
|
+
Label specification.
|
|
1119
|
+
main : bool
|
|
1120
|
+
If ``True``, the axis is on the bottom.
|
|
1121
|
+
edits : GEdit, GEditList, or None
|
|
1122
|
+
Edits to apply to children.
|
|
1123
|
+
name : str or None
|
|
1124
|
+
Grob name.
|
|
1125
|
+
gp : Gpar or None
|
|
1126
|
+
Graphical parameters.
|
|
1127
|
+
draw : bool
|
|
1128
|
+
If ``True``, draw immediately.
|
|
1129
|
+
vp : object or None
|
|
1130
|
+
Viewport.
|
|
1131
|
+
|
|
1132
|
+
Returns
|
|
1133
|
+
-------
|
|
1134
|
+
GTree
|
|
1135
|
+
A GTree with ``_grid_class="xaxis"``.
|
|
1136
|
+
"""
|
|
1137
|
+
if at is None:
|
|
1138
|
+
major = None
|
|
1139
|
+
ticks = None
|
|
1140
|
+
labels = None
|
|
1141
|
+
else:
|
|
1142
|
+
at = [float(a) for a in at]
|
|
1143
|
+
major = _make_xaxis_major(at, main)
|
|
1144
|
+
ticks = _make_xaxis_ticks(at, main)
|
|
1145
|
+
if isinstance(label, bool) and not label:
|
|
1146
|
+
labels = None
|
|
1147
|
+
else:
|
|
1148
|
+
labels = _make_xaxis_labels(at, label, main)
|
|
1149
|
+
|
|
1150
|
+
children_list = [g for g in (major, ticks, labels) if g is not None]
|
|
1151
|
+
xg = _XAxisGTree(
|
|
1152
|
+
at=at, label=label, main=main, edits=edits,
|
|
1153
|
+
children=GList(*children_list) if children_list else None,
|
|
1154
|
+
name=name, gp=gp, vp=vp,
|
|
1155
|
+
)
|
|
1156
|
+
if edits is not None:
|
|
1157
|
+
xg = apply_edits(xg, edits)
|
|
1158
|
+
if draw:
|
|
1159
|
+
grid_draw(xg)
|
|
1160
|
+
return xg
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
def yaxis_grob(
|
|
1164
|
+
at: Optional[Sequence[float]] = None,
|
|
1165
|
+
label: Any = True,
|
|
1166
|
+
main: bool = True,
|
|
1167
|
+
edits: Optional[Any] = None,
|
|
1168
|
+
name: Optional[str] = None,
|
|
1169
|
+
gp: Optional[Gpar] = None,
|
|
1170
|
+
vp: Optional[Any] = None,
|
|
1171
|
+
) -> GTree:
|
|
1172
|
+
"""Create a y-axis grob.
|
|
1173
|
+
|
|
1174
|
+
Parameters
|
|
1175
|
+
----------
|
|
1176
|
+
at : sequence of float or None
|
|
1177
|
+
Tick positions in native coordinates. If ``None``, tick positions
|
|
1178
|
+
are calculated on-the-fly when the grob is drawn.
|
|
1179
|
+
label : bool or sequence of str
|
|
1180
|
+
Label specification.
|
|
1181
|
+
main : bool
|
|
1182
|
+
If ``True`` (default), the axis is drawn on the left.
|
|
1183
|
+
edits : GEdit, GEditList, or None
|
|
1184
|
+
Edits to apply to children.
|
|
1185
|
+
name : str or None
|
|
1186
|
+
Grob name.
|
|
1187
|
+
gp : Gpar or None
|
|
1188
|
+
Graphical parameters.
|
|
1189
|
+
vp : object or None
|
|
1190
|
+
Viewport.
|
|
1191
|
+
|
|
1192
|
+
Returns
|
|
1193
|
+
-------
|
|
1194
|
+
GTree
|
|
1195
|
+
A GTree with ``_grid_class="yaxis"``.
|
|
1196
|
+
"""
|
|
1197
|
+
return grid_yaxis(at=at, label=label, main=main, edits=edits,
|
|
1198
|
+
name=name, gp=gp, draw=False, vp=vp)
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def grid_yaxis(
|
|
1202
|
+
at: Optional[Sequence[float]] = None,
|
|
1203
|
+
label: Any = True,
|
|
1204
|
+
main: bool = True,
|
|
1205
|
+
edits: Optional[Any] = None,
|
|
1206
|
+
name: Optional[str] = None,
|
|
1207
|
+
gp: Optional[Gpar] = None,
|
|
1208
|
+
draw: bool = True,
|
|
1209
|
+
vp: Optional[Any] = None,
|
|
1210
|
+
) -> GTree:
|
|
1211
|
+
"""Create and optionally draw a y-axis.
|
|
1212
|
+
|
|
1213
|
+
Parameters
|
|
1214
|
+
----------
|
|
1215
|
+
at : sequence of float or None
|
|
1216
|
+
Tick positions in native coordinates.
|
|
1217
|
+
label : bool or sequence of str
|
|
1218
|
+
Label specification.
|
|
1219
|
+
main : bool
|
|
1220
|
+
If ``True``, the axis is on the left.
|
|
1221
|
+
edits : GEdit, GEditList, or None
|
|
1222
|
+
Edits to apply to children.
|
|
1223
|
+
name : str or None
|
|
1224
|
+
Grob name.
|
|
1225
|
+
gp : Gpar or None
|
|
1226
|
+
Graphical parameters.
|
|
1227
|
+
draw : bool
|
|
1228
|
+
If ``True``, draw immediately.
|
|
1229
|
+
vp : object or None
|
|
1230
|
+
Viewport.
|
|
1231
|
+
|
|
1232
|
+
Returns
|
|
1233
|
+
-------
|
|
1234
|
+
GTree
|
|
1235
|
+
A GTree with ``_grid_class="yaxis"``.
|
|
1236
|
+
"""
|
|
1237
|
+
if at is None:
|
|
1238
|
+
major = None
|
|
1239
|
+
ticks = None
|
|
1240
|
+
labels = None
|
|
1241
|
+
else:
|
|
1242
|
+
at = [float(a) for a in at]
|
|
1243
|
+
major = _make_yaxis_major(at, main)
|
|
1244
|
+
ticks = _make_yaxis_ticks(at, main)
|
|
1245
|
+
if isinstance(label, bool) and not label:
|
|
1246
|
+
labels = None
|
|
1247
|
+
else:
|
|
1248
|
+
labels = _make_yaxis_labels(at, label, main)
|
|
1249
|
+
|
|
1250
|
+
children_list = [g for g in (major, ticks, labels) if g is not None]
|
|
1251
|
+
yg = _YAxisGTree(
|
|
1252
|
+
at=at, label=label, main=main, edits=edits,
|
|
1253
|
+
children=GList(*children_list) if children_list else None,
|
|
1254
|
+
name=name, gp=gp, vp=vp,
|
|
1255
|
+
)
|
|
1256
|
+
if edits is not None:
|
|
1257
|
+
yg = apply_edits(yg, edits)
|
|
1258
|
+
if draw:
|
|
1259
|
+
grid_draw(yg)
|
|
1260
|
+
return yg
|
|
1261
|
+
|
|
1262
|
+
|
|
1263
|
+
# ---------------------------------------------------------------------------
|
|
1264
|
+
# Legend
|
|
1265
|
+
# ---------------------------------------------------------------------------
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def legend_grob(
|
|
1269
|
+
labels: Sequence[str],
|
|
1270
|
+
nrow: Optional[int] = None,
|
|
1271
|
+
ncol: Optional[int] = None,
|
|
1272
|
+
byrow: bool = False,
|
|
1273
|
+
do_lines: bool = True,
|
|
1274
|
+
do_points: bool = True,
|
|
1275
|
+
lines_first: bool = True,
|
|
1276
|
+
pch: Optional[Sequence[int]] = None,
|
|
1277
|
+
hgap: Any = None,
|
|
1278
|
+
vgap: Any = None,
|
|
1279
|
+
default_units: str = "lines",
|
|
1280
|
+
gp: Optional[Gpar] = None,
|
|
1281
|
+
vp: Optional[Any] = None,
|
|
1282
|
+
) -> GTree:
|
|
1283
|
+
"""Create a legend grob.
|
|
1284
|
+
|
|
1285
|
+
This is the Python port of R's ``legendGrob``. It builds a frame
|
|
1286
|
+
grob containing symbol and text entries arranged in a grid.
|
|
1287
|
+
|
|
1288
|
+
Parameters
|
|
1289
|
+
----------
|
|
1290
|
+
labels : sequence of str
|
|
1291
|
+
Legend entry labels.
|
|
1292
|
+
nrow : int or None
|
|
1293
|
+
Number of rows. If ``None`` and *ncol* is also ``None``, defaults
|
|
1294
|
+
to ``len(labels)`` with ``ncol=1``.
|
|
1295
|
+
ncol : int or None
|
|
1296
|
+
Number of columns.
|
|
1297
|
+
byrow : bool
|
|
1298
|
+
If ``True``, fill by row; otherwise by column.
|
|
1299
|
+
do_lines : bool
|
|
1300
|
+
If ``True``, draw lines in the symbol column.
|
|
1301
|
+
do_points : bool
|
|
1302
|
+
If ``True``, draw points in the symbol column.
|
|
1303
|
+
lines_first : bool
|
|
1304
|
+
If ``True``, draw lines before points.
|
|
1305
|
+
pch : sequence of int or None
|
|
1306
|
+
Point characters.
|
|
1307
|
+
hgap : Unit or numeric or None
|
|
1308
|
+
Horizontal gap between columns. Defaults to ``Unit(1, "lines")``.
|
|
1309
|
+
vgap : Unit or numeric or None
|
|
1310
|
+
Vertical gap between rows. Defaults to ``Unit(1, "lines")``.
|
|
1311
|
+
default_units : str
|
|
1312
|
+
Unit type for bare numerics in *hgap* / *vgap*.
|
|
1313
|
+
gp : Gpar or None
|
|
1314
|
+
Graphical parameters (may contain ``col``, ``lty``, ``lwd``, ``fill``).
|
|
1315
|
+
vp : object or None
|
|
1316
|
+
Viewport.
|
|
1317
|
+
|
|
1318
|
+
Returns
|
|
1319
|
+
-------
|
|
1320
|
+
GTree
|
|
1321
|
+
A frame grob containing the legend entries.
|
|
1322
|
+
"""
|
|
1323
|
+
labels = [str(lb) for lb in labels]
|
|
1324
|
+
nkeys = len(labels)
|
|
1325
|
+
if nkeys == 0:
|
|
1326
|
+
return null_grob(vp=vp) # type: ignore[return-value]
|
|
1327
|
+
|
|
1328
|
+
# Defaults
|
|
1329
|
+
if hgap is None:
|
|
1330
|
+
hgap = Unit(1, "lines")
|
|
1331
|
+
elif not is_unit(hgap):
|
|
1332
|
+
hgap = Unit(hgap, default_units)
|
|
1333
|
+
if vgap is None:
|
|
1334
|
+
vgap = Unit(1, "lines")
|
|
1335
|
+
elif not is_unit(vgap):
|
|
1336
|
+
vgap = Unit(vgap, default_units)
|
|
1337
|
+
|
|
1338
|
+
# nrow / ncol defaults
|
|
1339
|
+
if nrow is not None and nrow < 1:
|
|
1340
|
+
raise ValueError("'nrow' must be >= 1")
|
|
1341
|
+
if ncol is not None and ncol < 1:
|
|
1342
|
+
raise ValueError("'ncol' must be >= 1")
|
|
1343
|
+
|
|
1344
|
+
if nrow is None and ncol is None:
|
|
1345
|
+
ncol = 1
|
|
1346
|
+
nrow = nkeys
|
|
1347
|
+
elif nrow is None:
|
|
1348
|
+
nrow = math.ceil(nkeys / ncol) # type: ignore[arg-type]
|
|
1349
|
+
elif ncol is None:
|
|
1350
|
+
ncol = math.ceil(nkeys / nrow)
|
|
1351
|
+
if nrow * ncol < nkeys: # type: ignore[operator]
|
|
1352
|
+
raise ValueError("nrow * ncol < number of legend labels")
|
|
1353
|
+
|
|
1354
|
+
# Recycle pch
|
|
1355
|
+
has_pch = pch is not None and len(pch) > 0
|
|
1356
|
+
if has_pch:
|
|
1357
|
+
pch_list = list(pch) # type: ignore[arg-type]
|
|
1358
|
+
while len(pch_list) < nkeys:
|
|
1359
|
+
pch_list = pch_list * (nkeys // len(pch_list) + 1)
|
|
1360
|
+
pch_list = pch_list[:nkeys]
|
|
1361
|
+
else:
|
|
1362
|
+
pch_list = []
|
|
1363
|
+
|
|
1364
|
+
# Extract per-key gp components
|
|
1365
|
+
gp_dict: Dict[str, Any] = {}
|
|
1366
|
+
if gp is not None:
|
|
1367
|
+
for attr in ("lty", "lwd", "col", "fill"):
|
|
1368
|
+
val = getattr(gp, attr, None)
|
|
1369
|
+
if val is not None:
|
|
1370
|
+
if isinstance(val, (list, tuple, np.ndarray)):
|
|
1371
|
+
lst = list(val)
|
|
1372
|
+
while len(lst) < nkeys:
|
|
1373
|
+
lst = lst * (nkeys // len(lst) + 1)
|
|
1374
|
+
gp_dict[attr] = lst[:nkeys]
|
|
1375
|
+
else:
|
|
1376
|
+
gp_dict[attr] = [val] * nkeys
|
|
1377
|
+
|
|
1378
|
+
u0 = Unit(0, "npc")
|
|
1379
|
+
u1 = Unit(1, "char")
|
|
1380
|
+
|
|
1381
|
+
fg = frame_grob(vp=vp)
|
|
1382
|
+
|
|
1383
|
+
for i in range(nkeys):
|
|
1384
|
+
if byrow:
|
|
1385
|
+
ci = 1 + (i % ncol) # type: ignore[operator]
|
|
1386
|
+
ri = 1 + (i // ncol) # type: ignore[operator]
|
|
1387
|
+
else:
|
|
1388
|
+
ci = 1 + (i // nrow)
|
|
1389
|
+
ri = 1 + (i % nrow)
|
|
1390
|
+
|
|
1391
|
+
# Build per-key gp
|
|
1392
|
+
gpi_kwargs: Dict[str, Any] = {}
|
|
1393
|
+
for attr in ("lty", "lwd", "col", "fill"):
|
|
1394
|
+
if attr in gp_dict:
|
|
1395
|
+
gpi_kwargs[attr] = gp_dict[attr][i]
|
|
1396
|
+
gpi = Gpar(**gpi_kwargs) if gpi_kwargs else (gp if gp is not None else Gpar())
|
|
1397
|
+
|
|
1398
|
+
# Borders
|
|
1399
|
+
vg = vgap if ri != nrow else u0
|
|
1400
|
+
symbol_border = [vg, u0, u0, hgap * 0.5]
|
|
1401
|
+
text_border = [vg, u0, u0, hgap if ci != ncol else u0] # type: ignore[operator]
|
|
1402
|
+
|
|
1403
|
+
# Points/lines grob
|
|
1404
|
+
if has_pch and do_lines:
|
|
1405
|
+
line_g = lines_grob(x=Unit([0, 1], "npc"), y=Unit(0.5, "npc"), gp=gpi)
|
|
1406
|
+
point_g = points_grob(
|
|
1407
|
+
x=Unit(0.5, "npc"), y=Unit(0.5, "npc"),
|
|
1408
|
+
pch=pch_list[i], gp=gpi,
|
|
1409
|
+
)
|
|
1410
|
+
if lines_first:
|
|
1411
|
+
pl_grob: Grob = GTree(children=GList(line_g, point_g))
|
|
1412
|
+
else:
|
|
1413
|
+
pl_grob = GTree(children=GList(point_g, line_g))
|
|
1414
|
+
elif has_pch:
|
|
1415
|
+
pl_grob = points_grob(
|
|
1416
|
+
x=Unit(0.5, "npc"), y=Unit(0.5, "npc"),
|
|
1417
|
+
pch=pch_list[i], gp=gpi,
|
|
1418
|
+
)
|
|
1419
|
+
elif do_lines:
|
|
1420
|
+
pl_grob = lines_grob(x=Unit([0, 1], "npc"), y=Unit(0.5, "npc"), gp=gpi)
|
|
1421
|
+
else:
|
|
1422
|
+
pl_grob = null_grob()
|
|
1423
|
+
|
|
1424
|
+
fg = pack_grob(
|
|
1425
|
+
fg, pl_grob,
|
|
1426
|
+
col=2 * ci - 1, row=ri,
|
|
1427
|
+
border=symbol_border,
|
|
1428
|
+
width=u1, height=u1,
|
|
1429
|
+
force_width=True,
|
|
1430
|
+
)
|
|
1431
|
+
|
|
1432
|
+
# Text grob
|
|
1433
|
+
gpi_text = Gpar(col="black")
|
|
1434
|
+
fg = pack_grob(
|
|
1435
|
+
fg,
|
|
1436
|
+
text_grob(label=labels[i], x=Unit(0, "npc"), y=Unit(0.5, "npc"),
|
|
1437
|
+
just=["left", "centre"], gp=gpi_text),
|
|
1438
|
+
col=2 * ci, row=ri,
|
|
1439
|
+
border=text_border,
|
|
1440
|
+
)
|
|
1441
|
+
|
|
1442
|
+
return fg
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
def grid_legend(
|
|
1446
|
+
labels: Sequence[str],
|
|
1447
|
+
nrow: Optional[int] = None,
|
|
1448
|
+
ncol: Optional[int] = None,
|
|
1449
|
+
byrow: bool = False,
|
|
1450
|
+
do_lines: bool = True,
|
|
1451
|
+
do_points: bool = True,
|
|
1452
|
+
lines_first: bool = True,
|
|
1453
|
+
pch: Optional[Sequence[int]] = None,
|
|
1454
|
+
hgap: Any = None,
|
|
1455
|
+
vgap: Any = None,
|
|
1456
|
+
default_units: str = "lines",
|
|
1457
|
+
gp: Optional[Gpar] = None,
|
|
1458
|
+
vp: Optional[Any] = None,
|
|
1459
|
+
draw: bool = True,
|
|
1460
|
+
) -> GTree:
|
|
1461
|
+
"""Create and optionally draw a legend.
|
|
1462
|
+
|
|
1463
|
+
Parameters
|
|
1464
|
+
----------
|
|
1465
|
+
labels : sequence of str
|
|
1466
|
+
Legend entry labels.
|
|
1467
|
+
nrow : int or None
|
|
1468
|
+
Number of rows.
|
|
1469
|
+
ncol : int or None
|
|
1470
|
+
Number of columns.
|
|
1471
|
+
byrow : bool
|
|
1472
|
+
Fill by row.
|
|
1473
|
+
do_lines : bool
|
|
1474
|
+
Draw lines in symbol column.
|
|
1475
|
+
do_points : bool
|
|
1476
|
+
Draw points in symbol column.
|
|
1477
|
+
lines_first : bool
|
|
1478
|
+
Draw lines before points.
|
|
1479
|
+
pch : sequence of int or None
|
|
1480
|
+
Point characters.
|
|
1481
|
+
hgap : Unit or numeric or None
|
|
1482
|
+
Horizontal gap.
|
|
1483
|
+
vgap : Unit or numeric or None
|
|
1484
|
+
Vertical gap.
|
|
1485
|
+
default_units : str
|
|
1486
|
+
Default unit type.
|
|
1487
|
+
gp : Gpar or None
|
|
1488
|
+
Graphical parameters.
|
|
1489
|
+
vp : object or None
|
|
1490
|
+
Viewport.
|
|
1491
|
+
draw : bool
|
|
1492
|
+
If ``True``, draw immediately.
|
|
1493
|
+
|
|
1494
|
+
Returns
|
|
1495
|
+
-------
|
|
1496
|
+
GTree
|
|
1497
|
+
The legend grob.
|
|
1498
|
+
"""
|
|
1499
|
+
g = legend_grob(
|
|
1500
|
+
labels, nrow=nrow, ncol=ncol, byrow=byrow,
|
|
1501
|
+
do_lines=do_lines, do_points=do_points,
|
|
1502
|
+
lines_first=lines_first, pch=pch,
|
|
1503
|
+
hgap=hgap, vgap=vgap, default_units=default_units,
|
|
1504
|
+
gp=gp, vp=vp,
|
|
1505
|
+
)
|
|
1506
|
+
if draw:
|
|
1507
|
+
grid_draw(g)
|
|
1508
|
+
return g
|
|
1509
|
+
|
|
1510
|
+
|
|
1511
|
+
# =========================================================================
|
|
1512
|
+
# High-level functions (highlevel.R)
|
|
1513
|
+
# =========================================================================
|
|
1514
|
+
|
|
1515
|
+
|
|
1516
|
+
def grid_grill(
|
|
1517
|
+
h: Optional[Any] = None,
|
|
1518
|
+
v: Optional[Any] = None,
|
|
1519
|
+
default_units: str = "npc",
|
|
1520
|
+
gp: Optional[Gpar] = None,
|
|
1521
|
+
vp: Optional[Any] = None,
|
|
1522
|
+
) -> None:
|
|
1523
|
+
"""Draw a grid of horizontal and vertical lines (background grid).
|
|
1524
|
+
|
|
1525
|
+
Parameters
|
|
1526
|
+
----------
|
|
1527
|
+
h : Unit, numeric, or None
|
|
1528
|
+
Horizontal line positions. Defaults to ``[0.25, 0.5, 0.75]`` in
|
|
1529
|
+
NPC coordinates.
|
|
1530
|
+
v : Unit, numeric, or None
|
|
1531
|
+
Vertical line positions. Defaults to ``[0.25, 0.5, 0.75]`` in
|
|
1532
|
+
NPC coordinates.
|
|
1533
|
+
default_units : str
|
|
1534
|
+
Unit type for bare numerics.
|
|
1535
|
+
gp : Gpar or None
|
|
1536
|
+
Graphical parameters. Defaults to ``Gpar(col="grey")``.
|
|
1537
|
+
vp : object or None
|
|
1538
|
+
Viewport.
|
|
1539
|
+
"""
|
|
1540
|
+
if h is None:
|
|
1541
|
+
h = Unit([0.25, 0.50, 0.75], "npc")
|
|
1542
|
+
if not is_unit(h):
|
|
1543
|
+
h = Unit(h, default_units)
|
|
1544
|
+
if v is None:
|
|
1545
|
+
v = Unit([0.25, 0.50, 0.75], "npc")
|
|
1546
|
+
if not is_unit(v):
|
|
1547
|
+
v = Unit(v, default_units)
|
|
1548
|
+
if gp is None:
|
|
1549
|
+
gp = Gpar(col="grey")
|
|
1550
|
+
|
|
1551
|
+
if vp is not None:
|
|
1552
|
+
push_viewport(vp)
|
|
1553
|
+
|
|
1554
|
+
# Vertical lines
|
|
1555
|
+
grid_segments(
|
|
1556
|
+
x0=v, y0=Unit(0, "npc"),
|
|
1557
|
+
x1=v, y1=Unit(1, "npc"),
|
|
1558
|
+
gp=gp,
|
|
1559
|
+
)
|
|
1560
|
+
# Horizontal lines
|
|
1561
|
+
grid_segments(
|
|
1562
|
+
x0=Unit(0, "npc"), y0=h,
|
|
1563
|
+
x1=Unit(1, "npc"), y1=h,
|
|
1564
|
+
gp=gp,
|
|
1565
|
+
)
|
|
1566
|
+
|
|
1567
|
+
if vp is not None:
|
|
1568
|
+
pop_viewport()
|
|
1569
|
+
|
|
1570
|
+
|
|
1571
|
+
def grid_show_layout(
|
|
1572
|
+
layout: GridLayout,
|
|
1573
|
+
newpage: bool = True,
|
|
1574
|
+
vp_ex: float = 0.8,
|
|
1575
|
+
bg: str = "light grey",
|
|
1576
|
+
cell_border: str = "blue",
|
|
1577
|
+
cell_fill: str = "light blue",
|
|
1578
|
+
cell_label: bool = True,
|
|
1579
|
+
label_col: str = "blue",
|
|
1580
|
+
unit_col: str = "red",
|
|
1581
|
+
vp: Optional[Any] = None,
|
|
1582
|
+
) -> Viewport:
|
|
1583
|
+
"""Visualize a :class:`GridLayout`.
|
|
1584
|
+
|
|
1585
|
+
Draws a representation of the layout on the current device, showing
|
|
1586
|
+
cell boundaries, labels, and dimension annotations.
|
|
1587
|
+
|
|
1588
|
+
Parameters
|
|
1589
|
+
----------
|
|
1590
|
+
layout : GridLayout
|
|
1591
|
+
The layout to visualize.
|
|
1592
|
+
newpage : bool
|
|
1593
|
+
If ``True``, start a new page before drawing.
|
|
1594
|
+
vp_ex : float
|
|
1595
|
+
Fraction of the page used for the viewport (0--1).
|
|
1596
|
+
bg : str
|
|
1597
|
+
Background colour.
|
|
1598
|
+
cell_border : str
|
|
1599
|
+
Cell border colour.
|
|
1600
|
+
cell_fill : str
|
|
1601
|
+
Cell fill colour.
|
|
1602
|
+
cell_label : bool
|
|
1603
|
+
If ``True``, label each cell with ``(row, col)``.
|
|
1604
|
+
label_col : str
|
|
1605
|
+
Colour for cell labels.
|
|
1606
|
+
unit_col : str
|
|
1607
|
+
Colour for dimension annotations.
|
|
1608
|
+
vp : object or None
|
|
1609
|
+
Viewport to push before drawing.
|
|
1610
|
+
|
|
1611
|
+
Returns
|
|
1612
|
+
-------
|
|
1613
|
+
Viewport
|
|
1614
|
+
The viewport used to represent the parent region.
|
|
1615
|
+
"""
|
|
1616
|
+
if newpage:
|
|
1617
|
+
grid_newpage()
|
|
1618
|
+
if vp is not None:
|
|
1619
|
+
push_viewport(vp)
|
|
1620
|
+
|
|
1621
|
+
grid_rect(gp=Gpar(col=None, fill=bg))
|
|
1622
|
+
|
|
1623
|
+
vp_mid = Viewport(
|
|
1624
|
+
x=Unit(0.5, "npc"), y=Unit(0.5, "npc"),
|
|
1625
|
+
width=Unit(vp_ex, "npc"), height=Unit(vp_ex, "npc"),
|
|
1626
|
+
layout=layout,
|
|
1627
|
+
)
|
|
1628
|
+
push_viewport(vp_mid)
|
|
1629
|
+
grid_rect(gp=Gpar(fill="white"))
|
|
1630
|
+
|
|
1631
|
+
gp_red = Gpar(col=unit_col)
|
|
1632
|
+
nr = layout_nrow(layout)
|
|
1633
|
+
nc = layout_ncol(layout)
|
|
1634
|
+
|
|
1635
|
+
for i in range(1, nr + 1):
|
|
1636
|
+
for j in range(1, nc + 1):
|
|
1637
|
+
vp_inner = Viewport(layout_pos_row=i, layout_pos_col=j)
|
|
1638
|
+
push_viewport(vp_inner)
|
|
1639
|
+
grid_rect(gp=Gpar(col=cell_border, fill=cell_fill))
|
|
1640
|
+
if cell_label:
|
|
1641
|
+
grid_text(label=f"({i}, {j})", gp=Gpar(col=label_col))
|
|
1642
|
+
# Dimension annotations on the edges
|
|
1643
|
+
if j == 1:
|
|
1644
|
+
grid_text(
|
|
1645
|
+
label=str(layout_heights(layout)),
|
|
1646
|
+
gp=gp_red,
|
|
1647
|
+
just=["right", "centre"],
|
|
1648
|
+
x=Unit(-0.05, "inches"),
|
|
1649
|
+
y=Unit(0.5, "npc"),
|
|
1650
|
+
rot=0,
|
|
1651
|
+
)
|
|
1652
|
+
if i == nr:
|
|
1653
|
+
grid_text(
|
|
1654
|
+
label=str(layout_widths(layout)),
|
|
1655
|
+
gp=gp_red,
|
|
1656
|
+
just=["centre", "top"],
|
|
1657
|
+
x=Unit(0.5, "npc"),
|
|
1658
|
+
y=Unit(-0.05, "inches"),
|
|
1659
|
+
rot=0,
|
|
1660
|
+
)
|
|
1661
|
+
if j == nc:
|
|
1662
|
+
grid_text(
|
|
1663
|
+
label=str(layout_heights(layout)),
|
|
1664
|
+
gp=gp_red,
|
|
1665
|
+
just=["left", "centre"],
|
|
1666
|
+
x=Unit(1, "npc") + Unit(0.05, "inches"),
|
|
1667
|
+
y=Unit(0.5, "npc"),
|
|
1668
|
+
rot=0,
|
|
1669
|
+
)
|
|
1670
|
+
if i == 1:
|
|
1671
|
+
grid_text(
|
|
1672
|
+
label=str(layout_widths(layout)),
|
|
1673
|
+
gp=gp_red,
|
|
1674
|
+
just=["centre", "bottom"],
|
|
1675
|
+
x=Unit(0.5, "npc"),
|
|
1676
|
+
y=Unit(1, "npc") + Unit(0.05, "inches"),
|
|
1677
|
+
rot=0,
|
|
1678
|
+
)
|
|
1679
|
+
pop_viewport()
|
|
1680
|
+
|
|
1681
|
+
pop_viewport()
|
|
1682
|
+
if vp is not None:
|
|
1683
|
+
pop_viewport()
|
|
1684
|
+
return vp_mid
|
|
1685
|
+
|
|
1686
|
+
|
|
1687
|
+
def grid_show_viewport(
|
|
1688
|
+
v: Optional[Viewport] = None,
|
|
1689
|
+
parent_layout: Optional[GridLayout] = None,
|
|
1690
|
+
newpage: bool = True,
|
|
1691
|
+
vp_ex: float = 0.8,
|
|
1692
|
+
border_fill: str = "light grey",
|
|
1693
|
+
vp_col: str = "blue",
|
|
1694
|
+
vp_fill: str = "light blue",
|
|
1695
|
+
scale_col: str = "red",
|
|
1696
|
+
vp: Optional[Any] = None,
|
|
1697
|
+
recurse: bool = True,
|
|
1698
|
+
depth: int = 0,
|
|
1699
|
+
) -> None:
|
|
1700
|
+
"""Visualize a viewport (or viewport tree).
|
|
1701
|
+
|
|
1702
|
+
Draws a representation of the viewport on the current device, showing
|
|
1703
|
+
its position, size, and native scale.
|
|
1704
|
+
|
|
1705
|
+
Parameters
|
|
1706
|
+
----------
|
|
1707
|
+
v : Viewport or None
|
|
1708
|
+
The viewport to visualize. If ``None``, uses the current viewport.
|
|
1709
|
+
parent_layout : GridLayout or None
|
|
1710
|
+
The parent viewport's layout (used when *v* has layout position).
|
|
1711
|
+
newpage : bool
|
|
1712
|
+
If ``True``, start a new page.
|
|
1713
|
+
vp_ex : float
|
|
1714
|
+
Fraction of the page used for the outer viewport.
|
|
1715
|
+
border_fill : str
|
|
1716
|
+
Fill colour for the outer border area.
|
|
1717
|
+
vp_col : str
|
|
1718
|
+
Border colour for the viewport rectangle.
|
|
1719
|
+
vp_fill : str
|
|
1720
|
+
Fill colour for the viewport rectangle.
|
|
1721
|
+
scale_col : str
|
|
1722
|
+
Colour for scale annotations.
|
|
1723
|
+
vp : object or None
|
|
1724
|
+
Viewport to push before drawing.
|
|
1725
|
+
recurse : bool
|
|
1726
|
+
If ``True``, recurse into child viewports (not yet implemented).
|
|
1727
|
+
depth : int
|
|
1728
|
+
Current recursion depth.
|
|
1729
|
+
"""
|
|
1730
|
+
if v is None:
|
|
1731
|
+
v = Viewport()
|
|
1732
|
+
|
|
1733
|
+
# Check if viewport has layout position and parent layout
|
|
1734
|
+
has_pos = (getattr(v, "layout_pos_row", None) is not None or
|
|
1735
|
+
getattr(v, "layout_pos_col", None) is not None)
|
|
1736
|
+
if has_pos and parent_layout is not None:
|
|
1737
|
+
# Show within parent layout context
|
|
1738
|
+
if vp is not None:
|
|
1739
|
+
push_viewport(vp)
|
|
1740
|
+
vp_mid = grid_show_layout(
|
|
1741
|
+
parent_layout, vp_ex=vp_ex,
|
|
1742
|
+
cell_border="grey", cell_fill="white",
|
|
1743
|
+
cell_label=False, newpage=newpage,
|
|
1744
|
+
)
|
|
1745
|
+
push_viewport(vp_mid)
|
|
1746
|
+
push_viewport(v)
|
|
1747
|
+
gp_red = Gpar(col=scale_col)
|
|
1748
|
+
grid_rect(gp=Gpar(col="blue", fill="light blue"))
|
|
1749
|
+
xscale = getattr(v, "xscale", [0, 1])
|
|
1750
|
+
at = grid_pretty(xscale)
|
|
1751
|
+
if len(at) >= 2:
|
|
1752
|
+
grid_xaxis(at=[min(at), max(at)], gp=gp_red)
|
|
1753
|
+
yscale = getattr(v, "yscale", [0, 1])
|
|
1754
|
+
at = grid_pretty(yscale)
|
|
1755
|
+
if len(at) >= 2:
|
|
1756
|
+
grid_yaxis(at=[min(at), max(at)], gp=gp_red)
|
|
1757
|
+
pop_viewport(2)
|
|
1758
|
+
if vp is not None:
|
|
1759
|
+
pop_viewport()
|
|
1760
|
+
else:
|
|
1761
|
+
# Standard display
|
|
1762
|
+
if newpage:
|
|
1763
|
+
grid_newpage()
|
|
1764
|
+
if vp is not None:
|
|
1765
|
+
push_viewport(vp)
|
|
1766
|
+
grid_rect(gp=Gpar(col=None, fill=border_fill))
|
|
1767
|
+
vp_mid = Viewport(
|
|
1768
|
+
x=Unit(0.5, "npc"), y=Unit(0.5, "npc"),
|
|
1769
|
+
width=Unit(vp_ex, "npc"), height=Unit(vp_ex, "npc"),
|
|
1770
|
+
)
|
|
1771
|
+
push_viewport(vp_mid)
|
|
1772
|
+
grid_rect(gp=Gpar(fill="white"))
|
|
1773
|
+
push_viewport(v)
|
|
1774
|
+
grid_rect(gp=Gpar(col=vp_col, fill=vp_fill))
|
|
1775
|
+
gp_red = Gpar(col=scale_col)
|
|
1776
|
+
xscale = getattr(v, "xscale", [0, 1])
|
|
1777
|
+
at = grid_pretty(xscale)
|
|
1778
|
+
if len(at) >= 2:
|
|
1779
|
+
grid_xaxis(at=[min(at), max(at)], gp=gp_red)
|
|
1780
|
+
yscale = getattr(v, "yscale", [0, 1])
|
|
1781
|
+
at = grid_pretty(yscale)
|
|
1782
|
+
if len(at) >= 2:
|
|
1783
|
+
grid_yaxis(at=[min(at), max(at)], gp=gp_red)
|
|
1784
|
+
pop_viewport(2)
|
|
1785
|
+
if vp is not None:
|
|
1786
|
+
pop_viewport()
|
|
1787
|
+
|
|
1788
|
+
|
|
1789
|
+
def grid_abline(
|
|
1790
|
+
intercept: float = 0,
|
|
1791
|
+
slope: float = 1,
|
|
1792
|
+
gp: Optional[Gpar] = None,
|
|
1793
|
+
draw: bool = True,
|
|
1794
|
+
name: Optional[str] = None,
|
|
1795
|
+
vp: Optional[Any] = None,
|
|
1796
|
+
) -> Grob:
|
|
1797
|
+
"""Draw a line from the equation ``y = intercept + slope * x``.
|
|
1798
|
+
|
|
1799
|
+
The line is drawn across the full NPC range ``[0, 1]``, mapping the
|
|
1800
|
+
x-values 0 and 1 through the linear equation to obtain y-values.
|
|
1801
|
+
|
|
1802
|
+
Parameters
|
|
1803
|
+
----------
|
|
1804
|
+
intercept : float
|
|
1805
|
+
Y-intercept of the line.
|
|
1806
|
+
slope : float
|
|
1807
|
+
Slope of the line.
|
|
1808
|
+
gp : Gpar or None
|
|
1809
|
+
Graphical parameters.
|
|
1810
|
+
draw : bool
|
|
1811
|
+
If ``True``, draw immediately.
|
|
1812
|
+
name : str or None
|
|
1813
|
+
Grob name.
|
|
1814
|
+
vp : object or None
|
|
1815
|
+
Viewport.
|
|
1816
|
+
|
|
1817
|
+
Returns
|
|
1818
|
+
-------
|
|
1819
|
+
Grob
|
|
1820
|
+
A lines grob representing the line.
|
|
1821
|
+
"""
|
|
1822
|
+
x = [0, 1]
|
|
1823
|
+
y = [intercept + slope * xi for xi in x]
|
|
1824
|
+
g = lines_grob(
|
|
1825
|
+
x=Unit(x, "npc"),
|
|
1826
|
+
y=Unit(y, "npc"),
|
|
1827
|
+
gp=gp,
|
|
1828
|
+
name=name,
|
|
1829
|
+
vp=vp,
|
|
1830
|
+
)
|
|
1831
|
+
if draw:
|
|
1832
|
+
grid_draw(g)
|
|
1833
|
+
return g
|
|
1834
|
+
|
|
1835
|
+
|
|
1836
|
+
def grid_plot_and_legend(
|
|
1837
|
+
plot_expr: Optional[Grob] = None,
|
|
1838
|
+
legend_expr: Optional[Grob] = None,
|
|
1839
|
+
widths: Optional[Any] = None,
|
|
1840
|
+
heights: Optional[Any] = None,
|
|
1841
|
+
) -> None:
|
|
1842
|
+
"""Layout a plot and legend side by side.
|
|
1843
|
+
|
|
1844
|
+
This is a simple demonstration function that creates a frame with
|
|
1845
|
+
the plot on the left and the legend on the right.
|
|
1846
|
+
|
|
1847
|
+
Parameters
|
|
1848
|
+
----------
|
|
1849
|
+
plot_expr : Grob or None
|
|
1850
|
+
A grob (or GTree) for the main plot area.
|
|
1851
|
+
legend_expr : Grob or None
|
|
1852
|
+
A grob (or GTree) for the legend.
|
|
1853
|
+
widths : Unit or None
|
|
1854
|
+
Column widths (unused in simple version).
|
|
1855
|
+
heights : Unit or None
|
|
1856
|
+
Row heights (unused in simple version).
|
|
1857
|
+
"""
|
|
1858
|
+
grid_newpage()
|
|
1859
|
+
top_vp = Viewport(width=Unit(0.8, "npc"), height=Unit(0.8, "npc"))
|
|
1860
|
+
push_viewport(top_vp)
|
|
1861
|
+
|
|
1862
|
+
lf = frame_grob()
|
|
1863
|
+
if plot_expr is not None:
|
|
1864
|
+
lf = pack_grob(lf, plot_expr)
|
|
1865
|
+
if legend_expr is not None:
|
|
1866
|
+
lf = pack_grob(lf, legend_expr,
|
|
1867
|
+
height=Unit(1, "null"), side="right")
|
|
1868
|
+
grid_draw(lf)
|
|
1869
|
+
|
|
1870
|
+
|
|
1871
|
+
def layout_torture(
|
|
1872
|
+
n_row: int = 2,
|
|
1873
|
+
n_col: int = 2,
|
|
1874
|
+
) -> None:
|
|
1875
|
+
"""Stress-test the layout system with a simple grid of cells.
|
|
1876
|
+
|
|
1877
|
+
Creates a layout with *n_row* rows and *n_col* columns, populates
|
|
1878
|
+
each cell with a labelled rectangle, and draws the result.
|
|
1879
|
+
|
|
1880
|
+
Parameters
|
|
1881
|
+
----------
|
|
1882
|
+
n_row : int
|
|
1883
|
+
Number of rows.
|
|
1884
|
+
n_col : int
|
|
1885
|
+
Number of columns.
|
|
1886
|
+
"""
|
|
1887
|
+
grid_newpage()
|
|
1888
|
+
lay = GridLayout(nrow=n_row, ncol=n_col)
|
|
1889
|
+
top_vp = Viewport(layout=lay)
|
|
1890
|
+
push_viewport(top_vp)
|
|
1891
|
+
|
|
1892
|
+
for i in range(1, n_row + 1):
|
|
1893
|
+
for j in range(1, n_col + 1):
|
|
1894
|
+
cell_vp = Viewport(layout_pos_row=i, layout_pos_col=j)
|
|
1895
|
+
push_viewport(cell_vp)
|
|
1896
|
+
grid_rect(gp=Gpar(col="blue", fill="light blue"))
|
|
1897
|
+
grid_text(label=f"({i}, {j})")
|
|
1898
|
+
pop_viewport()
|
|
1899
|
+
|
|
1900
|
+
pop_viewport()
|
|
1901
|
+
|
|
1902
|
+
|
|
1903
|
+
# ---------------------------------------------------------------------------
|
|
1904
|
+
# Panel / strip / multipanel stubs (highlevel.R)
|
|
1905
|
+
# ---------------------------------------------------------------------------
|
|
1906
|
+
|
|
1907
|
+
|
|
1908
|
+
def grid_strip(
|
|
1909
|
+
label: str = "whatever",
|
|
1910
|
+
range_full: Sequence[float] = (0, 1),
|
|
1911
|
+
range_thumb: Sequence[float] = (0.3, 0.6),
|
|
1912
|
+
fill: str = "#FFBF00",
|
|
1913
|
+
thumb: str = "#FF8000",
|
|
1914
|
+
vp: Optional[Any] = None,
|
|
1915
|
+
) -> None:
|
|
1916
|
+
"""Draw a strip indicator (simple stub).
|
|
1917
|
+
|
|
1918
|
+
Parameters
|
|
1919
|
+
----------
|
|
1920
|
+
label : str
|
|
1921
|
+
Label text.
|
|
1922
|
+
range_full : sequence of float
|
|
1923
|
+
Full range.
|
|
1924
|
+
range_thumb : sequence of float
|
|
1925
|
+
Thumb (selected) range.
|
|
1926
|
+
fill : str
|
|
1927
|
+
Background fill colour.
|
|
1928
|
+
thumb : str
|
|
1929
|
+
Thumb fill colour.
|
|
1930
|
+
vp : object or None
|
|
1931
|
+
Viewport.
|
|
1932
|
+
"""
|
|
1933
|
+
diff_full = range_full[1] - range_full[0]
|
|
1934
|
+
diff_thumb = range_thumb[1] - range_thumb[0]
|
|
1935
|
+
if vp is not None:
|
|
1936
|
+
push_viewport(vp)
|
|
1937
|
+
grid_rect(gp=Gpar(col=None, fill=fill))
|
|
1938
|
+
grid_rect(
|
|
1939
|
+
x=Unit((range_thumb[0] - range_full[0]) / diff_full, "npc"),
|
|
1940
|
+
y=Unit(0, "npc"),
|
|
1941
|
+
width=Unit(diff_thumb / diff_full, "npc"),
|
|
1942
|
+
height=Unit(1, "npc"),
|
|
1943
|
+
just=["left", "bottom"],
|
|
1944
|
+
gp=Gpar(col=None, fill=thumb),
|
|
1945
|
+
)
|
|
1946
|
+
grid_text(label=label)
|
|
1947
|
+
if vp is not None:
|
|
1948
|
+
pop_viewport()
|
|
1949
|
+
|
|
1950
|
+
|
|
1951
|
+
def grid_panel(
|
|
1952
|
+
x: Optional[Sequence[float]] = None,
|
|
1953
|
+
y: Optional[Sequence[float]] = None,
|
|
1954
|
+
zrange: Sequence[float] = (0, 1),
|
|
1955
|
+
zbin: Optional[Sequence[float]] = None,
|
|
1956
|
+
xscale: Optional[Sequence[float]] = None,
|
|
1957
|
+
yscale: Optional[Sequence[float]] = None,
|
|
1958
|
+
axis_left: bool = True,
|
|
1959
|
+
axis_left_label: bool = True,
|
|
1960
|
+
axis_right: bool = False,
|
|
1961
|
+
axis_right_label: bool = True,
|
|
1962
|
+
axis_bottom: bool = True,
|
|
1963
|
+
axis_bottom_label: bool = True,
|
|
1964
|
+
axis_top: bool = False,
|
|
1965
|
+
axis_top_label: bool = True,
|
|
1966
|
+
vp: Optional[Any] = None,
|
|
1967
|
+
) -> Dict[str, Viewport]:
|
|
1968
|
+
"""Draw a panel with optional axes and strip (simple stub).
|
|
1969
|
+
|
|
1970
|
+
Parameters
|
|
1971
|
+
----------
|
|
1972
|
+
x : sequence of float or None
|
|
1973
|
+
X data values.
|
|
1974
|
+
y : sequence of float or None
|
|
1975
|
+
Y data values.
|
|
1976
|
+
zrange : sequence of float
|
|
1977
|
+
Full z-range for the strip.
|
|
1978
|
+
zbin : sequence of float or None
|
|
1979
|
+
Z-bin for the strip.
|
|
1980
|
+
xscale : sequence of float or None
|
|
1981
|
+
X-axis scale.
|
|
1982
|
+
yscale : sequence of float or None
|
|
1983
|
+
Y-axis scale.
|
|
1984
|
+
axis_left : bool
|
|
1985
|
+
Show left axis.
|
|
1986
|
+
axis_left_label : bool
|
|
1987
|
+
Show left axis labels.
|
|
1988
|
+
axis_right : bool
|
|
1989
|
+
Show right axis.
|
|
1990
|
+
axis_right_label : bool
|
|
1991
|
+
Show right axis labels.
|
|
1992
|
+
axis_bottom : bool
|
|
1993
|
+
Show bottom axis.
|
|
1994
|
+
axis_bottom_label : bool
|
|
1995
|
+
Show bottom axis labels.
|
|
1996
|
+
axis_top : bool
|
|
1997
|
+
Show top axis.
|
|
1998
|
+
axis_top_label : bool
|
|
1999
|
+
Show top axis labels.
|
|
2000
|
+
vp : object or None
|
|
2001
|
+
Viewport.
|
|
2002
|
+
|
|
2003
|
+
Returns
|
|
2004
|
+
-------
|
|
2005
|
+
dict
|
|
2006
|
+
A dictionary with ``"strip_vp"`` and ``"plot_vp"`` keys.
|
|
2007
|
+
"""
|
|
2008
|
+
if x is None:
|
|
2009
|
+
x = list(np.random.uniform(size=10))
|
|
2010
|
+
if y is None:
|
|
2011
|
+
y = list(np.random.uniform(size=10))
|
|
2012
|
+
if zbin is None:
|
|
2013
|
+
zbin = list(np.random.uniform(size=2))
|
|
2014
|
+
if xscale is None:
|
|
2015
|
+
xscale = list(_extend_range(x))
|
|
2016
|
+
if yscale is None:
|
|
2017
|
+
yscale = list(_extend_range(y))
|
|
2018
|
+
|
|
2019
|
+
if vp is not None:
|
|
2020
|
+
push_viewport(vp)
|
|
2021
|
+
|
|
2022
|
+
temp_vp = Viewport(
|
|
2023
|
+
layout=GridLayout(
|
|
2024
|
+
nrow=2, ncol=1,
|
|
2025
|
+
heights=Unit([1, 1], ["lines", "null"]),
|
|
2026
|
+
)
|
|
2027
|
+
)
|
|
2028
|
+
push_viewport(temp_vp)
|
|
2029
|
+
|
|
2030
|
+
strip_vp = Viewport(layout_pos_row=1, layout_pos_col=1, xscale=xscale)
|
|
2031
|
+
push_viewport(strip_vp)
|
|
2032
|
+
grid_strip(range_full=zrange, range_thumb=zbin)
|
|
2033
|
+
grid_rect()
|
|
2034
|
+
if axis_top:
|
|
2035
|
+
grid_xaxis(main=False, label=axis_top_label)
|
|
2036
|
+
pop_viewport()
|
|
2037
|
+
|
|
2038
|
+
plot_vp = Viewport(
|
|
2039
|
+
layout_pos_row=2, layout_pos_col=1,
|
|
2040
|
+
xscale=xscale, yscale=yscale,
|
|
2041
|
+
)
|
|
2042
|
+
push_viewport(plot_vp)
|
|
2043
|
+
grid_grill()
|
|
2044
|
+
grid_points(x=x, y=y, gp=Gpar(col="blue"))
|
|
2045
|
+
grid_rect()
|
|
2046
|
+
if axis_left:
|
|
2047
|
+
grid_yaxis(label=axis_left_label)
|
|
2048
|
+
if axis_right:
|
|
2049
|
+
grid_yaxis(main=False, label=axis_right_label)
|
|
2050
|
+
if axis_bottom:
|
|
2051
|
+
grid_xaxis(label=axis_bottom_label)
|
|
2052
|
+
pop_viewport(2)
|
|
2053
|
+
|
|
2054
|
+
if vp is not None:
|
|
2055
|
+
pop_viewport()
|
|
2056
|
+
|
|
2057
|
+
return {"strip_vp": strip_vp, "plot_vp": plot_vp}
|
|
2058
|
+
|
|
2059
|
+
|
|
2060
|
+
def grid_multipanel(
|
|
2061
|
+
x: Optional[Sequence[float]] = None,
|
|
2062
|
+
y: Optional[Sequence[float]] = None,
|
|
2063
|
+
z: Optional[Sequence[float]] = None,
|
|
2064
|
+
nplots: int = 9,
|
|
2065
|
+
nrow: Optional[int] = None,
|
|
2066
|
+
ncol: Optional[int] = None,
|
|
2067
|
+
newpage: bool = True,
|
|
2068
|
+
vp: Optional[Any] = None,
|
|
2069
|
+
) -> None:
|
|
2070
|
+
"""Draw a multi-panel layout (simple stub).
|
|
2071
|
+
|
|
2072
|
+
Parameters
|
|
2073
|
+
----------
|
|
2074
|
+
x : sequence of float or None
|
|
2075
|
+
X data values.
|
|
2076
|
+
y : sequence of float or None
|
|
2077
|
+
Y data values.
|
|
2078
|
+
z : sequence of float or None
|
|
2079
|
+
Z data values used to split into panels.
|
|
2080
|
+
nplots : int
|
|
2081
|
+
Number of panels.
|
|
2082
|
+
nrow : int or None
|
|
2083
|
+
Number of rows (computed from *nplots* if ``None``).
|
|
2084
|
+
ncol : int or None
|
|
2085
|
+
Number of columns (computed from *nplots* if ``None``).
|
|
2086
|
+
newpage : bool
|
|
2087
|
+
If ``True``, start a new page.
|
|
2088
|
+
vp : object or None
|
|
2089
|
+
Viewport.
|
|
2090
|
+
"""
|
|
2091
|
+
if x is None:
|
|
2092
|
+
x = list(np.random.uniform(size=90))
|
|
2093
|
+
if y is None:
|
|
2094
|
+
y = list(np.random.uniform(size=90))
|
|
2095
|
+
if z is None:
|
|
2096
|
+
z = list(np.random.uniform(size=90))
|
|
2097
|
+
|
|
2098
|
+
if nplots < 1:
|
|
2099
|
+
raise ValueError("'nplots' must be >= 1")
|
|
2100
|
+
|
|
2101
|
+
# Smart defaults for nrow/ncol
|
|
2102
|
+
if nrow is None or ncol is None:
|
|
2103
|
+
ncol_auto = max(1, math.ceil(math.sqrt(nplots)))
|
|
2104
|
+
nrow_auto = math.ceil(nplots / ncol_auto)
|
|
2105
|
+
if nrow is None:
|
|
2106
|
+
nrow = nrow_auto
|
|
2107
|
+
if ncol is None:
|
|
2108
|
+
ncol = ncol_auto
|
|
2109
|
+
|
|
2110
|
+
if newpage:
|
|
2111
|
+
grid_newpage()
|
|
2112
|
+
if vp is not None:
|
|
2113
|
+
push_viewport(vp)
|
|
2114
|
+
|
|
2115
|
+
temp_vp = Viewport(layout=GridLayout(nrow=nrow, ncol=ncol))
|
|
2116
|
+
push_viewport(temp_vp)
|
|
2117
|
+
|
|
2118
|
+
xscale = list(_extend_range(x))
|
|
2119
|
+
yscale = list(_extend_range(y))
|
|
2120
|
+
breaks = list(np.linspace(min(z), max(z), nplots + 1))
|
|
2121
|
+
|
|
2122
|
+
for i in range(nplots):
|
|
2123
|
+
col_idx = i % ncol + 1
|
|
2124
|
+
row_idx = i // ncol + 1
|
|
2125
|
+
panel_vp = Viewport(layout_pos_row=row_idx, layout_pos_col=col_idx)
|
|
2126
|
+
|
|
2127
|
+
# Subset data
|
|
2128
|
+
mask = [(zv >= breaks[i] and zv <= breaks[i + 1]) for zv in z]
|
|
2129
|
+
panelx = [xv for xv, m in zip(x, mask) if m]
|
|
2130
|
+
panely = [yv for yv, m in zip(y, mask) if m]
|
|
2131
|
+
|
|
2132
|
+
if len(panelx) == 0:
|
|
2133
|
+
panelx = [0.5]
|
|
2134
|
+
panely = [0.5]
|
|
2135
|
+
|
|
2136
|
+
grid_panel(
|
|
2137
|
+
x=panelx, y=panely,
|
|
2138
|
+
zrange=[min(z), max(z)],
|
|
2139
|
+
zbin=[breaks[i], breaks[i + 1]],
|
|
2140
|
+
xscale=xscale, yscale=yscale,
|
|
2141
|
+
axis_left=(col_idx == 1),
|
|
2142
|
+
axis_right=(col_idx == ncol or i == nplots - 1),
|
|
2143
|
+
axis_bottom=(row_idx == nrow),
|
|
2144
|
+
axis_top=(row_idx == 1),
|
|
2145
|
+
axis_left_label=_is_even(row_idx),
|
|
2146
|
+
axis_right_label=_is_odd(row_idx),
|
|
2147
|
+
axis_bottom_label=_is_even(col_idx),
|
|
2148
|
+
axis_top_label=_is_odd(col_idx),
|
|
2149
|
+
vp=panel_vp,
|
|
2150
|
+
)
|
|
2151
|
+
|
|
2152
|
+
pop_viewport()
|
|
2153
|
+
if vp is not None:
|
|
2154
|
+
pop_viewport()
|
|
2155
|
+
|
|
2156
|
+
|
|
2157
|
+
# ---------------------------------------------------------------------------
|
|
2158
|
+
# Top-level viewport helper
|
|
2159
|
+
# ---------------------------------------------------------------------------
|
|
2160
|
+
|
|
2161
|
+
|
|
2162
|
+
def grid_top_level_vp() -> Viewport:
|
|
2163
|
+
"""Return a top-level viewport suitable for a standard multi-panel layout.
|
|
2164
|
+
|
|
2165
|
+
This viewport occupies 80% of the device centred in the page, matching
|
|
2166
|
+
the common R pattern for demonstration plots.
|
|
2167
|
+
|
|
2168
|
+
Returns
|
|
2169
|
+
-------
|
|
2170
|
+
Viewport
|
|
2171
|
+
A viewport with width and height of ``0.8 npc``.
|
|
2172
|
+
"""
|
|
2173
|
+
return Viewport(
|
|
2174
|
+
width=Unit(0.8, "npc"),
|
|
2175
|
+
height=Unit(0.8, "npc"),
|
|
2176
|
+
)
|