iplotx 0.2.0__py3-none-any.whl → 0.3.0__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/cascades.py +223 -0
- iplotx/edge/__init__.py +180 -420
- iplotx/edge/arrow.py +20 -20
- iplotx/edge/geometry.py +448 -0
- iplotx/edge/ports.py +7 -2
- iplotx/groups.py +24 -14
- iplotx/ingest/__init__.py +12 -4
- iplotx/ingest/heuristics.py +1 -3
- iplotx/ingest/providers/network/igraph.py +4 -2
- iplotx/ingest/providers/network/networkx.py +4 -2
- iplotx/ingest/providers/tree/biopython.py +21 -79
- iplotx/ingest/providers/tree/cogent3.py +17 -88
- iplotx/ingest/providers/tree/ete4.py +19 -87
- iplotx/ingest/providers/tree/skbio.py +17 -88
- iplotx/ingest/typing.py +225 -22
- iplotx/label.py +103 -21
- iplotx/layout.py +57 -36
- iplotx/network.py +9 -8
- iplotx/plotting.py +6 -3
- iplotx/style.py +36 -10
- iplotx/tree.py +237 -29
- iplotx/typing.py +19 -0
- iplotx/version.py +1 -1
- iplotx/vertex.py +122 -35
- {iplotx-0.2.0.dist-info → iplotx-0.3.0.dist-info}/METADATA +16 -3
- iplotx-0.3.0.dist-info/RECORD +32 -0
- iplotx-0.2.0.dist-info/RECORD +0 -30
- {iplotx-0.2.0.dist-info → iplotx-0.3.0.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()
|
|
@@ -300,9 +297,11 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
300
297
|
ports = None
|
|
301
298
|
|
|
302
299
|
waypoints = edge_stylei.get("waypoints", "none")
|
|
300
|
+
if waypoints != "none":
|
|
301
|
+
ports = edge_stylei.get("ports", (None, None))
|
|
303
302
|
|
|
304
303
|
# Compute actual edge path
|
|
305
|
-
path, angles =
|
|
304
|
+
path, angles = _compute_edge_path(
|
|
306
305
|
vcoord_data,
|
|
307
306
|
vpath_fig,
|
|
308
307
|
vsize_fig,
|
|
@@ -311,8 +310,25 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
311
310
|
tension=tension,
|
|
312
311
|
waypoints=waypoints,
|
|
313
312
|
ports=ports,
|
|
313
|
+
layout_coordinate_system=self._vertex_collection.get_layout_coordinate_system(),
|
|
314
314
|
)
|
|
315
315
|
|
|
316
|
+
offset = edge_stylei.get("offset", 0)
|
|
317
|
+
if np.isscalar(offset):
|
|
318
|
+
if offset == 0:
|
|
319
|
+
offset = (0, 0)
|
|
320
|
+
else:
|
|
321
|
+
vd_fig = trans(vcoord_data[1]) - trans(vcoord_data[0])
|
|
322
|
+
vd_fig /= np.linalg.norm(vd_fig)
|
|
323
|
+
vrot = vd_fig @ np.array([[0, -1], [1, 0]])
|
|
324
|
+
offset = offset * vrot
|
|
325
|
+
offset = np.asarray(offset, dtype=float)
|
|
326
|
+
# Scale by dpi
|
|
327
|
+
dpi = self.figure.dpi if hasattr(self, "figure") else 72.0
|
|
328
|
+
offset *= dpi / 72.0
|
|
329
|
+
if (offset != 0).any():
|
|
330
|
+
path.vertices[:] = trans_inv(trans(path.vertices) + offset)
|
|
331
|
+
|
|
316
332
|
# Collect angles for this vertex, to be used for loops plotting below
|
|
317
333
|
if v1 in loop_vertex_dict:
|
|
318
334
|
loop_vertex_dict[v1]["edge_angles"].append(angles[0])
|
|
@@ -334,13 +350,13 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
334
350
|
indices_inv = parallel_edges.pop((v2, v1), [])
|
|
335
351
|
ntot = len(indices) + len(indices_inv)
|
|
336
352
|
if ntot > 1:
|
|
337
|
-
|
|
353
|
+
_fix_parallel_edges_straight(
|
|
338
354
|
paths,
|
|
339
355
|
indices,
|
|
340
356
|
indices_inv,
|
|
341
357
|
trans,
|
|
342
358
|
trans_inv,
|
|
343
|
-
|
|
359
|
+
paralleloffset=self._style.get("paralleloffset", 3),
|
|
344
360
|
)
|
|
345
361
|
|
|
346
362
|
# 3. Deal with loops at the end
|
|
@@ -353,7 +369,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
353
369
|
|
|
354
370
|
# The space between the existing angles is where we can fit the loops
|
|
355
371
|
# One loop we can fit in the largest wedge, multiple loops we need
|
|
356
|
-
nloops_per_angle =
|
|
372
|
+
nloops_per_angle = _compute_loops_per_angle(nloops, edge_angles)
|
|
357
373
|
|
|
358
374
|
idx = 0
|
|
359
375
|
for theta1, theta2, nloops in nloops_per_angle:
|
|
@@ -366,7 +382,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
366
382
|
thetaj2 = thetaj1 + min(delta, loopmaxangle)
|
|
367
383
|
|
|
368
384
|
# Get the path for this loop
|
|
369
|
-
path =
|
|
385
|
+
path = _compute_loop_path(
|
|
370
386
|
vcoord_fig,
|
|
371
387
|
vpath,
|
|
372
388
|
vsize,
|
|
@@ -380,299 +396,6 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
380
396
|
|
|
381
397
|
self._paths = paths
|
|
382
398
|
|
|
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
399
|
def _update_labels(self):
|
|
677
400
|
if self._labels is None:
|
|
678
401
|
return
|
|
@@ -696,16 +419,15 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
696
419
|
|
|
697
420
|
def _update_arrows(
|
|
698
421
|
self,
|
|
699
|
-
which: str = "end",
|
|
700
422
|
) -> None:
|
|
701
423
|
"""Extract the start and/or end angles of the paths to compute arrows.
|
|
702
424
|
|
|
703
425
|
Parameters:
|
|
704
426
|
which: Which end of the edge to put an arrow on. Currently only "end" is accepted.
|
|
705
427
|
|
|
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.
|
|
428
|
+
NOTE: This function does *not* update the arrow sizes/_transforms to the correct dpi
|
|
429
|
+
scaling. That's ok since the correct dpi scaling is set whenever there is a different
|
|
430
|
+
figure (before first draw) and whenever a draw is called.
|
|
709
431
|
"""
|
|
710
432
|
if not hasattr(self, "_arrows"):
|
|
711
433
|
return
|
|
@@ -746,104 +468,141 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
746
468
|
# This sets the arrow sizes with dpi scaling
|
|
747
469
|
child.draw(renderer)
|
|
748
470
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
(
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
|
471
|
+
def get_ports(self) -> Optional[LeafProperty[Pair[Optional[str]]]]:
|
|
472
|
+
"""Get the ports for all edges.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
The ports for the edges, as a pair of strings or None for each edge. If None, it
|
|
476
|
+
means all edges are free.
|
|
477
|
+
"""
|
|
478
|
+
return self._style.get("ports", None)
|
|
479
|
+
|
|
480
|
+
def set_ports(self, ports: Optional[LeafProperty[Pair[Optional[str]]]]) -> None:
|
|
481
|
+
"""Set new ports for the edges.
|
|
482
|
+
|
|
483
|
+
Parameters:
|
|
484
|
+
ports: A pair of ports strings for each edge. Each port can be None to mean free
|
|
485
|
+
edge end.
|
|
486
|
+
"""
|
|
487
|
+
if ports is None:
|
|
488
|
+
del self._style["ports"]
|
|
489
|
+
else:
|
|
490
|
+
self._style["ports"] = ports
|
|
491
|
+
self.stale = True
|
|
492
|
+
|
|
493
|
+
def get_tension(self) -> Optional[LeafProperty[float]]:
|
|
494
|
+
"""Get the tension for the edges.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
The tension for the edges. If None, the edges are straight.
|
|
498
|
+
"""
|
|
499
|
+
return self._style.get("tension", None)
|
|
500
|
+
|
|
501
|
+
def set_tension(self, tension: Optional[LeafProperty[float]]) -> None:
|
|
502
|
+
"""Set new tension for the edges.
|
|
503
|
+
|
|
504
|
+
Parameters:
|
|
505
|
+
tension: The tension to use for curved edges. If None, the edges become straight.
|
|
506
|
+
|
|
507
|
+
Note: This function does not set self.set_curved(True) automatically. If you are
|
|
508
|
+
unsure whether that property is set already, you should call both functions.
|
|
509
|
+
|
|
510
|
+
Example:
|
|
511
|
+
# Set curved edges with different tensions
|
|
512
|
+
>>> network.get_edges().set_curved(True)
|
|
513
|
+
>>> network.get_edges().set_tension([1, 0.5])
|
|
514
|
+
|
|
515
|
+
# Set straight edges
|
|
516
|
+
# (the latter call is optional but helps readability)
|
|
517
|
+
>>> network.get_edges().set_curved(False)
|
|
518
|
+
>>> network.get_edges().set_tension(None)
|
|
519
|
+
|
|
520
|
+
"""
|
|
521
|
+
if tension is None:
|
|
522
|
+
del self._style["tension"]
|
|
833
523
|
else:
|
|
834
|
-
|
|
524
|
+
self._style["tension"] = tension
|
|
525
|
+
self.stale = True
|
|
835
526
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
527
|
+
get_tensions = get_tension
|
|
528
|
+
set_tensions = set_tension
|
|
529
|
+
|
|
530
|
+
def get_curved(self) -> bool:
|
|
531
|
+
"""Get whether the edges are curved or not.
|
|
532
|
+
|
|
533
|
+
Returns:
|
|
534
|
+
A bool that is True if the edges are curved, False if they are straight.
|
|
535
|
+
"""
|
|
536
|
+
return self._style.get("curved", False)
|
|
537
|
+
|
|
538
|
+
def set_curved(self, curved: bool) -> None:
|
|
539
|
+
"""Set whether the edges are curved or not.
|
|
540
|
+
|
|
541
|
+
Parameters:
|
|
542
|
+
curved: Whether the edges should be curved (True) or straight (False).
|
|
543
|
+
|
|
544
|
+
Note: If you want only some edges to be curved, set curved to True and set tensions to
|
|
545
|
+
0 for the straight edges.
|
|
546
|
+
"""
|
|
547
|
+
self._style["curved"] = bool(curved)
|
|
548
|
+
self.stale = True
|
|
549
|
+
|
|
550
|
+
def get_loopmaxangle(self) -> Optional[float]:
|
|
551
|
+
"""Get the maximum angle for loops.
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
The maximum angle in degrees that a loop can take. If None, the default is 60.
|
|
555
|
+
"""
|
|
556
|
+
return self._style.get("loopmaxangle", 60)
|
|
557
|
+
|
|
558
|
+
def set_loopmaxangle(self, loopmaxangle: float) -> None:
|
|
559
|
+
"""Set the maximum angle for loops.
|
|
560
|
+
|
|
561
|
+
Parameters:
|
|
562
|
+
loopmaxangle: The maximum angle in degrees that a loop can take.
|
|
563
|
+
"""
|
|
564
|
+
self._style["loopmaxangle"] = loopmaxangle
|
|
565
|
+
self.stale = True
|
|
566
|
+
|
|
567
|
+
def get_looptension(self) -> Optional[float]:
|
|
568
|
+
"""Get the tension for loops.
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
The tension for loops. If None, the default is 2.5.
|
|
572
|
+
"""
|
|
573
|
+
return self._style.get("looptension", 2.5)
|
|
574
|
+
|
|
575
|
+
def set_looptension(self, looptension: Optional[float]) -> None:
|
|
576
|
+
"""Set new tension for loops.
|
|
577
|
+
|
|
578
|
+
Parameters:
|
|
579
|
+
looptension: The tension to use for loops. If None, the default is 2.5.
|
|
580
|
+
"""
|
|
581
|
+
if looptension is None:
|
|
582
|
+
del self._style["looptension"]
|
|
583
|
+
else:
|
|
584
|
+
self._style["looptension"] = looptension
|
|
585
|
+
self.stale = True
|
|
586
|
+
|
|
587
|
+
def get_offset(self) -> Optional[float]:
|
|
588
|
+
"""Get the offset for parallel straight edges.
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
The offset in points for parallel straight edges. If None, the default is 3.
|
|
592
|
+
"""
|
|
593
|
+
return self._style.get("offset", 3)
|
|
594
|
+
|
|
595
|
+
def set_offset(self, offset: Optional[float]) -> None:
|
|
596
|
+
"""Set the offset for parallel straight edges.
|
|
597
|
+
|
|
598
|
+
Parameters:
|
|
599
|
+
offset: The offset in points for parallel straight edges. If None, the default is 3.
|
|
600
|
+
"""
|
|
601
|
+
if offset is None:
|
|
602
|
+
del self._style["offset"]
|
|
841
603
|
else:
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
ye = mtheta * xe
|
|
845
|
-
ve = np.array([xe, ye])
|
|
846
|
-
return ve * vsize
|
|
604
|
+
self._style["offset"] = offset
|
|
605
|
+
self.stale = True
|
|
847
606
|
|
|
848
607
|
|
|
849
608
|
def make_stub_patch(**kwargs):
|
|
@@ -868,6 +627,7 @@ def make_stub_patch(**kwargs):
|
|
|
868
627
|
"looptension",
|
|
869
628
|
"loopmaxangle",
|
|
870
629
|
"offset",
|
|
630
|
+
"paralleloffset",
|
|
871
631
|
"cmap",
|
|
872
632
|
]
|
|
873
633
|
for prop in forbidden_props:
|