rgrid-python 4.5.3.post4__py3-none-any.whl → 4.5.3.post5__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 +1 -1
- grid_py/_colour.py +9 -2
- grid_py/_draw.py +133 -40
- grid_py/_primitives.py +67 -0
- grid_py/_renderer_base.py +140 -18
- grid_py/_state.py +68 -10
- grid_py/_vp_calc.py +45 -3
- grid_py/renderer.py +264 -33
- grid_py/renderer_web.py +7 -1
- {rgrid_python-4.5.3.post4.dist-info → rgrid_python-4.5.3.post5.dist-info}/METADATA +1 -1
- {rgrid_python-4.5.3.post4.dist-info → rgrid_python-4.5.3.post5.dist-info}/RECORD +13 -13
- {rgrid_python-4.5.3.post4.dist-info → rgrid_python-4.5.3.post5.dist-info}/WHEEL +1 -1
- {rgrid_python-4.5.3.post4.dist-info → rgrid_python-4.5.3.post5.dist-info}/licenses/LICENSE +0 -0
grid_py/__init__.py
CHANGED
grid_py/_colour.py
CHANGED
|
@@ -761,8 +761,15 @@ def parse_r_colour(c: Any) -> Tuple[float, float, float, float]:
|
|
|
761
761
|
s = c.strip()
|
|
762
762
|
low = s.lower()
|
|
763
763
|
|
|
764
|
-
#
|
|
765
|
-
|
|
764
|
+
# R's ``col2rgb("transparent", alpha=TRUE)`` returns (255, 255, 255, 0)
|
|
765
|
+
# — by R convention the RGB channels are white even though α=0
|
|
766
|
+
# makes the colour invisible. Mirror exactly so hex round-trips
|
|
767
|
+
# and downstream tooling that compares against R's encoding agree.
|
|
768
|
+
if low == "transparent":
|
|
769
|
+
return (1.0, 1.0, 1.0, 0.0)
|
|
770
|
+
# NA / none / "" are Python-port-specific transparent sentinels not
|
|
771
|
+
# defined as parseable colours in R (``col2rgb('NA')`` errors).
|
|
772
|
+
if low in ("na", "none", ""):
|
|
766
773
|
return (0.0, 0.0, 0.0, 0.0)
|
|
767
774
|
|
|
768
775
|
# Hex colour
|
grid_py/_draw.py
CHANGED
|
@@ -25,6 +25,7 @@ from ._state import get_state
|
|
|
25
25
|
from ._display_list import DisplayList, DLDrawGrob
|
|
26
26
|
from ._units import Unit
|
|
27
27
|
from ._utils import grid_pretty as _grid_pretty
|
|
28
|
+
from ._primitives import valid_pch
|
|
28
29
|
|
|
29
30
|
__all__ = [
|
|
30
31
|
"grid_draw",
|
|
@@ -307,8 +308,16 @@ def _render_grob(
|
|
|
307
308
|
|
|
308
309
|
# ---- rect -----------------------------------------------------------
|
|
309
310
|
if cls == "rect":
|
|
310
|
-
|
|
311
|
-
|
|
311
|
+
# Use paired resolve_loc_array so that under a rotated viewport
|
|
312
|
+
# each rect's anchor (x_i, y_i) is mapped through the full 2-D
|
|
313
|
+
# CTM together, not via two independent 1-D projections (which
|
|
314
|
+
# silently drop the rotation contribution from the orthogonal
|
|
315
|
+
# axis — see _renderer_base.resolve_x_array docstring).
|
|
316
|
+
xs, ys = renderer.resolve_loc_array(
|
|
317
|
+
getattr(grob, "x", [0.0]),
|
|
318
|
+
getattr(grob, "y", [0.0]),
|
|
319
|
+
gp=gp,
|
|
320
|
+
)
|
|
312
321
|
ws = renderer.resolve_w_array(getattr(grob, "width", [1.0]), gp=gp)
|
|
313
322
|
hs = renderer.resolve_h_array(getattr(grob, "height", [1.0]), gp=gp)
|
|
314
323
|
hj, vj = _resolve_just(grob)
|
|
@@ -331,9 +340,11 @@ def _render_grob(
|
|
|
331
340
|
|
|
332
341
|
# ---- roundrect ------------------------------------------------------
|
|
333
342
|
elif cls == "roundrect":
|
|
343
|
+
ax, ay = renderer.resolve_loc(
|
|
344
|
+
getattr(grob, "x", 0.0), getattr(grob, "y", 0.0), gp=gp,
|
|
345
|
+
)
|
|
334
346
|
renderer.draw_roundrect(
|
|
335
|
-
x=
|
|
336
|
-
y=renderer.resolve_y(getattr(grob, "y", 0.0), gp=gp),
|
|
347
|
+
x=ax, y=ay,
|
|
337
348
|
w=renderer.resolve_w(getattr(grob, "width", 1.0), gp=gp),
|
|
338
349
|
h=renderer.resolve_h(getattr(grob, "height", 1.0), gp=gp),
|
|
339
350
|
r=renderer.resolve_w(getattr(grob, "r", 0.0), gp=gp),
|
|
@@ -344,17 +355,22 @@ def _render_grob(
|
|
|
344
355
|
|
|
345
356
|
# ---- circle ---------------------------------------------------------
|
|
346
357
|
elif cls == "circle":
|
|
358
|
+
cx, cy = renderer.resolve_loc(
|
|
359
|
+
getattr(grob, "x", 0.5), getattr(grob, "y", 0.5), gp=gp,
|
|
360
|
+
)
|
|
347
361
|
renderer.draw_circle(
|
|
348
|
-
x=
|
|
349
|
-
y=renderer.resolve_y(getattr(grob, "y", 0.5), gp=gp),
|
|
362
|
+
x=cx, y=cy,
|
|
350
363
|
r=renderer.resolve_w(getattr(grob, "r", 0.5), gp=gp),
|
|
351
364
|
gp=gp,
|
|
352
365
|
)
|
|
353
366
|
|
|
354
367
|
# ---- lines / polyline ------------------------------------------------
|
|
355
368
|
elif cls in ("lines", "polyline"):
|
|
356
|
-
x = renderer.
|
|
357
|
-
|
|
369
|
+
x, y = renderer.resolve_loc_array(
|
|
370
|
+
getattr(grob, "x", [0.0, 1.0]),
|
|
371
|
+
getattr(grob, "y", [0.0, 1.0]),
|
|
372
|
+
gp=gp,
|
|
373
|
+
)
|
|
358
374
|
id_ = getattr(grob, "id", None)
|
|
359
375
|
id_lengths = getattr(grob, "id_lengths", None)
|
|
360
376
|
# R polylineGrob supports either `id` (per-point group) or
|
|
@@ -368,12 +384,36 @@ def _render_grob(
|
|
|
368
384
|
id_ = np.atleast_1d(np.asarray(id_, dtype=int))
|
|
369
385
|
renderer.draw_polyline(x, y, id_=id_, gp=gp)
|
|
370
386
|
|
|
387
|
+
# R linesGrob / polylineGrob carry an optional ``arrow=`` (port
|
|
388
|
+
# of src/grid.c::L_lines / L_polyline arrowhead emission). The
|
|
389
|
+
# earlier code consumed it for ``segmentsGrob`` only, silently
|
|
390
|
+
# dropping it on lines / polyline — symptom: an arrow= argument
|
|
391
|
+
# produced a bare line. Apply ``_draw_arrow_heads`` per
|
|
392
|
+
# polyline (each unique id) here, with the same reference-point
|
|
393
|
+
# convention segments uses.
|
|
394
|
+
arr = getattr(grob, "arrow", None)
|
|
395
|
+
if arr is not None:
|
|
396
|
+
if id_ is not None:
|
|
397
|
+
for uid in np.unique(id_):
|
|
398
|
+
mask = id_ == uid
|
|
399
|
+
if int(np.sum(mask)) >= 2:
|
|
400
|
+
_draw_arrow_heads(
|
|
401
|
+
np.asarray(x)[mask], np.asarray(y)[mask],
|
|
402
|
+
arr, renderer, gp,
|
|
403
|
+
)
|
|
404
|
+
elif len(x) >= 2:
|
|
405
|
+
_draw_arrow_heads(
|
|
406
|
+
np.asarray(x), np.asarray(y), arr, renderer, gp,
|
|
407
|
+
)
|
|
408
|
+
|
|
371
409
|
# ---- segments --------------------------------------------------------
|
|
372
410
|
elif cls == "segments":
|
|
373
|
-
x0 = renderer.
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
y1 = renderer.
|
|
411
|
+
x0, y0 = renderer.resolve_loc_array(
|
|
412
|
+
getattr(grob, "x0", []), getattr(grob, "y0", []), gp=gp,
|
|
413
|
+
)
|
|
414
|
+
x1, y1 = renderer.resolve_loc_array(
|
|
415
|
+
getattr(grob, "x1", []), getattr(grob, "y1", []), gp=gp,
|
|
416
|
+
)
|
|
377
417
|
renderer.draw_segments(x0=x0, y0=y0, x1=x1, y1=y1, gp=gp)
|
|
378
418
|
|
|
379
419
|
# Each segment may carry its own arrowhead (``arrow=`` parameter on
|
|
@@ -392,8 +432,11 @@ def _render_grob(
|
|
|
392
432
|
elif cls == "xspline":
|
|
393
433
|
from ._curve import _calc_xspline_points # lazy to avoid import cycle
|
|
394
434
|
|
|
395
|
-
x = renderer.
|
|
396
|
-
|
|
435
|
+
x, y = renderer.resolve_loc_array(
|
|
436
|
+
getattr(grob, "x", [0.0, 1.0]),
|
|
437
|
+
getattr(grob, "y", [0.0, 1.0]),
|
|
438
|
+
gp=gp,
|
|
439
|
+
)
|
|
397
440
|
shape_raw = getattr(grob, "shape", 0.0)
|
|
398
441
|
open_ = bool(getattr(grob, "open_", True))
|
|
399
442
|
rep_ends = bool(getattr(grob, "repEnds", True))
|
|
@@ -474,8 +517,9 @@ def _render_grob(
|
|
|
474
517
|
|
|
475
518
|
# ---- polygon ---------------------------------------------------------
|
|
476
519
|
elif cls == "polygon":
|
|
477
|
-
px = renderer.
|
|
478
|
-
|
|
520
|
+
px, py = renderer.resolve_loc_array(
|
|
521
|
+
getattr(grob, "x", []), getattr(grob, "y", []), gp=gp,
|
|
522
|
+
)
|
|
479
523
|
pid = getattr(grob, "id", None)
|
|
480
524
|
if pid is not None:
|
|
481
525
|
# R semantics: polygonGrob(id=...) draws separate polygons
|
|
@@ -509,9 +553,8 @@ def _render_grob(
|
|
|
509
553
|
else:
|
|
510
554
|
labels = [str(label_raw)]
|
|
511
555
|
|
|
512
|
-
# Resolve x/y
|
|
513
|
-
xx = renderer.
|
|
514
|
-
yy = renderer.resolve_y_array(y_unit, gp=gp)
|
|
556
|
+
# Resolve x/y as pairs so rotation contribution is preserved.
|
|
557
|
+
xx, yy = renderer.resolve_loc_array(x_unit, y_unit, gp=gp)
|
|
515
558
|
|
|
516
559
|
# Normalise rot to array
|
|
517
560
|
if isinstance(rot_raw, (list, tuple, np.ndarray)):
|
|
@@ -553,16 +596,20 @@ def _render_grob(
|
|
|
553
596
|
# ---- points ----------------------------------------------------------
|
|
554
597
|
elif cls == "points":
|
|
555
598
|
pch_raw = getattr(grob, "pch", 19)
|
|
556
|
-
# pch may be a scalar or
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
599
|
+
# pch may be a scalar (int or single character such as "."/"A")
|
|
600
|
+
# or a per-point array (possibly MIXED int/str, e.g.
|
|
601
|
+
# [".", 19, "A"]). R keeps character pch as character and
|
|
602
|
+
# coerces numeric pch to int (grid:::valid.pch). Normalise via
|
|
603
|
+
# the same helper and pass the result through *unchanged* — the
|
|
604
|
+
# renderer dispatches symbol-vs-glyph per point. Do NOT force
|
|
605
|
+
# ``dtype=int`` (that crashes on "."), and do NOT substitute a
|
|
606
|
+
# silent default for non-numeric scalars (that masks bugs).
|
|
607
|
+
pch_val = valid_pch(pch_raw)
|
|
608
|
+
pts_x, pts_y = renderer.resolve_loc_array(
|
|
609
|
+
getattr(grob, "x", []), getattr(grob, "y", []), gp=gp,
|
|
610
|
+
)
|
|
563
611
|
renderer.draw_points(
|
|
564
|
-
x=
|
|
565
|
-
y=renderer.resolve_y_array(getattr(grob, "y", []), gp=gp),
|
|
612
|
+
x=pts_x, y=pts_y,
|
|
566
613
|
size=renderer.resolve_w(getattr(grob, "size", 1.0), gp=gp),
|
|
567
614
|
pch=pch_val,
|
|
568
615
|
gp=gp,
|
|
@@ -570,18 +617,63 @@ def _render_grob(
|
|
|
570
617
|
|
|
571
618
|
# ---- pathgrob --------------------------------------------------------
|
|
572
619
|
elif cls == "pathgrob":
|
|
573
|
-
x = renderer.
|
|
574
|
-
|
|
575
|
-
|
|
620
|
+
x, y = renderer.resolve_loc_array(
|
|
621
|
+
getattr(grob, "x", []), getattr(grob, "y", []), gp=gp,
|
|
622
|
+
)
|
|
623
|
+
# R ``pathGrob`` carries two grouping levels (src/grid.c::L_path):
|
|
624
|
+
#
|
|
625
|
+
# - id / id_lengths : per-point SUB-PATH identifier — each
|
|
626
|
+
# unique value is one closed Cairo sub-path.
|
|
627
|
+
# - path_id / path_id_lengths : per-sub-path COMPOUND grouping
|
|
628
|
+
# — each compound is filled+stroked independently with its
|
|
629
|
+
# own fill rule application.
|
|
630
|
+
#
|
|
631
|
+
# The earlier port read ``grob.pathId`` (camelCase, never stored
|
|
632
|
+
# by path_grob) and ignored ``grob.id`` entirely, so every path
|
|
633
|
+
# collapsed into a single 8-vertex sub-path that connected the
|
|
634
|
+
# two rectangles into a bow-tie. Read both attributes properly
|
|
635
|
+
# here and pass the per-point sub-path identifier through to
|
|
636
|
+
# ``draw_path`` (which iterates unique values).
|
|
637
|
+
sub_id = getattr(grob, "id", None)
|
|
638
|
+
sub_id_lengths = getattr(grob, "id_lengths", None)
|
|
639
|
+
if sub_id is None and sub_id_lengths is not None:
|
|
640
|
+
lengths = np.atleast_1d(np.asarray(sub_id_lengths, dtype=int))
|
|
641
|
+
sub_id = np.repeat(np.arange(1, len(lengths) + 1), lengths)
|
|
642
|
+
if sub_id is None:
|
|
643
|
+
sub_id = np.ones(len(x), dtype=int)
|
|
644
|
+
else:
|
|
645
|
+
sub_id = np.atleast_1d(np.asarray(sub_id, dtype=int))
|
|
646
|
+
|
|
647
|
+
# Optional second-level compound grouping. R fills each
|
|
648
|
+
# compound separately with its own fill-rule application. Most
|
|
649
|
+
# uses (and every current test) have a single compound; we
|
|
650
|
+
# iterate when more are supplied.
|
|
651
|
+
path_id = getattr(grob, "path_id", None)
|
|
652
|
+
path_id_lengths = getattr(grob, "path_id_lengths", None)
|
|
653
|
+
if path_id is None and path_id_lengths is not None:
|
|
654
|
+
lengths = np.atleast_1d(np.asarray(path_id_lengths, dtype=int))
|
|
655
|
+
path_id = np.repeat(np.arange(1, len(lengths) + 1), lengths)
|
|
656
|
+
rule = getattr(grob, "rule", "winding")
|
|
657
|
+
|
|
576
658
|
if path_id is None:
|
|
577
|
-
|
|
659
|
+
renderer.draw_path(
|
|
660
|
+
x=x, y=y, path_id=sub_id, rule=rule, gp=gp,
|
|
661
|
+
)
|
|
578
662
|
else:
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
663
|
+
# Map per-point compound id from per-sub-path path_id.
|
|
664
|
+
path_id_arr = np.atleast_1d(np.asarray(path_id, dtype=int))
|
|
665
|
+
unique_subs = np.unique(sub_id)
|
|
666
|
+
sub_to_compound = {int(s): int(path_id_arr[k % len(path_id_arr)])
|
|
667
|
+
for k, s in enumerate(unique_subs)}
|
|
668
|
+
point_compound = np.asarray(
|
|
669
|
+
[sub_to_compound[int(s)] for s in sub_id], dtype=int,
|
|
670
|
+
)
|
|
671
|
+
for ck in np.unique(point_compound):
|
|
672
|
+
mask = point_compound == ck
|
|
673
|
+
renderer.draw_path(
|
|
674
|
+
x=x[mask], y=y[mask], path_id=sub_id[mask],
|
|
675
|
+
rule=rule, gp=gp,
|
|
676
|
+
)
|
|
585
677
|
|
|
586
678
|
# ---- rastergrob ------------------------------------------------------
|
|
587
679
|
elif cls == "rastergrob":
|
|
@@ -590,8 +682,9 @@ def _render_grob(
|
|
|
590
682
|
image = getattr(grob, "image", None)
|
|
591
683
|
if image is not None:
|
|
592
684
|
hj, vj = _resolve_just(grob)
|
|
593
|
-
raw_x = renderer.
|
|
594
|
-
|
|
685
|
+
raw_x, raw_y = renderer.resolve_loc(
|
|
686
|
+
getattr(grob, "x", 0.0), getattr(grob, "y", 0.0), gp=gp,
|
|
687
|
+
)
|
|
595
688
|
raw_w = renderer.resolve_w(getattr(grob, "width", 1.0), gp=gp)
|
|
596
689
|
raw_h = renderer.resolve_h(getattr(grob, "height", 1.0), gp=gp)
|
|
597
690
|
# `renderer.draw_raster` expects the *top-left* corner in y-down
|
grid_py/_primitives.py
CHANGED
|
@@ -56,6 +56,7 @@ __all__ = [
|
|
|
56
56
|
"arrows_grob",
|
|
57
57
|
"grid_arrows",
|
|
58
58
|
# points
|
|
59
|
+
"valid_pch",
|
|
59
60
|
"points_grob",
|
|
60
61
|
"grid_points",
|
|
61
62
|
# rect
|
|
@@ -706,6 +707,70 @@ def grid_arrows(
|
|
|
706
707
|
# ===================================================================== #
|
|
707
708
|
|
|
708
709
|
|
|
710
|
+
def valid_pch(pch: Any) -> Any:
|
|
711
|
+
"""Validate / normalise a plotting character (``pch``).
|
|
712
|
+
|
|
713
|
+
Faithful port of R's ``grid:::valid.pch`` (primitives.R:1504-1512)::
|
|
714
|
+
|
|
715
|
+
valid.pch <- function(pch) {
|
|
716
|
+
if (length(pch) == 0L) stop("zero-length 'pch'")
|
|
717
|
+
if (is.null(pch)) pch <- 1L
|
|
718
|
+
else if (!is.character(pch)) pch <- as.integer(pch)
|
|
719
|
+
pch
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
Semantics, verified against grid 4.5.3:
|
|
723
|
+
|
|
724
|
+
* **Zero-length** input (e.g. ``[]``, ``np.array([])``) → ``ValueError``.
|
|
725
|
+
(In R ``length(NULL) == 0`` so ``NULL`` also hits this branch and
|
|
726
|
+
errors; the ``is.null`` arm is effectively unreachable. We keep a
|
|
727
|
+
``None`` → ``1`` arm for ergonomics, matching the *written* R source
|
|
728
|
+
rather than its dead-code quirk, since ``None`` is the natural
|
|
729
|
+
Python sentinel for "use the default".)
|
|
730
|
+
* **Character** ``pch`` is kept *as character* (e.g. ``"."`` stays
|
|
731
|
+
``"."``; the engine later draws it as a glyph / tiny point).
|
|
732
|
+
* **Any other (numeric)** ``pch`` is coerced to ``int`` (truncating
|
|
733
|
+
floats, mirroring ``as.integer``).
|
|
734
|
+
* **Mixed** sequences containing any character element are kept as an
|
|
735
|
+
object array of per-point values (R's ``c(".", 19, "A")`` coerces
|
|
736
|
+
to the character vector ``c(".", "19", "A")``; we preserve each
|
|
737
|
+
element so the per-point dispatch can decide glyph-vs-symbol).
|
|
738
|
+
|
|
739
|
+
Returns the normalised ``pch``: an ``int``, a ``str``, or a numpy
|
|
740
|
+
array of per-point values (int dtype if all-numeric, else object).
|
|
741
|
+
"""
|
|
742
|
+
if pch is None:
|
|
743
|
+
return 1
|
|
744
|
+
|
|
745
|
+
if isinstance(pch, str):
|
|
746
|
+
return pch
|
|
747
|
+
|
|
748
|
+
if isinstance(pch, (int, np.integer)):
|
|
749
|
+
return int(pch)
|
|
750
|
+
if isinstance(pch, (float, np.floating)):
|
|
751
|
+
return int(pch) # as.integer truncates toward zero
|
|
752
|
+
|
|
753
|
+
if isinstance(pch, (list, tuple, np.ndarray)):
|
|
754
|
+
arr = np.atleast_1d(np.asarray(pch, dtype=object))
|
|
755
|
+
if arr.size == 0:
|
|
756
|
+
raise ValueError("zero-length 'pch'")
|
|
757
|
+
# If any element is a (non-numeric) string, R coerces the whole
|
|
758
|
+
# vector to character. We keep per-element values in an object
|
|
759
|
+
# array so the renderer can dispatch each point individually.
|
|
760
|
+
has_char = any(isinstance(v, (str, bytes, np.str_)) for v in arr)
|
|
761
|
+
if has_char:
|
|
762
|
+
return np.asarray(
|
|
763
|
+
[v if isinstance(v, (str, bytes, np.str_)) else str(v)
|
|
764
|
+
for v in arr],
|
|
765
|
+
dtype=object,
|
|
766
|
+
)
|
|
767
|
+
# All numeric → integer array (as.integer).
|
|
768
|
+
return np.asarray([int(v) for v in arr], dtype=int)
|
|
769
|
+
|
|
770
|
+
# Fallback for any other scalar numeric-like (e.g. numpy bool):
|
|
771
|
+
return int(pch)
|
|
772
|
+
|
|
773
|
+
|
|
709
774
|
def points_grob(
|
|
710
775
|
x: Any = None,
|
|
711
776
|
y: Any = None,
|
|
@@ -770,6 +835,8 @@ def points_grob(
|
|
|
770
835
|
size = Unit(1, "char")
|
|
771
836
|
else:
|
|
772
837
|
size = _ensure_unit(size, default_units)
|
|
838
|
+
# R: validDetails.points -> x$pch <- valid.pch(x$pch)
|
|
839
|
+
pch = valid_pch(pch)
|
|
773
840
|
return Grob(
|
|
774
841
|
x=x, y=y, pch=pch, size=size,
|
|
775
842
|
name=name, gp=gp, vp=vp, _grid_class="points",
|
grid_py/_renderer_base.py
CHANGED
|
@@ -91,7 +91,17 @@ class GridRenderer(ABC):
|
|
|
91
91
|
# containing width_cm, height_cm, rotation_angle, 3×3 transform matrix,
|
|
92
92
|
# and ViewportContext (xscale/yscale).
|
|
93
93
|
# The root entry represents the device itself.
|
|
94
|
-
|
|
94
|
+
# ``y_down`` is True for raster surfaces (PNG / ImageSurface),
|
|
95
|
+
# False for vector surfaces (PDF / SVG / PS) — matches R's
|
|
96
|
+
# per-device behaviour in src/viewport.c:initVP. We probe the
|
|
97
|
+
# subclass via ``_surface_type`` (CairoRenderer's flag); raster
|
|
98
|
+
# is the default when the subclass has not declared one.
|
|
99
|
+
_surface_type = getattr(self, "_surface_type", "image")
|
|
100
|
+
root_vtr = calc_root_transform(
|
|
101
|
+
self._device_width_cm, self._device_height_cm,
|
|
102
|
+
dev_units_per_inch=self._dev_units_per_inch,
|
|
103
|
+
y_down=(_surface_type == "image"),
|
|
104
|
+
)
|
|
95
105
|
self._vp_transform_stack: List[ViewportTransformResult] = [root_vtr]
|
|
96
106
|
|
|
97
107
|
# Keep a parallel list of viewport objects for attribute access
|
|
@@ -216,7 +226,14 @@ class GridRenderer(ABC):
|
|
|
216
226
|
# layout (for its children). In R, layout_pos determines the
|
|
217
227
|
# viewport's own size/position first, then the layout applies
|
|
218
228
|
# within that region.
|
|
219
|
-
|
|
229
|
+
#
|
|
230
|
+
# Per R src/layout.c:617-633 ``calcViewportLocationFromLayout``:
|
|
231
|
+
# exactly ONE of layoutPosRow / layoutPosCol may be NULL — that
|
|
232
|
+
# is interpreted as "occupy all rows/cols". We mirror the
|
|
233
|
+
# behaviour here so e.g. ``viewport(layout_pos_col=i)`` in a
|
|
234
|
+
# 1-row layout correctly spans the whole row instead of
|
|
235
|
+
# collapsing back to centre.
|
|
236
|
+
if layout_pos_row is not None or layout_pos_col is not None:
|
|
220
237
|
if self._layout_stack:
|
|
221
238
|
grid = self._layout_stack[-1]
|
|
222
239
|
col_starts = grid["col_starts"]
|
|
@@ -224,11 +241,19 @@ class GridRenderer(ABC):
|
|
|
224
241
|
row_starts = grid["row_starts"]
|
|
225
242
|
row_heights = grid["row_heights"]
|
|
226
243
|
|
|
227
|
-
if
|
|
244
|
+
if layout_pos_row is None:
|
|
245
|
+
# NULL → span all rows (R layout.c:621-624).
|
|
246
|
+
t = 0
|
|
247
|
+
b = len(row_heights) - 1
|
|
248
|
+
elif isinstance(layout_pos_row, (list, tuple)):
|
|
228
249
|
t, b = int(layout_pos_row[0]) - 1, int(layout_pos_row[1]) - 1
|
|
229
250
|
else:
|
|
230
251
|
t = b = int(layout_pos_row) - 1
|
|
231
|
-
if
|
|
252
|
+
if layout_pos_col is None:
|
|
253
|
+
# NULL → span all cols (R layout.c:628-631).
|
|
254
|
+
l = 0
|
|
255
|
+
r = len(col_widths) - 1
|
|
256
|
+
elif isinstance(layout_pos_col, (list, tuple)):
|
|
232
257
|
l, r = int(layout_pos_col[0]) - 1, int(layout_pos_col[1]) - 1
|
|
233
258
|
else:
|
|
234
259
|
l = r = int(layout_pos_col) - 1
|
|
@@ -857,18 +882,24 @@ class GridRenderer(ABC):
|
|
|
857
882
|
h_in = _details_inches(height_details, "y")
|
|
858
883
|
|
|
859
884
|
# hjust / vjust control which corner of the box is anchored at (x, y).
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
885
|
+
#
|
|
886
|
+
# Per R/just.R (4.5.3) ``resolveHJust`` / ``resolveVJust``:
|
|
887
|
+
# when ``hjust`` / ``vjust`` is NULL the value is derived from
|
|
888
|
+
# ``just`` (which may be a string like ``"left"`` or a 2-element
|
|
889
|
+
# vector). Previously this code looked at ``grob.hjust`` and
|
|
890
|
+
# ``grob.vjust`` only, so a grob built with
|
|
891
|
+
# ``rect_grob(just="left")`` (which stores ``hjust=None``,
|
|
892
|
+
# ``just="left"``) fell back to centre. Going through the
|
|
893
|
+
# shared ``resolve_hjust`` / ``resolve_vjust`` helpers brings
|
|
894
|
+
# the bbox computation in line with R for every just-string
|
|
895
|
+
# form (single string, tuple of strings, numeric, mixed).
|
|
896
|
+
from ._just import resolve_hjust, resolve_vjust
|
|
897
|
+
|
|
898
|
+
just = getattr(grob, "just", None)
|
|
899
|
+
if just is None:
|
|
900
|
+
just = "centre"
|
|
901
|
+
hjust = float(resolve_hjust(just, getattr(grob, "hjust", None)))
|
|
902
|
+
vjust = float(resolve_vjust(just, getattr(grob, "vjust", None)))
|
|
872
903
|
|
|
873
904
|
# Centre of the bounding box in inches.
|
|
874
905
|
cx = x_inches + (0.5 - hjust) * w_in
|
|
@@ -1090,7 +1121,16 @@ class GridRenderer(ABC):
|
|
|
1090
1121
|
return self.inches_to_dev_h(inches)
|
|
1091
1122
|
|
|
1092
1123
|
def resolve_x_array(self, val: Any, gp: Optional[Any] = None) -> "np.ndarray":
|
|
1093
|
-
"""Resolve *val* to an array of device x-coordinates.
|
|
1124
|
+
"""Resolve *val* to an array of device x-coordinates.
|
|
1125
|
+
|
|
1126
|
+
WARNING: this method evaluates each ``x_i`` against a hard-coded
|
|
1127
|
+
``y=0``. That is exact for an unrotated viewport, but for a
|
|
1128
|
+
viewport with non-zero ``angle`` the rotation component of the
|
|
1129
|
+
2-D CTM mixes the x and y axes — the y=0 stub then drops part
|
|
1130
|
+
of the answer. ``resolve_loc_array`` (below) does the
|
|
1131
|
+
rotation-correct pairing and is what every drawing site
|
|
1132
|
+
should use whenever an associated ``y`` array exists.
|
|
1133
|
+
"""
|
|
1094
1134
|
from ._units import Unit
|
|
1095
1135
|
if isinstance(val, Unit):
|
|
1096
1136
|
out = np.empty(len(val), dtype=float)
|
|
@@ -1103,8 +1143,90 @@ class GridRenderer(ABC):
|
|
|
1103
1143
|
return np.asarray([self.resolve_x(v, gp) for v in val], dtype=float)
|
|
1104
1144
|
return np.atleast_1d(np.asarray(val, dtype=float))
|
|
1105
1145
|
|
|
1146
|
+
def resolve_loc(
|
|
1147
|
+
self,
|
|
1148
|
+
x_val: Any,
|
|
1149
|
+
y_val: Any,
|
|
1150
|
+
gp: Optional[Any] = None,
|
|
1151
|
+
) -> Tuple[float, float]:
|
|
1152
|
+
"""Resolve an ``(x, y)`` location pair to device coordinates.
|
|
1153
|
+
|
|
1154
|
+
Port of R's ``transformLocn`` (src/unit.c) one-shot pairing —
|
|
1155
|
+
the inches values for both axes are passed through the
|
|
1156
|
+
viewport's 3×3 transform together so that rotation correctly
|
|
1157
|
+
mixes them.
|
|
1158
|
+
|
|
1159
|
+
Unlike ``resolve_x(val) + resolve_y(val)``, which each call
|
|
1160
|
+
``transform_loc_to_device`` with the orthogonal coord forced to
|
|
1161
|
+
0 and so cannot represent the rotation contribution, this
|
|
1162
|
+
helper preserves both inputs and matches ``device_loc``'s
|
|
1163
|
+
algorithm.
|
|
1164
|
+
|
|
1165
|
+
Every grob-drawing site with a genuine 2-D anchor (rect,
|
|
1166
|
+
circle, text, roundrect, raster, ...) should use this in
|
|
1167
|
+
preference to the single-axis helpers.
|
|
1168
|
+
"""
|
|
1169
|
+
x_in = self._resolve_to_inches(x_val, axis="x", is_dim=False, gp=gp)
|
|
1170
|
+
y_in = self._resolve_to_inches(y_val, axis="y", is_dim=False, gp=gp)
|
|
1171
|
+
return self.transform_loc_to_device(x_in, y_in)
|
|
1172
|
+
|
|
1173
|
+
def resolve_loc_array(
|
|
1174
|
+
self,
|
|
1175
|
+
x_vals: Any,
|
|
1176
|
+
y_vals: Any,
|
|
1177
|
+
gp: Optional[Any] = None,
|
|
1178
|
+
) -> Tuple["np.ndarray", "np.ndarray"]:
|
|
1179
|
+
"""Resolve parallel ``x`` / ``y`` arrays to device coord pairs.
|
|
1180
|
+
|
|
1181
|
+
Each ``(x_i, y_i)`` pair is mapped through the full 2-D vp
|
|
1182
|
+
transform via ``transform_loc_to_device``, so polygon / lines /
|
|
1183
|
+
segments / points vertices stay self-consistent under
|
|
1184
|
+
rotation. Length follows R's recycling rule — the longer of
|
|
1185
|
+
``len(x_vals)`` and ``len(y_vals)``.
|
|
1186
|
+
"""
|
|
1187
|
+
from ._units import Unit
|
|
1188
|
+
|
|
1189
|
+
def _length(v: Any) -> int:
|
|
1190
|
+
return len(v) if hasattr(v, "__len__") and not isinstance(v, str) else 1
|
|
1191
|
+
|
|
1192
|
+
nx = _length(x_vals)
|
|
1193
|
+
ny = _length(y_vals)
|
|
1194
|
+
n = max(nx, ny)
|
|
1195
|
+
|
|
1196
|
+
out_x = np.empty(n, dtype=float)
|
|
1197
|
+
out_y = np.empty(n, dtype=float)
|
|
1198
|
+
|
|
1199
|
+
for i in range(n):
|
|
1200
|
+
# x_i in inches (viewport-local frame)
|
|
1201
|
+
if isinstance(x_vals, Unit):
|
|
1202
|
+
x_in = self._resolve_to_inches_idx(x_vals, i % nx, "x", False, gp)
|
|
1203
|
+
elif isinstance(x_vals, (list, tuple, np.ndarray)):
|
|
1204
|
+
x_in = self._resolve_to_inches(
|
|
1205
|
+
x_vals[i % nx], axis="x", is_dim=False, gp=gp,
|
|
1206
|
+
)
|
|
1207
|
+
else:
|
|
1208
|
+
x_in = self._resolve_to_inches(x_vals, axis="x", is_dim=False, gp=gp)
|
|
1209
|
+
# y_i in inches
|
|
1210
|
+
if isinstance(y_vals, Unit):
|
|
1211
|
+
y_in = self._resolve_to_inches_idx(y_vals, i % ny, "y", False, gp)
|
|
1212
|
+
elif isinstance(y_vals, (list, tuple, np.ndarray)):
|
|
1213
|
+
y_in = self._resolve_to_inches(
|
|
1214
|
+
y_vals[i % ny], axis="y", is_dim=False, gp=gp,
|
|
1215
|
+
)
|
|
1216
|
+
else:
|
|
1217
|
+
y_in = self._resolve_to_inches(y_vals, axis="y", is_dim=False, gp=gp)
|
|
1218
|
+
|
|
1219
|
+
out_x[i], out_y[i] = self.transform_loc_to_device(x_in, y_in)
|
|
1220
|
+
|
|
1221
|
+
return out_x, out_y
|
|
1222
|
+
|
|
1106
1223
|
def resolve_y_array(self, val: Any, gp: Optional[Any] = None) -> "np.ndarray":
|
|
1107
|
-
"""Resolve *val* to an array of device y-coordinates.
|
|
1224
|
+
"""Resolve *val* to an array of device y-coordinates.
|
|
1225
|
+
|
|
1226
|
+
Like ``resolve_x_array`` this loses the rotation contribution
|
|
1227
|
+
from the orthogonal axis (x is forced to 0). Prefer
|
|
1228
|
+
``resolve_loc_array`` whenever a paired x is available.
|
|
1229
|
+
"""
|
|
1108
1230
|
from ._units import Unit
|
|
1109
1231
|
if isinstance(val, Unit):
|
|
1110
1232
|
out = np.empty(len(val), dtype=float)
|
grid_py/_state.py
CHANGED
|
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
import copy
|
|
18
18
|
from collections import deque
|
|
19
|
+
from types import SimpleNamespace
|
|
19
20
|
from typing import Any, Deque, Dict, List, Optional, Sequence, Tuple, Union
|
|
20
21
|
|
|
21
22
|
import numpy as np
|
|
@@ -50,15 +51,31 @@ def _make_root_viewport() -> Any:
|
|
|
50
51
|
dict
|
|
51
52
|
A mapping that quacks like a viewport for bootstrap purposes.
|
|
52
53
|
"""
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
54
|
+
# Default xscale/yscale = (0, 1). At ``init_device`` time these get
|
|
55
|
+
# overwritten with (0, dev_w_px) / (0, dev_h_px) to mirror R's
|
|
56
|
+
# ``initVP`` (src/viewport.c:399-406):
|
|
57
|
+
# REAL(xscale)[0] = dd->dev->left;
|
|
58
|
+
# REAL(xscale)[1] = dd->dev->right;
|
|
59
|
+
# REAL(yscale)[0] = dd->dev->bottom;
|
|
60
|
+
# REAL(yscale)[1] = dd->dev->top;
|
|
61
|
+
# which is what makes ``unit(..., "native")`` resolve to device pixels
|
|
62
|
+
# at the root viewport.
|
|
63
|
+
#
|
|
64
|
+
# We expose the root as a ``SimpleNamespace`` so callers can use the
|
|
65
|
+
# R-style ``vp.xscale`` / ``vp.yscale`` attribute access (mirroring
|
|
66
|
+
# ``current.viewport()$xscale`` in R) while still keeping the duck-
|
|
67
|
+
# typed dict-style ``_vp_attr`` helper.
|
|
68
|
+
return SimpleNamespace(
|
|
69
|
+
name="ROOT",
|
|
70
|
+
parent=None,
|
|
71
|
+
children=[],
|
|
72
|
+
layout_pos=None,
|
|
73
|
+
gpar=Gpar(),
|
|
74
|
+
rotation=0.0,
|
|
75
|
+
transform=np.eye(3, dtype=np.float64),
|
|
76
|
+
xscale=(0.0, 1.0),
|
|
77
|
+
yscale=(0.0, 1.0),
|
|
78
|
+
)
|
|
62
79
|
|
|
63
80
|
|
|
64
81
|
def _vp_attr(vp: Any, attr: str, default: Any = None) -> Any:
|
|
@@ -508,10 +525,25 @@ class GridState:
|
|
|
508
525
|
float
|
|
509
526
|
The sum of ``rotation`` attributes from root to current viewport.
|
|
510
527
|
"""
|
|
528
|
+
# R always treats a missing or NULL rotation as 0 (src/viewport.c:
|
|
529
|
+
# initVP / addRotation paths default angle=0). Mirror that here
|
|
530
|
+
# without try/except.
|
|
531
|
+
#
|
|
532
|
+
# Two attribute names coexist in grid_py for historical reasons:
|
|
533
|
+
# ``Viewport`` instances expose the user-supplied angle as
|
|
534
|
+
# ``vp.angle`` (matching R's ``viewport(angle=)`` argument), while
|
|
535
|
+
# the root SimpleNamespace + pushedvp metadata uses ``rotation``.
|
|
536
|
+
# Both designate the same quantity — this viewport's own rotation
|
|
537
|
+
# contribution — so we read whichever is present, preferring
|
|
538
|
+
# ``angle`` for Viewport / R parity. R itself walks the vp stack
|
|
539
|
+
# summing each vp's ``angle`` slot, which is what we mirror.
|
|
511
540
|
total: float = 0.0
|
|
512
541
|
vp: Any = self._current_vp
|
|
513
542
|
while vp is not None:
|
|
514
|
-
|
|
543
|
+
rot = _vp_attr(vp, "angle", None)
|
|
544
|
+
if rot is None:
|
|
545
|
+
rot = _vp_attr(vp, "rotation", 0.0)
|
|
546
|
+
total += float(rot if rot is not None else 0.0)
|
|
515
547
|
vp = _vp_parent(vp)
|
|
516
548
|
return total
|
|
517
549
|
|
|
@@ -686,6 +718,32 @@ class GridState:
|
|
|
686
718
|
self._device_width_cm = float(width_cm)
|
|
687
719
|
self._device_height_cm = float(height_cm)
|
|
688
720
|
|
|
721
|
+
# Mirror R src/viewport.c:initVP — set the ROOT viewport's
|
|
722
|
+
# xscale/yscale to the device coord range. The orientation
|
|
723
|
+
# follows the device convention:
|
|
724
|
+
# raster (PNG, CairoImage): pixel coords, y INCREASES DOWN
|
|
725
|
+
# → xscale=(0, w_px), yscale=(h_px, 0)
|
|
726
|
+
# vector (PDF, SVG, PS): point coords, y INCREASES UP
|
|
727
|
+
# → xscale=(0, w_pt), yscale=(0, h_pt)
|
|
728
|
+
# The renderer's ``_surface_type`` ("image" vs anything else)
|
|
729
|
+
# tells us which axis convention to use. This keeps the dict-
|
|
730
|
+
# accessible xscale/yscale in sync with the transform stack's
|
|
731
|
+
# ``vpc`` (set by ``calc_root_transform`` in _vp_calc.py).
|
|
732
|
+
dpi = float(getattr(renderer, "dpi", 0.0) or 0.0)
|
|
733
|
+
if dpi > 0:
|
|
734
|
+
surface_type = getattr(renderer, "_surface_type", "image")
|
|
735
|
+
if surface_type == "image":
|
|
736
|
+
dev_w = self._device_width_cm / 2.54 * dpi
|
|
737
|
+
dev_h = self._device_height_cm / 2.54 * dpi
|
|
738
|
+
_vp_set_attr(self._vp_tree, "xscale", (0.0, dev_w))
|
|
739
|
+
_vp_set_attr(self._vp_tree, "yscale", (dev_h, 0.0))
|
|
740
|
+
else:
|
|
741
|
+
# Vector surface: 72 user units per inch, y-up.
|
|
742
|
+
dev_w = self._device_width_cm / 2.54 * 72.0
|
|
743
|
+
dev_h = self._device_height_cm / 2.54 * 72.0
|
|
744
|
+
_vp_set_attr(self._vp_tree, "xscale", (0.0, dev_w))
|
|
745
|
+
_vp_set_attr(self._vp_tree, "yscale", (0.0, dev_h))
|
|
746
|
+
|
|
689
747
|
def get_renderer(self) -> Any:
|
|
690
748
|
"""Return the current rendering backend.
|
|
691
749
|
|
grid_py/_vp_calc.py
CHANGED
|
@@ -955,16 +955,58 @@ def calc_viewport_transform(
|
|
|
955
955
|
def calc_root_transform(
|
|
956
956
|
device_width_cm: float,
|
|
957
957
|
device_height_cm: float,
|
|
958
|
+
dev_units_per_inch: Optional[float] = None,
|
|
959
|
+
y_down: bool = True,
|
|
958
960
|
) -> ViewportTransformResult:
|
|
959
961
|
"""Compute the root (device-level) viewport transform.
|
|
960
962
|
|
|
961
|
-
Port of ``viewport.c:
|
|
962
|
-
|
|
963
|
+
Port of ``viewport.c:initVP`` (src/viewport.c:384-420 in R 4.5.3).
|
|
964
|
+
After the R-level ``grid.top.level.vp()`` constructs a default
|
|
965
|
+
viewport (xscale=c(0,1), yscale=c(0,1)), R immediately overrides
|
|
966
|
+
those scales with the device coordinate range::
|
|
967
|
+
|
|
968
|
+
REAL(xscale)[0] = dd->dev->left;
|
|
969
|
+
REAL(xscale)[1] = dd->dev->right;
|
|
970
|
+
REAL(yscale)[0] = dd->dev->bottom;
|
|
971
|
+
REAL(yscale)[1] = dd->dev->top;
|
|
972
|
+
|
|
973
|
+
For a raster device the range is in pixels; for a vector device it
|
|
974
|
+
is in points (1/72 inch). In grid_py the unit-per-inch is encoded
|
|
975
|
+
by *dev_units_per_inch*: ``dpi`` for raster surfaces, ``72`` for
|
|
976
|
+
vector. When that argument is omitted we fall back to (0, 1) —
|
|
977
|
+
the historical behaviour — purely to avoid breaking callers that
|
|
978
|
+
don't yet thread dpi through; new code should pass it.
|
|
979
|
+
|
|
980
|
+
Parameters
|
|
981
|
+
----------
|
|
982
|
+
device_width_cm, device_height_cm
|
|
983
|
+
Physical canvas size in centimetres.
|
|
984
|
+
dev_units_per_inch
|
|
985
|
+
Device units per inch (``dpi`` for raster, ``72`` for vector).
|
|
986
|
+
When supplied, ``xscale=(0, dev_w_px)`` and
|
|
987
|
+
``yscale=(0, dev_h_px)`` so that ``unit(v, "native")`` at the
|
|
988
|
+
root viewport resolves to device pixels exactly as in R.
|
|
963
989
|
"""
|
|
990
|
+
if dev_units_per_inch is not None and dev_units_per_inch > 0:
|
|
991
|
+
dev_w_px = (device_width_cm / 2.54) * dev_units_per_inch
|
|
992
|
+
dev_h_px = (device_height_cm / 2.54) * dev_units_per_inch
|
|
993
|
+
xscale = (0.0, float(dev_w_px))
|
|
994
|
+
# R's PNG / raster devices have ``dev->bottom = h_px`` and
|
|
995
|
+
# ``dev->top = 0`` (pixel y increases downward), so initVP
|
|
996
|
+
# produces ``yscale = (h_px, 0)``. Vector devices (PDF, PS)
|
|
997
|
+
# are bottom-up: ``yscale = (0, h_pt)``. Mirror both.
|
|
998
|
+
if y_down:
|
|
999
|
+
yscale = (float(dev_h_px), 0.0)
|
|
1000
|
+
else:
|
|
1001
|
+
yscale = (0.0, float(dev_h_px))
|
|
1002
|
+
else:
|
|
1003
|
+
xscale = (0.0, 1.0)
|
|
1004
|
+
yscale = (0.0, 1.0)
|
|
1005
|
+
|
|
964
1006
|
return ViewportTransformResult(
|
|
965
1007
|
width_cm=device_width_cm,
|
|
966
1008
|
height_cm=device_height_cm,
|
|
967
1009
|
rotation_angle=0.0,
|
|
968
1010
|
transform=identity(),
|
|
969
|
-
vpc=ViewportContext(xscale=
|
|
1011
|
+
vpc=ViewportContext(xscale=xscale, yscale=yscale),
|
|
970
1012
|
)
|
grid_py/renderer.py
CHANGED
|
@@ -274,7 +274,11 @@ class CairoRenderer(GridRenderer):
|
|
|
274
274
|
from ._vp_calc import calc_root_transform
|
|
275
275
|
mask_w_in = float(dw) / self.dpi
|
|
276
276
|
mask_h_in = float(dh) / self.dpi
|
|
277
|
-
|
|
277
|
+
# Mask surfaces are raster (Cairo IMAGE) → use dpi as device units/in.
|
|
278
|
+
root_vtr = calc_root_transform(
|
|
279
|
+
mask_w_in * 2.54, mask_h_in * 2.54,
|
|
280
|
+
dev_units_per_inch=self.dpi,
|
|
281
|
+
)
|
|
278
282
|
mask_renderer._vp_transform_stack = [root_vtr]
|
|
279
283
|
mask_renderer._vp_obj_stack = [None]
|
|
280
284
|
mask_renderer._layout_stack = []
|
|
@@ -353,28 +357,48 @@ class CairoRenderer(GridRenderer):
|
|
|
353
357
|
|
|
354
358
|
# ---- gpar application --------------------------------------------------
|
|
355
359
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
360
|
+
# R-grid lwd unit conversion factor.
|
|
361
|
+
# ----------------------------------------------------------------
|
|
362
|
+
# R's gpar() lwd argument is in **1/96 inch** (per ?gpar in R 3.0+).
|
|
363
|
+
# Internally R's Cairo device (src/library/grDevices/src/cairoFns.c)
|
|
364
|
+
# converts lwd to cairo user-space (= points) using
|
|
365
|
+
# cairo_user = lwd * 72.27 / 96
|
|
366
|
+
# — the 72.27 is the TeX big-point per inch constant R inherits
|
|
367
|
+
# from its PostScript ancestry, retained for backward parity with
|
|
368
|
+
# R's vector devices. Reproducing the same constant here keeps
|
|
369
|
+
# stroke widths bit-for-bit identical with R Cairo across raster
|
|
370
|
+
# and vector surfaces.
|
|
371
|
+
_LWD_BIGPOINT_PER_96TH_INCH: float = 72.27 / 96.0
|
|
372
|
+
|
|
373
|
+
def _lwd_to_user(self, lwd: float) -> float:
|
|
374
|
+
"""Convert R-grid lwd (units of 1/96 inch) to a Cairo user-space
|
|
375
|
+
line width.
|
|
376
|
+
|
|
377
|
+
Earlier this method treated ``lwd`` as if it were in points
|
|
378
|
+
(1/72 inch). Empirical R-vs-grid_py measurement shows every
|
|
379
|
+
stroke is then 96/72 = 1.333× wider than R's — a port bug
|
|
380
|
+
affecting *all* primitives that stroke (rect / circle /
|
|
381
|
+
polygon / lines / segments / points-pch / arrow). R's actual
|
|
382
|
+
convention is lwd = 1/96 inch, mirrored here via
|
|
383
|
+
``_LWD_BIGPOINT_PER_96TH_INCH``.
|
|
384
|
+
|
|
385
|
+
Pipeline:
|
|
386
|
+
|
|
387
|
+
1. Convert ``lwd × (72.27/96)`` (TeX points) to device units —
|
|
388
|
+
raster surfaces use ``dpi/72``; vector surfaces keep the
|
|
389
|
+
value in points.
|
|
390
|
+
2. Run the result through ``device_to_user_distance`` so any
|
|
391
|
+
CTM scaling (rotated/scaled viewport) does not also scale
|
|
392
|
+
the stroke. This matches R's behaviour: ``lwd`` is a
|
|
393
|
+
device-level width, independent of viewport coordinate
|
|
394
|
+
changes.
|
|
373
395
|
"""
|
|
396
|
+
# 1/96 inch → TeX big-points (the unit R hands to Cairo)
|
|
397
|
+
lwd_bp = lwd * self._LWD_BIGPOINT_PER_96TH_INCH
|
|
374
398
|
if self._surface_type == "image":
|
|
375
|
-
lw_dev =
|
|
399
|
+
lw_dev = lwd_bp * self.dpi / 72.0
|
|
376
400
|
else:
|
|
377
|
-
lw_dev =
|
|
401
|
+
lw_dev = lwd_bp
|
|
378
402
|
try:
|
|
379
403
|
ux, uy = self._ctx.device_to_user_distance(lw_dev, lw_dev)
|
|
380
404
|
return max(abs(ux), abs(uy))
|
|
@@ -840,10 +864,46 @@ class CairoRenderer(GridRenderer):
|
|
|
840
864
|
vjust: float = 0.5,
|
|
841
865
|
gp: Optional[Gpar] = None,
|
|
842
866
|
) -> None:
|
|
843
|
-
"""Draw a rectangle.
|
|
867
|
+
"""Draw a rectangle.
|
|
868
|
+
|
|
869
|
+
Parameters
|
|
870
|
+
----------
|
|
871
|
+
x, y : float
|
|
872
|
+
Anchor point in **device coords** (already mapped through
|
|
873
|
+
the viewport CTM, including rotation, by
|
|
874
|
+
``resolve_loc_array``).
|
|
875
|
+
w, h : float
|
|
876
|
+
Width / height in device units of the viewport-local
|
|
877
|
+
(i.e. unrotated) frame.
|
|
878
|
+
hjust, vjust : float
|
|
879
|
+
Justification of the rect relative to ``(x, y)``.
|
|
880
|
+
gp : Gpar, optional
|
|
881
|
+
Graphical parameters.
|
|
882
|
+
|
|
883
|
+
Implementation note
|
|
884
|
+
-------------------
|
|
885
|
+
``(x, y)`` is the *rotated* anchor and ``(w, h)`` are the
|
|
886
|
+
*unrotated* dimensions, so we still have to orient the box.
|
|
887
|
+
We rotate the Cairo CTM around the anchor by the cumulative
|
|
888
|
+
viewport rotation; the axis-aligned ``ctx.rectangle`` then
|
|
889
|
+
becomes the rotated rect in device space. This matches R's
|
|
890
|
+
grid C engine which applies the device CTM (including
|
|
891
|
+
rotation) at draw time — see ``src/grid.c``'s ``L_rect``
|
|
892
|
+
+ the LTransform machinery in ``viewport.c``.
|
|
893
|
+
"""
|
|
894
|
+
from ._state import get_state
|
|
895
|
+
|
|
844
896
|
ctx = self._ctx
|
|
845
897
|
ctx.save()
|
|
846
898
|
|
|
899
|
+
rotation_deg = float(get_state().current_rotation())
|
|
900
|
+
if abs(rotation_deg) > 1e-9:
|
|
901
|
+
# Cairo's y axis grows downward, R-grid's grows upward —
|
|
902
|
+
# invert sign so a positive R angle visually matches.
|
|
903
|
+
ctx.translate(x, y)
|
|
904
|
+
ctx.rotate(-math.radians(rotation_deg))
|
|
905
|
+
ctx.translate(-x, -y)
|
|
906
|
+
|
|
847
907
|
# x,y is the anchor point; apply justification to get top-left
|
|
848
908
|
dx = x - w * hjust
|
|
849
909
|
dy = y - h * (1.0 - vjust) # device y increases downward
|
|
@@ -1366,6 +1426,85 @@ class CairoRenderer(GridRenderer):
|
|
|
1366
1426
|
|
|
1367
1427
|
ctx.restore()
|
|
1368
1428
|
|
|
1429
|
+
def _draw_pch_dot(
|
|
1430
|
+
self,
|
|
1431
|
+
ctx: Any,
|
|
1432
|
+
cx: float,
|
|
1433
|
+
cy: float,
|
|
1434
|
+
r: float,
|
|
1435
|
+
col_rgba: Tuple[float, float, float, float],
|
|
1436
|
+
cex: float = 1.0,
|
|
1437
|
+
) -> None:
|
|
1438
|
+
"""Draw R's ``pch="."`` — a *tiny* filled point.
|
|
1439
|
+
|
|
1440
|
+
R's engine (``GESymbol``) draws ``'.'`` as a small filled
|
|
1441
|
+
**rectangle** whose side is an engine constant (~0.01 inch)
|
|
1442
|
+
scaled by ``cex``, **independent of the symbol size**. Verified
|
|
1443
|
+
against grid 4.5.3: at 72 dpi the dot is ~1px regardless of the
|
|
1444
|
+
points-grob ``size``, growing only with large ``cex``. We
|
|
1445
|
+
mirror that by drawing a small filled square (a circle would
|
|
1446
|
+
antialias away at sub-pixel radius), so it reads as a single
|
|
1447
|
+
dot rather than a full symbol-19 disc or a full-size glyph.
|
|
1448
|
+
|
|
1449
|
+
``r`` (the resolved symbol radius) is intentionally unused for the
|
|
1450
|
+
dot's *size* — kept in the signature for call-site symmetry.
|
|
1451
|
+
"""
|
|
1452
|
+
# ~0.01 inch side at cex=1, in device units. For image surfaces
|
|
1453
|
+
# device units are pixels (dpi-scaled); for vector surfaces they
|
|
1454
|
+
# are points (72 per inch). Floor at 1 device unit so the dot is
|
|
1455
|
+
# always a solidly-visible pixel (matching R, whose '.' never
|
|
1456
|
+
# vanishes), exactly as R's GERect renders a minimum dot.
|
|
1457
|
+
if self._surface_type == "image":
|
|
1458
|
+
side = 0.01 * self.dpi * cex
|
|
1459
|
+
else:
|
|
1460
|
+
side = 0.01 * 72.0 * cex
|
|
1461
|
+
side = max(side, 1.0)
|
|
1462
|
+
half = side / 2.0
|
|
1463
|
+
if col_rgba[3] > 0:
|
|
1464
|
+
ctx.save()
|
|
1465
|
+
ctx.new_path()
|
|
1466
|
+
ctx.rectangle(cx - half, cy - half, side, side)
|
|
1467
|
+
ctx.set_source_rgba(*col_rgba)
|
|
1468
|
+
ctx.fill()
|
|
1469
|
+
ctx.restore()
|
|
1470
|
+
|
|
1471
|
+
def _draw_pch_glyph(
|
|
1472
|
+
self,
|
|
1473
|
+
ctx: Any,
|
|
1474
|
+
ch: str,
|
|
1475
|
+
cx: float,
|
|
1476
|
+
cy: float,
|
|
1477
|
+
r: float,
|
|
1478
|
+
col_rgba: Tuple[float, float, float, float],
|
|
1479
|
+
gp: Optional[Gpar] = None,
|
|
1480
|
+
) -> None:
|
|
1481
|
+
"""Draw a single-character pch (e.g. ``"A"``) as a glyph.
|
|
1482
|
+
|
|
1483
|
+
R's engine renders a non-``'.'`` character pch as that glyph,
|
|
1484
|
+
horizontally and vertically centred on the point, sized by the
|
|
1485
|
+
font (``cex * fontsize``). We reuse the renderer's font setup
|
|
1486
|
+
and centre the glyph on ``(cx, cy)`` via its ink extents — the
|
|
1487
|
+
same centring R uses for symbol text.
|
|
1488
|
+
"""
|
|
1489
|
+
if not ch or col_rgba[3] <= 0:
|
|
1490
|
+
return
|
|
1491
|
+
ctx.save()
|
|
1492
|
+
self._set_font(gp)
|
|
1493
|
+
ext = ctx.text_extents(ch)
|
|
1494
|
+
# Centre the glyph's ink box on (cx, cy). text_extents gives
|
|
1495
|
+
# x_bearing/y_bearing relative to the pen origin; centring the
|
|
1496
|
+
# ink box requires offsetting by bearing + half-extent.
|
|
1497
|
+
off_x = -(ext.x_bearing + ext.width / 2.0)
|
|
1498
|
+
off_y = -(ext.y_bearing + ext.height / 2.0)
|
|
1499
|
+
ctx.set_source_rgba(*col_rgba)
|
|
1500
|
+
ctx.move_to(cx + off_x, cy + off_y)
|
|
1501
|
+
if self._path_collecting:
|
|
1502
|
+
ctx.text_path(ch)
|
|
1503
|
+
ctx.fill()
|
|
1504
|
+
else:
|
|
1505
|
+
ctx.show_text(ch)
|
|
1506
|
+
ctx.restore()
|
|
1507
|
+
|
|
1369
1508
|
def draw_points(
|
|
1370
1509
|
self,
|
|
1371
1510
|
x: np.ndarray,
|
|
@@ -1397,24 +1536,61 @@ class CairoRenderer(GridRenderer):
|
|
|
1397
1536
|
return
|
|
1398
1537
|
|
|
1399
1538
|
# --- per-point pch array ---
|
|
1539
|
+
# ``pch`` may be a numeric symbol code (0-25), a single
|
|
1540
|
+
# character (a glyph such as "A", or "." for R's tiny point),
|
|
1541
|
+
# or a MIXED per-point sequence (e.g. [".", 19, "A"]). R keeps
|
|
1542
|
+
# character pch as character and coerces numerics to int
|
|
1543
|
+
# (grid:::valid.pch), so the value reaching us is already
|
|
1544
|
+
# normalised — we must NOT force ``dtype=int`` (it crashes on
|
|
1545
|
+
# "."), only preserve each element as int-or-str in an object
|
|
1546
|
+
# array and dispatch per point below.
|
|
1400
1547
|
if isinstance(pch, (list, tuple, np.ndarray)):
|
|
1401
|
-
pch_arr = np.asarray(pch, dtype=
|
|
1548
|
+
pch_arr = np.atleast_1d(np.asarray(pch, dtype=object))
|
|
1402
1549
|
else:
|
|
1403
|
-
pch_arr = np.
|
|
1550
|
+
pch_arr = np.array([pch], dtype=object)
|
|
1551
|
+
# Coerce numeric-looking scalars to int once, leave strings as-is.
|
|
1552
|
+
pch_arr = np.array(
|
|
1553
|
+
[int(p) if isinstance(p, (int, float, np.integer, np.floating))
|
|
1554
|
+
else p for p in pch_arr],
|
|
1555
|
+
dtype=object,
|
|
1556
|
+
)
|
|
1404
1557
|
if len(pch_arr) < n:
|
|
1405
1558
|
pch_arr = np.resize(pch_arr, n)
|
|
1406
1559
|
|
|
1407
|
-
# --- per-point sizes
|
|
1560
|
+
# --- per-point sizes ---------------------------------------
|
|
1561
|
+
# Two paths, distinguished by where the size originated:
|
|
1562
|
+
#
|
|
1563
|
+
# (1) ``gp.fontsize`` is set → ``size_arr`` is in points
|
|
1564
|
+
# (R: cex * fontsize), so we convert pt → device pixels
|
|
1565
|
+
# via ``dpi/72`` and halve to get a radius.
|
|
1566
|
+
#
|
|
1567
|
+
# (2) ``gp.fontsize`` is absent → ``size`` is what _draw.py
|
|
1568
|
+
# resolved from ``grob.size`` via ``renderer.resolve_w``,
|
|
1569
|
+
# which already returns DEVICE PIXELS. In that case the
|
|
1570
|
+
# previous code re-applied the dpi/72 scaling, producing
|
|
1571
|
+
# symbols ~``dpi/72``× too large (e.g. at 150 dpi the
|
|
1572
|
+
# diameter came out ~2× R). Mirror R's points.c which
|
|
1573
|
+
# uses the size unit-resolved value directly as the
|
|
1574
|
+
# symbol diameter, so radius = size / 2.
|
|
1575
|
+
size_from_fontsize: bool
|
|
1408
1576
|
fs = gp.get("fontsize", None) if gp else None
|
|
1409
1577
|
if isinstance(fs, (list, tuple, np.ndarray)):
|
|
1410
1578
|
size_arr = np.asarray(fs, dtype=float)
|
|
1579
|
+
size_from_fontsize = True
|
|
1411
1580
|
elif fs is not None:
|
|
1412
1581
|
size_arr = np.full(n, float(fs))
|
|
1582
|
+
size_from_fontsize = True
|
|
1413
1583
|
else:
|
|
1414
1584
|
size_arr = np.full(n, float(size))
|
|
1585
|
+
size_from_fontsize = False
|
|
1415
1586
|
|
|
1416
|
-
|
|
1417
|
-
|
|
1587
|
+
if size_from_fontsize:
|
|
1588
|
+
# fontsize is in points → convert to device-pixel radius.
|
|
1589
|
+
scale = (self.dpi / 72.0 * 0.5
|
|
1590
|
+
if self._surface_type == "image" else 0.5)
|
|
1591
|
+
else:
|
|
1592
|
+
# ``size`` already in device pixels (diameter) → radius=size/2.
|
|
1593
|
+
scale = 0.5
|
|
1418
1594
|
|
|
1419
1595
|
# --- per-point colours (col) ---
|
|
1420
1596
|
col_raw = gp.get("col", None) if gp else None
|
|
@@ -1445,17 +1621,49 @@ class CairoRenderer(GridRenderer):
|
|
|
1445
1621
|
else:
|
|
1446
1622
|
lwd_arr = np.full(n, 1.0)
|
|
1447
1623
|
|
|
1624
|
+
# --- per-point cex (used only for the pch="." dot size) ---
|
|
1625
|
+
cex_raw = gp.get("cex", None) if gp else None
|
|
1626
|
+
if isinstance(cex_raw, (list, tuple, np.ndarray)):
|
|
1627
|
+
cex_arr = np.asarray(cex_raw, dtype=float)
|
|
1628
|
+
elif cex_raw is not None:
|
|
1629
|
+
cex_arr = np.full(n, float(cex_raw))
|
|
1630
|
+
else:
|
|
1631
|
+
cex_arr = np.full(n, 1.0)
|
|
1632
|
+
|
|
1448
1633
|
for i in range(n):
|
|
1449
1634
|
cx = x[i]
|
|
1450
1635
|
cy = y[i]
|
|
1451
1636
|
r = size_arr[i] * scale if i < len(size_arr) else size * scale
|
|
1452
1637
|
lwd_i = float(lwd_arr[i % len(lwd_arr)])
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1638
|
+
pch_i = pch_arr[i]
|
|
1639
|
+
if isinstance(pch_i, (str, bytes, np.str_)):
|
|
1640
|
+
# Character pch. R's engine takes only the FIRST
|
|
1641
|
+
# character of the string for a per-point glyph.
|
|
1642
|
+
ch = str(pch_i)
|
|
1643
|
+
if ch == ".":
|
|
1644
|
+
# R's pch="." draws a tiny filled point (a small
|
|
1645
|
+
# engine-constant rect, scaled by cex). Render a
|
|
1646
|
+
# tiny filled circle so it reads as a single pixel-
|
|
1647
|
+
# scale dot rather than the symbol-19 disc or a
|
|
1648
|
+
# full-size glyph. ``r`` is the symbol radius; the
|
|
1649
|
+
# dot is a small fraction of it (matching R's "."
|
|
1650
|
+
# which is visually ~1px at default cex).
|
|
1651
|
+
self._draw_pch_dot(
|
|
1652
|
+
ctx, cx, cy, r, col_rgba=col_list[i],
|
|
1653
|
+
cex=float(cex_arr[i % len(cex_arr)]),
|
|
1654
|
+
)
|
|
1655
|
+
else:
|
|
1656
|
+
self._draw_pch_glyph(
|
|
1657
|
+
ctx, ch[:1], cx, cy, r,
|
|
1658
|
+
col_rgba=col_list[i], gp=gp,
|
|
1659
|
+
)
|
|
1660
|
+
else:
|
|
1661
|
+
self._draw_pch_shape(
|
|
1662
|
+
ctx, int(pch_i), cx, cy, r,
|
|
1663
|
+
col_rgba=col_list[i],
|
|
1664
|
+
fill_rgba=fill_list[i],
|
|
1665
|
+
lwd=lwd_i,
|
|
1666
|
+
)
|
|
1459
1667
|
|
|
1460
1668
|
ctx.restore()
|
|
1461
1669
|
|
|
@@ -1476,6 +1684,18 @@ class CairoRenderer(GridRenderer):
|
|
|
1476
1684
|
# Handle colour string arrays (e.g. from colourbar raster)
|
|
1477
1685
|
# Convert colour strings to uint8 RGBA
|
|
1478
1686
|
if img_array.dtype.kind in ("U", "S", "O"):
|
|
1687
|
+
# A colour-string raster must be 2-D (rows × cols of colours).
|
|
1688
|
+
# A 1-D vector here means an upstream bug fed a flat colour
|
|
1689
|
+
# vector instead of a matrix. Fail loud rather than silently
|
|
1690
|
+
# coercing it to a 1-row strip (that would mask the caller's
|
|
1691
|
+
# error and produce wrong output).
|
|
1692
|
+
if img_array.ndim != 2:
|
|
1693
|
+
ctx.restore()
|
|
1694
|
+
raise ValueError(
|
|
1695
|
+
"raster image must be 2-D (got shape "
|
|
1696
|
+
f"{img_array.shape}); a colour-string raster needs "
|
|
1697
|
+
"rows × columns of colours, not a flat vector"
|
|
1698
|
+
)
|
|
1479
1699
|
h_img, w_img = img_array.shape[:2]
|
|
1480
1700
|
rgba = np.zeros((h_img, w_img, 4), dtype=np.uint8)
|
|
1481
1701
|
for r in range(h_img):
|
|
@@ -1554,10 +1774,21 @@ class CairoRenderer(GridRenderer):
|
|
|
1554
1774
|
vjust: float = 0.5,
|
|
1555
1775
|
gp: Optional[Gpar] = None,
|
|
1556
1776
|
) -> None:
|
|
1557
|
-
"""Draw a rounded rectangle.
|
|
1777
|
+
"""Draw a rounded rectangle. Anchor in device units; (w, h)
|
|
1778
|
+
in the viewport-local (unrotated) frame. CTM rotation applied
|
|
1779
|
+
around the anchor for the same reason as ``draw_rect``.
|
|
1780
|
+
"""
|
|
1781
|
+
from ._state import get_state
|
|
1782
|
+
|
|
1558
1783
|
ctx = self._ctx
|
|
1559
1784
|
ctx.save()
|
|
1560
1785
|
|
|
1786
|
+
rotation_deg = float(get_state().current_rotation())
|
|
1787
|
+
if abs(rotation_deg) > 1e-9:
|
|
1788
|
+
ctx.translate(x, y)
|
|
1789
|
+
ctx.rotate(-math.radians(rotation_deg))
|
|
1790
|
+
ctx.translate(-x, -y)
|
|
1791
|
+
|
|
1561
1792
|
dx = x - w * hjust
|
|
1562
1793
|
dy = y - h * (1.0 - vjust)
|
|
1563
1794
|
dr = min(r, w / 2, h / 2)
|
grid_py/renderer_web.py
CHANGED
|
@@ -759,7 +759,13 @@ class WebRenderer(GridRenderer):
|
|
|
759
759
|
self._node_stack = [self._scene_root]
|
|
760
760
|
# Re-init base class viewport stack
|
|
761
761
|
from ._vp_calc import calc_root_transform
|
|
762
|
-
|
|
762
|
+
# Web renderer emits SVG → device units are CSS px = 1/96 inch.
|
|
763
|
+
# Use that so unit("native", ...) at root matches R's pixel-based
|
|
764
|
+
# convention for raster-like surfaces.
|
|
765
|
+
root_vtr = calc_root_transform(
|
|
766
|
+
self.width_in * 2.54, self.height_in * 2.54,
|
|
767
|
+
dev_units_per_inch=96.0,
|
|
768
|
+
)
|
|
763
769
|
self._vp_transform_stack = [root_vtr]
|
|
764
770
|
self._vp_obj_stack = [None]
|
|
765
771
|
self._layout_stack = []
|
|
@@ -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.post5
|
|
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
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
grid_py/__init__.py,sha256=
|
|
1
|
+
grid_py/__init__.py,sha256=CE6FeESr4RXg2kI4HbTA_d7vxOBwkcjxyMQ8kpz1J8Q,10520
|
|
2
2
|
grid_py/_arrow.py,sha256=pgn4OCgF6TZY2yXl7ML-kt08DbwqlvT1ulApDcmRz4k,10867
|
|
3
3
|
grid_py/_clippath.py,sha256=p6fUAkcEc5Bg8chv-lIZwaOjEUbLK57duwjtIpDjAkw,4015
|
|
4
|
-
grid_py/_colour.py,sha256=
|
|
4
|
+
grid_py/_colour.py,sha256=JOfUC5GPHIccVfmGwmK5DJEb-klPIbr9HzKra30K9UM,23353
|
|
5
5
|
grid_py/_coords.py,sha256=9cDD3MWHmw9TnT1I8QKHQA96SCG06OxRXNqYGFZsfwg,47210
|
|
6
6
|
grid_py/_curve.py,sha256=QnUkh9lWLd0OSm7h7QwS78JjzBAu9WHmRY4ZpZhccpY,54989
|
|
7
7
|
grid_py/_display_list.py,sha256=uMFAupSJaCgUCbLMv2-RDBQ-YLxCkAldypRqc7CnF3w,13622
|
|
8
|
-
grid_py/_draw.py,sha256=
|
|
8
|
+
grid_py/_draw.py,sha256=2vYdD9DqOUPtMtLazCN-n4jLlE3hZYDbTUNLRMfzUkY,55218
|
|
9
9
|
grid_py/_edit.py,sha256=vQDZGBTTYy6DmlW33l94s1KJLrzPNym21NR4Od4qIFw,22263
|
|
10
10
|
grid_py/_font_metrics.py,sha256=XC_dgIN3eB72VPXIRFU-tynnS3wJl3bPugGHrwVzyeo,11086
|
|
11
11
|
grid_py/_gpar.py,sha256=Bsq5oevTOf-ISb6T1K4cqPcA7UKX0XHbg-AddMomWzQ,21302
|
|
@@ -20,24 +20,24 @@ grid_py/_lty.py,sha256=ciQKy_P_QmJbmYTsSeQY8cGxXvnrowJbBS-Zzf4birE,7762
|
|
|
20
20
|
grid_py/_mask.py,sha256=d2Wm2z-RJnfEKHdAHcjteL7VubimcaK1_f9Us9MfrUk,4902
|
|
21
21
|
grid_py/_path.py,sha256=Tr5bNNcGwPpOsxWKqEp65MRri-KV8IqplUh2Z90sxnk,11421
|
|
22
22
|
grid_py/_patterns.py,sha256=t6IkayYPaupEbF9bFZQmS0iXZhB9TWrvN2xRZzR7baA,33302
|
|
23
|
-
grid_py/_primitives.py,sha256=
|
|
24
|
-
grid_py/_renderer_base.py,sha256=
|
|
23
|
+
grid_py/_primitives.py,sha256=YptgRYJxTbXgycUH21Ek_ZD3sK9x_Ys46KRdDPNpoBc,62302
|
|
24
|
+
grid_py/_renderer_base.py,sha256=SU-Hl0pxmnzm9DasodDiTA0pQQkkWUlq3Z3mtiWU4TQ,64497
|
|
25
25
|
grid_py/_scene_graph.py,sha256=mKhNEMkUolbWt4_CFgGhrGUudrYenxFNXaBp6GLN9_Y,7412
|
|
26
26
|
grid_py/_size.py,sha256=q9yYdhO7dcp-RtQzJIIsXssFH5oFbsm5_P6UxZlFKjg,43312
|
|
27
|
-
grid_py/_state.py,sha256=
|
|
27
|
+
grid_py/_state.py,sha256=3JWGYOAUjmuqsYGG93v7vcX4ZM3k1uXOepxlAkb690w,25978
|
|
28
28
|
grid_py/_transforms.py,sha256=mQvs_Qm6icti66peLXTsjgmSmvIUCCphf_-D-tiE1O4,11675
|
|
29
29
|
grid_py/_typeset.py,sha256=J_PJ5YcOAOnnFkBfo8IW86utJdkdsrnGZi3MvnHc8io,11516
|
|
30
30
|
grid_py/_units.py,sha256=bGrHV23Vr2A6lO15LJvcP7Qs1Yx_uQJLmyQ6bRB3yjc,61893
|
|
31
31
|
grid_py/_utils.py,sha256=QaWNkCF3BbPHyrqPPRxN7Ji2vB5ethNwVT3SJ1ad9Lc,8335
|
|
32
32
|
grid_py/_viewport.py,sha256=qscv8eRKt0vHH0osxZUkbwM9PfgZwTKXi8n7L8921DE,49490
|
|
33
|
-
grid_py/_vp_calc.py,sha256=
|
|
33
|
+
grid_py/_vp_calc.py,sha256=zW_nmO5KKdy2oW2H6tk34G4JAnEtmBsBkZr0fESIIUg,34805
|
|
34
34
|
grid_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
|
-
grid_py/renderer.py,sha256
|
|
36
|
-
grid_py/renderer_web.py,sha256=
|
|
35
|
+
grid_py/renderer.py,sha256=-A4SsBtJW4hds2caG_t_ewnsVmx_WdKoUs0cRTvCUJQ,75269
|
|
36
|
+
grid_py/renderer_web.py,sha256=hgABfks1K6oWyNDN5wPpxKP4HpPsYCXxagG_oO1QWpY,29716
|
|
37
37
|
grid_py/resources/d3.v7.min.js,sha256=8glLv2FBs1lyLE_kVOtsSw8OQswQzHr5IfwVj864ZTk,279706
|
|
38
38
|
grid_py/resources/gridpy.css,sha256=tR5LF2rLvi_bGRWuH9CnmLQk-aG2f-jlZjQZqS7_4uY,1351
|
|
39
39
|
grid_py/resources/gridpy.js,sha256=ekbmsIMCJU9ZqIU0WC1c-Y-LBEinGIL8UftFEVF9pC4,28555
|
|
40
|
-
rgrid_python-4.5.3.
|
|
41
|
-
rgrid_python-4.5.3.
|
|
42
|
-
rgrid_python-4.5.3.
|
|
43
|
-
rgrid_python-4.5.3.
|
|
40
|
+
rgrid_python-4.5.3.post5.dist-info/METADATA,sha256=sNw4AKkaHb203t8SaZJvY_qcRcD1WCvF5fciDQJbhhw,19910
|
|
41
|
+
rgrid_python-4.5.3.post5.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
42
|
+
rgrid_python-4.5.3.post5.dist-info/licenses/LICENSE,sha256=8zNQKZlkc4JOaW7br_a8aALg4baZwZkLKLTQx3kuHkc,48
|
|
43
|
+
rgrid_python-4.5.3.post5.dist-info/RECORD,,
|
|
File without changes
|