rgrid-python 4.5.3.post2__py3-none-any.whl → 4.5.3.post3__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 CHANGED
@@ -6,7 +6,7 @@ units, viewports, grobs (graphical objects), layouts, and rendering via
6
6
  Cairo (pycairo).
7
7
  """
8
8
 
9
- __version__ = "4.5.3.post2"
9
+ __version__ = "4.5.3.post3"
10
10
 
11
11
  # --- Utilities ---
12
12
  from grid_py._utils import depth, explode, grid_pretty, n2mfrow
grid_py/_curve.py CHANGED
@@ -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, default_units=default_units,
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
  )
grid_py/_draw.py CHANGED
@@ -412,7 +412,15 @@ def _render_grob(
412
412
  xs, ys = _calc_xspline_points(
413
413
  x, y, shape=shape_arr, open_=open_, repEnds=rep_ends,
414
414
  )
415
- renderer.draw_polyline(xs, ys, id_=None, gp=gp)
415
+ # R's ``xsplineGrob(open=FALSE)`` is a *filled closed* shape, not
416
+ # a stroked path; route through ``draw_polygon`` so ``gp$fill``
417
+ # actually paints (R/grid: drawDetails.xspline → C_xspline,
418
+ # filled when ``open == FALSE``). ``open=TRUE`` stays a stroked
419
+ # polyline.
420
+ if open_:
421
+ renderer.draw_polyline(xs, ys, id_=None, gp=gp)
422
+ else:
423
+ renderer.draw_polygon(xs, ys, gp=gp)
416
424
  if arr is not None and len(xs) >= 2:
417
425
  _draw_arrow_heads(xs, ys, arr, renderer, gp)
418
426
  else:
@@ -436,12 +444,24 @@ def _render_grob(
436
444
  out_id += 1
437
445
  per_group.append((xs_g, ys_g))
438
446
  if all_xs:
439
- renderer.draw_polyline(
440
- np.asarray(all_xs, dtype=float),
441
- np.asarray(all_ys, dtype=float),
442
- id_=np.asarray(all_ids, dtype=int),
443
- gp=gp,
444
- )
447
+ # Multi-id closed splines render as N separate filled
448
+ # subpolygons (R: xsplineGrob(id=..., open=FALSE)); use
449
+ # ``draw_path`` with the path_id array. Open splines stay
450
+ # multi-stroke polylines.
451
+ if open_:
452
+ renderer.draw_polyline(
453
+ np.asarray(all_xs, dtype=float),
454
+ np.asarray(all_ys, dtype=float),
455
+ id_=np.asarray(all_ids, dtype=int),
456
+ gp=gp,
457
+ )
458
+ else:
459
+ renderer.draw_path(
460
+ np.asarray(all_xs, dtype=float),
461
+ np.asarray(all_ys, dtype=float),
462
+ path_id=np.asarray(all_ids, dtype=int),
463
+ gp=gp,
464
+ )
445
465
  if arr is not None:
446
466
  for xs_g, ys_g in per_group:
447
467
  if len(xs_g) >= 2:
@@ -564,15 +584,18 @@ def _render_grob(
564
584
  if image is None:
565
585
  image = getattr(grob, "image", None)
566
586
  if image is not None:
567
- # Apply justification (same as rect_grob)
568
587
  hj, vj = _resolve_just(grob)
569
588
  raw_x = renderer.resolve_x(getattr(grob, "x", 0.0), gp=gp)
570
589
  raw_y = renderer.resolve_y(getattr(grob, "y", 0.0), gp=gp)
571
590
  raw_w = renderer.resolve_w(getattr(grob, "width", 1.0), gp=gp)
572
591
  raw_h = renderer.resolve_h(getattr(grob, "height", 1.0), gp=gp)
573
- # Compute bottom-left corner from anchor + justification
592
+ # `renderer.draw_raster` expects the *top-left* corner in y-down
593
+ # device pixels. `resolve_y` already returns y-down coords, so the
594
+ # y component of justification must be flipped relative to R's
595
+ # `justifyY(y, h, vjust) = y - h*vjust` (which assumes y-up NPC).
596
+ # Same correction `draw_rect` applies (renderer.py:815).
574
597
  x0 = raw_x - raw_w * hj
575
- y0 = raw_y - raw_h * vj
598
+ y0 = raw_y - raw_h * (1.0 - vj)
576
599
  renderer.draw_raster(
577
600
  image=image,
578
601
  x=x0,
grid_py/_renderer_base.py CHANGED
@@ -425,8 +425,15 @@ class GridRenderer(ABC):
425
425
  from ._layout import _calc_layout_sizes, GridLayout
426
426
 
427
427
  if isinstance(layout, GridLayout):
428
+ # Use device-units-per-inch (= dpi for raster ImageSurface, 72 for
429
+ # vector PDF/SVG/PS surfaces) so that absolute units (cm/mm/in/pt
430
+ # …) get converted to the SAME unit as ``parent_w``/``parent_h``.
431
+ # Passing ``self.dpi`` blindly would, for vector surfaces, scale
432
+ # absolute widths by dpi while the parent is measured in points,
433
+ # over-allocating fixed cells by ``dpi/72`` and shrinking the
434
+ # null-unit panel by the same factor.
428
435
  col_widths, row_heights = _calc_layout_sizes(
429
- layout, parent_w, parent_h, self.dpi,
436
+ layout, parent_w, parent_h, self._dev_units_per_inch,
430
437
  )
431
438
  else:
432
439
  nrow = getattr(layout, "nrow", 1)
grid_py/renderer.py CHANGED
@@ -141,6 +141,17 @@ class CairoRenderer(GridRenderer):
141
141
  if filename is None:
142
142
  raise ValueError("filename is required for SVG surface")
143
143
  self._surface = cairo.SVGSurface(filename, width_pt, height_pt)
144
+ # Cairo's SVGSurface measures user-space in points; emit explicit
145
+ # ``pt`` units in the resulting <svg width="...pt" height="...pt">
146
+ # so SVG renderers (browsers, cairosvg, Inkscape) interpret the
147
+ # canvas at the intended physical size instead of treating the
148
+ # raw numbers as user-units (= pixels by SVG default).
149
+ try:
150
+ self._surface.set_document_unit(cairo.SVG_UNIT_PT)
151
+ except (AttributeError, TypeError):
152
+ # Older pycairo / cairo without set_document_unit — file is
153
+ # still well-formed, just unit-less.
154
+ pass
144
155
  elif surface_type == "ps":
145
156
  if filename is None:
146
157
  raise ValueError("filename is required for PS surface")
@@ -322,6 +333,34 @@ class CairoRenderer(GridRenderer):
322
333
 
323
334
  # ---- gpar application --------------------------------------------------
324
335
 
336
+ def _lwd_to_user(self, lwd_pt: float) -> float:
337
+ """Convert R-grid lwd (points) to a Cairo user-space line width.
338
+
339
+ R's grid measures ``lwd`` in 1/72 inch (points) regardless of the
340
+ active viewport scale — a value of 1 should always produce a stroke
341
+ 1pt wide on the output medium. Cairo's ``set_line_width`` takes a
342
+ user-space distance, so we have to:
343
+
344
+ 1. Map points → device units. Raster ``ImageSurface`` has 1 user
345
+ unit = 1 pixel, so device units per point = ``dpi/72``. Vector
346
+ surfaces (PDF/SVG/PS) have 1 user unit = 1 pt, so the conversion
347
+ factor is 1.0. Equivalently, use ``_dev_units_per_inch / 72``.
348
+ 2. Undo any active CTM scaling via ``device_to_user_distance`` so
349
+ that nested viewports (which scale the CTM) do not also scale
350
+ the stroke.
351
+
352
+ This is the analogue of the font-size handling in ``_set_font``.
353
+ """
354
+ if self._surface_type == "image":
355
+ lw_dev = lwd_pt * self.dpi / 72.0
356
+ else:
357
+ lw_dev = lwd_pt
358
+ try:
359
+ ux, uy = self._ctx.device_to_user_distance(lw_dev, lw_dev)
360
+ return max(abs(ux), abs(uy))
361
+ except Exception:
362
+ return lw_dev
363
+
325
364
  def _apply_stroke(self, gp: Optional[Gpar]) -> Tuple[float, float, float, float]:
326
365
  """Set stroke colour, line width, dash, caps, joins from Gpar.
