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/network.py
CHANGED
|
@@ -1,50 +1,39 @@
|
|
|
1
|
-
from typing import
|
|
2
|
-
import warnings
|
|
1
|
+
from typing import Optional, Sequence
|
|
3
2
|
import numpy as np
|
|
4
3
|
import pandas as pd
|
|
5
4
|
import matplotlib as mpl
|
|
6
|
-
from matplotlib.transforms import Affine2D
|
|
7
5
|
|
|
8
6
|
from .typing import (
|
|
9
7
|
GraphType,
|
|
10
8
|
LayoutType,
|
|
11
9
|
)
|
|
12
|
-
from .
|
|
10
|
+
from .style import (
|
|
13
11
|
get_style,
|
|
14
12
|
rotate_style,
|
|
15
13
|
)
|
|
16
|
-
from .heuristics import (
|
|
17
|
-
network_library,
|
|
18
|
-
normalise_layout,
|
|
19
|
-
detect_directedness,
|
|
20
|
-
)
|
|
21
14
|
from .utils.matplotlib import (
|
|
22
15
|
_stale_wrapper,
|
|
23
16
|
_forwarder,
|
|
24
|
-
|
|
17
|
+
_build_cmap_fun,
|
|
18
|
+
)
|
|
19
|
+
from .ingest import (
|
|
20
|
+
ingest_network_data,
|
|
25
21
|
)
|
|
26
22
|
from .vertex import (
|
|
27
23
|
VertexCollection,
|
|
28
|
-
make_patch as make_vertex_patch,
|
|
29
24
|
)
|
|
30
|
-
from .edge
|
|
31
|
-
|
|
25
|
+
from .edge import (
|
|
26
|
+
EdgeCollection,
|
|
32
27
|
make_stub_patch as make_undirected_edge_patch,
|
|
33
28
|
)
|
|
34
|
-
from .edge.directed import (
|
|
35
|
-
DirectedEdgeCollection,
|
|
36
|
-
make_arrow_patch,
|
|
37
|
-
)
|
|
38
29
|
|
|
39
30
|
|
|
40
31
|
@_forwarder(
|
|
41
32
|
(
|
|
42
33
|
"set_clip_path",
|
|
43
34
|
"set_clip_box",
|
|
44
|
-
"set_transform",
|
|
45
35
|
"set_snap",
|
|
46
36
|
"set_sketch_params",
|
|
47
|
-
"set_figure",
|
|
48
37
|
"set_animated",
|
|
49
38
|
"set_picker",
|
|
50
39
|
)
|
|
@@ -53,50 +42,62 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
53
42
|
def __init__(
|
|
54
43
|
self,
|
|
55
44
|
network: GraphType,
|
|
56
|
-
layout: LayoutType = None,
|
|
57
|
-
vertex_labels:
|
|
58
|
-
edge_labels:
|
|
45
|
+
layout: Optional[LayoutType] = None,
|
|
46
|
+
vertex_labels: Optional[list | dict | pd.Series] = None,
|
|
47
|
+
edge_labels: Optional[Sequence] = None,
|
|
48
|
+
transform: mpl.transforms.Transform = mpl.transforms.IdentityTransform(),
|
|
49
|
+
offset_transform: Optional[mpl.transforms.Transform] = None,
|
|
59
50
|
):
|
|
60
51
|
"""Network container artist that groups all plotting elements.
|
|
61
52
|
|
|
62
53
|
Parameters:
|
|
63
|
-
network
|
|
64
|
-
layout
|
|
54
|
+
network: The network to plot.
|
|
55
|
+
layout: The layout of the network. If None, this function will attempt to
|
|
65
56
|
infer the layout from the network metadata, using heuristics. If that fails, an
|
|
66
57
|
exception will be raised.
|
|
67
|
-
vertex_labels
|
|
58
|
+
vertex_labels: The labels for the vertices. If None, no vertex labels
|
|
68
59
|
will be drawn. If a list, the labels are taken from the list. If a dict, the keys
|
|
69
60
|
should be the vertex IDs and the values should be the labels.
|
|
70
|
-
|
|
71
|
-
"""
|
|
72
|
-
super().__init__()
|
|
61
|
+
edge_labels: The labels for the edges. If None, no edge labels will be drawn.
|
|
73
62
|
|
|
63
|
+
"""
|
|
74
64
|
self.network = network
|
|
75
|
-
self._ipx_internal_data =
|
|
65
|
+
self._ipx_internal_data = ingest_network_data(
|
|
76
66
|
network,
|
|
77
67
|
layout,
|
|
78
68
|
vertex_labels=vertex_labels,
|
|
79
69
|
edge_labels=edge_labels,
|
|
80
70
|
)
|
|
81
|
-
self._clear_state()
|
|
82
71
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
self.
|
|
87
|
-
|
|
72
|
+
super().__init__()
|
|
73
|
+
|
|
74
|
+
# This is usually the identity (which scales poorly with dpi)
|
|
75
|
+
self.set_transform(transform)
|
|
76
|
+
|
|
77
|
+
# This is usually transData
|
|
78
|
+
self.set_offset_transform(offset_transform)
|
|
79
|
+
|
|
80
|
+
zorder = get_style(".network").get("zorder", 1)
|
|
81
|
+
self.set_zorder(zorder)
|
|
82
|
+
|
|
83
|
+
self._add_vertices()
|
|
84
|
+
self._add_edges()
|
|
88
85
|
|
|
89
86
|
def get_children(self):
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return
|
|
87
|
+
return (self._vertices, self._edges)
|
|
88
|
+
|
|
89
|
+
def set_figure(self, fig):
|
|
90
|
+
super().set_figure(fig)
|
|
91
|
+
for child in self.get_children():
|
|
92
|
+
child.set_figure(fig)
|
|
93
|
+
|
|
94
|
+
def get_offset_transform(self):
|
|
95
|
+
"""Get the offset transform (for vertices/edges)."""
|
|
96
|
+
return self._offset_transform
|
|
97
|
+
|
|
98
|
+
def set_offset_transform(self, offset_transform):
|
|
99
|
+
"""Set the offset transform (for vertices/edges)."""
|
|
100
|
+
self._offset_transform = offset_transform
|
|
100
101
|
|
|
101
102
|
def get_vertices(self):
|
|
102
103
|
"""Get VertexCollection artist."""
|
|
@@ -108,13 +109,13 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
108
109
|
|
|
109
110
|
def get_vertex_labels(self):
|
|
110
111
|
"""Get list of vertex label artists."""
|
|
111
|
-
return self.
|
|
112
|
+
return self._vertices.get_labels()
|
|
112
113
|
|
|
113
114
|
def get_edge_labels(self):
|
|
114
115
|
"""Get list of edge label artists."""
|
|
115
|
-
return self.
|
|
116
|
+
return self._edges.get_labels()
|
|
116
117
|
|
|
117
|
-
def get_datalim(self, transData, pad=0.
|
|
118
|
+
def get_datalim(self, transData, pad=0.15):
|
|
118
119
|
"""Get limits on x/y axes based on the graph layout data.
|
|
119
120
|
|
|
120
121
|
Parameters:
|
|
@@ -122,228 +123,104 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
122
123
|
pad (float): Padding to add to the limits. Default is 0.05.
|
|
123
124
|
Units are a fraction of total axis range before padding.
|
|
124
125
|
"""
|
|
125
|
-
|
|
126
|
-
import numpy as np
|
|
127
|
-
|
|
128
|
-
layout_columns = [
|
|
129
|
-
f"_ipx_layout_{i}" for i in range(self._ipx_internal_data["ndim"])
|
|
130
|
-
]
|
|
131
|
-
layout = self._ipx_internal_data["vertex_df"][layout_columns].values
|
|
126
|
+
layout = self.get_layout().values
|
|
132
127
|
|
|
133
128
|
if len(layout) == 0:
|
|
134
|
-
|
|
135
|
-
maxs = np.array([1, 1])
|
|
136
|
-
return (mins, maxs)
|
|
137
|
-
|
|
138
|
-
# Use the layout as a base, and expand using bboxes from other artists
|
|
139
|
-
mins = np.min(layout, axis=0).astype(float)
|
|
140
|
-
maxs = np.max(layout, axis=0).astype(float)
|
|
141
|
-
|
|
142
|
-
# NOTE: unlike other Collections, the vertices are basically a
|
|
143
|
-
# PatchCollection with an offset transform using transData. Therefore,
|
|
144
|
-
# care should be taken if one wants to include it here
|
|
145
|
-
if self._vertices is not None:
|
|
146
|
-
trans = transData.transform
|
|
147
|
-
trans_inv = transData.inverted().transform
|
|
148
|
-
verts = self._vertices
|
|
149
|
-
for path, offset in zip(verts.get_paths(), verts._offsets):
|
|
150
|
-
bbox = path.get_extents()
|
|
151
|
-
mins = np.minimum(mins, trans_inv(bbox.min + trans(offset)))
|
|
152
|
-
maxs = np.maximum(maxs, trans_inv(bbox.max + trans(offset)))
|
|
153
|
-
|
|
154
|
-
if self._edges is not None:
|
|
155
|
-
for path in self._edges.get_paths():
|
|
156
|
-
bbox = path.get_extents()
|
|
157
|
-
mins = np.minimum(mins, bbox.min)
|
|
158
|
-
maxs = np.maximum(maxs, bbox.max)
|
|
159
|
-
|
|
160
|
-
if hasattr(self, "_groups") and self._groups is not None:
|
|
161
|
-
for path in self._groups.get_paths():
|
|
162
|
-
bbox = path.get_extents()
|
|
163
|
-
mins = np.minimum(mins, bbox.min)
|
|
164
|
-
maxs = np.maximum(maxs, bbox.max)
|
|
165
|
-
|
|
166
|
-
# 5% padding, on each side
|
|
167
|
-
pad = (maxs - mins) * pad
|
|
168
|
-
mins -= pad
|
|
169
|
-
maxs += pad
|
|
170
|
-
|
|
171
|
-
return mpl.transforms.Bbox([mins, maxs])
|
|
129
|
+
return mpl.transforms.Bbox([[0, 0], [1, 1]])
|
|
172
130
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
f"_ipx_layout_{i}" for i in range(self._ipx_internal_data["ndim"])
|
|
179
|
-
]
|
|
180
|
-
vertex_layout_df = self._ipx_internal_data["vertex_df"][layout_columns]
|
|
181
|
-
if vertex_style.get("size") == "label":
|
|
182
|
-
if "label" not in self._ipx_internal_data["vertex_df"].columns:
|
|
183
|
-
warnings.warn(
|
|
184
|
-
"No labels found, cannot resize vertices based on labels."
|
|
185
|
-
)
|
|
186
|
-
vertex_style["size"] = get_style("default.vertex")["size"]
|
|
187
|
-
else:
|
|
188
|
-
vertex_labels = self._ipx_internal_data["vertex_df"]["label"]
|
|
189
|
-
|
|
190
|
-
# FIXME:: this would be better off in the VertexCollection itself, like we do for groups
|
|
191
|
-
offsets = []
|
|
192
|
-
patches = []
|
|
193
|
-
for i, (vid, row) in enumerate(vertex_layout_df.iterrows()):
|
|
194
|
-
# Centre of the vertex
|
|
195
|
-
offsets.append(list(row[layout_columns].values))
|
|
196
|
-
|
|
197
|
-
if vertex_style.get("size") == "label":
|
|
198
|
-
# NOTE: it's ok to overwrite the dict here
|
|
199
|
-
vertex_style["size"] = _get_label_width_height(
|
|
200
|
-
vertex_labels[vid], **vertex_style.get("label", {})
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
vertex_stylei = rotate_style(vertex_style, index=i, id=vid)
|
|
204
|
-
|
|
205
|
-
# Shape of the vertex (Patch)
|
|
206
|
-
art = make_vertex_patch(**vertex_stylei)
|
|
207
|
-
patches.append(art)
|
|
208
|
-
|
|
209
|
-
art = VertexCollection(
|
|
210
|
-
patches,
|
|
211
|
-
offsets=offsets if offsets else None,
|
|
212
|
-
offset_transform=self.axes.transData,
|
|
213
|
-
transform=Affine2D(),
|
|
214
|
-
match_original=True,
|
|
131
|
+
bbox = mpl.transforms.Bbox.union(
|
|
132
|
+
[
|
|
133
|
+
self._vertices.get_datalim(transData),
|
|
134
|
+
self._edges.get_datalim(transData),
|
|
135
|
+
]
|
|
215
136
|
)
|
|
216
|
-
self._vertices = art
|
|
217
|
-
|
|
218
|
-
def _add_vertex_labels(self):
|
|
219
|
-
"""Draw vertex labels."""
|
|
220
|
-
label_style = get_style(".vertex.label")
|
|
221
|
-
forbidden_props = ["hpadding", "vpadding"]
|
|
222
|
-
for prop in forbidden_props:
|
|
223
|
-
if prop in label_style:
|
|
224
|
-
del label_style[prop]
|
|
225
|
-
|
|
226
|
-
texts = []
|
|
227
|
-
vertex_labels = self._ipx_internal_data["vertex_df"]["label"]
|
|
228
|
-
for offset, label in zip(self._vertices._offsets, vertex_labels):
|
|
229
|
-
text = mpl.text.Text(
|
|
230
|
-
offset[0],
|
|
231
|
-
offset[1],
|
|
232
|
-
label,
|
|
233
|
-
transform=self.axes.transData,
|
|
234
|
-
**label_style,
|
|
235
|
-
)
|
|
236
|
-
texts.append(text)
|
|
237
|
-
self._vertex_labels = texts
|
|
238
137
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if "labels" in self._ipx_internal_data["edge_df"].columns:
|
|
242
|
-
labels = self._ipx_internal_data["edge_df"]["labels"]
|
|
243
|
-
else:
|
|
244
|
-
labels = None
|
|
138
|
+
bbox = bbox.expanded(sw=(1.0 + pad), sh=(1.0 + pad))
|
|
139
|
+
return bbox
|
|
245
140
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
"""Draw directed edges."""
|
|
252
|
-
edge_style = get_style(".edge")
|
|
253
|
-
arrow_style = get_style(".arrow")
|
|
141
|
+
def autoscale_view(self, tight=False):
|
|
142
|
+
"""Recompute data limits from this artist and set autoscale based on them."""
|
|
143
|
+
bbox = self.get_datalim(self.axes.transData)
|
|
144
|
+
self.axes.update_datalim(bbox)
|
|
145
|
+
self.axes.autoscale_view(tight=tight)
|
|
254
146
|
|
|
147
|
+
def get_layout(self):
|
|
255
148
|
layout_columns = [
|
|
256
149
|
f"_ipx_layout_{i}" for i in range(self._ipx_internal_data["ndim"])
|
|
257
150
|
]
|
|
258
151
|
vertex_layout_df = self._ipx_internal_data["vertex_df"][layout_columns]
|
|
259
|
-
|
|
260
|
-
["_ipx_source", "_ipx_target"]
|
|
261
|
-
)
|
|
152
|
+
return vertex_layout_df
|
|
262
153
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
154
|
+
def _get_label_series(self, kind):
|
|
155
|
+
if "label" in self._ipx_internal_data[f"{kind}_df"].columns:
|
|
156
|
+
return self._ipx_internal_data[f"{kind}_df"]["label"]
|
|
157
|
+
else:
|
|
158
|
+
return None
|
|
268
159
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
160
|
+
def _add_vertices(self):
|
|
161
|
+
"""Add vertices to the network artist."""
|
|
162
|
+
|
|
163
|
+
self._vertices = VertexCollection(
|
|
164
|
+
layout=self.get_layout(),
|
|
165
|
+
layout_coordinate_system=self._ipx_internal_data.get(
|
|
166
|
+
"layout_coordinate_system", "cartesian"
|
|
167
|
+
),
|
|
168
|
+
style=get_style(".vertex"),
|
|
169
|
+
labels=self._get_label_series("vertex"),
|
|
170
|
+
transform=self.get_transform(),
|
|
171
|
+
offset_transform=self.get_offset_transform(),
|
|
172
|
+
)
|
|
280
173
|
|
|
281
|
-
|
|
174
|
+
def _add_edges(self):
|
|
175
|
+
"""Add edges to the network artist.
|
|
176
|
+
|
|
177
|
+
NOTE: UndirectedEdgeCollection and ArrowCollection are both subclasses of
|
|
178
|
+
PatchCollection. When used with a cmap/norm, they set their facecolor
|
|
179
|
+
according to the cmap, even though most likely we only want the edgecolor
|
|
180
|
+
set that way. It can make for funny looking plots that are not uninteresting
|
|
181
|
+
but mostly niche at this stage. Therefore we sidestep the whole cmap thing
|
|
182
|
+
here.
|
|
183
|
+
"""
|
|
282
184
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
# factory (the collection) below
|
|
286
|
-
patch = make_undirected_edge_patch(
|
|
287
|
-
**edge_stylei,
|
|
288
|
-
)
|
|
289
|
-
edgepatches.append(patch)
|
|
290
|
-
adjacent_vertex_ids.append((vid1, vid2))
|
|
291
|
-
adjecent_vertex_centers.append((vcenter1, vcenter2))
|
|
292
|
-
adjecent_vertex_paths.append((vpath1, vpath2))
|
|
185
|
+
labels = self._get_label_series("edge")
|
|
186
|
+
edge_style = get_style(".edge")
|
|
293
187
|
|
|
294
|
-
|
|
295
|
-
|
|
188
|
+
if "cmap" in edge_style:
|
|
189
|
+
cmap_fun = _build_cmap_fun(
|
|
190
|
+
edge_style["color"],
|
|
191
|
+
edge_style["cmap"],
|
|
296
192
|
)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
adjacent_vertex_ids = np.array(adjacent_vertex_ids)
|
|
300
|
-
adjecent_vertex_centers = np.array(adjecent_vertex_centers)
|
|
301
|
-
# NOTE: the paths might have different number of sides, so it cannot be recast
|
|
302
|
-
|
|
303
|
-
# TODO:: deal with "ports" a la graphviz
|
|
304
|
-
|
|
305
|
-
art = DirectedEdgeCollection(
|
|
306
|
-
edges=edgepatches,
|
|
307
|
-
arrows=arrowpatches,
|
|
308
|
-
labels=labels,
|
|
309
|
-
vertex_ids=adjacent_vertex_ids,
|
|
310
|
-
vertex_paths=adjecent_vertex_paths,
|
|
311
|
-
vertex_centers=adjecent_vertex_centers,
|
|
312
|
-
transform=self.axes.transData,
|
|
313
|
-
style=edge_style,
|
|
314
|
-
)
|
|
315
|
-
self._edges = art
|
|
316
|
-
|
|
317
|
-
def _add_undirected_edges(self, labels=None):
|
|
318
|
-
"""Draw undirected edges."""
|
|
319
|
-
edge_style = get_style(".edge")
|
|
193
|
+
else:
|
|
194
|
+
cmap_fun = None
|
|
320
195
|
|
|
321
|
-
layout_columns = [
|
|
322
|
-
f"_ipx_layout_{i}" for i in range(self._ipx_internal_data["ndim"])
|
|
323
|
-
]
|
|
324
|
-
vertex_layout_df = self._ipx_internal_data["vertex_df"][layout_columns]
|
|
325
196
|
edge_df = self._ipx_internal_data["edge_df"].set_index(
|
|
326
197
|
["_ipx_source", "_ipx_target"]
|
|
327
198
|
)
|
|
328
199
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
vertex_indices = pd.Series(
|
|
332
|
-
np.arange(len(vertex_layout_df)), index=vertex_layout_df.index
|
|
333
|
-
)
|
|
334
|
-
|
|
200
|
+
if "cmap" in edge_style:
|
|
201
|
+
colorarray = []
|
|
335
202
|
edgepatches = []
|
|
336
203
|
adjacent_vertex_ids = []
|
|
337
|
-
adjecent_vertex_centers = []
|
|
338
|
-
adjecent_vertex_paths = []
|
|
339
204
|
for i, (vid1, vid2) in enumerate(edge_df.index):
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
205
|
+
edge_stylei = rotate_style(edge_style, index=i, key=(vid1, vid2))
|
|
206
|
+
|
|
207
|
+
# FIXME:: Improve this logic. We have three layers of priority:
|
|
208
|
+
# 1. Explicitely set in the style of "plot"
|
|
209
|
+
# 2. Internal through network attributes
|
|
210
|
+
# 3. Default styles
|
|
211
|
+
# Because 1 and 3 are merged as a style context on the way in,
|
|
212
|
+
# it's hard to squeeze 2 in the middle. For now, we will assume
|
|
213
|
+
# the priority order is 2-1-3 instead (internal property is
|
|
214
|
+
# highest priority).
|
|
215
|
+
# This is also why we cannot shift this logic further into the
|
|
216
|
+
# EdgeCollection class, which is oblivious of NetworkArtist's
|
|
217
|
+
# internal data. In fact, one would argue this needs to be
|
|
218
|
+
# pushed outwards to deal with the wrong ordering.
|
|
219
|
+
_update_from_internal(edge_stylei, edge_df.iloc[i], kind="edge")
|
|
220
|
+
|
|
221
|
+
if cmap_fun is not None:
|
|
222
|
+
colorarray.append(edge_stylei["color"])
|
|
223
|
+
edge_stylei["color"] = cmap_fun(edge_stylei["color"])
|
|
347
224
|
|
|
348
225
|
# These are not the actual edges drawn, only stubs to establish
|
|
349
226
|
# the styles which are then fed into the dynamic, optimised
|
|
@@ -353,155 +230,60 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
353
230
|
)
|
|
354
231
|
edgepatches.append(patch)
|
|
355
232
|
adjacent_vertex_ids.append((vid1, vid2))
|
|
356
|
-
adjecent_vertex_centers.append((vcenter1, vcenter2))
|
|
357
|
-
adjecent_vertex_paths.append((vpath1, vpath2))
|
|
358
233
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
234
|
+
if "cmap" in edge_style:
|
|
235
|
+
vmin = np.min(colorarray)
|
|
236
|
+
vmax = np.max(colorarray)
|
|
237
|
+
norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
|
|
238
|
+
edge_style["norm"] = norm
|
|
362
239
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
art = UndirectedEdgeCollection(
|
|
240
|
+
self._edges = EdgeCollection(
|
|
366
241
|
edgepatches,
|
|
367
|
-
labels=labels,
|
|
368
242
|
vertex_ids=adjacent_vertex_ids,
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
transform=self.
|
|
243
|
+
vertex_collection=self._vertices,
|
|
244
|
+
labels=labels,
|
|
245
|
+
transform=self.get_offset_transform(),
|
|
372
246
|
style=edge_style,
|
|
247
|
+
directed=self._ipx_internal_data["directed"],
|
|
373
248
|
)
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
def _process(self):
|
|
377
|
-
self._clear_state()
|
|
378
|
-
|
|
379
|
-
# TODO: some more things might be plotted before this
|
|
380
|
-
|
|
381
|
-
# NOTE: we plot vertices first to get size etc. for edge shortening
|
|
382
|
-
# but when the mpl engine runs down all children artists for actual
|
|
383
|
-
# drawing it uses get_children() to get the order. Whatever is last
|
|
384
|
-
# in that order will get drawn on top (vis-a-vis zorder).
|
|
385
|
-
self._add_vertices()
|
|
386
|
-
self._add_edges()
|
|
387
|
-
if "label" in self._ipx_internal_data["vertex_df"].columns:
|
|
388
|
-
self._add_vertex_labels()
|
|
389
|
-
|
|
390
|
-
# TODO: callbacks for stale vertices/edges
|
|
391
|
-
|
|
392
|
-
# Forward mpl properties to children
|
|
393
|
-
# TODO sort out all of the things that need to be forwarded
|
|
394
|
-
for child in self.get_children():
|
|
395
|
-
# set the figure & axes on child, this ensures each artist
|
|
396
|
-
# down the hierarchy knows where to draw
|
|
397
|
-
if hasattr(child, "set_figure"):
|
|
398
|
-
child.set_figure(self.figure)
|
|
399
|
-
child.axes = self.axes
|
|
400
|
-
|
|
401
|
-
# forward the clippath/box to the children need this logic
|
|
402
|
-
# because mpl exposes some fast-path logic
|
|
403
|
-
clip_path = self.get_clip_path()
|
|
404
|
-
if clip_path is None:
|
|
405
|
-
clip_box = self.get_clip_box()
|
|
406
|
-
child.set_clip_box(clip_box)
|
|
407
|
-
else:
|
|
408
|
-
child.set_clip_path(clip_path)
|
|
249
|
+
if "cmap" in edge_style:
|
|
250
|
+
self._edges.set_array(colorarray)
|
|
409
251
|
|
|
410
252
|
@_stale_wrapper
|
|
411
|
-
def draw(self, renderer
|
|
253
|
+
def draw(self, renderer):
|
|
412
254
|
"""Draw each of the children, with some buffering mechanism."""
|
|
255
|
+
if not self.get_children():
|
|
256
|
+
self._add_vertices()
|
|
257
|
+
self._add_edges()
|
|
258
|
+
|
|
413
259
|
if not self.get_visible():
|
|
414
260
|
return
|
|
415
261
|
|
|
416
|
-
|
|
417
|
-
self._process()
|
|
262
|
+
# FIXME: Callbacks on stale vertices/edges??
|
|
418
263
|
|
|
419
264
|
# NOTE: looks like we have to manage the zorder ourselves
|
|
420
265
|
# this is kind of funny actually
|
|
421
266
|
children = list(self.get_children())
|
|
422
267
|
children.sort(key=lambda x: x.zorder)
|
|
423
268
|
for art in children:
|
|
424
|
-
art.draw(renderer
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
if vertex_labels is not None:
|
|
446
|
-
if len(vertex_labels) != len(vertex_df):
|
|
447
|
-
raise ValueError(
|
|
448
|
-
"Vertex labels must be the same length as the number of vertices."
|
|
449
|
-
)
|
|
450
|
-
vertex_df["label"] = vertex_labels
|
|
451
|
-
|
|
452
|
-
# Edges are a list of tuples, because of multiedges
|
|
453
|
-
tmp = []
|
|
454
|
-
for u, v, d in network.edges.data():
|
|
455
|
-
row = {"_ipx_source": u, "_ipx_target": v}
|
|
456
|
-
row.update(d)
|
|
457
|
-
tmp.append(row)
|
|
458
|
-
edge_df = pd.DataFrame(tmp)
|
|
459
|
-
del tmp
|
|
460
|
-
|
|
461
|
-
# Edge labels
|
|
462
|
-
if edge_labels is not None:
|
|
463
|
-
if len(edge_labels) != len(edge_df):
|
|
464
|
-
raise ValueError(
|
|
465
|
-
"Edge labels must be the same length as the number of edges."
|
|
466
|
-
)
|
|
467
|
-
edge_df["labels"] = edge_labels
|
|
468
|
-
|
|
469
|
-
else:
|
|
470
|
-
# Vertices are ordered integers, no gaps
|
|
471
|
-
vertex_df = normalise_layout(layout)
|
|
472
|
-
ndim = vertex_df.shape[1]
|
|
473
|
-
vertex_df.columns = [f"_ipx_layout_{i}" for i in range(ndim)]
|
|
474
|
-
|
|
475
|
-
# Vertex labels
|
|
476
|
-
if vertex_labels is not None:
|
|
477
|
-
if len(vertex_labels) != len(vertex_df):
|
|
478
|
-
raise ValueError(
|
|
479
|
-
"Vertex labels must be the same length as the number of vertices."
|
|
480
|
-
)
|
|
481
|
-
vertex_df["label"] = vertex_labels
|
|
482
|
-
|
|
483
|
-
# Edges are a list of tuples, because of multiedges
|
|
484
|
-
tmp = []
|
|
485
|
-
for edge in network.es:
|
|
486
|
-
row = {"_ipx_source": edge.source, "_ipx_target": edge.target}
|
|
487
|
-
row.update(edge.attributes())
|
|
488
|
-
tmp.append(row)
|
|
489
|
-
edge_df = pd.DataFrame(tmp)
|
|
490
|
-
del tmp
|
|
491
|
-
|
|
492
|
-
# Edge labels
|
|
493
|
-
if edge_labels is not None:
|
|
494
|
-
if len(edge_labels) != len(edge_df):
|
|
495
|
-
raise ValueError(
|
|
496
|
-
"Edge labels must be the same length as the number of edges."
|
|
497
|
-
)
|
|
498
|
-
edge_df["labels"] = edge_labels
|
|
499
|
-
|
|
500
|
-
internal_data = {
|
|
501
|
-
"vertex_df": vertex_df,
|
|
502
|
-
"edge_df": edge_df,
|
|
503
|
-
"directed": directed,
|
|
504
|
-
"network_library": nl,
|
|
505
|
-
"ndim": ndim,
|
|
506
|
-
}
|
|
507
|
-
return internal_data
|
|
269
|
+
art.draw(renderer)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _update_from_internal(style, row, kind):
|
|
273
|
+
"""Update single vertex/edge style from internal data."""
|
|
274
|
+
if "color" in row:
|
|
275
|
+
style["color"] = row["color"]
|
|
276
|
+
if "facecolor" in row:
|
|
277
|
+
style["facecolor"] = row["facecolor"]
|
|
278
|
+
if "edgecolor" in row:
|
|
279
|
+
if kind == "vertex":
|
|
280
|
+
style["edgecolor"] = row["edgecolor"]
|
|
281
|
+
else:
|
|
282
|
+
style["color"] = row["edgecolor"]
|
|
283
|
+
|
|
284
|
+
if "linewidth" in row:
|
|
285
|
+
style["linewidth"] = row["linewidth"]
|
|
286
|
+
if "linestyle" in row:
|
|
287
|
+
style["linestyle"] = row["linestyle"]
|
|
288
|
+
if "alpha" in row:
|
|
289
|
+
style["alpha"] = row["alpha"]
|