iplotx 0.1.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/__init__.py +22 -1
- iplotx/edge/__init__.py +623 -0
- iplotx/edge/arrow.py +220 -10
- iplotx/edge/geometry.py +392 -0
- iplotx/edge/ports.py +47 -0
- iplotx/groups.py +93 -45
- iplotx/ingest/__init__.py +155 -0
- iplotx/ingest/heuristics.py +209 -0
- iplotx/ingest/providers/network/igraph.py +96 -0
- iplotx/ingest/providers/network/networkx.py +133 -0
- iplotx/ingest/providers/tree/biopython.py +105 -0
- iplotx/ingest/providers/tree/cogent3.py +112 -0
- iplotx/ingest/providers/tree/ete4.py +112 -0
- iplotx/ingest/providers/tree/skbio.py +112 -0
- iplotx/ingest/typing.py +100 -0
- iplotx/label.py +162 -0
- iplotx/layout.py +139 -0
- iplotx/network.py +161 -379
- iplotx/plotting.py +157 -56
- iplotx/style.py +391 -0
- iplotx/tree.py +312 -0
- iplotx/typing.py +55 -41
- iplotx/utils/geometry.py +128 -81
- iplotx/utils/internal.py +3 -0
- iplotx/utils/matplotlib.py +58 -38
- iplotx/utils/style.py +1 -0
- iplotx/version.py +5 -1
- iplotx/vertex.py +305 -55
- iplotx-0.2.1.dist-info/METADATA +88 -0
- iplotx-0.2.1.dist-info/RECORD +31 -0
- iplotx/edge/common.py +0 -47
- iplotx/edge/directed.py +0 -149
- iplotx/edge/label.py +0 -50
- iplotx/edge/undirected.py +0 -447
- iplotx/heuristics.py +0 -114
- iplotx/importing.py +0 -13
- iplotx/styles.py +0 -186
- iplotx-0.1.0.dist-info/METADATA +0 -47
- iplotx-0.1.0.dist-info/RECORD +0 -20
- {iplotx-0.1.0.dist-info → iplotx-0.2.1.dist-info}/WHEEL +0 -0
iplotx/edge/arrow.py
CHANGED
|
@@ -1,16 +1,171 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for edge arrows in iplotx.
|
|
3
|
+
"""
|
|
4
|
+
|
|
1
5
|
import numpy as np
|
|
2
6
|
import matplotlib as mpl
|
|
3
7
|
from matplotlib.patches import PathPatch
|
|
4
8
|
|
|
9
|
+
from ..style import (
|
|
10
|
+
get_style,
|
|
11
|
+
rotate_style,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EdgeArrowCollection(mpl.collections.PatchCollection):
|
|
16
|
+
"""Collection of arrow patches for plotting directed edgs."""
|
|
17
|
+
|
|
18
|
+
_factor = 1.0
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
edge_collection,
|
|
23
|
+
*args,
|
|
24
|
+
transform: mpl.transforms.Transform = mpl.transforms.IdentityTransform(),
|
|
25
|
+
**kwargs,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Initialize the edge arrow collection.
|
|
28
|
+
|
|
29
|
+
Parameters:
|
|
30
|
+
edge_collection: The edge collection to which these arrows belong.
|
|
31
|
+
transform: The transform to apply to the arrows. This related to the arrow size
|
|
32
|
+
scaling, not the arrow tip position which is controlled by set_offset_transform.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
self._edge_collection = edge_collection
|
|
36
|
+
self._style = get_style(".edge.arrow")
|
|
37
|
+
|
|
38
|
+
patches, sizes = self._create_artists()
|
|
39
|
+
|
|
40
|
+
if "cmap" in self._edge_collection._style:
|
|
41
|
+
kwargs["cmap"] = self._edge_collection._style["cmap"]
|
|
42
|
+
kwargs["norm"] = self._edge_collection._style["norm"]
|
|
43
|
+
|
|
44
|
+
super().__init__(
|
|
45
|
+
patches,
|
|
46
|
+
offsets=np.zeros((len(patches), 2)),
|
|
47
|
+
offset_transform=self.get_offset_transform(),
|
|
48
|
+
transform=transform,
|
|
49
|
+
match_original=True,
|
|
50
|
+
*args,
|
|
51
|
+
**kwargs,
|
|
52
|
+
)
|
|
53
|
+
self._angles = np.zeros(len(self._paths))
|
|
54
|
+
|
|
55
|
+
# Compute _transforms like in _CollectionWithScales for dpi issues
|
|
56
|
+
self.set_sizes(sizes)
|
|
57
|
+
|
|
58
|
+
def get_sizes(self):
|
|
59
|
+
"""Get arrow sizes (max of width and height), not scaled by dpi."""
|
|
60
|
+
return self._sizes
|
|
61
|
+
|
|
62
|
+
def get_sizes_dpi(self):
|
|
63
|
+
"""Get arrow sizes (max of width and height) scaled by dpi."""
|
|
64
|
+
return self._transforms[:, 0, 0]
|
|
65
|
+
|
|
66
|
+
def set_sizes(self, sizes, dpi=72.0):
|
|
67
|
+
"""Set vertex sizes.
|
|
68
|
+
|
|
69
|
+
This rescales the current vertex symbol/path linearly, using this
|
|
70
|
+
value as the largest of width and height.
|
|
71
|
+
|
|
72
|
+
@param sizes: A sequence of vertex sizes or a single size.
|
|
73
|
+
"""
|
|
74
|
+
if sizes is None:
|
|
75
|
+
self._sizes = np.array([])
|
|
76
|
+
self._transforms = np.empty((0, 3, 3))
|
|
77
|
+
else:
|
|
78
|
+
self._sizes = np.asarray(sizes)
|
|
79
|
+
self._transforms = np.zeros((len(self._sizes), 3, 3))
|
|
80
|
+
scale = self._sizes * dpi / 72.0 * self._factor
|
|
81
|
+
self._transforms[:, 0, 0] = scale
|
|
82
|
+
self._transforms[:, 1, 1] = scale
|
|
83
|
+
self._transforms[:, 2, 2] = 1.0
|
|
84
|
+
self.stale = True
|
|
85
|
+
|
|
86
|
+
get_size = get_sizes
|
|
87
|
+
set_size = set_sizes
|
|
88
|
+
|
|
89
|
+
def set_figure(self, fig) -> None:
|
|
90
|
+
"""Set the figure for this artist and all children."""
|
|
91
|
+
super().set_figure(fig)
|
|
92
|
+
self.set_sizes(self._sizes, self.get_figure(root=True).dpi)
|
|
93
|
+
for child in self.get_children():
|
|
94
|
+
child.set_figure(fig)
|
|
95
|
+
|
|
96
|
+
def get_offset_transform(self):
|
|
97
|
+
"""Get offset transform for the edge arrows. This sets the tip of each arrow."""
|
|
98
|
+
return self._edge_collection.get_transform()
|
|
99
|
+
|
|
100
|
+
get_size = get_sizes
|
|
101
|
+
set_size = set_sizes
|
|
102
|
+
|
|
103
|
+
def _create_artists(self):
|
|
104
|
+
style = self._style if self._style is not None else {}
|
|
105
|
+
|
|
106
|
+
patches = []
|
|
107
|
+
sizes = []
|
|
108
|
+
for i in range(len(self._edge_collection._vertex_ids)):
|
|
109
|
+
stylei = rotate_style(style, index=i)
|
|
110
|
+
if ("facecolor" not in stylei) and ("color" not in stylei):
|
|
111
|
+
stylei["facecolor"] = self._edge_collection.get_edgecolors()[i][:3]
|
|
112
|
+
if ("edgecolor" not in stylei) and ("color" not in stylei):
|
|
113
|
+
stylei["edgecolor"] = self._edge_collection.get_edgecolors()[i][:3]
|
|
114
|
+
if "alpha" not in stylei:
|
|
115
|
+
stylei["alpha"] = self._edge_collection.get_edgecolors()[i][3]
|
|
116
|
+
if "linewidth" not in stylei:
|
|
117
|
+
stylei["linewidth"] = self._edge_collection.get_linewidths()[i]
|
|
118
|
+
|
|
119
|
+
patch, size = make_arrow_patch(
|
|
120
|
+
**stylei,
|
|
121
|
+
)
|
|
122
|
+
patches.append(patch)
|
|
123
|
+
sizes.append(size)
|
|
124
|
+
|
|
125
|
+
return patches, sizes
|
|
126
|
+
|
|
127
|
+
def set_array(self, A):
|
|
128
|
+
"""Set the array for cmap/norm coloring, but keep the facecolors as set (usually 'none')."""
|
|
129
|
+
raise ValueError("Setting an array for arrows directly is not supported.")
|
|
130
|
+
|
|
131
|
+
def set_colors(self, colors):
|
|
132
|
+
"""Set arrow colors (edge and/or face) based on a colormap."""
|
|
133
|
+
# NOTE: facecolors is always an array because we come from patches
|
|
134
|
+
# It can have zero alpha (i.e. if we choose "none", or a hollow marker)
|
|
135
|
+
self.set_edgecolor(colors)
|
|
136
|
+
has_facecolor = self._facecolors[:, 3] > 0
|
|
137
|
+
self._facecolors[has_facecolor] = colors[has_facecolor]
|
|
138
|
+
|
|
139
|
+
@mpl.artist.allow_rasterization
|
|
140
|
+
def draw(self, renderer):
|
|
141
|
+
self.set_sizes(self._sizes, self.get_figure(root=True).dpi)
|
|
142
|
+
super().draw(renderer)
|
|
143
|
+
|
|
5
144
|
|
|
6
145
|
def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
7
146
|
"""Make a patch of the given marker shape and size."""
|
|
8
147
|
height = kwargs.pop("height", width * 1.3)
|
|
9
148
|
|
|
149
|
+
# Normalise by the max size, this is taken care of in _transforms
|
|
150
|
+
# subsequently in a way that is nice to dpi scaling
|
|
151
|
+
size_max = max(width, height)
|
|
152
|
+
if size_max > 0:
|
|
153
|
+
height /= size_max
|
|
154
|
+
width /= size_max
|
|
155
|
+
|
|
10
156
|
if marker == "|>":
|
|
11
|
-
codes = ["MOVETO", "LINETO", "LINETO"]
|
|
157
|
+
codes = ["MOVETO", "LINETO", "LINETO", "CLOSEPOLY"]
|
|
158
|
+
if "color" in kwargs:
|
|
159
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
12
160
|
path = mpl.path.Path(
|
|
13
|
-
np.array(
|
|
161
|
+
np.array(
|
|
162
|
+
[
|
|
163
|
+
[-height, width * 0.5],
|
|
164
|
+
[-height, -width * 0.5],
|
|
165
|
+
[0, 0],
|
|
166
|
+
[-height, width * 0.5],
|
|
167
|
+
]
|
|
168
|
+
),
|
|
14
169
|
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
15
170
|
closed=True,
|
|
16
171
|
)
|
|
@@ -25,8 +180,10 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
|
25
180
|
closed=False,
|
|
26
181
|
)
|
|
27
182
|
elif marker == ">>":
|
|
183
|
+
if "color" in kwargs:
|
|
184
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
28
185
|
overhang = kwargs.pop("overhang", 0.25)
|
|
29
|
-
codes = ["MOVETO", "LINETO", "LINETO", "LINETO"]
|
|
186
|
+
codes = ["MOVETO", "LINETO", "LINETO", "LINETO", "CLOSEPOLY"]
|
|
30
187
|
path = mpl.path.Path(
|
|
31
188
|
np.array(
|
|
32
189
|
[
|
|
@@ -34,14 +191,17 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
|
34
191
|
[-height, -width * 0.5],
|
|
35
192
|
[-height * (1.0 - overhang), 0],
|
|
36
193
|
[-height, width * 0.5],
|
|
194
|
+
[0, 0],
|
|
37
195
|
]
|
|
38
196
|
),
|
|
39
197
|
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
40
198
|
closed=True,
|
|
41
199
|
)
|
|
42
200
|
elif marker == ")>":
|
|
201
|
+
if "color" in kwargs:
|
|
202
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
43
203
|
overhang = kwargs.pop("overhang", 0.25)
|
|
44
|
-
codes = ["MOVETO", "LINETO", "CURVE3", "CURVE3"]
|
|
204
|
+
codes = ["MOVETO", "LINETO", "CURVE3", "CURVE3", "CLOSEPOLY"]
|
|
45
205
|
path = mpl.path.Path(
|
|
46
206
|
np.array(
|
|
47
207
|
[
|
|
@@ -49,6 +209,7 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
|
49
209
|
[-height, -width * 0.5],
|
|
50
210
|
[-height * (1.0 - overhang), 0],
|
|
51
211
|
[-height, width * 0.5],
|
|
212
|
+
[0, 0],
|
|
52
213
|
]
|
|
53
214
|
),
|
|
54
215
|
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
@@ -70,8 +231,37 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
|
70
231
|
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
71
232
|
closed=False,
|
|
72
233
|
)
|
|
234
|
+
elif marker == "|":
|
|
235
|
+
kwargs["facecolor"] = "none"
|
|
236
|
+
if "color" in kwargs:
|
|
237
|
+
kwargs["edgecolor"] = kwargs.pop("color")
|
|
238
|
+
codes = ["MOVETO", "LINETO"]
|
|
239
|
+
path = mpl.path.Path(
|
|
240
|
+
np.array([[-height, width * 0.5], [-height, -width * 0.5]]),
|
|
241
|
+
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
242
|
+
closed=False,
|
|
243
|
+
)
|
|
244
|
+
elif marker == "s":
|
|
245
|
+
if "color" in kwargs:
|
|
246
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
247
|
+
codes = ["MOVETO", "LINETO", "LINETO", "LINETO", "CLOSEPOLY"]
|
|
248
|
+
path = mpl.path.Path(
|
|
249
|
+
np.array(
|
|
250
|
+
[
|
|
251
|
+
[-height, width * 0.5],
|
|
252
|
+
[-height, -width * 0.5],
|
|
253
|
+
[0, -width * 0.5],
|
|
254
|
+
[0, width * 0.5],
|
|
255
|
+
[-height, width * 0.5],
|
|
256
|
+
]
|
|
257
|
+
),
|
|
258
|
+
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
259
|
+
closed=True,
|
|
260
|
+
)
|
|
73
261
|
elif marker == "d":
|
|
74
|
-
|
|
262
|
+
if "color" in kwargs:
|
|
263
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
264
|
+
codes = ["MOVETO", "LINETO", "LINETO", "LINETO", "CLOSEPOLY"]
|
|
75
265
|
path = mpl.path.Path(
|
|
76
266
|
np.array(
|
|
77
267
|
[
|
|
@@ -79,13 +269,16 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
|
79
269
|
[-height, 0],
|
|
80
270
|
[-height * 0.5, -width * 0.5],
|
|
81
271
|
[0, 0],
|
|
272
|
+
[-height * 0.5, width * 0.5],
|
|
82
273
|
]
|
|
83
274
|
),
|
|
84
275
|
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
85
276
|
closed=True,
|
|
86
277
|
)
|
|
87
278
|
elif marker == "p":
|
|
88
|
-
|
|
279
|
+
if "color" in kwargs:
|
|
280
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
281
|
+
codes = ["MOVETO", "LINETO", "LINETO", "LINETO", "CLOSEPOLY"]
|
|
89
282
|
path = mpl.path.Path(
|
|
90
283
|
np.array(
|
|
91
284
|
[
|
|
@@ -93,13 +286,16 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
|
93
286
|
[0, 0],
|
|
94
287
|
[0, -width],
|
|
95
288
|
[-height, -width],
|
|
289
|
+
[-height, 0],
|
|
96
290
|
]
|
|
97
291
|
),
|
|
98
292
|
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
99
293
|
closed=True,
|
|
100
294
|
)
|
|
101
295
|
elif marker == "q":
|
|
102
|
-
|
|
296
|
+
if "color" in kwargs:
|
|
297
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
298
|
+
codes = ["MOVETO", "LINETO", "LINETO", "LINETO", "CLOSEPOLY"]
|
|
103
299
|
path = mpl.path.Path(
|
|
104
300
|
np.array(
|
|
105
301
|
[
|
|
@@ -107,16 +303,30 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
|
107
303
|
[0, 0],
|
|
108
304
|
[0, width],
|
|
109
305
|
[-height, width],
|
|
306
|
+
[-height, 0],
|
|
307
|
+
]
|
|
308
|
+
),
|
|
309
|
+
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
310
|
+
closed=True,
|
|
311
|
+
)
|
|
312
|
+
elif marker == "none":
|
|
313
|
+
if "color" in kwargs:
|
|
314
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
315
|
+
codes = ["MOVETO"]
|
|
316
|
+
path = mpl.path.Path(
|
|
317
|
+
np.array(
|
|
318
|
+
[
|
|
319
|
+
[0, 0],
|
|
110
320
|
]
|
|
111
321
|
),
|
|
112
322
|
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
113
323
|
closed=True,
|
|
114
324
|
)
|
|
325
|
+
else:
|
|
326
|
+
raise ValueError(f"Arrow marker not found: {marker}.")
|
|
115
327
|
|
|
116
328
|
patch = PathPatch(
|
|
117
329
|
path,
|
|
118
330
|
**kwargs,
|
|
119
331
|
)
|
|
120
|
-
return patch
|
|
121
|
-
|
|
122
|
-
raise KeyError(f"Unknown marker: {marker}")
|
|
332
|
+
return patch, size_max
|
iplotx/edge/geometry.py
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Support module with geometry- and path-related functions for edges.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from math import atan2, tan, pi
|
|
7
|
+
import numpy as np
|
|
8
|
+
import matplotlib as mpl
|
|
9
|
+
|
|
10
|
+
from ..typing import (
|
|
11
|
+
Pair,
|
|
12
|
+
)
|
|
13
|
+
from .ports import _get_port_unit_vector
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _compute_loops_per_angle(nloops, angles):
|
|
17
|
+
if len(angles) == 0:
|
|
18
|
+
return [(0, 2 * pi, nloops)]
|
|
19
|
+
|
|
20
|
+
angles_sorted_closed = list(sorted(angles))
|
|
21
|
+
angles_sorted_closed.append(angles_sorted_closed[0] + 2 * pi)
|
|
22
|
+
deltas = np.diff(angles_sorted_closed)
|
|
23
|
+
|
|
24
|
+
# Now we have the deltas and the total number of loops
|
|
25
|
+
# 1. Assign all loops to the largest wedge
|
|
26
|
+
idx_dmax = deltas.argmax()
|
|
27
|
+
if nloops == 1:
|
|
28
|
+
return [
|
|
29
|
+
(
|
|
30
|
+
angles_sorted_closed[idx_dmax],
|
|
31
|
+
angles_sorted_closed[idx_dmax + 1],
|
|
32
|
+
nloops,
|
|
33
|
+
)
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# 2. Check if any other wedges are larger than this
|
|
37
|
+
# If not, we are done (this is the algo in igraph)
|
|
38
|
+
dsplit = deltas[idx_dmax] / nloops
|
|
39
|
+
if (deltas > dsplit).sum() < 2:
|
|
40
|
+
return [
|
|
41
|
+
(
|
|
42
|
+
angles_sorted_closed[idx_dmax],
|
|
43
|
+
angles_sorted_closed[idx_dmax + 1],
|
|
44
|
+
nloops,
|
|
45
|
+
)
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# 3. Check how small the second-largest wedge would become
|
|
49
|
+
idx_dsort = np.argsort(deltas)
|
|
50
|
+
return [
|
|
51
|
+
(
|
|
52
|
+
angles_sorted_closed[idx_dmax],
|
|
53
|
+
angles_sorted_closed[idx_dmax + 1],
|
|
54
|
+
nloops - 1,
|
|
55
|
+
),
|
|
56
|
+
(
|
|
57
|
+
angles_sorted_closed[idx_dsort[-2]],
|
|
58
|
+
angles_sorted_closed[idx_dsort[-2] + 1],
|
|
59
|
+
1,
|
|
60
|
+
),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_shorter_edge_coords(vpath, vsize, theta):
|
|
65
|
+
# Bound theta from -pi to pi (why is that not guaranteed?)
|
|
66
|
+
theta = (theta + pi) % (2 * pi) - pi
|
|
67
|
+
|
|
68
|
+
# Size zero vertices need no shortening
|
|
69
|
+
if vsize == 0:
|
|
70
|
+
return np.array([0, 0])
|
|
71
|
+
|
|
72
|
+
for i in range(len(vpath)):
|
|
73
|
+
v1 = vpath.vertices[i]
|
|
74
|
+
v2 = vpath.vertices[(i + 1) % len(vpath)]
|
|
75
|
+
theta1 = atan2(*((v1)[::-1]))
|
|
76
|
+
theta2 = atan2(*((v2)[::-1]))
|
|
77
|
+
|
|
78
|
+
# atan2 ranges ]-3.14, 3.14]
|
|
79
|
+
# so it can be that theta1 is -3 and theta2 is +3
|
|
80
|
+
# therefore we need two separate cases, one that cuts at pi and one at 0
|
|
81
|
+
cond1 = theta1 <= theta <= theta2
|
|
82
|
+
cond2 = (
|
|
83
|
+
(theta1 + 2 * pi) % (2 * pi)
|
|
84
|
+
<= (theta + 2 * pi) % (2 * pi)
|
|
85
|
+
<= (theta2 + 2 * pi) % (2 * pi)
|
|
86
|
+
)
|
|
87
|
+
if cond1 or cond2:
|
|
88
|
+
break
|
|
89
|
+
else:
|
|
90
|
+
raise ValueError("Angle for patch not found")
|
|
91
|
+
|
|
92
|
+
# The edge meets the patch of the vertex on the v1-v2 size,
|
|
93
|
+
# at angle theta from the center
|
|
94
|
+
mtheta = tan(theta)
|
|
95
|
+
if v2[0] == v1[0]:
|
|
96
|
+
xe = v1[0]
|
|
97
|
+
else:
|
|
98
|
+
m12 = (v2[1] - v1[1]) / (v2[0] - v1[0])
|
|
99
|
+
xe = (v1[1] - m12 * v1[0]) / (mtheta - m12)
|
|
100
|
+
ye = mtheta * xe
|
|
101
|
+
ve = np.array([xe, ye])
|
|
102
|
+
return ve * vsize
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _fix_parallel_edges_straight(
|
|
106
|
+
paths,
|
|
107
|
+
indices,
|
|
108
|
+
indices_inv,
|
|
109
|
+
trans,
|
|
110
|
+
trans_inv,
|
|
111
|
+
offset=3,
|
|
112
|
+
):
|
|
113
|
+
"""Offset parallel edges along the same path."""
|
|
114
|
+
ntot = len(indices) + len(indices_inv)
|
|
115
|
+
|
|
116
|
+
# This is straight so two vertices anyway
|
|
117
|
+
# NOTE: all paths will be the same, which is why we need to offset them
|
|
118
|
+
vs, ve = trans(paths[indices[0]].vertices)
|
|
119
|
+
|
|
120
|
+
# Move orthogonal to the line
|
|
121
|
+
fracs = (vs - ve) / np.sqrt(((vs - ve) ** 2).sum()) @ np.array([[0, 1], [-1, 0]])
|
|
122
|
+
|
|
123
|
+
# NOTE: for now treat both direction the same
|
|
124
|
+
for i, idx in enumerate(indices + indices_inv):
|
|
125
|
+
# Offset the path
|
|
126
|
+
paths[idx].vertices = trans_inv(
|
|
127
|
+
trans(paths[idx].vertices) + fracs * offset * (i - ntot / 2)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _compute_loop_path(
|
|
132
|
+
vcoord_fig,
|
|
133
|
+
vpath,
|
|
134
|
+
vsize,
|
|
135
|
+
angle1,
|
|
136
|
+
angle2,
|
|
137
|
+
trans_inv,
|
|
138
|
+
looptension,
|
|
139
|
+
):
|
|
140
|
+
# Shorten at starting angle
|
|
141
|
+
start = _get_shorter_edge_coords(vpath, vsize, angle1) + vcoord_fig
|
|
142
|
+
# Shorten at end angle
|
|
143
|
+
end = _get_shorter_edge_coords(vpath, vsize, angle2) + vcoord_fig
|
|
144
|
+
|
|
145
|
+
aux1 = (start - vcoord_fig) * looptension + vcoord_fig
|
|
146
|
+
aux2 = (end - vcoord_fig) * looptension + vcoord_fig
|
|
147
|
+
|
|
148
|
+
vertices = np.vstack(
|
|
149
|
+
[
|
|
150
|
+
start,
|
|
151
|
+
aux1,
|
|
152
|
+
aux2,
|
|
153
|
+
end,
|
|
154
|
+
]
|
|
155
|
+
)
|
|
156
|
+
codes = ["MOVETO"] + ["CURVE4"] * 3
|
|
157
|
+
|
|
158
|
+
# Offset to place and transform to data coordinates
|
|
159
|
+
vertices = trans_inv(vertices)
|
|
160
|
+
codes = [getattr(mpl.path.Path, x) for x in codes]
|
|
161
|
+
path = mpl.path.Path(
|
|
162
|
+
vertices,
|
|
163
|
+
codes=codes,
|
|
164
|
+
)
|
|
165
|
+
return path
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _compute_edge_path_straight(
|
|
169
|
+
vcoord_data,
|
|
170
|
+
vpath_fig,
|
|
171
|
+
vsize_fig,
|
|
172
|
+
trans,
|
|
173
|
+
trans_inv,
|
|
174
|
+
**kwargs,
|
|
175
|
+
):
|
|
176
|
+
|
|
177
|
+
# Coordinates in figure (default) coords
|
|
178
|
+
vcoord_fig = trans(vcoord_data)
|
|
179
|
+
|
|
180
|
+
points = []
|
|
181
|
+
|
|
182
|
+
# Angle of the straight line
|
|
183
|
+
theta = atan2(*((vcoord_fig[1] - vcoord_fig[0])[::-1]))
|
|
184
|
+
|
|
185
|
+
# Shorten at starting vertex
|
|
186
|
+
vs = _get_shorter_edge_coords(vpath_fig[0], vsize_fig[0], theta) + vcoord_fig[0]
|
|
187
|
+
points.append(vs)
|
|
188
|
+
|
|
189
|
+
# Shorten at end vertex
|
|
190
|
+
ve = (
|
|
191
|
+
_get_shorter_edge_coords(vpath_fig[1], vsize_fig[1], theta + pi) + vcoord_fig[1]
|
|
192
|
+
)
|
|
193
|
+
points.append(ve)
|
|
194
|
+
|
|
195
|
+
codes = ["MOVETO", "LINETO"]
|
|
196
|
+
path = mpl.path.Path(
|
|
197
|
+
points,
|
|
198
|
+
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
199
|
+
)
|
|
200
|
+
path.vertices = trans_inv(path.vertices)
|
|
201
|
+
return path, (theta, theta + np.pi)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _compute_edge_path_waypoints(
|
|
205
|
+
waypoints,
|
|
206
|
+
vcoord_data,
|
|
207
|
+
vpath_fig,
|
|
208
|
+
vsize_fig,
|
|
209
|
+
trans,
|
|
210
|
+
trans_inv,
|
|
211
|
+
layout_coordinate_system: str = "cartesian",
|
|
212
|
+
points_per_curve: int = 30,
|
|
213
|
+
**kwargs,
|
|
214
|
+
):
|
|
215
|
+
|
|
216
|
+
if waypoints in ("x0y1", "y0x1"):
|
|
217
|
+
assert layout_coordinate_system == "cartesian"
|
|
218
|
+
|
|
219
|
+
# Coordinates in figure (default) coords
|
|
220
|
+
vcoord_fig = trans(vcoord_data)
|
|
221
|
+
|
|
222
|
+
if waypoints == "x0y1":
|
|
223
|
+
waypoint = np.array([vcoord_fig[0][0], vcoord_fig[1][1]])
|
|
224
|
+
else:
|
|
225
|
+
waypoint = np.array([vcoord_fig[1][0], vcoord_fig[0][1]])
|
|
226
|
+
|
|
227
|
+
# Angles of the straight lines
|
|
228
|
+
theta0 = atan2(*((waypoint - vcoord_fig[0])[::-1]))
|
|
229
|
+
theta1 = atan2(*((waypoint - vcoord_fig[1])[::-1]))
|
|
230
|
+
|
|
231
|
+
# Shorten at starting vertex
|
|
232
|
+
vs = (
|
|
233
|
+
_get_shorter_edge_coords(vpath_fig[0], vsize_fig[0], theta0) + vcoord_fig[0]
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Shorten at end vertex
|
|
237
|
+
ve = (
|
|
238
|
+
_get_shorter_edge_coords(vpath_fig[1], vsize_fig[1], theta1) + vcoord_fig[1]
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
points = [vs, waypoint, ve]
|
|
242
|
+
codes = ["MOVETO", "LINETO", "LINETO"]
|
|
243
|
+
angles = (theta0, theta1)
|
|
244
|
+
elif waypoints == "r0a1":
|
|
245
|
+
assert layout_coordinate_system == "polar"
|
|
246
|
+
|
|
247
|
+
r0, alpha0 = vcoord_data[0]
|
|
248
|
+
r1, alpha1 = vcoord_data[1]
|
|
249
|
+
idx_inner = np.argmin([r0, r1])
|
|
250
|
+
idx_outer = 1 - idx_inner
|
|
251
|
+
alpha_outer = [alpha0, alpha1][idx_outer]
|
|
252
|
+
|
|
253
|
+
# FIXME: this is aware of chirality as stored by the layout function
|
|
254
|
+
betas = np.linspace(alpha0, alpha1, points_per_curve)
|
|
255
|
+
waypoints = [r0, r1][idx_inner] * np.vstack([np.cos(betas), np.sin(betas)]).T
|
|
256
|
+
endpoint = [r0, r1][idx_outer] * np.array(
|
|
257
|
+
[np.cos(alpha_outer), np.sin(alpha_outer)]
|
|
258
|
+
)
|
|
259
|
+
points = np.array(list(waypoints) + [endpoint])
|
|
260
|
+
points = trans(points)
|
|
261
|
+
codes = ["MOVETO"] + ["LINETO"] * len(waypoints)
|
|
262
|
+
# FIXME: same as previus comment
|
|
263
|
+
angles = (alpha0 + pi / 2, alpha1)
|
|
264
|
+
|
|
265
|
+
else:
|
|
266
|
+
raise NotImplementedError(
|
|
267
|
+
f"Edge shortening with waypoints not implemented yet: {waypoints}.",
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
path = mpl.path.Path(
|
|
271
|
+
points,
|
|
272
|
+
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
path.vertices = trans_inv(path.vertices)
|
|
276
|
+
return path, angles
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _compute_edge_path_curved(
|
|
280
|
+
tension,
|
|
281
|
+
vcoord_data,
|
|
282
|
+
vpath_fig,
|
|
283
|
+
vsize_fig,
|
|
284
|
+
trans,
|
|
285
|
+
trans_inv,
|
|
286
|
+
ports=(None, None),
|
|
287
|
+
):
|
|
288
|
+
"""Shorten the edge path along a cubic Bezier between the vertex centres.
|
|
289
|
+
|
|
290
|
+
The most important part is that the derivative of the Bezier at the start
|
|
291
|
+
and end point towards the vertex centres: people notice if they do not.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
# Coordinates in figure (default) coords
|
|
295
|
+
vcoord_fig = trans(vcoord_data)
|
|
296
|
+
|
|
297
|
+
dv = vcoord_fig[1] - vcoord_fig[0]
|
|
298
|
+
edge_straight_length = np.sqrt((dv**2).sum())
|
|
299
|
+
|
|
300
|
+
auxs = [None, None]
|
|
301
|
+
for i in range(2):
|
|
302
|
+
if ports[i] is not None:
|
|
303
|
+
der = _get_port_unit_vector(ports[i], trans_inv)
|
|
304
|
+
auxs[i] = der * edge_straight_length * tension + vcoord_fig[i]
|
|
305
|
+
|
|
306
|
+
# Both ports defined, just use them and hope for the best
|
|
307
|
+
# Obviously, if the user specifies ports that make no sense,
|
|
308
|
+
# this is going to be a (technically valid) mess.
|
|
309
|
+
if all(aux is not None for aux in auxs):
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
# If no ports are specified (the most common case), compute
|
|
313
|
+
# the Bezier and shorten it
|
|
314
|
+
elif all(aux is None for aux in auxs):
|
|
315
|
+
# Put auxs along the way
|
|
316
|
+
auxs = np.array(
|
|
317
|
+
[
|
|
318
|
+
vcoord_fig[0] + 0.33 * dv,
|
|
319
|
+
vcoord_fig[1] - 0.33 * dv,
|
|
320
|
+
]
|
|
321
|
+
)
|
|
322
|
+
# Right rotation from the straight edge
|
|
323
|
+
dv_rot = -0.1 * dv @ np.array([[0, 1], [-1, 0]])
|
|
324
|
+
# Shift the auxs orthogonal to the straight edge
|
|
325
|
+
auxs += dv_rot * tension
|
|
326
|
+
|
|
327
|
+
# First port is defined
|
|
328
|
+
elif (auxs[0] is not None) and (auxs[1] is None):
|
|
329
|
+
auxs[1] = auxs[0]
|
|
330
|
+
|
|
331
|
+
# Second port is defined
|
|
332
|
+
else:
|
|
333
|
+
auxs[0] = auxs[1]
|
|
334
|
+
|
|
335
|
+
vs = [None, None]
|
|
336
|
+
thetas = [None, None]
|
|
337
|
+
for i in range(2):
|
|
338
|
+
thetas[i] = atan2(*((auxs[i] - vcoord_fig[i])[::-1]))
|
|
339
|
+
vs[i] = (
|
|
340
|
+
_get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], thetas[i])
|
|
341
|
+
+ vcoord_fig[i]
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
path = {
|
|
345
|
+
"vertices": [
|
|
346
|
+
vs[0],
|
|
347
|
+
auxs[0],
|
|
348
|
+
auxs[1],
|
|
349
|
+
vs[1],
|
|
350
|
+
],
|
|
351
|
+
"codes": ["MOVETO"] + ["CURVE4"] * 3,
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
path = mpl.path.Path(
|
|
355
|
+
path["vertices"],
|
|
356
|
+
codes=[getattr(mpl.path.Path, x) for x in path["codes"]],
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Return to data transform
|
|
360
|
+
path.vertices = trans_inv(path.vertices)
|
|
361
|
+
return path, tuple(thetas)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _compute_edge_path(
|
|
365
|
+
*args,
|
|
366
|
+
tension: float = 0,
|
|
367
|
+
waypoints: str = "none",
|
|
368
|
+
ports: Pair[Optional[str]] = (None, None),
|
|
369
|
+
layout_coordinate_system: str = "cartesian",
|
|
370
|
+
**kwargs,
|
|
371
|
+
):
|
|
372
|
+
"""Compute the edge path in a few different ways."""
|
|
373
|
+
if (waypoints != "none") and (tension != 0):
|
|
374
|
+
raise ValueError("Waypoints not supported for curved edges.")
|
|
375
|
+
|
|
376
|
+
if waypoints != "none":
|
|
377
|
+
return _compute_edge_path_waypoints(
|
|
378
|
+
waypoints,
|
|
379
|
+
*args,
|
|
380
|
+
layout_coordinate_system=layout_coordinate_system,
|
|
381
|
+
**kwargs,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
if tension == 0:
|
|
385
|
+
return _compute_edge_path_straight(*args, **kwargs)
|
|
386
|
+
|
|
387
|
+
return _compute_edge_path_curved(
|
|
388
|
+
tension,
|
|
389
|
+
*args,
|
|
390
|
+
ports=ports,
|
|
391
|
+
**kwargs,
|
|
392
|
+
)
|