rgrid-python 4.5.3.post2__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.post2 → rgrid_python-4.5.3.post5}/PKG-INFO +1 -1
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/__init__.py +1 -1
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_colour.py +9 -2
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_curve.py +22 -1
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_draw.py +172 -51
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_gpar.py +44 -18
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_grob.py +6 -3
- rgrid_python-4.5.3.post5/grid_py/_lty.py +197 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_patterns.py +11 -4
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_primitives.py +85 -5
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_renderer_base.py +240 -26
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_state.py +68 -10
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_viewport.py +28 -7
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_vp_calc.py +45 -3
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/renderer.py +363 -72
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/renderer_web.py +38 -3
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/resources/gridpy.js +22 -25
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/pyproject.toml +1 -1
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/.gitattributes +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/.gitignore +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/LICENSE +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/README.md +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_arrow.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_clippath.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_coords.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_display_list.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_edit.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_font_metrics.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_grab.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_group.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_highlevel.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_just.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_layout.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_ls.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_mask.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_path.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_scene_graph.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_size.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_transforms.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_typeset.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_units.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/_utils.py +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/py.typed +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/resources/d3.v7.min.js +0 -0
- {rgrid_python-4.5.3.post2 → rgrid_python-4.5.3.post5}/grid_py/resources/gridpy.css +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
|
|
@@ -1383,6 +1383,8 @@ def grid_curve(
|
|
|
1383
1383
|
def xspline_grob(
|
|
1384
1384
|
x: Optional[Any] = None,
|
|
1385
1385
|
y: Optional[Any] = None,
|
|
1386
|
+
id: Optional[Any] = None,
|
|
1387
|
+
id_lengths: Optional[Any] = None,
|
|
1386
1388
|
default_units: str = "npc",
|
|
1387
1389
|
shape: Union[float, Sequence[float]] = 0.0,
|
|
1388
1390
|
open_: bool = True,
|
|
@@ -1402,6 +1404,15 @@ def xspline_grob(
|
|
|
1402
1404
|
x, y : Unit, numeric, sequence, or None
|
|
1403
1405
|
Control-point coordinates. Defaults to ``Unit([0, 1], "npc")``
|
|
1404
1406
|
when ``None``.
|
|
1407
|
+
id : array-like of int or None
|
|
1408
|
+
Group label for each control point. Points sharing an ``id`` are
|
|
1409
|
+
rendered as one X-spline; the grob therefore renders one spline
|
|
1410
|
+
per unique ``id`` value. Mirrors R ``xsplineGrob(id=...)``.
|
|
1411
|
+
Mutually meaningful with ``id_lengths``: pass at most one.
|
|
1412
|
+
id_lengths : array-like of int or None
|
|
1413
|
+
Run-length encoding of ``id``: the n-th entry is the number of
|
|
1414
|
+
consecutive control points belonging to spline n. Mirrors R's
|
|
1415
|
+
``xsplineGrob(id.lengths=...)``.
|
|
1405
1416
|
default_units : str
|
|
1406
1417
|
Unit type for bare numerics.
|
|
1407
1418
|
shape : float or sequence of float
|
|
@@ -1439,9 +1450,16 @@ def xspline_grob(
|
|
|
1439
1450
|
if np.any((shape_arr < -1) | (shape_arr > 1)):
|
|
1440
1451
|
raise ValueError("all 'shape' values must be between -1 and 1")
|
|
1441
1452
|
|
|
1453
|
+
id_arr = None if id is None else np.asarray(id, dtype=np.int64)
|
|
1454
|
+
id_lengths_arr = (
|
|
1455
|
+
None if id_lengths is None else np.asarray(id_lengths, dtype=np.int64)
|
|
1456
|
+
)
|
|
1457
|
+
|
|
1442
1458
|
return Grob(
|
|
1443
1459
|
x=x,
|
|
1444
1460
|
y=y,
|
|
1461
|
+
id=id_arr,
|
|
1462
|
+
id_lengths=id_lengths_arr,
|
|
1445
1463
|
shape=shape_arr,
|
|
1446
1464
|
open_=bool(open_),
|
|
1447
1465
|
arrow=arrow,
|
|
@@ -1456,6 +1474,8 @@ def xspline_grob(
|
|
|
1456
1474
|
def grid_xspline(
|
|
1457
1475
|
x: Optional[Any] = None,
|
|
1458
1476
|
y: Optional[Any] = None,
|
|
1477
|
+
id: Optional[Any] = None,
|
|
1478
|
+
id_lengths: Optional[Any] = None,
|
|
1459
1479
|
default_units: str = "npc",
|
|
1460
1480
|
shape: Union[float, Sequence[float]] = 0.0,
|
|
1461
1481
|
open_: bool = True,
|
|
@@ -1497,7 +1517,8 @@ def grid_xspline(
|
|
|
1497
1517
|
The xspline grob.
|
|
1498
1518
|
"""
|
|
1499
1519
|
grob = xspline_grob(
|
|
1500
|
-
x=x, y=y,
|
|
1520
|
+
x=x, y=y, id=id, id_lengths=id_lengths,
|
|
1521
|
+
default_units=default_units,
|
|
1501
1522
|
shape=shape, open_=open_, arrow=arrow,
|
|
1502
1523
|
repEnds=repEnds, name=name, gp=gp, vp=vp,
|
|
1503
1524
|
)
|
|
@@ -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",
|
|
@@ -215,7 +216,12 @@ def _draw_arrow_heads(
|
|
|
215
216
|
try:
|
|
216
217
|
l_w = float(renderer.resolve_w(length_unit, gp=gp))
|
|
217
218
|
l_h = float(renderer.resolve_h(length_unit, gp=gp))
|
|
218
|
-
except
|
|
219
|
+
except (ValueError, AttributeError, TypeError) as exc:
|
|
220
|
+
warnings.warn(
|
|
221
|
+
f"arrow length cannot be resolved ({exc}); arrow head skipped",
|
|
222
|
+
UserWarning,
|
|
223
|
+
stacklevel=2,
|
|
224
|
+
)
|
|
219
225
|
return
|
|
220
226
|
length_dev = min(l_w, l_h)
|
|
221
227
|
if not (length_dev > 0):
|
|
@@ -302,8 +308,16 @@ def _render_grob(
|
|
|
302
308
|
|
|
303
309
|
# ---- rect -----------------------------------------------------------
|
|
304
310
|
if cls == "rect":
|
|
305
|
-
|
|
306
|
-
|
|
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
|
+
)
|
|
307
321
|
ws = renderer.resolve_w_array(getattr(grob, "width", [1.0]), gp=gp)
|
|
308
322
|
hs = renderer.resolve_h_array(getattr(grob, "height", [1.0]), gp=gp)
|
|
309
323
|
hj, vj = _resolve_just(grob)
|
|
@@ -326,9 +340,11 @@ def _render_grob(
|
|
|
326
340
|
|
|
327
341
|
# ---- roundrect ------------------------------------------------------
|
|
328
342
|
elif cls == "roundrect":
|
|
343
|
+
ax, ay = renderer.resolve_loc(
|
|
344
|
+
getattr(grob, "x", 0.0), getattr(grob, "y", 0.0), gp=gp,
|
|
345
|
+
)
|
|
329
346
|
renderer.draw_roundrect(
|
|
330
|
-
x=
|
|
331
|
-
y=renderer.resolve_y(getattr(grob, "y", 0.0), gp=gp),
|
|
347
|
+
x=ax, y=ay,
|
|
332
348
|
w=renderer.resolve_w(getattr(grob, "width", 1.0), gp=gp),
|
|
333
349
|
h=renderer.resolve_h(getattr(grob, "height", 1.0), gp=gp),
|
|
334
350
|
r=renderer.resolve_w(getattr(grob, "r", 0.0), gp=gp),
|
|
@@ -339,17 +355,22 @@ def _render_grob(
|
|
|
339
355
|
|
|
340
356
|
# ---- circle ---------------------------------------------------------
|
|
341
357
|
elif cls == "circle":
|
|
358
|
+
cx, cy = renderer.resolve_loc(
|
|
359
|
+
getattr(grob, "x", 0.5), getattr(grob, "y", 0.5), gp=gp,
|
|
360
|
+
)
|
|
342
361
|
renderer.draw_circle(
|
|
343
|
-
x=
|
|
344
|
-
y=renderer.resolve_y(getattr(grob, "y", 0.5), gp=gp),
|
|
362
|
+
x=cx, y=cy,
|
|
345
363
|
r=renderer.resolve_w(getattr(grob, "r", 0.5), gp=gp),
|
|
346
364
|
gp=gp,
|
|
347
365
|
)
|
|
348
366
|
|
|
349
367
|
# ---- lines / polyline ------------------------------------------------
|
|
350
368
|
elif cls in ("lines", "polyline"):
|
|
351
|
-
x = renderer.
|
|
352
|
-
|
|
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
|
+
)
|
|
353
374
|
id_ = getattr(grob, "id", None)
|
|
354
375
|
id_lengths = getattr(grob, "id_lengths", None)
|
|
355
376
|
# R polylineGrob supports either `id` (per-point group) or
|
|
@@ -363,12 +384,36 @@ def _render_grob(
|
|
|
363
384
|
id_ = np.atleast_1d(np.asarray(id_, dtype=int))
|
|
364
385
|
renderer.draw_polyline(x, y, id_=id_, gp=gp)
|
|
365
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
|
+
|
|
366
409
|
# ---- segments --------------------------------------------------------
|
|
367
410
|
elif cls == "segments":
|
|
368
|
-
x0 = renderer.
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
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
|
+
)
|
|
372
417
|
renderer.draw_segments(x0=x0, y0=y0, x1=x1, y1=y1, gp=gp)
|
|
373
418
|
|
|
374
419
|
# Each segment may carry its own arrowhead (``arrow=`` parameter on
|
|
@@ -387,8 +432,11 @@ def _render_grob(
|
|
|
387
432
|
elif cls == "xspline":
|
|
388
433
|
from ._curve import _calc_xspline_points # lazy to avoid import cycle
|
|
389
434
|
|
|
390
|
-
x = renderer.
|
|
391
|
-
|
|
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
|
+
)
|
|
392
440
|
shape_raw = getattr(grob, "shape", 0.0)
|
|
393
441
|
open_ = bool(getattr(grob, "open_", True))
|
|
394
442
|
rep_ends = bool(getattr(grob, "repEnds", True))
|
|
@@ -412,7 +460,15 @@ def _render_grob(
|
|
|
412
460
|
xs, ys = _calc_xspline_points(
|
|
413
461
|
x, y, shape=shape_arr, open_=open_, repEnds=rep_ends,
|
|
414
462
|
)
|
|
415
|
-
|
|
463
|
+
# R's ``xsplineGrob(open=FALSE)`` is a *filled closed* shape, not
|
|
464
|
+
# a stroked path; route through ``draw_polygon`` so ``gp$fill``
|
|
465
|
+
# actually paints (R/grid: drawDetails.xspline → C_xspline,
|
|
466
|
+
# filled when ``open == FALSE``). ``open=TRUE`` stays a stroked
|
|
467
|
+
# polyline.
|
|
468
|
+
if open_:
|
|
469
|
+
renderer.draw_polyline(xs, ys, id_=None, gp=gp)
|
|
470
|
+
else:
|
|
471
|
+
renderer.draw_polygon(xs, ys, gp=gp)
|
|
416
472
|
if arr is not None and len(xs) >= 2:
|
|
417
473
|
_draw_arrow_heads(xs, ys, arr, renderer, gp)
|
|
418
474
|
else:
|
|
@@ -436,12 +492,24 @@ def _render_grob(
|
|
|
436
492
|
out_id += 1
|
|
437
493
|
per_group.append((xs_g, ys_g))
|
|
438
494
|
if all_xs:
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
495
|
+
# Multi-id closed splines render as N separate filled
|
|
496
|
+
# subpolygons (R: xsplineGrob(id=..., open=FALSE)); use
|
|
497
|
+
# ``draw_path`` with the path_id array. Open splines stay
|
|
498
|
+
# multi-stroke polylines.
|
|
499
|
+
if open_:
|
|
500
|
+
renderer.draw_polyline(
|
|
501
|
+
np.asarray(all_xs, dtype=float),
|
|
502
|
+
np.asarray(all_ys, dtype=float),
|
|
503
|
+
id_=np.asarray(all_ids, dtype=int),
|
|
504
|
+
gp=gp,
|
|
505
|
+
)
|
|
506
|
+
else:
|
|
507
|
+
renderer.draw_path(
|
|
508
|
+
np.asarray(all_xs, dtype=float),
|
|
509
|
+
np.asarray(all_ys, dtype=float),
|
|
510
|
+
path_id=np.asarray(all_ids, dtype=int),
|
|
511
|
+
gp=gp,
|
|
512
|
+
)
|
|
445
513
|
if arr is not None:
|
|
446
514
|
for xs_g, ys_g in per_group:
|
|
447
515
|
if len(xs_g) >= 2:
|
|
@@ -449,8 +517,9 @@ def _render_grob(
|
|
|
449
517
|
|
|
450
518
|
# ---- polygon ---------------------------------------------------------
|
|
451
519
|
elif cls == "polygon":
|
|
452
|
-
px = renderer.
|
|
453
|
-
|
|
520
|
+
px, py = renderer.resolve_loc_array(
|
|
521
|
+
getattr(grob, "x", []), getattr(grob, "y", []), gp=gp,
|
|
522
|
+
)
|
|
454
523
|
pid = getattr(grob, "id", None)
|
|
455
524
|
if pid is not None:
|
|
456
525
|
# R semantics: polygonGrob(id=...) draws separate polygons
|
|
@@ -484,9 +553,8 @@ def _render_grob(
|
|
|
484
553
|
else:
|
|
485
554
|
labels = [str(label_raw)]
|
|
486
555
|
|
|
487
|
-
# Resolve x/y
|
|
488
|
-
xx = renderer.
|
|
489
|
-
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)
|
|
490
558
|
|
|
491
559
|
# Normalise rot to array
|
|
492
560
|
if isinstance(rot_raw, (list, tuple, np.ndarray)):
|
|
@@ -528,16 +596,20 @@ def _render_grob(
|
|
|
528
596
|
# ---- points ----------------------------------------------------------
|
|
529
597
|
elif cls == "points":
|
|
530
598
|
pch_raw = getattr(grob, "pch", 19)
|
|
531
|
-
# pch may be a scalar or
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
+
)
|
|
538
611
|
renderer.draw_points(
|
|
539
|
-
x=
|
|
540
|
-
y=renderer.resolve_y_array(getattr(grob, "y", []), gp=gp),
|
|
612
|
+
x=pts_x, y=pts_y,
|
|
541
613
|
size=renderer.resolve_w(getattr(grob, "size", 1.0), gp=gp),
|
|
542
614
|
pch=pch_val,
|
|
543
615
|
gp=gp,
|
|
@@ -545,18 +617,63 @@ def _render_grob(
|
|
|
545
617
|
|
|
546
618
|
# ---- pathgrob --------------------------------------------------------
|
|
547
619
|
elif cls == "pathgrob":
|
|
548
|
-
x = renderer.
|
|
549
|
-
|
|
550
|
-
|
|
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
|
+
|
|
551
658
|
if path_id is None:
|
|
552
|
-
|
|
659
|
+
renderer.draw_path(
|
|
660
|
+
x=x, y=y, path_id=sub_id, rule=rule, gp=gp,
|
|
661
|
+
)
|
|
553
662
|
else:
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
+
)
|
|
560
677
|
|
|
561
678
|
# ---- rastergrob ------------------------------------------------------
|
|
562
679
|
elif cls == "rastergrob":
|
|
@@ -564,15 +681,19 @@ def _render_grob(
|
|
|
564
681
|
if image is None:
|
|
565
682
|
image = getattr(grob, "image", None)
|
|
566
683
|
if image is not None:
|
|
567
|
-
# Apply justification (same as rect_grob)
|
|
568
684
|
hj, vj = _resolve_just(grob)
|
|
569
|
-
raw_x = renderer.
|
|
570
|
-
|
|
685
|
+
raw_x, raw_y = renderer.resolve_loc(
|
|
686
|
+
getattr(grob, "x", 0.0), getattr(grob, "y", 0.0), gp=gp,
|
|
687
|
+
)
|
|
571
688
|
raw_w = renderer.resolve_w(getattr(grob, "width", 1.0), gp=gp)
|
|
572
689
|
raw_h = renderer.resolve_h(getattr(grob, "height", 1.0), gp=gp)
|
|
573
|
-
#
|
|
690
|
+
# `renderer.draw_raster` expects the *top-left* corner in y-down
|
|
691
|
+
# device pixels. `resolve_y` already returns y-down coords, so the
|
|
692
|
+
# y component of justification must be flipped relative to R's
|
|
693
|
+
# `justifyY(y, h, vjust) = y - h*vjust` (which assumes y-up NPC).
|
|
694
|
+
# Same correction `draw_rect` applies (renderer.py:815).
|
|
574
695
|
x0 = raw_x - raw_w * hj
|
|
575
|
-
y0 = raw_y - raw_h * vj
|
|
696
|
+
y0 = raw_y - raw_h * (1.0 - vj)
|
|
576
697
|
renderer.draw_raster(
|
|
577
698
|
image=image,
|
|
578
699
|
x=x0,
|
|
@@ -20,14 +20,12 @@ __all__ = ["Gpar", "gpar", "get_gpar"]
|
|
|
20
20
|
# Constants
|
|
21
21
|
# ---------------------------------------------------------------------------
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"twodash",
|
|
30
|
-
}
|
|
23
|
+
# Derived from grid_py._lty so there is exactly one source of truth for
|
|
24
|
+
# the named-lty set. ``"blank"`` is added because R par/grid accepts it
|
|
25
|
+
# even though it has no hex equivalent (it short-circuits the stroke).
|
|
26
|
+
from ._lty import valid_named_lty as _valid_named_lty
|
|
27
|
+
|
|
28
|
+
_VALID_LTY: frozenset[str] = _valid_named_lty()
|
|
31
29
|
|
|
32
30
|
_VALID_LINEEND: set[str] = {"round", "butt", "square"}
|
|
33
31
|
|
|
@@ -77,9 +75,12 @@ def _as_list(value: Any) -> list:
|
|
|
77
75
|
return [value]
|
|
78
76
|
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
# NOTE: ``_is_hex_lty`` used to live here. It has been removed because
|
|
79
|
+
# hex-string validity is now decided by ``grid_py._lty.resolve_lty``,
|
|
80
|
+
# which is the single source of truth for both "is this lty valid?" and
|
|
81
|
+
# "what dash array does it expand to?". Keeping a separate validator
|
|
82
|
+
# here would create the same kind of dual-source landmine that
|
|
83
|
+
# motivated B7 in the first place.
|
|
83
84
|
|
|
84
85
|
|
|
85
86
|
def _resolve_fontface(value: Any) -> int:
|
|
@@ -254,14 +255,19 @@ class Gpar:
|
|
|
254
255
|
) from exc
|
|
255
256
|
|
|
256
257
|
elif name == "lty":
|
|
258
|
+
# Validation delegates to grid_py._lty so accept/reject
|
|
259
|
+
# decisions match the renderer-time resolver exactly.
|
|
260
|
+
# ``resolve_lty`` raises ValueError on bad input with a
|
|
261
|
+
# message already containing the offending value, so we
|
|
262
|
+
# let it propagate (no try/except — principle 4).
|
|
263
|
+
from ._lty import is_blank_lty, resolve_lty
|
|
257
264
|
for v in vals:
|
|
258
|
-
if
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
elif not isinstance(v, (int, float, np.integer, np.floating)):
|
|
265
|
+
if is_blank_lty(v):
|
|
266
|
+
continue
|
|
267
|
+
if isinstance(v, (str, int, float, np.integer, np.floating)) \
|
|
268
|
+
and not isinstance(v, bool):
|
|
269
|
+
resolve_lty(v) # raises ValueError if invalid
|
|
270
|
+
else:
|
|
265
271
|
raise TypeError(
|
|
266
272
|
f"'lty' must be str or numeric, got {type(v).__name__}"
|
|
267
273
|
)
|
|
@@ -462,6 +468,26 @@ class Gpar:
|
|
|
462
468
|
gp._params = merged
|
|
463
469
|
return gp
|
|
464
470
|
|
|
471
|
+
# -- mod (R: mod.gpar) -------------------------------------------------
|
|
472
|
+
|
|
473
|
+
def _mod(self, new_gp: "Gpar") -> "Gpar":
|
|
474
|
+
"""Edit-time merge (R ``mod.gpar`` — gpar.R:298-304).
|
|
475
|
+
|
|
476
|
+
Plain key overwrite — ``new_gp``'s keys replace ``self``'s keys,
|
|
477
|
+
unmentioned keys are kept unchanged. Unlike :meth:`_merge`
|
|
478
|
+
(which mirrors R ``set.gpar`` and multiplies cumulative
|
|
479
|
+
params like ``cex`` / ``alpha`` / ``lex``), this method is the
|
|
480
|
+
one used by ``editGrob`` and does NOT multiply — R's
|
|
481
|
+
``mod.gpar`` does a single ``gp[names(newgp)] <- newgp``.
|
|
482
|
+
"""
|
|
483
|
+
if not self._params:
|
|
484
|
+
return new_gp
|
|
485
|
+
merged = copy.deepcopy(self._params)
|
|
486
|
+
merged.update(copy.deepcopy(new_gp._params))
|
|
487
|
+
gp = object.__new__(Gpar)
|
|
488
|
+
gp._params = merged
|
|
489
|
+
return gp
|
|
490
|
+
|
|
465
491
|
# -- display -----------------------------------------------------------
|
|
466
492
|
|
|
467
493
|
def __repr__(self) -> str:
|
|
@@ -1157,12 +1157,15 @@ def _edit_this_grob(grob: Grob, specs: dict[str, Any]) -> Grob:
|
|
|
1157
1157
|
if not key:
|
|
1158
1158
|
continue
|
|
1159
1159
|
if key == "gp":
|
|
1160
|
-
# Special handling
|
|
1160
|
+
# Special handling — R editGrob uses ``mod.gpar`` (gpar.R:298-304)
|
|
1161
|
+
# which does a plain key overwrite (``gp[names(newgp)] <- newgp``)
|
|
1162
|
+
# *without* the cumulative cex/alpha/lex multiplication that
|
|
1163
|
+
# viewport-push's ``set.gpar`` applies. ``Gpar._mod`` is the
|
|
1164
|
+
# mirror; ``Gpar._merge`` is the cumulative one.
|
|
1161
1165
|
if value is None:
|
|
1162
1166
|
grob._gp = None
|
|
1163
1167
|
elif grob._gp is not None:
|
|
1164
|
-
|
|
1165
|
-
grob._gp = grob._gp.merge(value) if hasattr(grob._gp, "merge") else value
|
|
1168
|
+
grob._gp = grob._gp._mod(value)
|
|
1166
1169
|
else:
|
|
1167
1170
|
grob._gp = value
|
|
1168
1171
|
elif key == "name":
|