iplotx 0.0.1__py3-none-any.whl → 0.2.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 +22 -1
- iplotx/edge/__init__.py +882 -0
- iplotx/edge/arrow.py +220 -10
- iplotx/edge/ports.py +42 -0
- iplotx/groups.py +79 -41
- 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 +127 -0
- iplotx/layout.py +139 -0
- iplotx/network.py +156 -375
- iplotx/plotting.py +157 -55
- iplotx/style.py +379 -0
- iplotx/tree.py +285 -0
- iplotx/typing.py +33 -38
- 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 +250 -55
- iplotx-0.2.0.dist-info/METADATA +76 -0
- iplotx-0.2.0.dist-info/RECORD +30 -0
- {iplotx-0.0.1.dist-info → iplotx-0.2.0.dist-info}/WHEEL +0 -1
- 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.0.1.dist-info/METADATA +0 -39
- iplotx-0.0.1.dist-info/RECORD +0 -20
iplotx/edge/directed.py
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
from copy import deepcopy
|
|
2
|
-
from math import atan2, tan, cos, pi, sin
|
|
3
|
-
import numpy as np
|
|
4
|
-
import matplotlib as mpl
|
|
5
|
-
from matplotlib.transforms import Affine2D
|
|
6
|
-
|
|
7
|
-
from .common import _compute_loops_per_angle
|
|
8
|
-
from .undirected import UndirectedEdgeCollection
|
|
9
|
-
from .arrow import make_arrow_patch
|
|
10
|
-
from ..utils.matplotlib import (
|
|
11
|
-
_stale_wrapper,
|
|
12
|
-
_forwarder,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@_forwarder(
|
|
17
|
-
(
|
|
18
|
-
"set_clip_path",
|
|
19
|
-
"set_clip_box",
|
|
20
|
-
"set_transform",
|
|
21
|
-
"set_snap",
|
|
22
|
-
"set_sketch_params",
|
|
23
|
-
"set_figure",
|
|
24
|
-
"set_animated",
|
|
25
|
-
"set_picker",
|
|
26
|
-
)
|
|
27
|
-
)
|
|
28
|
-
class DirectedEdgeCollection(mpl.artist.Artist):
|
|
29
|
-
def __init__(self, edges, arrows, labels=None, **kwargs):
|
|
30
|
-
super().__init__()
|
|
31
|
-
|
|
32
|
-
# FIXME: do we need a separate _clear_state and _process like in the network
|
|
33
|
-
self._edges = UndirectedEdgeCollection(edges, labels=labels, **kwargs)
|
|
34
|
-
|
|
35
|
-
# NOTE: offsets are a placeholder for later
|
|
36
|
-
self._arrows = EdgeArrowCollection(
|
|
37
|
-
arrows,
|
|
38
|
-
offsets=np.zeros((len(arrows), 2)),
|
|
39
|
-
offset_transform=kwargs["transform"],
|
|
40
|
-
transform=Affine2D(),
|
|
41
|
-
match_original=True,
|
|
42
|
-
)
|
|
43
|
-
self._processed = False
|
|
44
|
-
|
|
45
|
-
def get_children(self):
|
|
46
|
-
artists = []
|
|
47
|
-
# Collect edges first. This way vertices are on top of edges,
|
|
48
|
-
# since vertices are drawn later. That is what most people expect.
|
|
49
|
-
if self._edges is not None:
|
|
50
|
-
artists.append(self._edges)
|
|
51
|
-
if self._arrows is not None:
|
|
52
|
-
artists.append(self._arrows)
|
|
53
|
-
return tuple(artists)
|
|
54
|
-
|
|
55
|
-
def get_edges(self):
|
|
56
|
-
"""Get UndirectedEdgeCollection artist."""
|
|
57
|
-
return self._edges
|
|
58
|
-
|
|
59
|
-
def get_arrows(self):
|
|
60
|
-
"""Get EdgeArrowCollection artist."""
|
|
61
|
-
return self._arrows
|
|
62
|
-
|
|
63
|
-
def get_paths(self):
|
|
64
|
-
"""Get the edge paths."""
|
|
65
|
-
return self._edges.get_paths()
|
|
66
|
-
|
|
67
|
-
def _process(self):
|
|
68
|
-
# Forward mpl properties to children
|
|
69
|
-
# TODO sort out all of the things that need to be forwarded
|
|
70
|
-
for child in self.get_children():
|
|
71
|
-
# set the figure & axes on child, this ensures each artist
|
|
72
|
-
# down the hierarchy knows where to draw
|
|
73
|
-
if hasattr(child, "set_figure"):
|
|
74
|
-
child.set_figure(self.figure)
|
|
75
|
-
child.axes = self.axes
|
|
76
|
-
|
|
77
|
-
# forward the clippath/box to the children need this logic
|
|
78
|
-
# because mpl exposes some fast-path logic
|
|
79
|
-
clip_path = self.get_clip_path()
|
|
80
|
-
if clip_path is None:
|
|
81
|
-
clip_box = self.get_clip_box()
|
|
82
|
-
child.set_clip_box(clip_box)
|
|
83
|
-
else:
|
|
84
|
-
child.set_clip_path(clip_path)
|
|
85
|
-
|
|
86
|
-
self._processed = True
|
|
87
|
-
|
|
88
|
-
def _set_edge_info_for_arrows(
|
|
89
|
-
self,
|
|
90
|
-
which="end",
|
|
91
|
-
transform=None,
|
|
92
|
-
):
|
|
93
|
-
"""Extract the start and/or end angles of the paths to compute arrows."""
|
|
94
|
-
if transform is None:
|
|
95
|
-
transform = self.get_edges().get_transform()
|
|
96
|
-
trans = transform.transform
|
|
97
|
-
trans_inv = transform.inverted().transform
|
|
98
|
-
|
|
99
|
-
arrow_offsets = self._arrows._offsets
|
|
100
|
-
for i, epath in enumerate(self._edges._paths):
|
|
101
|
-
# Offset the arrow to point to the end of the edge
|
|
102
|
-
self._arrows._offsets[i] = epath.vertices[-1]
|
|
103
|
-
|
|
104
|
-
# Rotate the arrow to point in the direction of the edge
|
|
105
|
-
apath = self._arrows._paths[i]
|
|
106
|
-
# NOTE: because the tip of the arrow is at (0, 0) in patch space,
|
|
107
|
-
# in theory it will rotate around that point already
|
|
108
|
-
v2 = trans(epath.vertices[-1])
|
|
109
|
-
v1 = trans(epath.vertices[-2])
|
|
110
|
-
dv = v2 - v1
|
|
111
|
-
theta = atan2(*(dv[::-1]))
|
|
112
|
-
theta_old = self._arrows._angles[i]
|
|
113
|
-
dtheta = theta - theta_old
|
|
114
|
-
mrot = np.array([[cos(dtheta), sin(dtheta)], [-sin(dtheta), cos(dtheta)]])
|
|
115
|
-
apath.vertices = apath.vertices @ mrot
|
|
116
|
-
self._arrows._angles[i] = theta
|
|
117
|
-
|
|
118
|
-
@_stale_wrapper
|
|
119
|
-
def draw(self, renderer, *args, **kwds):
|
|
120
|
-
"""Draw each of the children, with some buffering mechanism."""
|
|
121
|
-
if not self.get_visible():
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
if not self._processed:
|
|
125
|
-
self._process()
|
|
126
|
-
|
|
127
|
-
# We should manage zorder ourselves, but we need to compute
|
|
128
|
-
# the new offsets and angles of arrows from the edges before drawing them
|
|
129
|
-
self._edges.draw(renderer, *args, **kwds)
|
|
130
|
-
self._set_edge_info_for_arrows(which="end")
|
|
131
|
-
self._arrows.draw(renderer, *args, **kwds)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
class EdgeArrowCollection(mpl.collections.PatchCollection):
|
|
135
|
-
"""Collection of arrow patches for plotting directed edgs."""
|
|
136
|
-
|
|
137
|
-
def __init__(self, *args, **kwargs):
|
|
138
|
-
super().__init__(*args, **kwargs)
|
|
139
|
-
self._angles = np.zeros(len(self._paths))
|
|
140
|
-
|
|
141
|
-
@property
|
|
142
|
-
def stale(self):
|
|
143
|
-
return super().stale
|
|
144
|
-
|
|
145
|
-
@stale.setter
|
|
146
|
-
def stale(self, val):
|
|
147
|
-
mpl.collections.PatchCollection.stale.fset(self, val)
|
|
148
|
-
if val and hasattr(self, "stale_callback_post"):
|
|
149
|
-
self.stale_callback_post(self)
|
iplotx/edge/label.py
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import matplotlib as mpl
|
|
3
|
-
|
|
4
|
-
from ..utils.matplotlib import (
|
|
5
|
-
_stale_wrapper,
|
|
6
|
-
_forwarder,
|
|
7
|
-
_additional_set_methods,
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class LabelCollection(mpl.artist.Artist):
|
|
12
|
-
def __init__(self, labels, style=None):
|
|
13
|
-
self._labels = labels
|
|
14
|
-
self._style = style
|
|
15
|
-
super().__init__()
|
|
16
|
-
|
|
17
|
-
def _create_labels(self):
|
|
18
|
-
style = self._style if self._style is not None else {}
|
|
19
|
-
|
|
20
|
-
arts = []
|
|
21
|
-
for label in self._labels:
|
|
22
|
-
art = mpl.text.Text(
|
|
23
|
-
0,
|
|
24
|
-
0,
|
|
25
|
-
label,
|
|
26
|
-
transform=self.axes.transData,
|
|
27
|
-
**style,
|
|
28
|
-
)
|
|
29
|
-
art.set_figure(self.figure)
|
|
30
|
-
art.axes = self.axes
|
|
31
|
-
arts.append(art)
|
|
32
|
-
self._labels = arts
|
|
33
|
-
|
|
34
|
-
def get_children(self):
|
|
35
|
-
return self._labels
|
|
36
|
-
|
|
37
|
-
def set_offsets(self, offsets):
|
|
38
|
-
for art, offset in zip(self._labels, offsets):
|
|
39
|
-
art.set_position((offset[0], offset[1]))
|
|
40
|
-
|
|
41
|
-
@_stale_wrapper
|
|
42
|
-
def draw(self, renderer, *args, **kwds):
|
|
43
|
-
"""Draw each of the children, with some buffering mechanism."""
|
|
44
|
-
if not self.get_visible():
|
|
45
|
-
return
|
|
46
|
-
|
|
47
|
-
# We should manage zorder ourselves, but we need to compute
|
|
48
|
-
# the new offsets and angles of arrows from the edges before drawing them
|
|
49
|
-
for art in self.get_children():
|
|
50
|
-
art.draw(renderer, *args, **kwds)
|
iplotx/edge/undirected.py
DELETED
|
@@ -1,447 +0,0 @@
|
|
|
1
|
-
from math import atan2, tan, cos, pi, sin
|
|
2
|
-
from collections import defaultdict
|
|
3
|
-
import numpy as np
|
|
4
|
-
import matplotlib as mpl
|
|
5
|
-
|
|
6
|
-
from .common import _compute_loops_per_angle
|
|
7
|
-
from .label import LabelCollection
|
|
8
|
-
from ..utils.matplotlib import (
|
|
9
|
-
_compute_mid_coord,
|
|
10
|
-
_stale_wrapper,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class UndirectedEdgeCollection(mpl.collections.PatchCollection):
|
|
15
|
-
def __init__(self, *args, **kwargs):
|
|
16
|
-
kwargs["match_original"] = True
|
|
17
|
-
self._vertex_ids = kwargs.pop("vertex_ids", None)
|
|
18
|
-
self._vertex_centers = kwargs.pop("vertex_centers", None)
|
|
19
|
-
self._vertex_paths = kwargs.pop("vertex_paths", None)
|
|
20
|
-
self._style = kwargs.pop("style", None)
|
|
21
|
-
self._labels = kwargs.pop("labels", None)
|
|
22
|
-
super().__init__(*args, **kwargs)
|
|
23
|
-
|
|
24
|
-
@staticmethod
|
|
25
|
-
def _get_edge_vertex_sizes(edge_vertices):
|
|
26
|
-
sizes = []
|
|
27
|
-
for visual_vertex in edge_vertices:
|
|
28
|
-
if visual_vertex.size is not None:
|
|
29
|
-
sizes.append(visual_vertex.size)
|
|
30
|
-
else:
|
|
31
|
-
sizes.append(max(visual_vertex.width, visual_vertex.height))
|
|
32
|
-
return sizes
|
|
33
|
-
|
|
34
|
-
@staticmethod
|
|
35
|
-
def _compute_edge_angles(path, trans):
|
|
36
|
-
"""Compute edge angles for both starting and ending vertices.
|
|
37
|
-
|
|
38
|
-
NOTE: The domain of atan2 is (-pi, pi].
|
|
39
|
-
"""
|
|
40
|
-
positions = trans(path.vertices)
|
|
41
|
-
|
|
42
|
-
# first angle
|
|
43
|
-
x1, y1 = positions[0]
|
|
44
|
-
x2, y2 = positions[1]
|
|
45
|
-
angle1 = atan2(y2 - y1, x2 - x1)
|
|
46
|
-
|
|
47
|
-
# second angle
|
|
48
|
-
x1, y1 = positions[-1]
|
|
49
|
-
x2, y2 = positions[-2]
|
|
50
|
-
angle2 = atan2(y2 - y1, x2 - x1)
|
|
51
|
-
return (angle1, angle2)
|
|
52
|
-
|
|
53
|
-
def _compute_paths(self, transform=None):
|
|
54
|
-
"""Compute paths for the edges.
|
|
55
|
-
|
|
56
|
-
Loops split the largest wedge left open by other
|
|
57
|
-
edges of that vertex. The algo is:
|
|
58
|
-
(i) Find what vertices each loop belongs to
|
|
59
|
-
(ii) While going through the edges, record the angles
|
|
60
|
-
for vertices with loops
|
|
61
|
-
(iii) Plot each loop based on the recorded angles
|
|
62
|
-
"""
|
|
63
|
-
vids = self._vertex_ids
|
|
64
|
-
vpaths = self._vertex_paths
|
|
65
|
-
vcenters = self._vertex_centers
|
|
66
|
-
if transform is None:
|
|
67
|
-
transform = self.get_transform()
|
|
68
|
-
trans = transform.transform
|
|
69
|
-
trans_inv = transform.inverted().transform
|
|
70
|
-
|
|
71
|
-
# 1. Make a list of vertices with loops, and store them for later
|
|
72
|
-
loop_vertex_dict = {}
|
|
73
|
-
for i, (v1, v2) in enumerate(vids):
|
|
74
|
-
if v1 != v2:
|
|
75
|
-
continue
|
|
76
|
-
if v1 not in loop_vertex_dict:
|
|
77
|
-
loop_vertex_dict[v1] = {
|
|
78
|
-
"indices": [],
|
|
79
|
-
"edge_angles": [],
|
|
80
|
-
}
|
|
81
|
-
loop_vertex_dict[v1]["indices"].append(i)
|
|
82
|
-
|
|
83
|
-
# 2. Make paths for non-loop edges
|
|
84
|
-
# NOTE: keep track of parallel edges to offset them
|
|
85
|
-
parallel_edges = defaultdict(list)
|
|
86
|
-
|
|
87
|
-
# Get actual coordinates of the vertex border
|
|
88
|
-
paths = []
|
|
89
|
-
for i, (v1, v2) in enumerate(vids):
|
|
90
|
-
# Postpone loops (step 3)
|
|
91
|
-
if v1 == v2:
|
|
92
|
-
paths.append(None)
|
|
93
|
-
continue
|
|
94
|
-
|
|
95
|
-
# Coordinates of the adjacent vertices, in data coords
|
|
96
|
-
vcoord_data = vcenters[i]
|
|
97
|
-
|
|
98
|
-
# Coordinates in figure (default) coords
|
|
99
|
-
vcoord_fig = trans(vcoord_data)
|
|
100
|
-
|
|
101
|
-
# Vertex paths in figure (default) coords
|
|
102
|
-
vpath_fig = vpaths[i]
|
|
103
|
-
|
|
104
|
-
# Shorten edge
|
|
105
|
-
if not self._style.get("curved", False):
|
|
106
|
-
path = self._shorten_path_undirected_straight(
|
|
107
|
-
vcoord_fig,
|
|
108
|
-
vpath_fig,
|
|
109
|
-
trans_inv,
|
|
110
|
-
)
|
|
111
|
-
else:
|
|
112
|
-
path = self._shorten_path_undirected_curved(
|
|
113
|
-
vcoord_fig,
|
|
114
|
-
vpath_fig,
|
|
115
|
-
trans_inv,
|
|
116
|
-
tension=self._style.get("tension", 1.5),
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
# Collect angles for this vertex, to be used for loops plotting below
|
|
120
|
-
if (v1 in loop_vertex_dict) or (v2 in loop_vertex_dict):
|
|
121
|
-
angles = self._compute_edge_angles(
|
|
122
|
-
path,
|
|
123
|
-
trans,
|
|
124
|
-
)
|
|
125
|
-
if v1 in loop_vertex_dict:
|
|
126
|
-
loop_vertex_dict[v1]["edge_angles"].append(angles[0])
|
|
127
|
-
if v2 in loop_vertex_dict:
|
|
128
|
-
loop_vertex_dict[v2]["edge_angles"].append(angles[1])
|
|
129
|
-
|
|
130
|
-
# Add the path for this non-loop edge
|
|
131
|
-
paths.append(path)
|
|
132
|
-
# FIXME: curved parallel edges depend on the direction of curvature...!
|
|
133
|
-
parallel_edges[(v1, v2)].append(i)
|
|
134
|
-
|
|
135
|
-
# Fix parallel edges
|
|
136
|
-
# If none found, empty the dictionary already
|
|
137
|
-
if max(parallel_edges.values(), key=len) == 1:
|
|
138
|
-
parallel_edges = {}
|
|
139
|
-
if not self._style.get("curved", False):
|
|
140
|
-
while len(parallel_edges) > 0:
|
|
141
|
-
(v1, v2), indices = parallel_edges.popitem()
|
|
142
|
-
indices_inv = parallel_edges.pop((v2, v1), [])
|
|
143
|
-
nparallel = len(indices)
|
|
144
|
-
nparallel_inv = len(indices_inv)
|
|
145
|
-
ntot = len(indices) + len(indices_inv)
|
|
146
|
-
if ntot > 1:
|
|
147
|
-
self._fix_parallel_edges_straight(
|
|
148
|
-
paths,
|
|
149
|
-
indices,
|
|
150
|
-
indices_inv,
|
|
151
|
-
trans,
|
|
152
|
-
trans_inv,
|
|
153
|
-
offset=self._style.get("offset", 3),
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
# 3. Deal with loops at the end
|
|
157
|
-
for vid, ldict in loop_vertex_dict.items():
|
|
158
|
-
vpath = vpaths[ldict["indices"][0]][0]
|
|
159
|
-
vcoord_fig = trans(vcenters[ldict["indices"][0]][0])
|
|
160
|
-
nloops = len(ldict["indices"])
|
|
161
|
-
edge_angles = ldict["edge_angles"]
|
|
162
|
-
|
|
163
|
-
# The space between the existing angles is where we can fit the loops
|
|
164
|
-
# One loop we can fit in the largest wedge, multiple loops we need
|
|
165
|
-
nloops_per_angle = _compute_loops_per_angle(nloops, edge_angles)
|
|
166
|
-
|
|
167
|
-
idx = 0
|
|
168
|
-
for theta1, theta2, nloops in nloops_per_angle:
|
|
169
|
-
# Angular size of each loop in this wedge
|
|
170
|
-
delta = (theta2 - theta1) / nloops
|
|
171
|
-
|
|
172
|
-
# Iterate over individual loops
|
|
173
|
-
for j in range(nloops):
|
|
174
|
-
thetaj1 = theta1 + j * delta
|
|
175
|
-
# Use 60 degrees as the largest possible loop wedge
|
|
176
|
-
thetaj2 = thetaj1 + min(delta, pi / 3)
|
|
177
|
-
|
|
178
|
-
# Get the path for this loop
|
|
179
|
-
path = self._compute_loop_path(
|
|
180
|
-
vcoord_fig,
|
|
181
|
-
vpath,
|
|
182
|
-
thetaj1,
|
|
183
|
-
thetaj2,
|
|
184
|
-
trans_inv,
|
|
185
|
-
)
|
|
186
|
-
paths[ldict["indices"][idx]] = path
|
|
187
|
-
idx += 1
|
|
188
|
-
|
|
189
|
-
return paths
|
|
190
|
-
|
|
191
|
-
def _fix_parallel_edges_straight(
|
|
192
|
-
self,
|
|
193
|
-
paths,
|
|
194
|
-
indices,
|
|
195
|
-
indices_inv,
|
|
196
|
-
trans,
|
|
197
|
-
trans_inv,
|
|
198
|
-
offset=3,
|
|
199
|
-
):
|
|
200
|
-
"""Offset parallel edges along the same path."""
|
|
201
|
-
ntot = len(indices) + len(indices_inv)
|
|
202
|
-
|
|
203
|
-
# This is straight so two vertices anyway
|
|
204
|
-
# NOTE: all paths will be the same, which is why we need to offset them
|
|
205
|
-
vs, ve = trans(paths[indices[0]].vertices)
|
|
206
|
-
|
|
207
|
-
# Move orthogonal to the line
|
|
208
|
-
fracs = (
|
|
209
|
-
(vs - ve) / np.sqrt(((vs - ve) ** 2).sum()) @ np.array([[0, 1], [-1, 0]])
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
# NOTE: for now treat both direction the same
|
|
213
|
-
for i, idx in enumerate(indices + indices_inv):
|
|
214
|
-
# Offset the path
|
|
215
|
-
paths[idx].vertices = trans_inv(
|
|
216
|
-
trans(paths[idx].vertices) + fracs * offset * (i - ntot / 2)
|
|
217
|
-
)
|
|
218
|
-
|
|
219
|
-
def _compute_loop_path(
|
|
220
|
-
self,
|
|
221
|
-
vcoord_fig,
|
|
222
|
-
vpath,
|
|
223
|
-
angle1,
|
|
224
|
-
angle2,
|
|
225
|
-
trans_inv,
|
|
226
|
-
):
|
|
227
|
-
# Shorten at starting angle
|
|
228
|
-
start = _get_shorter_edge_coords(vpath, angle1) + vcoord_fig
|
|
229
|
-
# Shorten at end angle
|
|
230
|
-
end = _get_shorter_edge_coords(vpath, angle2) + vcoord_fig
|
|
231
|
-
|
|
232
|
-
aux1 = (start - vcoord_fig) * 2.5 + vcoord_fig
|
|
233
|
-
aux2 = (end - vcoord_fig) * 2.5 + vcoord_fig
|
|
234
|
-
|
|
235
|
-
vertices = np.vstack(
|
|
236
|
-
[
|
|
237
|
-
start,
|
|
238
|
-
aux1,
|
|
239
|
-
aux2,
|
|
240
|
-
end,
|
|
241
|
-
]
|
|
242
|
-
)
|
|
243
|
-
codes = ["MOVETO"] + ["CURVE4"] * 3
|
|
244
|
-
|
|
245
|
-
# Offset to place and transform to data coordinates
|
|
246
|
-
vertices = trans_inv(vertices)
|
|
247
|
-
codes = [getattr(mpl.path.Path, x) for x in codes]
|
|
248
|
-
path = mpl.path.Path(
|
|
249
|
-
vertices,
|
|
250
|
-
codes=codes,
|
|
251
|
-
)
|
|
252
|
-
return path
|
|
253
|
-
|
|
254
|
-
def _shorten_path_undirected_straight(
|
|
255
|
-
self,
|
|
256
|
-
vcoord_fig,
|
|
257
|
-
vpath_fig,
|
|
258
|
-
trans_inv,
|
|
259
|
-
):
|
|
260
|
-
# Straight SVG instructions
|
|
261
|
-
path = {
|
|
262
|
-
"vertices": [],
|
|
263
|
-
"codes": ["MOVETO", "LINETO"],
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
# Angle of the straight line
|
|
267
|
-
theta = atan2(*((vcoord_fig[1] - vcoord_fig[0])[::-1]))
|
|
268
|
-
|
|
269
|
-
# Shorten at starting vertex
|
|
270
|
-
vs = _get_shorter_edge_coords(vpath_fig[0], theta) + vcoord_fig[0]
|
|
271
|
-
path["vertices"].append(vs)
|
|
272
|
-
|
|
273
|
-
# Shorten at end vertex
|
|
274
|
-
ve = _get_shorter_edge_coords(vpath_fig[1], theta + pi) + vcoord_fig[1]
|
|
275
|
-
path["vertices"].append(ve)
|
|
276
|
-
|
|
277
|
-
path = mpl.path.Path(
|
|
278
|
-
path["vertices"],
|
|
279
|
-
codes=[getattr(mpl.path.Path, x) for x in path["codes"]],
|
|
280
|
-
)
|
|
281
|
-
path.vertices = trans_inv(path.vertices)
|
|
282
|
-
return path
|
|
283
|
-
|
|
284
|
-
def _shorten_path_undirected_curved(
|
|
285
|
-
self,
|
|
286
|
-
vcoord_fig,
|
|
287
|
-
vpath_fig,
|
|
288
|
-
trans_inv,
|
|
289
|
-
tension=+1.5,
|
|
290
|
-
):
|
|
291
|
-
# Angle of the straight line
|
|
292
|
-
theta = atan2(*((vcoord_fig[1] - vcoord_fig[0])[::-1]))
|
|
293
|
-
|
|
294
|
-
# Shorten at starting vertex
|
|
295
|
-
vs = _get_shorter_edge_coords(vpath_fig[0], theta) + vcoord_fig[0]
|
|
296
|
-
|
|
297
|
-
# Shorten at end vertex
|
|
298
|
-
ve = _get_shorter_edge_coords(vpath_fig[1], theta + pi) + vcoord_fig[1]
|
|
299
|
-
|
|
300
|
-
edge_straight_length = np.sqrt(((ve - vs) ** 2).sum())
|
|
301
|
-
|
|
302
|
-
aux1 = vs + 0.33 * (ve - vs)
|
|
303
|
-
aux2 = vs + 0.67 * (ve - vs)
|
|
304
|
-
|
|
305
|
-
# Move Bezier points orthogonal to the line
|
|
306
|
-
fracs = (
|
|
307
|
-
(vs - ve) / np.sqrt(((vs - ve) ** 2).sum()) @ np.array([[0, 1], [-1, 0]])
|
|
308
|
-
)
|
|
309
|
-
aux1 += 0.1 * fracs * tension * edge_straight_length
|
|
310
|
-
aux2 += 0.1 * fracs * tension * edge_straight_length
|
|
311
|
-
|
|
312
|
-
path = {
|
|
313
|
-
"vertices": [
|
|
314
|
-
vs,
|
|
315
|
-
aux1,
|
|
316
|
-
aux2,
|
|
317
|
-
ve,
|
|
318
|
-
],
|
|
319
|
-
"codes": ["MOVETO"] + ["CURVE4"] * 3,
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
path = mpl.path.Path(
|
|
323
|
-
path["vertices"],
|
|
324
|
-
codes=[getattr(mpl.path.Path, x) for x in path["codes"]],
|
|
325
|
-
)
|
|
326
|
-
path.vertices = trans_inv(path.vertices)
|
|
327
|
-
return path
|
|
328
|
-
|
|
329
|
-
def _compute_labels(self):
|
|
330
|
-
style = self._style.get("label", None) if self._style is not None else None
|
|
331
|
-
offsets = []
|
|
332
|
-
for path in self._paths:
|
|
333
|
-
offset = _compute_mid_coord(path)
|
|
334
|
-
offsets.append(offset)
|
|
335
|
-
|
|
336
|
-
if not hasattr(self, "_label_collection"):
|
|
337
|
-
self._label_collection = LabelCollection(
|
|
338
|
-
self._labels,
|
|
339
|
-
style=style,
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
# Forward a bunch of mpl settings that are needed
|
|
343
|
-
self._label_collection.set_figure(self.figure)
|
|
344
|
-
self._label_collection.axes = self.axes
|
|
345
|
-
# forward the clippath/box to the children need this logic
|
|
346
|
-
# because mpl exposes some fast-path logic
|
|
347
|
-
clip_path = self.get_clip_path()
|
|
348
|
-
if clip_path is None:
|
|
349
|
-
clip_box = self.get_clip_box()
|
|
350
|
-
self._label_collection.set_clip_box(clip_box)
|
|
351
|
-
else:
|
|
352
|
-
self._label_collection.set_clip_path(clip_path)
|
|
353
|
-
|
|
354
|
-
# Finally make the patches
|
|
355
|
-
self._label_collection._create_labels()
|
|
356
|
-
self._label_collection.set_offsets(offsets)
|
|
357
|
-
|
|
358
|
-
def get_children(self):
|
|
359
|
-
children = []
|
|
360
|
-
if hasattr(self, "_label_collection"):
|
|
361
|
-
children.append(self._label_collection)
|
|
362
|
-
return children
|
|
363
|
-
|
|
364
|
-
@_stale_wrapper
|
|
365
|
-
def draw(self, renderer, *args, **kwds):
|
|
366
|
-
if self._vertex_paths is not None:
|
|
367
|
-
self._paths = self._compute_paths()
|
|
368
|
-
if self._labels is not None:
|
|
369
|
-
self._compute_labels()
|
|
370
|
-
super().draw(renderer)
|
|
371
|
-
|
|
372
|
-
for child in self.get_children():
|
|
373
|
-
child.draw(renderer, *args, **kwds)
|
|
374
|
-
|
|
375
|
-
@property
|
|
376
|
-
def stale(self):
|
|
377
|
-
return super().stale
|
|
378
|
-
|
|
379
|
-
@stale.setter
|
|
380
|
-
def stale(self, val):
|
|
381
|
-
mpl.collections.PatchCollection.stale.fset(self, val)
|
|
382
|
-
if val and hasattr(self, "stale_callback_post"):
|
|
383
|
-
self.stale_callback_post(self)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
def make_stub_patch(**kwargs):
|
|
387
|
-
"""Make a stub undirected edge patch, without actual path information."""
|
|
388
|
-
kwargs["clip_on"] = kwargs.get("clip_on", True)
|
|
389
|
-
if ("color" in kwargs) and ("edgecolor" not in kwargs):
|
|
390
|
-
kwargs["edgecolor"] = kwargs.pop("color")
|
|
391
|
-
# Edges are always hollow, because they are not closed paths
|
|
392
|
-
kwargs["facecolor"] = "none"
|
|
393
|
-
|
|
394
|
-
# Forget specific properties that are not supported here
|
|
395
|
-
forbidden_props = [
|
|
396
|
-
"curved",
|
|
397
|
-
"tension",
|
|
398
|
-
"offset",
|
|
399
|
-
"label",
|
|
400
|
-
]
|
|
401
|
-
for prop in forbidden_props:
|
|
402
|
-
if prop in kwargs:
|
|
403
|
-
kwargs.pop(prop)
|
|
404
|
-
|
|
405
|
-
# NOTE: the path is overwritten later anyway, so no reason to spend any time here
|
|
406
|
-
art = mpl.patches.PathPatch(
|
|
407
|
-
mpl.path.Path([[0, 0]]),
|
|
408
|
-
**kwargs,
|
|
409
|
-
)
|
|
410
|
-
return art
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
def _get_shorter_edge_coords(vpath, theta):
|
|
414
|
-
# Bound theta from -pi to pi (why is that not guaranteed?)
|
|
415
|
-
theta = (theta + pi) % (2 * pi) - pi
|
|
416
|
-
|
|
417
|
-
for i in range(len(vpath)):
|
|
418
|
-
v1 = vpath.vertices[i]
|
|
419
|
-
v2 = vpath.vertices[(i + 1) % len(vpath)]
|
|
420
|
-
theta1 = atan2(*((v1)[::-1]))
|
|
421
|
-
theta2 = atan2(*((v2)[::-1]))
|
|
422
|
-
|
|
423
|
-
# atan2 ranges ]-3.14, 3.14]
|
|
424
|
-
# so it can be that theta1 is -3 and theta2 is +3
|
|
425
|
-
# therefore we need two separate cases, one that cuts at pi and one at 0
|
|
426
|
-
cond1 = theta1 <= theta <= theta2
|
|
427
|
-
cond2 = (
|
|
428
|
-
(theta1 + 2 * pi) % (2 * pi)
|
|
429
|
-
<= (theta + 2 * pi) % (2 * pi)
|
|
430
|
-
<= (theta2 + 2 * pi) % (2 * pi)
|
|
431
|
-
)
|
|
432
|
-
if cond1 or cond2:
|
|
433
|
-
break
|
|
434
|
-
else:
|
|
435
|
-
raise ValueError("Angle for patch not found")
|
|
436
|
-
|
|
437
|
-
# The edge meets the patch of the vertex on the v1-v2 size,
|
|
438
|
-
# at angle theta from the center
|
|
439
|
-
mtheta = tan(theta)
|
|
440
|
-
if v2[0] == v1[0]:
|
|
441
|
-
xe = v1[0]
|
|
442
|
-
else:
|
|
443
|
-
m12 = (v2[1] - v1[1]) / (v2[0] - v1[0])
|
|
444
|
-
xe = (v1[1] - m12 * v1[0]) / (mtheta - m12)
|
|
445
|
-
ye = mtheta * xe
|
|
446
|
-
ve = np.array([xe, ye])
|
|
447
|
-
return ve
|