iplotx 0.1.0__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 -56
- 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.1.0.dist-info → iplotx-0.2.0.dist-info}/METADATA +37 -8
- iplotx-0.2.0.dist-info/RECORD +30 -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/RECORD +0 -20
- {iplotx-0.1.0.dist-info → iplotx-0.2.0.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,101 @@ 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)
|
|
129
|
+
return mpl.transforms.Bbox([[0, 0], [1, 1]])
|
|
137
130
|
|
|
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
131
|
if self._vertices is not None:
|
|
146
|
-
|
|
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)))
|
|
132
|
+
bbox = self._vertices.get_datalim(transData)
|
|
153
133
|
|
|
154
134
|
if self._edges is not None:
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
135
|
+
edge_bbox = self._edges.get_datalim(transData)
|
|
136
|
+
bbox = mpl.transforms.Bbox.union([bbox, edge_bbox])
|
|
170
137
|
|
|
171
|
-
|
|
138
|
+
bbox = bbox.expanded(sw=(1.0 + pad), sh=(1.0 + pad))
|
|
139
|
+
return bbox
|
|
172
140
|
|
|
173
|
-
def
|
|
174
|
-
"""
|
|
175
|
-
|
|
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)
|
|
176
146
|
|
|
147
|
+
def get_layout(self):
|
|
177
148
|
layout_columns = [
|
|
178
149
|
f"_ipx_layout_{i}" for i in range(self._ipx_internal_data["ndim"])
|
|
179
150
|
]
|
|
180
151
|
vertex_layout_df = self._ipx_internal_data["vertex_df"][layout_columns]
|
|
181
|
-
|
|
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,
|
|
215
|
-
)
|
|
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
|
|
152
|
+
return vertex_layout_df
|
|
238
153
|
|
|
239
|
-
def
|
|
240
|
-
""
|
|
241
|
-
|
|
242
|
-
labels = self._ipx_internal_data["edge_df"]["labels"]
|
|
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"]
|
|
243
157
|
else:
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if self._ipx_internal_data["directed"]:
|
|
247
|
-
return self._add_directed_edges(labels=labels)
|
|
248
|
-
return self._add_undirected_edges(labels=labels)
|
|
249
|
-
|
|
250
|
-
def _add_directed_edges(self, labels=None):
|
|
251
|
-
"""Draw directed edges."""
|
|
252
|
-
edge_style = get_style(".edge")
|
|
253
|
-
arrow_style = get_style(".arrow")
|
|
254
|
-
|
|
255
|
-
layout_columns = [
|
|
256
|
-
f"_ipx_layout_{i}" for i in range(self._ipx_internal_data["ndim"])
|
|
257
|
-
]
|
|
258
|
-
vertex_layout_df = self._ipx_internal_data["vertex_df"][layout_columns]
|
|
259
|
-
edge_df = self._ipx_internal_data["edge_df"].set_index(
|
|
260
|
-
["_ipx_source", "_ipx_target"]
|
|
261
|
-
)
|
|
158
|
+
return None
|
|
262
159
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
160
|
+
def _add_vertices(self):
|
|
161
|
+
"""Add vertices to the network artist."""
|
|
162
|
+
|
|
163
|
+
self._vertices = VertexCollection(
|
|
164
|
+
layout=self.get_layout(),
|
|
165
|
+
style=get_style(".vertex"),
|
|
166
|
+
labels=self._get_label_series("vertex"),
|
|
167
|
+
transform=self.get_transform(),
|
|
168
|
+
offset_transform=self.get_offset_transform(),
|
|
267
169
|
)
|
|
268
170
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
vpath2 = vertex_paths[vertex_indices[vid2]]
|
|
280
|
-
|
|
281
|
-
edge_stylei = rotate_style(edge_style, index=i, id=(vid1, vid2))
|
|
171
|
+
def _add_edges(self):
|
|
172
|
+
"""Add edges to the network artist.
|
|
173
|
+
|
|
174
|
+
NOTE: UndirectedEdgeCollection and ArrowCollection are both subclasses of
|
|
175
|
+
PatchCollection. When used with a cmap/norm, they set their facecolor
|
|
176
|
+
according to the cmap, even though most likely we only want the edgecolor
|
|
177
|
+
set that way. It can make for funny looking plots that are not uninteresting
|
|
178
|
+
but mostly niche at this stage. Therefore we sidestep the whole cmap thing
|
|
179
|
+
here.
|
|
180
|
+
"""
|
|
282
181
|
|
|
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))
|
|
182
|
+
labels = self._get_label_series("edge")
|
|
183
|
+
edge_style = get_style(".edge")
|
|
293
184
|
|
|
294
|
-
|
|
295
|
-
|
|
185
|
+
if "cmap" in edge_style:
|
|
186
|
+
cmap_fun = _build_cmap_fun(
|
|
187
|
+
edge_style["color"],
|
|
188
|
+
edge_style["cmap"],
|
|
296
189
|
)
|
|
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")
|
|
190
|
+
else:
|
|
191
|
+
cmap_fun = None
|
|
320
192
|
|
|
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
193
|
edge_df = self._ipx_internal_data["edge_df"].set_index(
|
|
326
194
|
["_ipx_source", "_ipx_target"]
|
|
327
195
|
)
|
|
328
196
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
vertex_indices = pd.Series(
|
|
332
|
-
np.arange(len(vertex_layout_df)), index=vertex_layout_df.index
|
|
333
|
-
)
|
|
334
|
-
|
|
197
|
+
if "cmap" in edge_style:
|
|
198
|
+
colorarray = []
|
|
335
199
|
edgepatches = []
|
|
336
200
|
adjacent_vertex_ids = []
|
|
337
|
-
adjecent_vertex_centers = []
|
|
338
|
-
adjecent_vertex_paths = []
|
|
339
201
|
for i, (vid1, vid2) in enumerate(edge_df.index):
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
202
|
+
edge_stylei = rotate_style(edge_style, index=i, key=(vid1, vid2))
|
|
203
|
+
|
|
204
|
+
# FIXME:: Improve this logic. We have three layers of priority:
|
|
205
|
+
# 1. Explicitely set in the style of "plot"
|
|
206
|
+
# 2. Internal through network attributes
|
|
207
|
+
# 3. Default styles
|
|
208
|
+
# Because 1 and 3 are merged as a style context on the way in,
|
|
209
|
+
# it's hard to squeeze 2 in the middle. For now, we will assume
|
|
210
|
+
# the priority order is 2-1-3 instead (internal property is
|
|
211
|
+
# highest priority).
|
|
212
|
+
# This is also why we cannot shift this logic further into the
|
|
213
|
+
# EdgeCollection class, which is oblivious of NetworkArtist's
|
|
214
|
+
# internal data. In fact, one would argue this needs to be
|
|
215
|
+
# pushed outwards to deal with the wrong ordering.
|
|
216
|
+
_update_from_internal(edge_stylei, edge_df.iloc[i], kind="edge")
|
|
217
|
+
|
|
218
|
+
if cmap_fun is not None:
|
|
219
|
+
colorarray.append(edge_stylei["color"])
|
|
220
|
+
edge_stylei["color"] = cmap_fun(edge_stylei["color"])
|
|
347
221
|
|
|
348
222
|
# These are not the actual edges drawn, only stubs to establish
|
|
349
223
|
# the styles which are then fed into the dynamic, optimised
|
|
@@ -353,155 +227,62 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
353
227
|
)
|
|
354
228
|
edgepatches.append(patch)
|
|
355
229
|
adjacent_vertex_ids.append((vid1, vid2))
|
|
356
|
-
adjecent_vertex_centers.append((vcenter1, vcenter2))
|
|
357
|
-
adjecent_vertex_paths.append((vpath1, vpath2))
|
|
358
|
-
|
|
359
|
-
adjacent_vertex_ids = np.array(adjacent_vertex_ids)
|
|
360
|
-
adjecent_vertex_centers = np.array(adjecent_vertex_centers)
|
|
361
|
-
# NOTE: the paths might have different number of sides, so it cannot be recast
|
|
362
230
|
|
|
363
|
-
|
|
231
|
+
if "cmap" in edge_style:
|
|
232
|
+
vmin = np.min(colorarray)
|
|
233
|
+
vmax = np.max(colorarray)
|
|
234
|
+
norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
|
|
235
|
+
edge_style["norm"] = norm
|
|
364
236
|
|
|
365
|
-
|
|
237
|
+
self._edges = EdgeCollection(
|
|
366
238
|
edgepatches,
|
|
367
|
-
labels=labels,
|
|
368
239
|
vertex_ids=adjacent_vertex_ids,
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
240
|
+
vertex_collection=self._vertices,
|
|
241
|
+
layout=self.get_layout(),
|
|
242
|
+
layout_coordinate_system="cartesian",
|
|
243
|
+
labels=labels,
|
|
244
|
+
transform=self.get_offset_transform(),
|
|
372
245
|
style=edge_style,
|
|
246
|
+
directed=self._ipx_internal_data["directed"],
|
|
373
247
|
)
|
|
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)
|
|
248
|
+
if "cmap" in edge_style:
|
|
249
|
+
self._edges.set_array(colorarray)
|
|
409
250
|
|
|
410
251
|
@_stale_wrapper
|
|
411
|
-
def draw(self, renderer
|
|
252
|
+
def draw(self, renderer):
|
|
412
253
|
"""Draw each of the children, with some buffering mechanism."""
|
|
254
|
+
if not self.get_children():
|
|
255
|
+
self._add_vertices()
|
|
256
|
+
self._add_edges()
|
|
257
|
+
|
|
413
258
|
if not self.get_visible():
|
|
414
259
|
return
|
|
415
260
|
|
|
416
|
-
|
|
417
|
-
self._process()
|
|
261
|
+
# FIXME: Callbacks on stale vertices/edges??
|
|
418
262
|
|
|
419
263
|
# NOTE: looks like we have to manage the zorder ourselves
|
|
420
264
|
# this is kind of funny actually
|
|
421
265
|
children = list(self.get_children())
|
|
422
266
|
children.sort(key=lambda x: x.zorder)
|
|
423
267
|
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
|
|
268
|
+
art.draw(renderer)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _update_from_internal(style, row, kind):
|
|
272
|
+
"""Update single vertex/edge style from internal data."""
|
|
273
|
+
if "color" in row:
|
|
274
|
+
style["color"] = row["color"]
|
|
275
|
+
if "facecolor" in row:
|
|
276
|
+
style["facecolor"] = row["facecolor"]
|
|
277
|
+
if "edgecolor" in row:
|
|
278
|
+
if kind == "vertex":
|
|
279
|
+
style["edgecolor"] = row["edgecolor"]
|
|
280
|
+
else:
|
|
281
|
+
style["color"] = row["edgecolor"]
|
|
282
|
+
|
|
283
|
+
if "linewidth" in row:
|
|
284
|
+
style["linewidth"] = row["linewidth"]
|
|
285
|
+
if "linestyle" in row:
|
|
286
|
+
style["linestyle"] = row["linestyle"]
|
|
287
|
+
if "alpha" in row:
|
|
288
|
+
style["alpha"] = row["alpha"]
|