iplotx 0.3.1__py3-none-any.whl → 0.5.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/__init__.py +5 -0
- iplotx/artists.py +24 -0
- iplotx/cascades.py +22 -31
- iplotx/edge/__init__.py +122 -49
- iplotx/edge/arrow.py +44 -3
- iplotx/edge/geometry.py +30 -21
- iplotx/edge/leaf.py +117 -0
- iplotx/edge/ports.py +3 -2
- iplotx/groups.py +1 -3
- iplotx/ingest/__init__.py +6 -20
- iplotx/ingest/heuristics.py +4 -36
- iplotx/ingest/providers/network/igraph.py +20 -18
- iplotx/ingest/providers/network/networkx.py +20 -24
- iplotx/ingest/providers/network/simple.py +114 -0
- iplotx/ingest/providers/tree/biopython.py +15 -5
- iplotx/ingest/providers/tree/cogent3.py +9 -5
- iplotx/ingest/providers/tree/ete4.py +2 -5
- iplotx/ingest/providers/tree/simple.py +97 -0
- iplotx/ingest/providers/tree/skbio.py +2 -5
- iplotx/ingest/typing.py +109 -19
- iplotx/label.py +42 -12
- iplotx/layout.py +5 -1
- iplotx/network.py +69 -18
- iplotx/plotting.py +9 -9
- iplotx/{style.py → style/__init__.py} +150 -187
- iplotx/style/leaf_info.py +44 -0
- iplotx/style/library.py +324 -0
- iplotx/tree.py +279 -51
- iplotx/typing.py +2 -0
- iplotx/utils/geometry.py +32 -40
- iplotx/utils/matplotlib.py +43 -22
- iplotx/utils/style.py +17 -1
- iplotx/version.py +1 -1
- iplotx/vertex.py +63 -15
- {iplotx-0.3.1.dist-info → iplotx-0.5.0.dist-info}/METADATA +2 -1
- iplotx-0.5.0.dist-info/RECORD +38 -0
- iplotx-0.3.1.dist-info/RECORD +0 -32
- {iplotx-0.3.1.dist-info → iplotx-0.5.0.dist-info}/WHEEL +0 -0
iplotx/__init__.py
CHANGED
|
@@ -11,6 +11,9 @@ from .plotting import (
|
|
|
11
11
|
network,
|
|
12
12
|
tree,
|
|
13
13
|
)
|
|
14
|
+
import iplotx.artists as artists
|
|
15
|
+
import iplotx.style as style
|
|
16
|
+
|
|
14
17
|
|
|
15
18
|
# Shortcut to iplotx.plotting.network
|
|
16
19
|
plot = network
|
|
@@ -19,5 +22,7 @@ __all__ = [
|
|
|
19
22
|
"network",
|
|
20
23
|
"tree",
|
|
21
24
|
"plot",
|
|
25
|
+
"artists",
|
|
26
|
+
"style",
|
|
22
27
|
"__version__",
|
|
23
28
|
]
|
iplotx/artists.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
All artists defined in iplotx.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .network import NetworkArtist
|
|
6
|
+
from .tree import TreeArtist
|
|
7
|
+
from .vertex import VertexCollection
|
|
8
|
+
from .edge import EdgeCollection
|
|
9
|
+
from .label import LabelCollection
|
|
10
|
+
from .edge.arrow import EdgeArrowCollection
|
|
11
|
+
from .edge.leaf import LeafEdgeCollection
|
|
12
|
+
from .cascades import CascadeCollection
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
___all__ = (
|
|
16
|
+
NetworkArtist,
|
|
17
|
+
TreeArtist,
|
|
18
|
+
VertexCollection,
|
|
19
|
+
EdgeCollection,
|
|
20
|
+
LeafEdgeCollection,
|
|
21
|
+
LabelCollection,
|
|
22
|
+
EdgeArrowCollection,
|
|
23
|
+
CascadeCollection,
|
|
24
|
+
)
|
iplotx/cascades.py
CHANGED
|
@@ -2,6 +2,7 @@ from typing import (
|
|
|
2
2
|
Any,
|
|
3
3
|
Optional,
|
|
4
4
|
)
|
|
5
|
+
import warnings
|
|
5
6
|
import numpy as np
|
|
6
7
|
import pandas as pd
|
|
7
8
|
|
|
@@ -38,7 +39,7 @@ class CascadeCollection(mpl.collections.PatchCollection):
|
|
|
38
39
|
|
|
39
40
|
# NOTE: there is a weird bug in pandas when using generic Hashable-s
|
|
40
41
|
# with .loc. Seems like doing .T[...] works for individual index
|
|
41
|
-
# elements only though
|
|
42
|
+
# elements only though (i.e. using __getitem__ a la dict)
|
|
42
43
|
def get_node_coords(node):
|
|
43
44
|
return layout.T[node].values
|
|
44
45
|
|
|
@@ -54,9 +55,19 @@ class CascadeCollection(mpl.collections.PatchCollection):
|
|
|
54
55
|
# These patches need at least a facecolor (usually) or an edgecolor
|
|
55
56
|
# so it's safe to make a list from these
|
|
56
57
|
nodes_unordered = set()
|
|
57
|
-
for prop in ("facecolor", "edgecolor"):
|
|
58
|
+
for prop in ("facecolor", "edgecolor", "linewidth", "linestyle"):
|
|
58
59
|
if prop in style:
|
|
59
|
-
|
|
60
|
+
value = style[prop]
|
|
61
|
+
if isinstance(value, dict):
|
|
62
|
+
nodes_unordered |= set(value.keys())
|
|
63
|
+
|
|
64
|
+
if len(nodes_unordered) == 0:
|
|
65
|
+
warnings.warn(
|
|
66
|
+
"No nodes found in the style for the cascading patches. "
|
|
67
|
+
"Please provide a style with at least one dict-like "
|
|
68
|
+
"specification among the following properties: 'facecolor', "
|
|
69
|
+
"'edgecolor', 'color', 'linewidth', or 'linestyle'.",
|
|
70
|
+
)
|
|
60
71
|
|
|
61
72
|
# Draw the patches from the closest to the root (earlier drawing)
|
|
62
73
|
# to the closer to the leaves (later drawing).
|
|
@@ -70,32 +81,15 @@ class CascadeCollection(mpl.collections.PatchCollection):
|
|
|
70
81
|
f"Cascading patches not implemented for layout: {layout_name}.",
|
|
71
82
|
)
|
|
72
83
|
|
|
73
|
-
nleaves = sum(1 for leaf in provider(tree).get_leaves())
|
|
74
|
-
extend_mode = style.get("extend", False)
|
|
75
|
-
if extend_mode and (extend_mode != "leaf_labels"):
|
|
76
|
-
if layout_name == "horizontal":
|
|
77
|
-
if orientation == "right":
|
|
78
|
-
maxdepth = layout.values[:, 0].max()
|
|
79
|
-
else:
|
|
80
|
-
maxdepth = layout.values[:, 0].min()
|
|
81
|
-
elif layout_name == "vertical":
|
|
82
|
-
if orientation == "descending":
|
|
83
|
-
maxdepth = layout.values[:, 1].min()
|
|
84
|
-
else:
|
|
85
|
-
maxdepth = layout.values[:, 1].max()
|
|
86
|
-
elif layout_name == "radial":
|
|
87
|
-
# layout values are: r, theta
|
|
88
|
-
maxdepth = layout.values[:, 0].max()
|
|
89
84
|
self._maxdepth = maxdepth
|
|
90
85
|
|
|
91
86
|
cascading_patches = []
|
|
87
|
+
nleaves = sum(1 for leaf in provider(tree).get_leaves())
|
|
92
88
|
for node in drawing_order:
|
|
93
89
|
stylei = rotate_style(style, key=node)
|
|
94
90
|
stylei.pop("extend", None)
|
|
95
91
|
# Default alpha is 0.5 for simple colors
|
|
96
|
-
if isinstance(stylei.get("facecolor", None), str) and (
|
|
97
|
-
"alpha" not in stylei
|
|
98
|
-
):
|
|
92
|
+
if isinstance(stylei.get("facecolor", None), str) and ("alpha" not in stylei):
|
|
99
93
|
stylei["alpha"] = 0.5
|
|
100
94
|
|
|
101
95
|
provider_node = provider(node)
|
|
@@ -137,9 +131,7 @@ class CascadeCollection(mpl.collections.PatchCollection):
|
|
|
137
131
|
rmax = maxdepth if extend else leaves_coords[:, 0].max()
|
|
138
132
|
thetamin = leaves_coords[:, 1].min() - 0.5 * dtheta
|
|
139
133
|
thetamax = leaves_coords[:, 1].max() + 0.5 * dtheta
|
|
140
|
-
thetas = np.linspace(
|
|
141
|
-
thetamin, thetamax, max(30, (thetamax - thetamin) // 3)
|
|
142
|
-
)
|
|
134
|
+
thetas = np.linspace(thetamin, thetamax, max(30, (thetamax - thetamin) // 3))
|
|
143
135
|
xs = list(rmin * np.cos(thetas)) + list(rmax * np.cos(thetas[::-1]))
|
|
144
136
|
ys = list(rmin * np.sin(thetas)) + list(rmax * np.sin(thetas[::-1]))
|
|
145
137
|
points = list(zip(xs, ys))
|
|
@@ -200,23 +192,22 @@ class CascadeCollection(mpl.collections.PatchCollection):
|
|
|
200
192
|
for path in self.get_paths():
|
|
201
193
|
# Old radii
|
|
202
194
|
r2old = np.linalg.norm(path.vertices[-2])
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
)
|
|
195
|
+
# Update the outer part of the wedge patch
|
|
196
|
+
path.vertices[(len(path.vertices) - 1) // 2 :] *= self.get_maxdepth() / r2old
|
|
206
197
|
return
|
|
207
198
|
|
|
208
199
|
if (layout_name, orientation) == ("horizontal", "right"):
|
|
209
200
|
for path in self.get_paths():
|
|
210
201
|
path.vertices[[1, 2], 0] = self.get_maxdepth()
|
|
211
|
-
elif (layout_name, orientation) == ("horizontal", "
|
|
202
|
+
elif (layout_name, orientation) == ("horizontal", "left"):
|
|
212
203
|
for path in self.get_paths():
|
|
213
204
|
path.vertices[[0, 3], 0] = self.get_maxdepth()
|
|
214
205
|
elif (layout_name, orientation) == ("vertical", "descending"):
|
|
215
206
|
for path in self.get_paths():
|
|
216
|
-
path.vertices[[
|
|
207
|
+
path.vertices[[0, 1], 1] = self.get_maxdepth()
|
|
217
208
|
elif (layout_name, orientation) == ("vertical", "ascending"):
|
|
218
209
|
for path in self.get_paths():
|
|
219
|
-
path.vertices[[
|
|
210
|
+
path.vertices[[2, 3], 1] = self.get_maxdepth()
|
|
220
211
|
else:
|
|
221
212
|
raise ValueError(
|
|
222
213
|
f"Layout name and orientation not supported: {layout_name}, {orientation}."
|
iplotx/edge/__init__.py
CHANGED
|
@@ -121,8 +121,47 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
121
121
|
transform=transform,
|
|
122
122
|
)
|
|
123
123
|
|
|
124
|
+
if "split" in self._style:
|
|
125
|
+
self._add_subedges(
|
|
126
|
+
len(patches),
|
|
127
|
+
self._style["split"],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def _add_subedges(
|
|
131
|
+
self,
|
|
132
|
+
nedges,
|
|
133
|
+
style,
|
|
134
|
+
):
|
|
135
|
+
"""Add subedges to shadow the current edges."""
|
|
136
|
+
segments = [np.zeros((2, 2)) for i in range(nedges)]
|
|
137
|
+
kwargs = {
|
|
138
|
+
"linewidths": [],
|
|
139
|
+
"edgecolors": [],
|
|
140
|
+
"linestyles": [],
|
|
141
|
+
}
|
|
142
|
+
for i in range(nedges):
|
|
143
|
+
vids = self._vertex_ids[i]
|
|
144
|
+
stylei = rotate_style(style, index=i, key=vids, key2=vids[-1])
|
|
145
|
+
for key, values in kwargs.items():
|
|
146
|
+
# iplotx uses singular style properties
|
|
147
|
+
key = key.rstrip("s")
|
|
148
|
+
# "color" has higher priority than "edgecolor"
|
|
149
|
+
if (key == "edgecolor") and ("color" in stylei):
|
|
150
|
+
val = stylei["color"]
|
|
151
|
+
else:
|
|
152
|
+
val = stylei.get(key.rstrip("s"), getattr(self, f"get_{key}")()[i])
|
|
153
|
+
values.append(val)
|
|
154
|
+
|
|
155
|
+
self._subedges = mpl.collections.LineCollection(
|
|
156
|
+
segments,
|
|
157
|
+
transform=self.get_transform(),
|
|
158
|
+
**kwargs,
|
|
159
|
+
)
|
|
160
|
+
|
|
124
161
|
def get_children(self) -> tuple:
|
|
125
162
|
children = []
|
|
163
|
+
if hasattr(self, "_subedges"):
|
|
164
|
+
children.append(self._subedges)
|
|
126
165
|
if hasattr(self, "_arrows"):
|
|
127
166
|
children.append(self._arrows)
|
|
128
167
|
if hasattr(self, "_label_collection"):
|
|
@@ -209,7 +248,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
209
248
|
index = pd.Series(
|
|
210
249
|
np.arange(len(index)),
|
|
211
250
|
index=index,
|
|
212
|
-
)
|
|
251
|
+
).to_dict()
|
|
213
252
|
|
|
214
253
|
voffsets = []
|
|
215
254
|
vpaths = []
|
|
@@ -262,11 +301,13 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
262
301
|
trans_inv = transform.inverted().transform
|
|
263
302
|
|
|
264
303
|
# 1. Make a list of vertices with loops, and store them for later
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
304
|
+
# NOTE: vinfo["loops"] can be False when we want no loops (e.g. leaf edges)
|
|
305
|
+
if vinfo.get("loops", True):
|
|
306
|
+
loop_vertex_dict = defaultdict(lambda: dict(indices=[], edge_angles=[]))
|
|
307
|
+
for i, (v1, v2) in enumerate(vids):
|
|
308
|
+
# Postpone loops (step 3)
|
|
309
|
+
if v1 == v2:
|
|
310
|
+
loop_vertex_dict[v1]["indices"].append(i)
|
|
270
311
|
|
|
271
312
|
# 2. Make paths for non-loop edges
|
|
272
313
|
# NOTE: keep track of parallel edges to offset them
|
|
@@ -274,7 +315,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
274
315
|
paths = []
|
|
275
316
|
for i, (v1, v2) in enumerate(vids):
|
|
276
317
|
# Postpone loops (step 3)
|
|
277
|
-
if v1 == v2:
|
|
318
|
+
if vinfo.get("loops", True) and (v1 == v2):
|
|
278
319
|
paths.append(None)
|
|
279
320
|
continue
|
|
280
321
|
|
|
@@ -296,10 +337,20 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
296
337
|
tension = 0
|
|
297
338
|
ports = None
|
|
298
339
|
|
|
340
|
+
# False is a synonym for "none"
|
|
299
341
|
waypoints = edge_stylei.get("waypoints", "none")
|
|
342
|
+
if waypoints is False or waypoints is np.False_:
|
|
343
|
+
waypoints = "none"
|
|
344
|
+
elif waypoints is True or waypoints is np.True_:
|
|
345
|
+
raise ValueError(
|
|
346
|
+
"Could not determine automatically type of edge waypoints.",
|
|
347
|
+
)
|
|
300
348
|
if waypoints != "none":
|
|
301
349
|
ports = edge_stylei.get("ports", (None, None))
|
|
302
350
|
|
|
351
|
+
if not isinstance(waypoints, str):
|
|
352
|
+
__import__("ipdb").set_trace()
|
|
353
|
+
|
|
303
354
|
# Compute actual edge path
|
|
304
355
|
path, angles = _compute_edge_path(
|
|
305
356
|
vcoord_data,
|
|
@@ -329,11 +380,23 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
329
380
|
if (offset != 0).any():
|
|
330
381
|
path.vertices[:] = trans_inv(trans(path.vertices) + offset)
|
|
331
382
|
|
|
383
|
+
# If splitting is active, split the path here, shedding off the last straight
|
|
384
|
+
# segment but only if waypoints were used
|
|
385
|
+
if hasattr(self, "_subedges") and waypoints != "none":
|
|
386
|
+
# NOTE: we are already in the middle of a redraw, so we can happily avoid
|
|
387
|
+
# causing stale of the subedges. They are already scheduled to be redrawn
|
|
388
|
+
# at the end of this function.
|
|
389
|
+
self._subedges._paths[i].vertices[:] = path.vertices[-2:].copy()
|
|
390
|
+
# NOTE: instead of shortening the path, we just make the last bit invisible
|
|
391
|
+
# that makes it easier on memory management etc.
|
|
392
|
+
path.vertices[-1] = path.vertices[-2]
|
|
393
|
+
|
|
332
394
|
# Collect angles for this vertex, to be used for loops plotting below
|
|
333
|
-
if
|
|
334
|
-
loop_vertex_dict
|
|
335
|
-
|
|
336
|
-
loop_vertex_dict
|
|
395
|
+
if vinfo.get("loops", True):
|
|
396
|
+
if v1 in loop_vertex_dict:
|
|
397
|
+
loop_vertex_dict[v1]["edge_angles"].append(angles[0])
|
|
398
|
+
if v2 in loop_vertex_dict:
|
|
399
|
+
loop_vertex_dict[v2]["edge_angles"].append(angles[1])
|
|
337
400
|
|
|
338
401
|
# Add the path for this non-loop edge
|
|
339
402
|
paths.append(path)
|
|
@@ -360,41 +423,45 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
360
423
|
)
|
|
361
424
|
|
|
362
425
|
# 3. Deal with loops at the end
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
426
|
+
if vinfo.get("loops", True):
|
|
427
|
+
for vid, ldict in loop_vertex_dict.items():
|
|
428
|
+
vpath = vpaths[ldict["indices"][0]][0]
|
|
429
|
+
vsize = vsizes[ldict["indices"][0]][0]
|
|
430
|
+
vcoord_fig = trans(vcenters[ldict["indices"][0]][0])
|
|
431
|
+
nloops = len(ldict["indices"])
|
|
432
|
+
edge_angles = ldict["edge_angles"]
|
|
433
|
+
|
|
434
|
+
# The space between the existing angles is where we can fit the loops
|
|
435
|
+
# One loop we can fit in the largest wedge, multiple loops we need
|
|
436
|
+
nloops_per_angle = _compute_loops_per_angle(nloops, edge_angles)
|
|
437
|
+
|
|
438
|
+
idx = 0
|
|
439
|
+
for theta1, theta2, nloops in nloops_per_angle:
|
|
440
|
+
# Angular size of each loop in this wedge
|
|
441
|
+
delta = (theta2 - theta1) / nloops
|
|
442
|
+
|
|
443
|
+
# Iterate over individual loops
|
|
444
|
+
for j in range(nloops):
|
|
445
|
+
thetaj1 = theta1 + j * delta + max(delta - loopmaxangle, 0) / 2
|
|
446
|
+
thetaj2 = thetaj1 + min(delta, loopmaxangle)
|
|
447
|
+
|
|
448
|
+
# Get the path for this loop
|
|
449
|
+
path = _compute_loop_path(
|
|
450
|
+
vcoord_fig,
|
|
451
|
+
vpath,
|
|
452
|
+
vsize,
|
|
453
|
+
thetaj1,
|
|
454
|
+
thetaj2,
|
|
455
|
+
trans_inv,
|
|
456
|
+
looptension=self._style.get("looptension", 2.5),
|
|
457
|
+
)
|
|
458
|
+
paths[ldict["indices"][idx]] = path
|
|
459
|
+
idx += 1
|
|
396
460
|
|
|
397
461
|
self._paths = paths
|
|
462
|
+
# FIXME:??
|
|
463
|
+
# if hasattr(self, "_subedges"):
|
|
464
|
+
# self._subedges.stale = True
|
|
398
465
|
|
|
399
466
|
def _update_labels(self):
|
|
400
467
|
if self._labels is None:
|
|
@@ -459,13 +526,13 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
459
526
|
if not self.get_visible():
|
|
460
527
|
return
|
|
461
528
|
|
|
529
|
+
# This includes the subedges if present
|
|
462
530
|
self._update_paths()
|
|
463
531
|
# This sets the arrow offsets
|
|
464
532
|
self._update_children()
|
|
465
533
|
|
|
466
534
|
super().draw(renderer)
|
|
467
535
|
for child in self.get_children():
|
|
468
|
-
# This sets the arrow sizes with dpi scaling
|
|
469
536
|
child.draw(renderer)
|
|
470
537
|
|
|
471
538
|
def get_ports(self) -> Optional[LeafProperty[Pair[Optional[str]]]]:
|
|
@@ -485,7 +552,8 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
485
552
|
edge end.
|
|
486
553
|
"""
|
|
487
554
|
if ports is None:
|
|
488
|
-
|
|
555
|
+
if "ports" in self._style:
|
|
556
|
+
del self._style["ports"]
|
|
489
557
|
else:
|
|
490
558
|
self._style["ports"] = ports
|
|
491
559
|
self.stale = True
|
|
@@ -519,7 +587,8 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
519
587
|
|
|
520
588
|
"""
|
|
521
589
|
if tension is None:
|
|
522
|
-
|
|
590
|
+
if "tension" in self._style:
|
|
591
|
+
del self._style["tension"]
|
|
523
592
|
else:
|
|
524
593
|
self._style["tension"] = tension
|
|
525
594
|
self.stale = True
|
|
@@ -579,7 +648,8 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
579
648
|
looptension: The tension to use for loops. If None, the default is 2.5.
|
|
580
649
|
"""
|
|
581
650
|
if looptension is None:
|
|
582
|
-
|
|
651
|
+
if "looptension" in self._style:
|
|
652
|
+
del self._style["looptension"]
|
|
583
653
|
else:
|
|
584
654
|
self._style["looptension"] = looptension
|
|
585
655
|
self.stale = True
|
|
@@ -599,7 +669,8 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
599
669
|
offset: The offset in points for parallel straight edges. If None, the default is 3.
|
|
600
670
|
"""
|
|
601
671
|
if offset is None:
|
|
602
|
-
|
|
672
|
+
if "offset" in self._style:
|
|
673
|
+
del self._style["offset"]
|
|
603
674
|
else:
|
|
604
675
|
self._style["offset"] = offset
|
|
605
676
|
self.stale = True
|
|
@@ -629,6 +700,8 @@ def make_stub_patch(**kwargs):
|
|
|
629
700
|
"offset",
|
|
630
701
|
"paralleloffset",
|
|
631
702
|
"cmap",
|
|
703
|
+
"norm",
|
|
704
|
+
"split",
|
|
632
705
|
]
|
|
633
706
|
for prop in forbidden_props:
|
|
634
707
|
if prop in kwargs:
|
iplotx/edge/arrow.py
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Module for edge arrows in iplotx.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
from typing import Never
|
|
6
|
+
|
|
5
7
|
import numpy as np
|
|
6
8
|
import matplotlib as mpl
|
|
7
9
|
from matplotlib.patches import PathPatch
|
|
@@ -124,12 +126,17 @@ class EdgeArrowCollection(mpl.collections.PatchCollection):
|
|
|
124
126
|
|
|
125
127
|
return patches, sizes
|
|
126
128
|
|
|
127
|
-
def set_array(self, A):
|
|
129
|
+
def set_array(self, A: np.ndarray) -> Never:
|
|
128
130
|
"""Set the array for cmap/norm coloring, but keep the facecolors as set (usually 'none')."""
|
|
129
131
|
raise ValueError("Setting an array for arrows directly is not supported.")
|
|
130
132
|
|
|
131
|
-
def set_colors(self, colors):
|
|
132
|
-
"""Set arrow colors (edge and/or face) based on a colormap.
|
|
133
|
+
def set_colors(self, colors: np.ndarray) -> None:
|
|
134
|
+
"""Set arrow colors (edge and/or face) based on a colormap.
|
|
135
|
+
|
|
136
|
+
Parameters:
|
|
137
|
+
colors: Color array to apply. This must be an Nx3 or Nx4 vector of RGB or RGBA colors.
|
|
138
|
+
This function will NOT attempt to convert other color descriptions to RGB/RGBA.
|
|
139
|
+
"""
|
|
133
140
|
# NOTE: facecolors is always an array because we come from patches
|
|
134
141
|
# It can have zero alpha (i.e. if we choose "none", or a hollow marker)
|
|
135
142
|
self.set_edgecolor(colors)
|
|
@@ -145,6 +152,8 @@ class EdgeArrowCollection(mpl.collections.PatchCollection):
|
|
|
145
152
|
def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
146
153
|
"""Make a patch of the given marker shape and size."""
|
|
147
154
|
height = kwargs.pop("height", width * 1.3)
|
|
155
|
+
if height == "width":
|
|
156
|
+
height = width
|
|
148
157
|
|
|
149
158
|
# Normalise by the max size, this is taken care of in _transforms
|
|
150
159
|
# subsequently in a way that is nice to dpi scaling
|
|
@@ -169,6 +178,38 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
|
169
178
|
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
170
179
|
closed=True,
|
|
171
180
|
)
|
|
181
|
+
elif marker == "|\\":
|
|
182
|
+
codes = ["MOVETO", "LINETO", "LINETO", "CLOSEPOLY"]
|
|
183
|
+
if "color" in kwargs:
|
|
184
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
185
|
+
path = mpl.path.Path(
|
|
186
|
+
np.array(
|
|
187
|
+
[
|
|
188
|
+
[-height, width * 0.5],
|
|
189
|
+
[-height, 0],
|
|
190
|
+
[0, 0],
|
|
191
|
+
[-height, width * 0.5],
|
|
192
|
+
]
|
|
193
|
+
),
|
|
194
|
+
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
195
|
+
closed=True,
|
|
196
|
+
)
|
|
197
|
+
elif marker == "|/":
|
|
198
|
+
codes = ["MOVETO", "LINETO", "LINETO", "CLOSEPOLY"]
|
|
199
|
+
if "color" in kwargs:
|
|
200
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
201
|
+
path = mpl.path.Path(
|
|
202
|
+
np.array(
|
|
203
|
+
[
|
|
204
|
+
[-height, 0],
|
|
205
|
+
[-height, -width * 0.5],
|
|
206
|
+
[0, 0],
|
|
207
|
+
[-height, 0],
|
|
208
|
+
]
|
|
209
|
+
),
|
|
210
|
+
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
211
|
+
closed=True,
|
|
212
|
+
)
|
|
172
213
|
elif marker == ">":
|
|
173
214
|
kwargs["facecolor"] = "none"
|
|
174
215
|
if "color" in kwargs:
|
iplotx/edge/geometry.py
CHANGED
|
@@ -96,6 +96,7 @@ def _get_shorter_edge_coords(vpath, vsize, theta):
|
|
|
96
96
|
xe = v1[0]
|
|
97
97
|
else:
|
|
98
98
|
m12 = (v2[1] - v1[1]) / (v2[0] - v1[0])
|
|
99
|
+
print(m12, mtheta)
|
|
99
100
|
xe = (v1[1] - m12 * v1[0]) / (mtheta - m12)
|
|
100
101
|
ye = mtheta * xe
|
|
101
102
|
ve = np.array([xe, ye])
|
|
@@ -171,25 +172,41 @@ def _compute_edge_path_straight(
|
|
|
171
172
|
vsize_fig,
|
|
172
173
|
trans,
|
|
173
174
|
trans_inv,
|
|
175
|
+
layout_coordinate_system: str = "cartesian",
|
|
174
176
|
**kwargs,
|
|
175
177
|
):
|
|
178
|
+
if layout_coordinate_system not in ("cartesian", "polar"):
|
|
179
|
+
raise ValueError(
|
|
180
|
+
f"Layout coordinate system not supported for straight edges: {layout_coordinate_system}.",
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if layout_coordinate_system == "polar":
|
|
184
|
+
r0, theta0 = vcoord_data[0]
|
|
185
|
+
r1, theta1 = vcoord_data[1]
|
|
186
|
+
vcoord_data_cart = np.array(
|
|
187
|
+
[
|
|
188
|
+
[r0 * np.cos(theta0), r0 * np.sin(theta0)],
|
|
189
|
+
[r1 * np.cos(theta1), r1 * np.sin(theta1)],
|
|
190
|
+
]
|
|
191
|
+
)
|
|
192
|
+
else:
|
|
193
|
+
vcoord_data_cart = vcoord_data
|
|
176
194
|
|
|
177
195
|
# Coordinates in figure (default) coords
|
|
178
|
-
vcoord_fig = trans(
|
|
196
|
+
vcoord_fig = trans(vcoord_data_cart)
|
|
179
197
|
|
|
180
198
|
points = []
|
|
181
199
|
|
|
182
200
|
# Angle of the straight line
|
|
183
201
|
theta = atan2(*((vcoord_fig[1] - vcoord_fig[0])[::-1]))
|
|
202
|
+
print(vcoord_data_cart, vcoord_fig, theta)
|
|
184
203
|
|
|
185
204
|
# Shorten at starting vertex
|
|
186
205
|
vs = _get_shorter_edge_coords(vpath_fig[0], vsize_fig[0], theta) + vcoord_fig[0]
|
|
187
206
|
points.append(vs)
|
|
188
207
|
|
|
189
208
|
# Shorten at end vertex
|
|
190
|
-
ve = (
|
|
191
|
-
_get_shorter_edge_coords(vpath_fig[1], vsize_fig[1], theta + pi) + vcoord_fig[1]
|
|
192
|
-
)
|
|
209
|
+
ve = _get_shorter_edge_coords(vpath_fig[1], vsize_fig[1], theta + pi) + vcoord_fig[1]
|
|
193
210
|
points.append(ve)
|
|
194
211
|
|
|
195
212
|
codes = ["MOVETO", "LINETO"]
|
|
@@ -213,7 +230,6 @@ def _compute_edge_path_waypoints(
|
|
|
213
230
|
ports: Pair[Optional[str]] = (None, None),
|
|
214
231
|
**kwargs,
|
|
215
232
|
):
|
|
216
|
-
|
|
217
233
|
if waypoints in ("x0y1", "y0x1"):
|
|
218
234
|
assert layout_coordinate_system == "cartesian"
|
|
219
235
|
|
|
@@ -236,8 +252,7 @@ def _compute_edge_path_waypoints(
|
|
|
236
252
|
|
|
237
253
|
# Shorten at vertex border
|
|
238
254
|
vshorts[i] = (
|
|
239
|
-
_get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], thetas[i])
|
|
240
|
-
+ vcoord_fig[i]
|
|
255
|
+
_get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], thetas[i]) + vcoord_fig[i]
|
|
241
256
|
)
|
|
242
257
|
|
|
243
258
|
# Shorten waypoints to keep the angles right
|
|
@@ -285,10 +300,7 @@ def _compute_edge_path_waypoints(
|
|
|
285
300
|
theta = atan2(*(_get_port_unit_vector(ports[i], trans_inv)[::-1]))
|
|
286
301
|
|
|
287
302
|
# Shorten at vertex border
|
|
288
|
-
vshort = (
|
|
289
|
-
_get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], theta)
|
|
290
|
-
+ vcoord_fig[i]
|
|
291
|
-
)
|
|
303
|
+
vshort = _get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], theta) + vcoord_fig[i]
|
|
292
304
|
thetas.append(theta)
|
|
293
305
|
vshorts.append(vshort)
|
|
294
306
|
|
|
@@ -305,16 +317,12 @@ def _compute_edge_path_waypoints(
|
|
|
305
317
|
idx_outer = 1 - idx_inner
|
|
306
318
|
alpha_outer = [alpha0, alpha1][idx_outer]
|
|
307
319
|
|
|
308
|
-
# FIXME: this is aware of chirality as stored by the layout function
|
|
309
320
|
betas = np.linspace(alpha0, alpha1, points_per_curve)
|
|
310
321
|
waypoints = [r0, r1][idx_inner] * np.vstack([np.cos(betas), np.sin(betas)]).T
|
|
311
|
-
endpoint = [r0, r1][idx_outer] * np.array(
|
|
312
|
-
[np.cos(alpha_outer), np.sin(alpha_outer)]
|
|
313
|
-
)
|
|
322
|
+
endpoint = [r0, r1][idx_outer] * np.array([np.cos(alpha_outer), np.sin(alpha_outer)])
|
|
314
323
|
points = np.array(list(waypoints) + [endpoint])
|
|
315
324
|
points = trans(points)
|
|
316
325
|
codes = ["MOVETO"] + ["LINETO"] * len(waypoints)
|
|
317
|
-
# FIXME: same as previus comment
|
|
318
326
|
angles = (alpha0 + pi / 2, alpha1)
|
|
319
327
|
|
|
320
328
|
else:
|
|
@@ -391,10 +399,7 @@ def _compute_edge_path_curved(
|
|
|
391
399
|
thetas = [None, None]
|
|
392
400
|
for i in range(2):
|
|
393
401
|
thetas[i] = atan2(*((auxs[i] - vcoord_fig[i])[::-1]))
|
|
394
|
-
vs[i] = (
|
|
395
|
-
_get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], thetas[i])
|
|
396
|
-
+ vcoord_fig[i]
|
|
397
|
-
)
|
|
402
|
+
vs[i] = _get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], thetas[i]) + vcoord_fig[i]
|
|
398
403
|
|
|
399
404
|
path = {
|
|
400
405
|
"vertices": [
|
|
@@ -438,7 +443,11 @@ def _compute_edge_path(
|
|
|
438
443
|
)
|
|
439
444
|
|
|
440
445
|
if tension == 0:
|
|
441
|
-
return _compute_edge_path_straight(
|
|
446
|
+
return _compute_edge_path_straight(
|
|
447
|
+
*args,
|
|
448
|
+
layout_coordinate_system=layout_coordinate_system,
|
|
449
|
+
**kwargs,
|
|
450
|
+
)
|
|
442
451
|
|
|
443
452
|
return _compute_edge_path_curved(
|
|
444
453
|
tension,
|