327
366
 
@@ -331,7 +370,7 @@ class CairoRenderer(GridRenderer):
331
370
  ctx = self._ctx
332
371
  if gp is None:
333
372
  ctx.set_source_rgba(0, 0, 0, 1)
334
- ctx.set_line_width(1.0)
373
+ ctx.set_line_width(self._lwd_to_user(1.0))
335
374
  return (0.0, 0.0, 0.0, 1.0)
336
375
 
337
376
  col = gp.get("col", None)
@@ -367,24 +406,7 @@ class CairoRenderer(GridRenderer):
367
406
  # R semantics: lwd=0 means invisible line
368
407
  if lw <= 0:
369
408
  return (0.0, 0.0, 0.0, 0.0)
370
- # R grid semantics: ``lwd`` is always in **points** (1/72 inch)
371
- # regardless of the current viewport's scale. Cairo's
372
- # ``set_line_width`` takes a user-space distance, which the
373
- # viewport CTM has scaled down to NPC-like units — so a value
374
- # of 0.5 user-space becomes sub-pixel after ``scale(w, h)``.
375
- # Convert ``lw`` from points → device pixels using the
376
- # renderer's DPI, then back to user-space via
377
- # ``device_to_user_distance`` so the stroke width stays at
378
- # 0.5pt on the output device no matter how deep the
379
- # viewport stack is (matches R grid's device-unit lwd).
380
- dpi = getattr(self, "dpi", None) or getattr(self, "_dpi", 72.0) or 72.0
381
- lw_px = lw * dpi / 72.0
382
- try:
383
- ux, uy = ctx.device_to_user_distance(lw_px, lw_px)
384
- lw_user = max(abs(ux), abs(uy))
385
- except Exception:
386
- lw_user = lw
387
- ctx.set_line_width(lw_user)
409
+ ctx.set_line_width(self._lwd_to_user(lw))
388
410
 
