rgrid-python 4.5.3.post4__tar.gz → 4.5.3.post5__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.post4 → rgrid_python-4.5.3.post5}/PKG-INFO +1 -1
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/__init__.py +1 -1
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_colour.py +9 -2
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_draw.py +133 -40
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_primitives.py +67 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_renderer_base.py +140 -18
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_state.py +68 -10
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_vp_calc.py +45 -3
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/renderer.py +264 -33
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/renderer_web.py +7 -1
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/pyproject.toml +1 -1
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/.gitattributes +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/.gitignore +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/LICENSE +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/README.md +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_arrow.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_clippath.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_coords.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_curve.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_display_list.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_edit.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_font_metrics.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_gpar.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_grab.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_grob.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_group.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_highlevel.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_just.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_layout.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_ls.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_lty.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_mask.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_path.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_patterns.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_scene_graph.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_size.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_transforms.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_typeset.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_units.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_utils.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/_viewport.py +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/py.typed +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/resources/d3.v7.min.js +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/grid_py/resources/gridpy.css +0 -0
- {rgrid_python-4.5.3.post4 → rgrid_python-4.5.3.post5}/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.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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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",
|
|
@@ -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)
|