iplotx 0.2.0__py3-none-any.whl → 0.2.1__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.
iplotx/edge/__init__.py CHANGED
@@ -7,15 +7,18 @@ Some supporting functions are also defined here.
7
7
  from typing import (
8
8
  Sequence,
9
9
  Optional,
10
- Never,
11
10
  Any,
12
11
  )
13
- from math import atan2, tan, cos, pi, sin
12
+ from math import atan2, cos, pi, sin
14
13
  from collections import defaultdict
15
14
  import numpy as np
16
15
  import pandas as pd
17
16
  import matplotlib as mpl
18
17
 
18
+ from ..typing import (
19
+ Pair,
20
+ LeafProperty,
21
+ )
19
22
  from ..utils.matplotlib import (
20
23
  _compute_mid_coord_and_rot,
21
24
  _stale_wrapper,
@@ -27,7 +30,12 @@ from ..style import (
27
30
  from ..label import LabelCollection
28
31
  from ..vertex import VertexCollection
29
32
  from .arrow import EdgeArrowCollection
30
- from .ports import _get_port_unit_vector
33
+ from .geometry import (
34
+ _compute_loops_per_angle,
35
+ _fix_parallel_edges_straight,
36
+ _compute_loop_path,
37
+ _compute_edge_path,
38
+ )
31
39
 
32
40
 
33
41
  @_forwarder(
@@ -58,15 +66,13 @@ class EdgeCollection(mpl.collections.PatchCollection):
58
66
  patches: Sequence[mpl.patches.Patch],
59
67
  vertex_ids: Sequence[tuple],
60
68
  vertex_collection: VertexCollection,
61
- layout: pd.DataFrame,
62
69
  *args,
63
- layout_coordinate_system: str = "cartesian",
64
70
  transform: mpl.transforms.Transform = mpl.transforms.IdentityTransform(),
65
71
  arrow_transform: mpl.transforms.Transform = mpl.transforms.IdentityTransform(),
66
72
  directed: bool = False,
67
73
  style: Optional[dict[str, Any]] = None,
68
74
  **kwargs,
69
- ) -> Never:
75
+ ) -> None:
70
76
  """Initialise an EdgeCollection.
71
77
 
72
78
  Parameters:
@@ -75,10 +81,6 @@ class EdgeCollection(mpl.collections.PatchCollection):
75
81
  end of an edge.
76
82
  vertex_collection: The VertexCollection instance containing the Artist for the
77
83
  vertices. This is needed to compute vertex borders and adjust edges accordingly.
78
- layout: The vertex layout.
79
- layout_coordinate_system: The coordinate system the previous parameter is in. For
80
- certain layouts, this might not be "cartesian" (e.g. "polar" layour for radial
81
- trees).
82
84
  transform: The matplotlib transform for the edges, usually transData.
83
85
  arrow_transform: The matplotlib transform for the arrow patches. This is not the
84
86
  *offset_transform* of arrows, which is set equal to the edge transform (previous
@@ -92,11 +94,6 @@ class EdgeCollection(mpl.collections.PatchCollection):
92
94
  self._vertex_ids = vertex_ids
93
95
 
94
96
  self._vertex_collection = vertex_collection
95
- # NOTE: the layout is needed for non-cartesian coordinate systems
96
- # for which information is lost upon cartesianisation (e.g. polar,
97
- # for which multiple angles are degenerate in cartesian space).
98
- self._layout = layout
99
- self._layout_coordinate_system = layout_coordinate_system
100
97
  self._style = style if style is not None else {}
101
98
  self._labels = kwargs.pop("labels", None)
102
99
  self._directed = directed
@@ -132,7 +129,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
132
129
  children.append(self._label_collection)
133
130
  return tuple(children)
134
131
 
135
- def set_figure(self, fig) -> Never:
132
+ def set_figure(self, fig) -> None:
136
133
  super().set_figure(fig)
137
134
  self._update_paths()
138
135
  # NOTE: This sets the correct offsets in the arrows,
@@ -152,7 +149,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
152
149
  return self._directed
153
150
 
154
151
  @directed.setter
155
- def directed(self, value) -> Never:
152
+ def directed(self, value) -> None:
156
153
  """Setter for the directed property.
157
154
 
158
155
  Changing this property triggers the addition/removal of arrows from the plot.
@@ -175,7 +172,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
175
172
  # and that will update children. We might need to verify that.
176
173
  self.stale = True
177
174
 
178
- def set_array(self, A) -> Never:
175
+ def set_array(self, A) -> None:
179
176
  """Set the array for cmap/norm coloring."""
180
177
  # Preserve the alpha channel
181
178
  super().set_array(A)
@@ -185,7 +182,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
185
182
  # This is necessary to ensure edgecolors are bool-flagged correctly
186
183
  self.set_edgecolor(None)
187
184
 
188
- def update_scalarmappable(self) -> Never:
185
+ def update_scalarmappable(self) -> None:
189
186
  """Update colors from the scalar mappable array, if any.
190
187
 
191
188
  Assign edge colors from a numerical array, and match arrow colors
@@ -195,7 +192,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
195
192
  super().update_scalarmappable()
196
193
  # Now self._edgecolors has the correct colorspace values
197
194
  if hasattr(self, "_arrows"):
198
- self._arrows.set_colors(self.get_edgecolors())
195
+ self._arrows.set_colors(self.get_edgecolor())
199
196
 
200
197
  def get_labels(self) -> Optional[LabelCollection]:
201
198
  """Get LabelCollection artist for labels if present."""
@@ -220,8 +217,8 @@ class EdgeCollection(mpl.collections.PatchCollection):
220
217
  for v1, v2 in self._vertex_ids:
221
218
  # NOTE: these are in the original layout coordinate system
222
219
  # not cartesianised yet.
223
- offset1 = self._layout.values[index[v1]]
224
- offset2 = self._layout.values[index[v2]]
220
+ offset1 = self._vertex_collection.get_layout().values[index[v1]]
221
+ offset2 = self._vertex_collection.get_layout().values[index[v2]]
225
222
  voffsets.append((offset1, offset2))
226
223
 
227
224
  path1 = self._vertex_collection.get_paths()[index[v1]]
@@ -257,7 +254,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
257
254
  vcenters = vinfo["offsets"]
258
255
  vpaths = vinfo["paths"]
259
256
  vsizes = vinfo["sizes"]
260
- loopmaxangle = pi / 180.0 * self._style.get("loopmaxangle", pi / 3)
257
+ loopmaxangle = pi / 180.0 * self._style.get("loopmaxangle", 60.0)
261
258
 
262
259
  if transform is None:
263
260
  transform = self.get_transform()
@@ -302,7 +299,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
302
299
  waypoints = edge_stylei.get("waypoints", "none")
303
300
 
304
301
  # Compute actual edge path
305
- path, angles = self._compute_edge_path(
302
+ path, angles = _compute_edge_path(
306
303
  vcoord_data,
307
304
  vpath_fig,
308
305
  vsize_fig,
@@ -311,6 +308,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
311
308
  tension=tension,
312
309
  waypoints=waypoints,
313
310
  ports=ports,
311
+ layout_coordinate_system=self._vertex_collection.get_layout_coordinate_system(),
314
312
  )
315
313
 
316
314
  # Collect angles for this vertex, to be used for loops plotting below
@@ -334,7 +332,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
334
332
  indices_inv = parallel_edges.pop((v2, v1), [])
335
333
  ntot = len(indices) + len(indices_inv)
336
334
  if ntot > 1:
337
- self._fix_parallel_edges_straight(
335
+ _fix_parallel_edges_straight(
338
336
  paths,
339
337
  indices,
340
338
  indices_inv,
@@ -353,7 +351,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
353
351
 
354
352
  # The space between the existing angles is where we can fit the loops
355
353
  # One loop we can fit in the largest wedge, multiple loops we need
356
- nloops_per_angle = self._compute_loops_per_angle(nloops, edge_angles)
354
+ nloops_per_angle = _compute_loops_per_angle(nloops, edge_angles)
357
355
 
358
356
  idx = 0
359
357
  for theta1, theta2, nloops in nloops_per_angle:
@@ -366,7 +364,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
366
364
  thetaj2 = thetaj1 + min(delta, loopmaxangle)
367
365
 
368
366
  # Get the path for this loop
369
- path = self._compute_loop_path(
367
+ path = _compute_loop_path(
370
368
  vcoord_fig,
371
369
  vpath,
372
370
  vsize,
@@ -380,299 +378,6 @@ class EdgeCollection(mpl.collections.PatchCollection):
380
378
 
381
379
  self._paths = paths
382
380
 
383
- def _fix_parallel_edges_straight(
384
- self,
385
- paths,
386
- indices,
387
- indices_inv,
388
- trans,
389
- trans_inv,
390
- offset=3,
391
- ):
392
- """Offset parallel edges along the same path."""
393
- ntot = len(indices) + len(indices_inv)
394
-
395
- # This is straight so two vertices anyway
396
- # NOTE: all paths will be the same, which is why we need to offset them
397
- vs, ve = trans(paths[indices[0]].vertices)
398
-
399
- # Move orthogonal to the line
400
- fracs = (
401
- (vs - ve) / np.sqrt(((vs - ve) ** 2).sum()) @ np.array([[0, 1], [-1, 0]])
402
- )
403
-
404
- # NOTE: for now treat both direction the same
405
- for i, idx in enumerate(indices + indices_inv):
406
- # Offset the path
407
- paths[idx].vertices = trans_inv(
408
- trans(paths[idx].vertices) + fracs * offset * (i - ntot / 2)
409
- )
410
-
411
- def _compute_loop_path(
412
- self,
413
- vcoord_fig,
414
- vpath,
415
- vsize,
416
- angle1,
417
- angle2,
418
- trans_inv,
419
- looptension,
420
- ):
421
- # Shorten at starting angle
422
- start = self._get_shorter_edge_coords(vpath, vsize, angle1) + vcoord_fig
423
- # Shorten at end angle
424
- end = self._get_shorter_edge_coords(vpath, vsize, angle2) + vcoord_fig
425
-
426
- aux1 = (start - vcoord_fig) * looptension + vcoord_fig
427
- aux2 = (end - vcoord_fig) * looptension + vcoord_fig
428
-
429
- vertices = np.vstack(
430
- [
431
- start,
432
- aux1,
433
- aux2,
434
- end,
435
- ]
436
- )
437
- codes = ["MOVETO"] + ["CURVE4"] * 3
438
-
439
- # Offset to place and transform to data coordinates
440
- vertices = trans_inv(vertices)
441
- codes = [getattr(mpl.path.Path, x) for x in codes]
442
- path = mpl.path.Path(
443
- vertices,
444
- codes=codes,
445
- )
446
- return path
447
-
448
- def _compute_edge_path(
449
- self,
450
- *args,
451
- **kwargs,
452
- ):
453
- tension = kwargs.pop("tension", 0)
454
- waypoints = kwargs.pop("waypoints", "none")
455
- ports = kwargs.pop("ports", (None, None))
456
-
457
- if (waypoints != "none") and (tension != 0):
458
- raise ValueError("Waypoints not supported for curved edges.")
459
-
460
- if waypoints != "none":
461
- return self._compute_edge_path_waypoints(waypoints, *args, **kwargs)
462
-
463
- if tension == 0:
464
- return self._compute_edge_path_straight(*args, **kwargs)
465
-
466
- return self._compute_edge_path_curved(
467
- tension,
468
- *args,
469
- ports=ports,
470
- **kwargs,
471
- )
472
-
473
- def _compute_edge_path_waypoints(
474
- self,
475
- waypoints,
476
- vcoord_data,
477
- vpath_fig,
478
- vsize_fig,
479
- trans,
480
- trans_inv,
481
- points_per_curve=30,
482
- **kwargs,
483
- ):
484
-
485
- if waypoints in ("x0y1", "y0x1"):
486
- assert self._layout_coordinate_system == "cartesian"
487
-
488
- # Coordinates in figure (default) coords
489
- vcoord_fig = trans(vcoord_data)
490
-
491
- if waypoints == "x0y1":
492
- waypoint = np.array([vcoord_fig[0][0], vcoord_fig[1][1]])
493
- else:
494
- waypoint = np.array([vcoord_fig[1][0], vcoord_fig[0][1]])
495
-
496
- # Angles of the straight lines
497
- theta0 = atan2(*((waypoint - vcoord_fig[0])[::-1]))
498
- theta1 = atan2(*((waypoint - vcoord_fig[1])[::-1]))
499
-
500
- # Shorten at starting vertex
501
- vs = (
502
- self._get_shorter_edge_coords(vpath_fig[0], vsize_fig[0], theta0)
503
- + vcoord_fig[0]
504
- )
505
-
506
- # Shorten at end vertex
507
- ve = (
508
- self._get_shorter_edge_coords(vpath_fig[1], vsize_fig[1], theta1)
509
- + vcoord_fig[1]
510
- )
511
-
512
- points = [vs, waypoint, ve]
513
- codes = ["MOVETO", "LINETO", "LINETO"]
514
- angles = (theta0, theta1)
515
- elif waypoints == "r0a1":
516
- assert self._layout_coordinate_system == "polar"
517
-
518
- r0, alpha0 = vcoord_data[0]
519
- r1, alpha1 = vcoord_data[1]
520
- idx_inner = np.argmin([r0, r1])
521
- idx_outer = 1 - idx_inner
522
- alpha_outer = [alpha0, alpha1][idx_outer]
523
-
524
- # FIXME: this is aware of chirality as stored by the layout function
525
- betas = np.linspace(alpha0, alpha1, points_per_curve)
526
- waypoints = [r0, r1][idx_inner] * np.vstack(
527
- [np.cos(betas), np.sin(betas)]
528
- ).T
529
- endpoint = [r0, r1][idx_outer] * np.array(
530
- [np.cos(alpha_outer), np.sin(alpha_outer)]
531
- )
532
- points = np.array(list(waypoints) + [endpoint])
533
- points = trans(points)
534
- codes = ["MOVETO"] + ["LINETO"] * len(waypoints)
535
- # FIXME: same as previus comment
536
- angles = (alpha0 + pi / 2, alpha1)
537
-
538
- else:
539
- raise NotImplementedError(
540
- f"Edge shortening with waypoints not implemented yet: {waypoints}.",
541
- )
542
-
543
- path = mpl.path.Path(
544
- points,
545
- codes=[getattr(mpl.path.Path, x) for x in codes],
546
- )
547
-
548
- path.vertices = trans_inv(path.vertices)
549
- return path, angles
550
-
551
- def _compute_edge_path_straight(
552
- self,
553
- vcoord_data,
554
- vpath_fig,
555
- vsize_fig,
556
- trans,
557
- trans_inv,
558
- **kwargs,
559
- ):
560
-
561
- # Coordinates in figure (default) coords
562
- vcoord_fig = trans(vcoord_data)
563
-
564
- points = []
565
-
566
- # Angle of the straight line
567
- theta = atan2(*((vcoord_fig[1] - vcoord_fig[0])[::-1]))
568
-
569
- # Shorten at starting vertex
570
- vs = (
571
- self._get_shorter_edge_coords(vpath_fig[0], vsize_fig[0], theta)
572
- + vcoord_fig[0]
573
- )
574
- points.append(vs)
575
-
576
- # Shorten at end vertex
577
- ve = (
578
- self._get_shorter_edge_coords(vpath_fig[1], vsize_fig[1], theta + pi)
579
- + vcoord_fig[1]
580
- )
581
- points.append(ve)
582
-
583
- codes = ["MOVETO", "LINETO"]
584
- path = mpl.path.Path(
585
- points,
586
- codes=[getattr(mpl.path.Path, x) for x in codes],
587
- )
588
- path.vertices = trans_inv(path.vertices)
589
- return path, (theta, theta + np.pi)
590
-
591
- def _compute_edge_path_curved(
592
- self,
593
- tension,
594
- vcoord_data,
595
- vpath_fig,
596
- vsize_fig,
597
- trans,
598
- trans_inv,
599
- ports=(None, None),
600
- ):
601
- """Shorten the edge path along a cubic Bezier between the vertex centres.
602
-
603
- The most important part is that the derivative of the Bezier at the start
604
- and end point towards the vertex centres: people notice if they do not.
605
- """
606
-
607
- # Coordinates in figure (default) coords
608
- vcoord_fig = trans(vcoord_data)
609
-
610
- dv = vcoord_fig[1] - vcoord_fig[0]
611
- edge_straight_length = np.sqrt((dv**2).sum())
612
-
613
- auxs = [None, None]
614
- for i in range(2):
615
- if ports[i] is not None:
616
- der = _get_port_unit_vector(ports[i], trans_inv)
617
- auxs[i] = der * edge_straight_length * tension + vcoord_fig[i]
618
-
619
- # Both ports defined, just use them and hope for the best
620
- # Obviously, if the user specifies ports that make no sense,
621
- # this is going to be a (technically valid) mess.
622
- if all(aux is not None for aux in auxs):
623
- pass
624
-
625
- # If no ports are specified (the most common case), compute
626
- # the Bezier and shorten it
627
- elif all(aux is None for aux in auxs):
628
- # Put auxs along the way
629
- auxs = np.array(
630
- [
631
- vcoord_fig[0] + 0.33 * dv,
632
- vcoord_fig[1] - 0.33 * dv,
633
- ]
634
- )
635
- # Right rotation from the straight edge
636
- dv_rot = -0.1 * dv @ np.array([[0, 1], [-1, 0]])
637
- # Shift the auxs orthogonal to the straight edge
638
- auxs += dv_rot * tension
639
-
640
- # First port is defined
641
- elif (auxs[0] is not None) and (auxs[1] is None):
642
- auxs[1] = auxs[0]
643
-
644
- # Second port is defined
645
- else:
646
- auxs[0] = auxs[1]
647
-
648
- vs = [None, None]
649
- thetas = [None, None]
650
- for i in range(2):
651
- thetas[i] = atan2(*((auxs[i] - vcoord_fig[i])[::-1]))
652
- vs[i] = (
653
- self._get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], thetas[i])
654
- + vcoord_fig[i]
655
- )
656
-
657
- path = {
658
- "vertices": [
659
- vs[0],
660
- auxs[0],
661
- auxs[1],
662
- vs[1],
663
- ],
664
- "codes": ["MOVETO"] + ["CURVE4"] * 3,
665
- }
666
-
667
- path = mpl.path.Path(
668
- path["vertices"],
669
- codes=[getattr(mpl.path.Path, x) for x in path["codes"]],
670
- )
671
-
672
- # Return to data transform
673
- path.vertices = trans_inv(path.vertices)
674
- return path, tuple(thetas)
675
-
676
381
  def _update_labels(self):
677
382
  if self._labels is None:
678
383
  return
@@ -696,16 +401,15 @@ class EdgeCollection(mpl.collections.PatchCollection):
696
401
 
697
402
  def _update_arrows(
698
403
  self,
699
- which: str = "end",
700
404
  ) -> None:
701
405
  """Extract the start and/or end angles of the paths to compute arrows.
702
406
 
703
407
  Parameters:
704
408
  which: Which end of the edge to put an arrow on. Currently only "end" is accepted.
705
409
 
706
- NOTE: This function does *not* update the arrow sizes/_transforms to the correct dpi scaling.
707
- That's ok since the correct dpi scaling is set whenever there is a different figure (before
708
- first draw) and whenever a draw is called.
410
+ NOTE: This function does *not* update the arrow sizes/_transforms to the correct dpi
411
+ scaling. That's ok since the correct dpi scaling is set whenever there is a different
412
+ figure (before first draw) and whenever a draw is called.
709
413
  """
710
414
  if not hasattr(self, "_arrows"):
711
415
  return
@@ -746,104 +450,141 @@ class EdgeCollection(mpl.collections.PatchCollection):
746
450
  # This sets the arrow sizes with dpi scaling
747
451
  child.draw(renderer)
748
452
 
749
- @property
750
- def stale(self):
751
- return super().stale
752
-
753
- @stale.setter
754
- def stale(self, val):
755
- mpl.collections.PatchCollection.stale.fset(self, val)
756
- if val and hasattr(self, "stale_callback_post"):
757
- self.stale_callback_post(self)
758
-
759
- @staticmethod
760
- def _compute_loops_per_angle(nloops, angles):
761
- if len(angles) == 0:
762
- return [(0, 2 * pi, nloops)]
763
-
764
- angles_sorted_closed = list(sorted(angles))
765
- angles_sorted_closed.append(angles_sorted_closed[0] + 2 * pi)
766
- deltas = np.diff(angles_sorted_closed)
767
-
768
- # Now we have the deltas and the total number of loops
769
- # 1. Assign all loops to the largest wedge
770
- idx_dmax = deltas.argmax()
771
- if nloops == 1:
772
- return [
773
- (
774
- angles_sorted_closed[idx_dmax],
775
- angles_sorted_closed[idx_dmax + 1],
776
- nloops,
777
- )
778
- ]
779
-
780
- # 2. Check if any other wedges are larger than this
781
- # If not, we are done (this is the algo in igraph)
782
- dsplit = deltas[idx_dmax] / nloops
783
- if (deltas > dsplit).sum() < 2:
784
- return [
785
- (
786
- angles_sorted_closed[idx_dmax],
787
- angles_sorted_closed[idx_dmax + 1],
788
- nloops,
789
- )
790
- ]
791
-
792
- # 3. Check how small the second-largest wedge would become
793
- idx_dsort = np.argsort(deltas)
794
- return [
795
- (
796
- angles_sorted_closed[idx_dmax],
797
- angles_sorted_closed[idx_dmax + 1],
798
- nloops - 1,
799
- ),
800
- (
801
- angles_sorted_closed[idx_dsort[-2]],
802
- angles_sorted_closed[idx_dsort[-2] + 1],
803
- 1,
804
- ),
805
- ]
806
-
807
- @staticmethod
808
- def _get_shorter_edge_coords(vpath, vsize, theta):
809
- # Bound theta from -pi to pi (why is that not guaranteed?)
810
- theta = (theta + pi) % (2 * pi) - pi
811
-
812
- # Size zero vertices need no shortening
813
- if vsize == 0:
814
- return np.array([0, 0])
815
-
816
- for i in range(len(vpath)):
817
- v1 = vpath.vertices[i]
818
- v2 = vpath.vertices[(i + 1) % len(vpath)]
819
- theta1 = atan2(*((v1)[::-1]))
820
- theta2 = atan2(*((v2)[::-1]))
821
-
822
- # atan2 ranges ]-3.14, 3.14]
823
- # so it can be that theta1 is -3 and theta2 is +3
824
- # therefore we need two separate cases, one that cuts at pi and one at 0
825
- cond1 = theta1 <= theta <= theta2
826
- cond2 = (
827
- (theta1 + 2 * pi) % (2 * pi)
828
- <= (theta + 2 * pi) % (2 * pi)
829
- <= (theta2 + 2 * pi) % (2 * pi)
830
- )
831
- if cond1 or cond2:
832
- break
453
+ def get_ports(self) -> Optional[LeafProperty[Pair[Optional[str]]]]:
454
+ """Get the ports for all edges.
455
+
456
+ Returns:
457
+ The ports for the edges, as a pair of strings or None for each edge. If None, it
458
+ means all edges are free.
459
+ """
460
+ return self._style.get("ports", None)
461
+
462
+ def set_ports(self, ports: Optional[LeafProperty[Pair[Optional[str]]]]) -> None:
463
+ """Set new ports for the edges.
464
+
465
+ Parameters:
466
+ ports: A pair of ports strings for each edge. Each port can be None to mean free
467
+ edge end.
468
+ """
469
+ if ports is None:
470
+ del self._style["ports"]
833
471
  else:
834
- raise ValueError("Angle for patch not found")
472
+ self._style["ports"] = ports
473
+ self.stale = True
474
+
475
+ def get_tension(self) -> Optional[LeafProperty[float]]:
476
+ """Get the tension for the edges.
477
+
478
+ Returns:
479
+ The tension for the edges. If None, the edges are straight.
480
+ """
481
+ return self._style.get("tension", None)
482
+
483
+ def set_tension(self, tension: Optional[LeafProperty[float]]) -> None:
484
+ """Set new tension for the edges.
485
+
486
+ Parameters:
487
+ tension: The tension to use for curved edges. If None, the edges become straight.
488
+
489
+ Note: This function does not set self.set_curved(True) automatically. If you are
490
+ unsure whether that property is set already, you should call both functions.
491
+
492
+ Example:
493
+ # Set curved edges with different tensions
494
+ >>> network.get_edges().set_curved(True)
495
+ >>> network.get_edges().set_tension([1, 0.5])
496
+
497
+ # Set straight edges
498
+ # (the latter call is optional but helps readability)
499
+ >>> network.get_edges().set_curved(False)
500
+ >>> network.get_edges().set_tension(None)
501
+
502
+ """
503
+ if tension is None:
504
+ del self._style["tension"]
505
+ else:
506
+ self._style["tension"] = tension
507
+ self.stale = True
508
+
509
+ get_tensions = get_tension
510
+ set_tensions = set_tension
835
511
 
836
- # The edge meets the patch of the vertex on the v1-v2 size,
837
- # at angle theta from the center
838
- mtheta = tan(theta)
839
- if v2[0] == v1[0]:
840
- xe = v1[0]
512
+ def get_curved(self) -> bool:
513
+ """Get whether the edges are curved or not.
514
+
515
+ Returns:
516
+ A bool that is True if the edges are curved, False if they are straight.
517
+ """
518
+ return self._style.get("curved", False)
519
+
520
+ def set_curved(self, curved: bool) -> None:
521
+ """Set whether the edges are curved or not.
522
+
523
+ Parameters:
524
+ curved: Whether the edges should be curved (True) or straight (False).
525
+
526
+ Note: If you want only some edges to be curved, set curved to True and set tensions to
527
+ 0 for the straight edges.
528
+ """
529
+ self._style["curved"] = bool(curved)
530
+ self.stale = True
531
+
532
+ def get_loopmaxangle(self) -> Optional[float]:
533
+ """Get the maximum angle for loops.
534
+
535
+ Returns:
536
+ The maximum angle in degrees that a loop can take. If None, the default is 60.
537
+ """
538
+ return self._style.get("loopmaxangle", 60)
539
+
540
+ def set_loopmaxangle(self, loopmaxangle: float) -> None:
541
+ """Set the maximum angle for loops.
542
+
543
+ Parameters:
544
+ loopmaxangle: The maximum angle in degrees that a loop can take.
545
+ """
546
+ self._style["loopmaxangle"] = loopmaxangle
547
+ self.stale = True
548
+
549
+ def get_looptension(self) -> Optional[float]:
550
+ """Get the tension for loops.
551
+
552
+ Returns:
553
+ The tension for loops. If None, the default is 2.5.
554
+ """
555
+ return self._style.get("looptension", 2.5)
556
+
557
+ def set_looptension(self, looptension: Optional[float]) -> None:
558
+ """Set new tension for loops.
559
+
560
+ Parameters:
561
+ looptension: The tension to use for loops. If None, the default is 2.5.
562
+ """
563
+ if looptension is None:
564
+ del self._style["looptension"]
565
+ else:
566
+ self._style["looptension"] = looptension
567
+ self.stale = True
568
+
569
+ def get_offset(self) -> Optional[float]:
570
+ """Get the offset for parallel straight edges.
571
+
572
+ Returns:
573
+ The offset in points for parallel straight edges. If None, the default is 3.
574
+ """
575
+ return self._style.get("offset", 3)
576
+
577
+ def set_offset(self, offset: Optional[float]) -> None:
578
+ """Set the offset for parallel straight edges.
579
+
580
+ Parameters:
581
+ offset: The offset in points for parallel straight edges. If None, the default is 3.
582
+ """
583
+ if offset is None:
584
+ del self._style["offset"]
841
585
  else:
842
- m12 = (v2[1] - v1[1]) / (v2[0] - v1[0])
843
- xe = (v1[1] - m12 * v1[0]) / (mtheta - m12)
844
- ye = mtheta * xe
845
- ve = np.array([xe, ye])
846
- return ve * vsize
586
+ self._style["offset"] = offset
587
+ self.stale = True
847
588
 
848
589
 
849
590
  def make_stub_patch(**kwargs):