389
411
  lty = gp.get("lty", None)
390
412
  if lty is not None:
@@ -1197,7 +1219,7 @@ class CairoRenderer(GridRenderer):
1197
1219
  """
1198
1220
  ctx.save()
1199
1221
  ctx.new_path() # clear any residual path from prior draws
1200
- ctx.set_line_width(lwd)
1222
+ ctx.set_line_width(self._lwd_to_user(lwd))
1201
1223
 
1202
1224
  if pch_val <= 14:
1203
1225
  # --- Group 0-14: stroke-only (use col for outline, no fill) ---
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rgrid-python
3
- Version: 4.5.3.post2
3
+ Version: 4.5.3.post3
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=UWOAMmQkCKI5H7Kx1yPSwFvI6pLQRci2jVDURr8d6H8,10520
1
+ grid_py/__init__.py,sha256=tSRqBr3SxkmX3f3x0tT6G-kMFeApA6cjrhD1CGiKWNs,10520
2
2
  grid_py/_arrow.py,sha256=pgn4OCgF6TZY2yXl7ML-kt08DbwqlvT1ulApDcmRz4k,10867
3
3
  grid_py/_clippath.py,sha256=p6fUAkcEc5Bg8chv-lIZwaOjEUbLK57duwjtIpDjAkw,4015
4
4
  grid_py/_colour.py,sha256=WqaxGop-SM1LYZG4uMPmMX3Zjh6Y_ip0HcanP5wEij8,22872
5
5
  grid_py/_coords.py,sha256=9cDD3MWHmw9TnT1I8QKHQA96SCG06OxRXNqYGFZsfwg,47210
6
- grid_py/_curve.py,sha256=MV7hl1MR-4FZfKBKEm4MCSvHgJLYP9UaWLp8eHXPhv0,54041
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=seDvNzkxz3PYcWLeI6MXQ80elWaY18HgfyKshr2cMl0,49662
8
+ grid_py/_draw.py,sha256=WcbxIX9GPEZBf5L8KRZpYiFNWyHVttGBrroKO4m6LUY,50953
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=AW3PNlD7UWOozzZvY5Y3f7ZGTWCTRnOs2YqOvlFD1_A,19757
@@ -20,7 +20,7 @@ grid_py/_mask.py,sha256=d2Wm2z-RJnfEKHdAHcjteL7VubimcaK1_f9Us9MfrUk,4902
20
20
  grid_py/_path.py,sha256=Tr5bNNcGwPpOsxWKqEp65MRri-KV8IqplUh2Z90sxnk,11421
21
21
  grid_py/_patterns.py,sha256=PBwV2b3oJkbueTLCku6cxeZ1PThREjiPcA6lmrwH3pE,32691
22
22
  grid_py/_primitives.py,sha256=dpX5_q5CS8p1vhuVzTa77MVbtaxigGBA5mmUzfFfHks,58861
23
- grid_py/_renderer_base.py,sha256=-wZYsKXaetQw1wPDsjUJoJffIDmjEVZN6P_lj5xOPT4,54239
23
+ grid_py/_renderer_base.py,sha256=UD24cRzJlknMSSs2sl27XaDZ1e7XdIuhRDVhDxjiW-o,54770
24
24
  grid_py/_scene_graph.py,sha256=mKhNEMkUolbWt4_CFgGhrGUudrYenxFNXaBp6GLN9_Y,7412
25
25
  grid_py/_size.py,sha256=q9yYdhO7dcp-RtQzJIIsXssFH5oFbsm5_P6UxZlFKjg,43312
26
26
  grid_py/_state.py,sha256=SsEzcicTmxWaGExDeLLNNVXT-G38aUFtgSuZTJEYBbw,22800
@@ -31,12 +31,12 @@ grid_py/_utils.py,sha256=QaWNkCF3BbPHyrqPPRxN7Ji2vB5ethNwVT3SJ1ad9Lc,8335
31
31
  grid_py/_viewport.py,sha256=AOrYmv05Y4QJbsTksn86RLStfHdbOHERc2YDHdZAekE,48477
32
32
  grid_py/_vp_calc.py,sha256=v0MJc-YmWohO5t7LHFopk5ogH-Sink_A_zNFV_1769w,32935
33
33
  grid_py/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
- grid_py/renderer.py,sha256=hzCRgHMrR8tFpsdqT1al09gnZhgShoe_kIEO4MTYZNY,61620
34
+ grid_py/renderer.py,sha256=6L6Rb-2AfjmvaeQYhWQKlU1L1HYHkKAma03v6YdZcTk,62658
35
35
  grid_py/renderer_web.py,sha256=pbOijES1eUUqkeAadXVyZPCBRIUIgzs3HIOgAElTYmo,27958
36
36
  grid_py/resources/d3.v7.min.js,sha256=8glLv2FBs1lyLE_kVOtsSw8OQswQzHr5IfwVj864ZTk,279706
37
37
  grid_py/resources/gridpy.css,sha256=tR5LF2rLvi_bGRWuH9CnmLQk-aG2f-jlZjQZqS7_4uY,1351
38
38
  grid_py/resources/gridpy.js,sha256=l8KGgK-qZbJPVvrMAmLUu57RhpSAo_LMibs6rx3RnvA,28340
39
- rgrid_python-4.5.3.post2.dist-info/METADATA,sha256=uGrI2aBhk9PL1tjCk4izXQNr0niwxEGVhjbI4eOpmJM,19910
40
- rgrid_python-4.5.3.post2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
41
- rgrid_python-4.5.3.post2.dist-info/licenses/LICENSE,sha256=8zNQKZlkc4JOaW7br_a8aALg4baZwZkLKLTQx3kuHkc,48
42
- rgrid_python-4.5.3.post2.dist-info/RECORD,,
39
+ rgrid_python-4.5.3.post3.dist-info/METADATA,sha256=U2Lx9ccpNvIWTBozFmqItTD-AWHPY9Wq9zdeKQelK68,19910
40
+ rgrid_python-4.5.3.post3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
41
+ rgrid_python-4.5.3.post3.dist-info/licenses/LICENSE,sha256=8zNQKZlkc4JOaW7br_a8aALg4baZwZkLKLTQx3kuHkc,48
42
+ rgrid_python-4.5.3.post3.dist-info/RECORD,,