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/network.py CHANGED
@@ -1,50 +1,39 @@
1
- from typing import Union, Sequence
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 .styles import (
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
- _get_label_width_height,
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.undirected import (
31
- UndirectedEdgeCollection,
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: Union[None, list, dict, pd.Series] = None,
58
- edge_labels: Union[None, Sequence] = None,
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 (networkx.Graph or igraph.Graph): The network to plot.
64
- layout (array-like): The layout of the network. If None, this function will attempt to
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 (list, dict, or pandas.Series): The labels for the vertices. If None, no 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
- elge_labels (sequence): The labels for the edges. If None, no edge labels will be drawn.
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 = _create_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
- def _clear_state(self):
84
- self._vertices = None
85
- self._edges = None
86
- self._vertex_labels = []
87
- self._edge_labels = []
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
- artists = []
91
- # Collect edges first. This way vertices are on top of edges,
92
- # since vertices are drawn later. That is what most people expect.
93
- if self._edges is not None:
94
- artists.append(self._edges)
95
- if self._vertices is not None:
96
- artists.append(self._vertices)
97
- artists.extend(self._edge_labels)
98
- artists.extend(self._vertex_labels)
99
- return tuple(artists)
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._vertex_labels
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._edge_labels
116
+ return self._edges.get_labels()
116
117
 
117
- def get_datalim(self, transData, pad=0.05):
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
- # FIXME: transData works here, but it's probably kind of broken in general
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
- mins = np.array([0, 0])
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
- 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)))
132
+ bbox = self._vertices.get_datalim(transData)
153
133
 
154
134
  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
135
+ edge_bbox = self._edges.get_datalim(transData)
136
+ bbox = mpl.transforms.Bbox.union([bbox, edge_bbox])
170
137
 
171
- return mpl.transforms.Bbox([mins, maxs])
138
+ bbox = bbox.expanded(sw=(1.0 + pad), sh=(1.0 + pad))
139
+ return bbox
172
140
 
173
- def _add_vertices(self):
174
- """Draw the vertices"""
175
- vertex_style = get_style(".vertex")
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
- 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,
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 _add_edges(self):
240
- """Draw the edges."""
241
- if "labels" in self._ipx_internal_data["edge_df"].columns:
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
- labels = None
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
- # This contains the patches for vertices, for edge shortening and such
264
- vertex_paths = self._vertices._paths
265
- vertex_indices = pd.Series(
266
- np.arange(len(vertex_layout_df)), index=vertex_layout_df.index
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
- edgepatches = []
270
- arrowpatches = []
271
- adjacent_vertex_ids = []
272
- adjecent_vertex_centers = []
273
- adjecent_vertex_paths = []
274
- for i, (vid1, vid2) in enumerate(edge_df.index):
275
- # Get the vertices for this edge
276
- vcenter1 = vertex_layout_df.loc[vid1, layout_columns].values
277
- vcenter2 = vertex_layout_df.loc[vid2, layout_columns].values
278
- vpath1 = vertex_paths[vertex_indices[vid1]]
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
- # These are not the actual edges drawn, only stubs to establish
284
- # the styles which are then fed into the dynamic, optimised
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
- arrow_patch = make_arrow_patch(
295
- **arrow_style,
185
+ if "cmap" in edge_style:
186
+ cmap_fun = _build_cmap_fun(
187
+ edge_style["color"],
188
+ edge_style["cmap"],
296
189
  )
297
- arrowpatches.append(arrow_patch)
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
- # This contains the patches for vertices, for edge shortening and such
330
- vertex_paths = self._vertices._paths
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
- # Get the vertices for this edge
341
- vcenter1 = vertex_layout_df.loc[vid1, layout_columns].values
342
- vcenter2 = vertex_layout_df.loc[vid2, layout_columns].values
343
- vpath1 = vertex_paths[vertex_indices[vid1]]
344
- vpath2 = vertex_paths[vertex_indices[vid2]]
345
-
346
- edge_stylei = rotate_style(edge_style, index=i, id=(vid1, vid2))
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
- # TODO:: deal with "ports" a la graphviz
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
- art = UndirectedEdgeCollection(
237
+ self._edges = EdgeCollection(
366
238
  edgepatches,
367
- labels=labels,
368
239
  vertex_ids=adjacent_vertex_ids,
369
- vertex_paths=adjecent_vertex_paths,
370
- vertex_centers=adjecent_vertex_centers,
371
- transform=self.axes.transData,
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
- self._edges = art
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, *args, **kwds):
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
- if not self.get_children():
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, *args, **kwds)
425
-
426
-
427
- # INTERNAL ROUTINES
428
- def _create_internal_data(
429
- network,
430
- layout=None,
431
- vertex_labels=None,
432
- edge_labels=None,
433
- ):
434
- """Create internal data for the network."""
435
- nl = network_library(network)
436
- directed = detect_directedness(network)
437
-
438
- if nl == "networkx":
439
- # Vertices are indexed by node ID
440
- vertex_df = normalise_layout(layout).loc[pd.Index(network.nodes)]
441
- ndim = vertex_df.shape[1]
442
- vertex_df.columns = [f"_ipx_layout_{i}" for i in range(ndim)]
443
-
444
- # Vertex labels
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"]