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/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,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
- # 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)
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
- def _add_vertices(self):
174
- """Draw the vertices"""
175
- vertex_style = get_style(".vertex")
176
-
177
- layout_columns = [
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
- 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"]
243
- else:
244
- labels = None
138
+ bbox = bbox.expanded(sw=(1.0 + pad), sh=(1.0 + pad))
139
+ return bbox
245
140
 
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")
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
- edge_df = self._ipx_internal_data["edge_df"].set_index(
260
- ["_ipx_source", "_ipx_target"]
261
- )
152
+ return vertex_layout_df
262
153
 
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
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
- 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]]
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
- edge_stylei = rotate_style(edge_style, index=i, id=(vid1, vid2))
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
- # 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))
185
+ labels = self._get_label_series("edge")
186
+ edge_style = get_style(".edge")
293
187
 
294
- arrow_patch = make_arrow_patch(
295
- **arrow_style,
188
+ if "cmap" in edge_style:
189
+ cmap_fun = _build_cmap_fun(
190
+ edge_style["color"],
191
+ edge_style["cmap"],
296
192
  )
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")
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
- # 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
-
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
- # 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))
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
- 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
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
- # TODO:: deal with "ports" a la graphviz
364
-
365
- art = UndirectedEdgeCollection(
240
+ self._edges = EdgeCollection(
366
241
  edgepatches,
367
- labels=labels,
368
242
  vertex_ids=adjacent_vertex_ids,
369
- vertex_paths=adjecent_vertex_paths,
370
- vertex_centers=adjecent_vertex_centers,
371
- transform=self.axes.transData,
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
- 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)
249
+ if "cmap" in edge_style:
250
+ self._edges.set_array(colorarray)
409
251
 
410
252
  @_stale_wrapper
411
- def draw(self, renderer, *args, **kwds):
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
- if not self.get_children():
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, *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
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"]