rgrid-python 4.5.3.post1__tar.gz → 4.5.3.post2__tar.gz
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.
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/PKG-INFO +1 -1
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/__init__.py +3 -3
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_draw.py +25 -5
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_gpar.py +12 -1
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_layout.py +9 -2
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_renderer_base.py +119 -10
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_size.py +36 -10
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_state.py +42 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_units.py +14 -1
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/pyproject.toml +1 -1
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/.gitattributes +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/.gitignore +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/LICENSE +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/README.md +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_arrow.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_clippath.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_colour.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_coords.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_curve.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_display_list.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_edit.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_font_metrics.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_grab.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_grob.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_group.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_highlevel.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_just.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_ls.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_mask.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_path.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_patterns.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_primitives.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_scene_graph.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_transforms.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_typeset.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_utils.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_viewport.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/_vp_calc.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/py.typed +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/renderer.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/renderer_web.py +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/resources/d3.v7.min.js +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/resources/gridpy.css +0 -0
- {rgrid_python-4.5.3.post1 → rgrid_python-4.5.3.post2}/grid_py/resources/gridpy.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rgrid-python
|
|
3
|
-
Version: 4.5.3.
|
|
3
|
+
Version: 4.5.3.post2
|
|
4
4
|
Summary: Python port of the R grid package (tracks R grid 4.5.3)
|
|
5
5
|
Project-URL: Homepage, https://github.com/Bio-Babel/grid_py
|
|
6
6
|
Project-URL: Repository, https://github.com/Bio-Babel/grid_py
|
|
@@ -6,7 +6,7 @@ units, viewports, grobs (graphical objects), layouts, and rendering via
|
|
|
6
6
|
Cairo (pycairo).
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
__version__ = "4.5.3.
|
|
9
|
+
__version__ = "4.5.3.post2"
|
|
10
10
|
|
|
11
11
|
# --- Utilities ---
|
|
12
12
|
from grid_py._utils import depth, explode, grid_pretty, n2mfrow
|
|
@@ -25,7 +25,7 @@ from grid_py._units import (
|
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
# --- Graphical Parameters ---
|
|
28
|
-
from grid_py._gpar import Gpar, get_gpar
|
|
28
|
+
from grid_py._gpar import Gpar, get_gpar, gpar
|
|
29
29
|
|
|
30
30
|
# --- Arrow ---
|
|
31
31
|
from grid_py._arrow import Arrow, arrow
|
|
@@ -232,7 +232,7 @@ __all__ = [
|
|
|
232
232
|
"absolute_size",
|
|
233
233
|
"convert_unit", "convert_x", "convert_y", "convert_width", "convert_height",
|
|
234
234
|
# Gpar
|
|
235
|
-
"Gpar", "get_gpar",
|
|
235
|
+
"Gpar", "gpar", "get_gpar",
|
|
236
236
|
# Arrow
|
|
237
237
|
"Arrow", "arrow",
|
|
238
238
|
# Path
|
|
@@ -690,24 +690,44 @@ def _pop_grob_vp(vp: Any) -> None:
|
|
|
690
690
|
def _vp_depth(vp: Any) -> int:
|
|
691
691
|
"""Return the depth of a viewport (number of levels it adds).
|
|
692
692
|
|
|
693
|
+
Mirrors R's ``depth()`` generic (grid R/grid.R) for every viewport
|
|
694
|
+
container: VpStack pushes ``sum(depth(child))`` levels, VpList
|
|
695
|
+
pushes ``depth(last)``, VpTree pushes ``depth(parent) + depth(last
|
|
696
|
+
child)``, plain Viewport pushes 1, VpPath pushes ``n``.
|
|
697
|
+
|
|
698
|
+
Recognised types are routed through :func:`._viewport.depth` (the
|
|
699
|
+
canonical R-faithful dispatch); this is what guarantees
|
|
700
|
+
``_pop_grob_vp(grob.vp)`` pops the right count when a Gtable's
|
|
701
|
+
``make_context`` returns ``VpStack(orig_vp, layout_vp)``. The
|
|
702
|
+
prior implementation only checked ``hasattr(vp, "depth")`` —
|
|
703
|
+
VpStack/VpList don't expose a method, so it fell through to
|
|
704
|
+
``return 1`` and left every nested layout vp on the layout_stack
|
|
705
|
+
after the gtable rendered, breaking every grob drawn after.
|
|
706
|
+
|
|
707
|
+
Duck-typed objects with a ``.depth()`` method (used by tests and
|
|
708
|
+
user-defined viewport-like objects) are still honoured as a
|
|
709
|
+
fallback for unknown types.
|
|
710
|
+
|
|
693
711
|
Parameters
|
|
694
712
|
----------
|
|
695
713
|
vp : Any
|
|
696
|
-
A viewport, VpPath, VpStack, VpList, or
|
|
714
|
+
A viewport, VpPath, VpStack, VpList, VpTree, or any
|
|
715
|
+
depth-bearing duck-typed object.
|
|
697
716
|
|
|
698
717
|
Returns
|
|
699
718
|
-------
|
|
700
719
|
int
|
|
701
720
|
The depth.
|
|
702
721
|
"""
|
|
722
|
+
from ._viewport import (
|
|
723
|
+
Viewport, VpList, VpStack, VpTree, depth as _depth,
|
|
724
|
+
)
|
|
703
725
|
from ._path import VpPath
|
|
704
726
|
|
|
705
|
-
if isinstance(vp, VpPath):
|
|
706
|
-
|
|
707
|
-
return getattr(vp, "n", 1)
|
|
727
|
+
if isinstance(vp, (Viewport, VpList, VpStack, VpTree, VpPath)):
|
|
728
|
+
return _depth(vp)
|
|
708
729
|
if hasattr(vp, "depth"):
|
|
709
730
|
return vp.depth()
|
|
710
|
-
# Default single viewport depth
|
|
711
731
|
return 1
|
|
712
732
|
|
|
713
733
|
|
|
@@ -14,7 +14,7 @@ from typing import Any, Dict, List, Optional, Sequence, Union
|
|
|
14
14
|
|
|
15
15
|
import numpy as np
|
|
16
16
|
|
|
17
|
-
__all__ = ["Gpar", "get_gpar"]
|
|
17
|
+
__all__ = ["Gpar", "gpar", "get_gpar"]
|
|
18
18
|
|
|
19
19
|
# ---------------------------------------------------------------------------
|
|
20
20
|
# Constants
|
|
@@ -586,3 +586,14 @@ def get_gpar(names: Optional[Sequence[str]] = None) -> Gpar:
|
|
|
586
586
|
gp = object.__new__(Gpar)
|
|
587
587
|
gp._params = subset
|
|
588
588
|
return gp
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def gpar(**kwargs: Any) -> Gpar:
|
|
592
|
+
"""Factory mirroring R ``grid::gpar(...)``.
|
|
593
|
+
|
|
594
|
+
R's ``gpar`` function constructs a ``gpar`` object from arbitrary
|
|
595
|
+
keyword arguments. ``Gpar(**kwargs)`` does the same, so this is a
|
|
596
|
+
thin alias kept for direct R-to-Python translation of code that
|
|
597
|
+
reads ``gpar(col="red")``.
|
|
598
|
+
"""
|
|
599
|
+
return Gpar(**kwargs)
|
|
@@ -400,16 +400,23 @@ def _calc_layout_sizes(
|
|
|
400
400
|
reduced_h = max(reduced_h, 0.0)
|
|
401
401
|
|
|
402
402
|
# ---- Phase 2: allocate respected null units (layout.c:allocateRespected)
|
|
403
|
+
# R sums ALL relative widths/heights via totalWidth/totalHeight
|
|
404
|
+
# (layout.c:154-194) — not just the respected ones — when computing
|
|
405
|
+
# the aspect-ratio normalisation denominator. This is what lets a
|
|
406
|
+
# respect matrix that marks a single cell coexist with other null
|
|
407
|
+
# cells in the same dimension; restricting the sum to respected
|
|
408
|
+
# cells (the prior Python implementation) over-allocated the
|
|
409
|
+
# respected slot and collapsed everything else to 0.
|
|
403
410
|
if layout._valid_respect > 0 and (reduced_w > 0 or reduced_h > 0):
|
|
404
411
|
sum_w = sum(
|
|
405
412
|
float(widths._values[i])
|
|
406
413
|
for i in range(ncol)
|
|
407
|
-
if relative_w[i]
|
|
414
|
+
if relative_w[i]
|
|
408
415
|
)
|
|
409
416
|
sum_h = sum(
|
|
410
417
|
float(heights._values[j])
|
|
411
418
|
for j in range(nrow)
|
|
412
|
-
if relative_h[j]
|
|
419
|
+
if relative_h[j]
|
|
413
420
|
)
|
|
414
421
|
|
|
415
422
|
temp_w = reduced_w
|
|
@@ -249,9 +249,30 @@ class GridRenderer(ABC):
|
|
|
249
249
|
# Cell position in parent's NPC then inches
|
|
250
250
|
parent_w_dev = parent_vtr.width_cm / 2.54 * self._dev_units_per_inch
|
|
251
251
|
parent_h_dev = parent_vtr.height_cm / 2.54 * self._dev_units_per_inch
|
|
252
|
-
|
|
253
|
-
#
|
|
254
|
-
|
|
252
|
+
|
|
253
|
+
# Apply layout-level hjust / vjust (R: layout.c::subRegion
|
|
254
|
+
# lines 447-450). When the cells' total dimensions don't
|
|
255
|
+
# fill the parent vp (common for chrome-merge slices in
|
|
256
|
+
# patchwork), the leftover space is distributed per the
|
|
257
|
+
# layout's justification (default 0.5 → centred). The
|
|
258
|
+
# prior implementation always pinned cells to top-left
|
|
259
|
+
# (vjust=1, hjust=0), which placed merged chrome at the
|
|
260
|
+
# top of the parent panel cell rather than just outside
|
|
261
|
+
# it, breaking simplify_fixed renders.
|
|
262
|
+
hjust = float(grid.get("hjust", 0.0))
|
|
263
|
+
vjust = float(grid.get("vjust", 1.0))
|
|
264
|
+
total_w_dev = sum(col_widths)
|
|
265
|
+
total_h_dev = sum(row_heights)
|
|
266
|
+
hjust_offset_dev = hjust * (parent_w_dev - total_w_dev)
|
|
267
|
+
vjust_offset_dev = (1.0 - vjust) * (parent_h_dev - total_h_dev)
|
|
268
|
+
|
|
269
|
+
cell_x_in = (cell_x0_dev + hjust_offset_dev) / self._dev_units_per_inch
|
|
270
|
+
# Device y is top-down; convert to bottom-up inches.
|
|
271
|
+
# vjust_offset_dev shifts the block downward in device-y
|
|
272
|
+
# (i.e. upward in bottom-up coords) by the leftover * (1-vjust).
|
|
273
|
+
cell_y_in = parent_h_in - (
|
|
274
|
+
cell_y0_dev + cell_h_dev + vjust_offset_dev
|
|
275
|
+
) / self._dev_units_per_inch
|
|
255
276
|
|
|
256
277
|
# Build a simple translation transform for the cell
|
|
257
278
|
from ._vp_calc import translation, multiply
|
|
@@ -280,9 +301,12 @@ class GridRenderer(ABC):
|
|
|
280
301
|
if layout is not None:
|
|
281
302
|
w_dev = cell_w_dev
|
|
282
303
|
h_dev = cell_h_dev
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
304
|
+
# R: ``calcViewportLayout`` (layout.c:492-590) reads
|
|
305
|
+
# ``layoutRespect(layout)`` / ``layoutRespectMat(layout)``
|
|
306
|
+
# straight off the layout object — there is no caller-side
|
|
307
|
+
# respect argument. Mirror that: ``_calc_layout_sizes``
|
|
308
|
+
# consumes ``layout._valid_respect`` directly.
|
|
309
|
+
grid_info = self._compute_grid(layout, w_dev, h_dev)
|
|
286
310
|
self._layout_stack.append(grid_info)
|
|
287
311
|
self._layout_depth_stack.append(
|
|
288
312
|
len(self._vp_transform_stack))
|
|
@@ -294,8 +318,9 @@ class GridRenderer(ABC):
|
|
|
294
318
|
# Compute grid in device units for layout children.
|
|
295
319
|
w_dev = parent_vtr.width_cm / 2.54 * self._dev_units_per_inch
|
|
296
320
|
h_dev = parent_vtr.height_cm / 2.54 * self._dev_units_per_inch
|
|
297
|
-
|
|
298
|
-
|
|
321
|
+
# See R layout.c:492-590 — respect lives on the layout object;
|
|
322
|
+
# callers don't pass it down.
|
|
323
|
+
grid_info = self._compute_grid(layout, w_dev, h_dev)
|
|
299
324
|
|
|
300
325
|
# The layout viewport itself has the same transform as parent
|
|
301
326
|
# but we create a new VTR with the vp's xscale/yscale
|
|
@@ -388,9 +413,15 @@ class GridRenderer(ABC):
|
|
|
388
413
|
|
|
389
414
|
def _compute_grid(
|
|
390
415
|
self, layout: Any, parent_w: float, parent_h: float,
|
|
391
|
-
respect: bool = False,
|
|
392
416
|
) -> dict:
|
|
393
|
-
"""Compute row/column positions for a GridLayout within the parent.
|
|
417
|
+
"""Compute row/column positions for a GridLayout within the parent.
|
|
418
|
+
|
|
419
|
+
R reference: ``layout.c:calcViewportLayout`` (lines 492-590).
|
|
420
|
+
Respect (full or matrix-form) lives on the layout object itself —
|
|
421
|
+
``_calc_layout_sizes`` reads ``layout._valid_respect`` directly,
|
|
422
|
+
so this signature has no ``respect`` argument (matches R, where
|
|
423
|
+
there is no caller-side respect parameter either).
|
|
424
|
+
"""
|
|
394
425
|
from ._layout import _calc_layout_sizes, GridLayout
|
|
395
426
|
|
|
396
427
|
if isinstance(layout, GridLayout):
|
|
@@ -412,9 +443,24 @@ class GridRenderer(ABC):
|
|
|
412
443
|
col_starts = [sum(col_widths[:i]) for i in range(ncol)]
|
|
413
444
|
row_starts = [sum(row_heights[:i]) for i in range(nrow)]
|
|
414
445
|
|
|
446
|
+
# Layout justification (R: layout.c::subRegion lines 433-450).
|
|
447
|
+
# When the cells' total size is less than the parent vp, hjust /
|
|
448
|
+
# vjust shift the layout block within the parent (default 0.5 →
|
|
449
|
+
# centred). ``GridLayout`` carries this on ``_valid_just`` as
|
|
450
|
+
# (hjust, vjust); fall back to (0, 1) for non-GridLayout
|
|
451
|
+
# containers (matching the prior top-left alignment).
|
|
452
|
+
valid_just = getattr(layout, "_valid_just", None)
|
|
453
|
+
if valid_just is not None:
|
|
454
|
+
hjust = float(valid_just[0])
|
|
455
|
+
vjust = float(valid_just[1])
|
|
456
|
+
else:
|
|
457
|
+
hjust, vjust = 0.0, 1.0
|
|
458
|
+
|
|
415
459
|
return {
|
|
416
460
|
"col_starts": col_starts, "col_widths": col_widths,
|
|
417
461
|
"row_starts": row_starts, "row_heights": row_heights,
|
|
462
|
+
"parent_w": parent_w, "parent_h": parent_h,
|
|
463
|
+
"hjust": hjust, "vjust": vjust,
|
|
418
464
|
}
|
|
419
465
|
|
|
420
466
|
def _resolve_sizes(self, unit_obj: Any, n: int, total: float,
|
|
@@ -492,6 +538,28 @@ class GridRenderer(ABC):
|
|
|
492
538
|
# evaluateGrobUnit -- port of R unit.c:325-590 #
|
|
493
539
|
# ===================================================================== #
|
|
494
540
|
|
|
541
|
+
# Per-renderer cycle guard — populated with id(grob) for every grob
|
|
542
|
+
# currently being measured. ``_evaluate_grob_unit`` consults this
|
|
543
|
+
# before pushing the grob's vp; re-entry for the same grob means
|
|
544
|
+
# the vp itself references its own grob*Details (e.g.
|
|
545
|
+
# ``viewport(width = grobWidth(self))``), which would otherwise
|
|
546
|
+
# infinite-recurse through calcViewportTransform → grob_metric_fn
|
|
547
|
+
# → _evaluate_grob_unit → push vp → ...
|
|
548
|
+
#
|
|
549
|
+
# R bounds the same recursion implicitly because its
|
|
550
|
+
# widthDetails.gtable returns an absolute mm sum, and R's
|
|
551
|
+
# setviewport/calcViewportTransform doesn't re-trigger the metric
|
|
552
|
+
# path during the recursive pushViewport (verified empirically:
|
|
553
|
+
# preDraw.gtable runs exactly 3 times for one toplevel grid.draw —
|
|
554
|
+
# not unbounded). We mirror the bound explicitly.
|
|
555
|
+
@property
|
|
556
|
+
def _measuring_grobs(self) -> set:
|
|
557
|
+
s = getattr(self, "_measuring_grobs_set", None)
|
|
558
|
+
if s is None:
|
|
559
|
+
s = set()
|
|
560
|
+
self._measuring_grobs_set = s
|
|
561
|
+
return s
|
|
562
|
+
|
|
495
563
|
def _evaluate_grob_unit(
|
|
496
564
|
self,
|
|
497
565
|
grob: Any,
|
|
@@ -545,6 +613,46 @@ class GridRenderer(ABC):
|
|
|
545
613
|
if not isinstance(grob, Grob):
|
|
546
614
|
return 0.0
|
|
547
615
|
|
|
616
|
+
# --- Cycle break for self-referential grobwidth/grobheight ---
|
|
617
|
+
# When we're already measuring this grob and the grob's vp
|
|
618
|
+
# references its own width/height (e.g. patchwork's
|
|
619
|
+
# ``as_patch.GT`` builds ``viewport(width=grobWidth(grob))``),
|
|
620
|
+
# the recursive vp push would trigger _evaluate_grob_unit for
|
|
621
|
+
# the same grob again, etc. Resolve widthDetails/heightDetails
|
|
622
|
+
# directly without the recursive push — for non-context-
|
|
623
|
+
# dependent grobs (gtable widthDetails returns absolute_size of
|
|
624
|
+
# the widths sum) this gives the same answer as the full path,
|
|
625
|
+
# matching R's behaviour where preDraw.gtable runs a bounded
|
|
626
|
+
# number of times rather than unboundedly.
|
|
627
|
+
if id(grob) in self._measuring_grobs:
|
|
628
|
+
if unit_type == "grobwidth":
|
|
629
|
+
ru = width_details(grob)
|
|
630
|
+
axis = "x"
|
|
631
|
+
elif unit_type == "grobheight":
|
|
632
|
+
ru = height_details(grob)
|
|
633
|
+
axis = "y"
|
|
634
|
+
elif unit_type == "grobascent":
|
|
635
|
+
ru = ascent_details(grob)
|
|
636
|
+
axis = "y"
|
|
637
|
+
elif unit_type == "grobdescent":
|
|
638
|
+
ru = descent_details(grob)
|
|
639
|
+
axis = "y"
|
|
640
|
+
else:
|
|
641
|
+
return 0.0
|
|
642
|
+
if ru is None:
|
|
643
|
+
return 0.0
|
|
644
|
+
from ._units import Unit as _U
|
|
645
|
+
if not isinstance(ru, _U):
|
|
646
|
+
return 0.0
|
|
647
|
+
if len(ru) == 1 and ru._units[0] == "null":
|
|
648
|
+
return 0.0
|
|
649
|
+
return float(self._resolve_to_inches(ru, axis, True))
|
|
650
|
+
|
|
651
|
+
# Capture id BEFORE make_context() rebinds ``grob`` to the
|
|
652
|
+
# context-wrapped copy — we need to release the same id we added.
|
|
653
|
+
_measuring_id = id(grob)
|
|
654
|
+
self._measuring_grobs.add(_measuring_id)
|
|
655
|
+
|
|
548
656
|
# --- Save state (R unit.c:355-377) ---
|
|
549
657
|
saved_dl_on = state._dl_on
|
|
550
658
|
state.set_display_list_on(False)
|
|
@@ -620,6 +728,7 @@ class GridRenderer(ABC):
|
|
|
620
728
|
state.replace_gpar(saved_gpar)
|
|
621
729
|
state._current_grob = saved_current_grob
|
|
622
730
|
state.set_display_list_on(saved_dl_on)
|
|
731
|
+
self._measuring_grobs.discard(_measuring_id)
|
|
623
732
|
|
|
624
733
|
return result
|
|
625
734
|
|
|
@@ -232,13 +232,36 @@ def _resolve_grob_gp(grob: Any) -> "Optional[Gpar]":
|
|
|
232
232
|
def _text_bbox(grob: Any) -> tuple:
|
|
233
233
|
"""Compute (width, height) of the text bounding box in inches.
|
|
234
234
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
235
|
+
Ports R's ``heightDetails.text`` / ``widthDetails.text`` (grid's
|
|
236
|
+
``primitives.R:1430-1452``) — R's ``grobHeight`` on a text grob
|
|
237
|
+
returns the **ascent only** (glyph extent above baseline), never
|
|
238
|
+
the descent. ``grobDescent`` is a separate method (see
|
|
239
|
+
``descentDetails.text``) and is exposed independently so that
|
|
240
|
+
callers can add it when needed. ggplot2's ``titleGrob``
|
|
241
|
+
(margins.R:115-132) relies on this convention — it uses
|
|
242
|
+
``unit(1, "grobheight", grob) + y_descent`` to assemble the final
|
|
243
|
+
height — so any deviation here double-counts the descent.
|
|
244
|
+
|
|
245
|
+
Empirical verification (R 4.4 ``cairo_png`` at 150 dpi, default
|
|
246
|
+
Helvetica, fontsize 13.2 pt):
|
|
247
|
+
|
|
248
|
+
grobHeight(textGrob("A long title", fs=13.2)) = 3.293 mm
|
|
249
|
+
grobHeight(textGrob("gjpqy", fs=13.2)) = 3.293 mm
|
|
250
|
+
grobHeight(textGrob("y", fs=13.2)) = 3.293 mm
|
|
251
|
+
|
|
252
|
+
— i.e. the value is a font-level constant, independent of the
|
|
253
|
+
label content. Our cairo-backed ``calc_string_metric`` returns a
|
|
254
|
+
per-label ascent that varies slightly with glyph mix, which is the
|
|
255
|
+
closest we can get without implementing a full AFM font-metric
|
|
256
|
+
path; residual ≤ 0.3 mm discrepancy is a font-file difference
|
|
257
|
+
(AFM Helvetica vs. cairo's "Sans" fallback) and outside the
|
|
258
|
+
scope of this bbox function.
|
|
259
|
+
|
|
260
|
+
Height formula (port of R's ``GEStrHeight``):
|
|
261
|
+
|
|
262
|
+
width = max(per-line ink widths)
|
|
263
|
+
height = ascent(first line)
|
|
240
264
|
+ (n - 1) × cex × lineheight × fontsize × 1.2 / 72
|
|
241
|
-
— R's ``GEStrHeight``
|
|
242
265
|
|
|
243
266
|
The per-extra-line gap ``1.2 × fontsize / 72`` is R's device-level
|
|
244
267
|
``cra[1] × ipr[1] / default_ps`` collapsed for the standard cairo /
|
|
@@ -287,11 +310,14 @@ def _text_bbox(grob: Any) -> tuple:
|
|
|
287
310
|
n_lines = len(lines)
|
|
288
311
|
# Width: max per-line width (R's ``GEStrWidth`` walks each line).
|
|
289
312
|
w = max(calc_string_metric(ln, gp=gp)["width"] for ln in lines)
|
|
290
|
-
# Height:
|
|
291
|
-
#
|
|
292
|
-
#
|
|
313
|
+
# Height: ascent of the first line + per-line gap × (n - 1).
|
|
314
|
+
# R's ``heightDetails.text`` / ``GEStrHeight`` returns ASCENT
|
|
315
|
+
# only — never descent. Descent is a separate method
|
|
316
|
+
# (``descentDetails.text``). Including descent here would
|
|
317
|
+
# double-count it downstream in ggplot2 ``titleGrob``, which
|
|
318
|
+
# adds ``grobDescent`` manually to form the rendered height.
|
|
293
319
|
m0 = calc_string_metric(lines[0], gp=gp)
|
|
294
|
-
h = m0["ascent"] +
|
|
320
|
+
h = m0["ascent"] + (n_lines - 1) * inter_line_gap
|
|
295
321
|
|
|
296
322
|
if rot == 0.0:
|
|
297
323
|
# No rotation: bbox is just the text extent
|
|
@@ -224,6 +224,12 @@ class GridState:
|
|
|
224
224
|
# GSS_GROUPS: group registry for define/use (R grid.h:63, state.c:51)
|
|
225
225
|
# Maps group name → dict with keys: ref, xy, xyin, wh, r, etc.
|
|
226
226
|
self._groups: Dict[str, Any] = {}
|
|
227
|
+
# Per-push counter of redundant self-pushes (the same vp pushed
|
|
228
|
+
# again while already current). Each genuine push appends a 0;
|
|
229
|
+
# a redundant push increments the top entry; pops decrement
|
|
230
|
+
# before actually unwinding. Prevents the cyclic parent chain
|
|
231
|
+
# that would otherwise hang ``current_vp_path``.
|
|
232
|
+
self._redundant_push_count: List[int] = [0]
|
|
227
233
|
|
|
228
234
|
# ---- reset ------------------------------------------------------------
|
|
229
235
|
|
|
@@ -244,7 +250,27 @@ class GridState:
|
|
|
244
250
|
vp : Any
|
|
245
251
|
A viewport-like object. Must expose ``name``, ``parent``,
|
|
246
252
|
and ``children`` attributes (or dict keys).
|
|
253
|
+
|
|
254
|
+
Notes
|
|
255
|
+
-----
|
|
256
|
+
Pushing the same viewport object twice (e.g. when a Gtable's
|
|
257
|
+
``make_context`` builds ``VpStack(orig_vp, layout_vp)`` and
|
|
258
|
+
the iterating push hits ``orig_vp`` while ``orig_vp`` is
|
|
259
|
+
already the current vp — happens in ``_evaluate_grob_unit``'s
|
|
260
|
+
preDraw for self-referential ``viewport(width = grobWidth(self))``
|
|
261
|
+
setups) would otherwise set ``vp.parent = vp`` and produce a
|
|
262
|
+
cyclic parent chain that deadlocks ``current_vp_path``. The
|
|
263
|
+
Python port stores parent on the vp object itself (rather than
|
|
264
|
+
on a separate per-push record like R's grid does), so we
|
|
265
|
+
track redundant self-pushes on a counter stack and treat the
|
|
266
|
+
matching ``up_viewport`` / ``pop_viewport`` as no-ops so depth
|
|
267
|
+
stays balanced.
|
|
247
268
|
"""
|
|
269
|
+
if vp is self._current_vp:
|
|
270
|
+
self._redundant_push_count[-1] = (
|
|
271
|
+
self._redundant_push_count[-1] + 1
|
|
272
|
+
)
|
|
273
|
+
return
|
|
248
274
|
_vp_set_attr(vp, "parent", self._current_vp)
|
|
249
275
|
children = _vp_children(self._current_vp)
|
|
250
276
|
if children is None:
|
|
@@ -252,6 +278,7 @@ class GridState:
|
|
|
252
278
|
_vp_set_attr(self._current_vp, "children", children)
|
|
253
279
|
children.append(vp)
|
|
254
280
|
self._current_vp = vp
|
|
281
|
+
self._redundant_push_count.append(0)
|
|
255
282
|
|
|
256
283
|
def pop_viewport(self, n: int = 1) -> None:
|
|
257
284
|
"""Pop *n* viewports, navigating back toward the root.
|
|
@@ -272,8 +299,14 @@ class GridState:
|
|
|
272
299
|
if n == 0:
|
|
273
300
|
# Pop to root.
|
|
274
301
|
self._current_vp = self._vp_tree
|
|
302
|
+
self._redundant_push_count = [0]
|
|
275
303
|
return
|
|
276
304
|
for _ in range(n):
|
|
305
|
+
# First absorb any redundant self-pushes recorded for the
|
|
306
|
+
# current vp (see ``push_viewport`` notes).
|
|
307
|
+
if self._redundant_push_count and self._redundant_push_count[-1] > 0:
|
|
308
|
+
self._redundant_push_count[-1] -= 1
|
|
309
|
+
continue
|
|
277
310
|
parent = _vp_parent(self._current_vp)
|
|
278
311
|
if parent is None:
|
|
279
312
|
raise ValueError(
|
|
@@ -286,6 +319,8 @@ class GridState:
|
|
|
286
319
|
except ValueError:
|
|
287
320
|
pass
|
|
288
321
|
self._current_vp = parent
|
|
322
|
+
if len(self._redundant_push_count) > 1:
|
|
323
|
+
self._redundant_push_count.pop()
|
|
289
324
|
|
|
290
325
|
def up_viewport(self, n: int = 1) -> None:
|
|
291
326
|
"""Navigate up *n* levels without removing viewports from the tree.
|
|
@@ -305,14 +340,21 @@ class GridState:
|
|
|
305
340
|
raise ValueError(f"'n' must be non-negative, got {n}")
|
|
306
341
|
if n == 0:
|
|
307
342
|
self._current_vp = self._vp_tree
|
|
343
|
+
self._redundant_push_count = [0]
|
|
308
344
|
return
|
|
309
345
|
for _ in range(n):
|
|
346
|
+
# Absorb redundant self-pushes first.
|
|
347
|
+
if self._redundant_push_count and self._redundant_push_count[-1] > 0:
|
|
348
|
+
self._redundant_push_count[-1] -= 1
|
|
349
|
+
continue
|
|
310
350
|
parent = _vp_parent(self._current_vp)
|
|
311
351
|
if parent is None:
|
|
312
352
|
raise ValueError(
|
|
313
353
|
"Cannot navigate above the root viewport."
|
|
314
354
|
)
|
|
315
355
|
self._current_vp = parent
|
|
356
|
+
if len(self._redundant_push_count) > 1:
|
|
357
|
+
self._redundant_push_count.pop()
|
|
316
358
|
|
|
317
359
|
def down_viewport(self, name: str, strict: bool = False) -> int:
|
|
318
360
|
"""Navigate down to a named viewport (breadth-first search).
|
|
@@ -301,7 +301,20 @@ def _try_resolve_with_renderer(
|
|
|
301
301
|
state = get_state()
|
|
302
302
|
renderer = state.get_renderer()
|
|
303
303
|
|
|
304
|
-
if renderer is None
|
|
304
|
+
if renderer is None:
|
|
305
|
+
# R parity: `convertUnit` (unit.R:59-75) dispatches to `L_convert` in
|
|
306
|
+
# src/unit.c, which resolves context via `GEcurrentDevice()`. When no
|
|
307
|
+
# device is open, R's graphics system auto-opens its default device
|
|
308
|
+
# (PDF, 7×7 in) and converts against it. We replicate that here by
|
|
309
|
+
# lazily installing a default 7×7 in CairoRenderer the first time a
|
|
310
|
+
# context-dependent conversion is requested — matching R's observed
|
|
311
|
+
# behaviour: `convertHeight(unit(0.3,"npc"),"mm") == 53.34` (=0.3×7in
|
|
312
|
+
# in mm) without any prior `grid_newpage()` / `pdf()` call.
|
|
313
|
+
from .renderer import CairoRenderer
|
|
314
|
+
renderer = CairoRenderer(width=7.0, height=7.0)
|
|
315
|
+
state.init_device(renderer)
|
|
316
|
+
|
|
317
|
+
if not hasattr(renderer, "_resolve_to_inches_idx"):
|
|
305
318
|
return None
|
|
306
319
|
|
|
307
320
|
# Build a single-element Unit for the source
|
|
@@ -12,7 +12,7 @@ name = "rgrid-python"
|
|
|
12
12
|
# a corresponding R update, bump a PEP 440 post-release suffix:
|
|
13
13
|
# "4.5.3" → "4.5.3.post1" → "4.5.3.post2" → ...
|
|
14
14
|
# When R grid itself ships a new upstream version, move to e.g. "4.5.4".
|
|
15
|
-
version = "4.5.3.
|
|
15
|
+
version = "4.5.3.post2"
|
|
16
16
|
description = "Python port of the R grid package (tracks R grid 4.5.3)"
|
|
17
17
|
readme = "README.md"
|
|
18
18
|
requires-python = ">=3.10"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|