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 +160 -419
- iplotx/edge/arrow.py +20 -20
- iplotx/edge/geometry.py +392 -0
- iplotx/edge/ports.py +7 -2
- iplotx/groups.py +24 -14
- iplotx/label.py +49 -14
- iplotx/layout.py +3 -3
- iplotx/network.py +9 -8
- iplotx/style.py +18 -6
- iplotx/tree.py +48 -21
- iplotx/typing.py +19 -0
- iplotx/version.py +1 -1
- iplotx/vertex.py +84 -29
- {iplotx-0.2.0.dist-info → iplotx-0.2.1.dist-info}/METADATA +15 -3
- {iplotx-0.2.0.dist-info → iplotx-0.2.1.dist-info}/RECORD +16 -15
- {iplotx-0.2.0.dist-info → iplotx-0.2.1.dist-info}/WHEEL +0 -0
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,
|
|
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 .
|
|
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
|
-
) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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.
|
|
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.
|
|
224
|
-
offset2 = self.
|
|
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",
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
707
|
-
That's ok since the correct dpi scaling is set whenever there is a different
|
|
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
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
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
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
843
|
-
|
|
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):
|