iplotx 0.3.0__py3-none-any.whl → 0.4.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 CHANGED
@@ -11,6 +11,9 @@ from .plotting import (
11
11
  network,
12
12
  tree,
13
13
  )
14
+ import iplotx.artists as artists
15
+ import iplotx.style as style
16
+
14
17
 
15
18
  # Shortcut to iplotx.plotting.network
16
19
  plot = network
@@ -19,5 +22,7 @@ __all__ = [
19
22
  "network",
20
23
  "tree",
21
24
  "plot",
25
+ "artists",
26
+ "style",
22
27
  "__version__",
23
28
  ]
iplotx/artists.py ADDED
@@ -0,0 +1,24 @@
1
+ """
2
+ All artists defined in iplotx.
3
+ """
4
+
5
+ from .network import NetworkArtist
6
+ from .tree import TreeArtist
7
+ from .vertex import VertexCollection
8
+ from .edge import EdgeCollection
9
+ from .label import LabelCollection
10
+ from .edge.arrow import EdgeArrowCollection
11
+ from .edge.leaf import LeafEdgeCollection
12
+ from .cascades import CascadeCollection
13
+
14
+
15
+ ___all__ = (
16
+ NetworkArtist,
17
+ TreeArtist,
18
+ VertexCollection,
19
+ EdgeCollection,
20
+ LeafEdgeCollection,
21
+ LabelCollection,
22
+ EdgeArrowCollection,
23
+ CascadeCollection,
24
+ )
iplotx/cascades.py CHANGED
@@ -208,15 +208,15 @@ class CascadeCollection(mpl.collections.PatchCollection):
208
208
  if (layout_name, orientation) == ("horizontal", "right"):
209
209
  for path in self.get_paths():
210
210
  path.vertices[[1, 2], 0] = self.get_maxdepth()
211
- elif (layout_name, orientation) == ("horizontal", "right"):
211
+ elif (layout_name, orientation) == ("horizontal", "left"):
212
212
  for path in self.get_paths():
213
213
  path.vertices[[0, 3], 0] = self.get_maxdepth()
214
214
  elif (layout_name, orientation) == ("vertical", "descending"):
215
215
  for path in self.get_paths():
216
- path.vertices[[1, 2], 1] = self.get_maxdepth()
216
+ path.vertices[[0, 1], 1] = self.get_maxdepth()
217
217
  elif (layout_name, orientation) == ("vertical", "ascending"):
218
218
  for path in self.get_paths():
219
- path.vertices[[0, 3], 1] = self.get_maxdepth()
219
+ path.vertices[[2, 3], 1] = self.get_maxdepth()
220
220
  else:
221
221
  raise ValueError(
222
222
  f"Layout name and orientation not supported: {layout_name}, {orientation}."
iplotx/edge/__init__.py CHANGED
@@ -262,11 +262,13 @@ class EdgeCollection(mpl.collections.PatchCollection):
262
262
  trans_inv = transform.inverted().transform
263
263
 
264
264
  # 1. Make a list of vertices with loops, and store them for later
265
- loop_vertex_dict = defaultdict(lambda: dict(indices=[], edge_angles=[]))
266
- for i, (v1, v2) in enumerate(vids):
267
- # Postpone loops (step 3)
268
- if v1 == v2:
269
- loop_vertex_dict[v1]["indices"].append(i)
265
+ # NOTE: vinfo["loops"] can be False when we want no loops (e.g. leaf edges)
266
+ if vinfo.get("loops", True):
267
+ loop_vertex_dict = defaultdict(lambda: dict(indices=[], edge_angles=[]))
268
+ for i, (v1, v2) in enumerate(vids):
269
+ # Postpone loops (step 3)
270
+ if v1 == v2:
271
+ loop_vertex_dict[v1]["indices"].append(i)
270
272
 
271
273
  # 2. Make paths for non-loop edges
272
274
  # NOTE: keep track of parallel edges to offset them
@@ -274,7 +276,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
274
276
  paths = []
275
277
  for i, (v1, v2) in enumerate(vids):
276
278
  # Postpone loops (step 3)
277
- if v1 == v2:
279
+ if vinfo.get("loops", True) and (v1 == v2):
278
280
  paths.append(None)
279
281
  continue
280
282
 
@@ -330,10 +332,11 @@ class EdgeCollection(mpl.collections.PatchCollection):
330
332
  path.vertices[:] = trans_inv(trans(path.vertices) + offset)
331
333
 
332
334
  # Collect angles for this vertex, to be used for loops plotting below
333
- if v1 in loop_vertex_dict:
334
- loop_vertex_dict[v1]["edge_angles"].append(angles[0])
335
- if v2 in loop_vertex_dict:
336
- loop_vertex_dict[v2]["edge_angles"].append(angles[1])
335
+ if vinfo.get("loops", True):
336
+ if v1 in loop_vertex_dict:
337
+ loop_vertex_dict[v1]["edge_angles"].append(angles[0])
338
+ if v2 in loop_vertex_dict:
339
+ loop_vertex_dict[v2]["edge_angles"].append(angles[1])
337
340
 
338
341
  # Add the path for this non-loop edge
339
342
  paths.append(path)
@@ -360,39 +363,40 @@ class EdgeCollection(mpl.collections.PatchCollection):
360
363
  )
361
364
 
362
365
  # 3. Deal with loops at the end
363
- for vid, ldict in loop_vertex_dict.items():
364
- vpath = vpaths[ldict["indices"][0]][0]
365
- vsize = vsizes[ldict["indices"][0]][0]
366
- vcoord_fig = trans(vcenters[ldict["indices"][0]][0])
367
- nloops = len(ldict["indices"])
368
- edge_angles = ldict["edge_angles"]
369
-
370
- # The space between the existing angles is where we can fit the loops
371
- # One loop we can fit in the largest wedge, multiple loops we need
372
- nloops_per_angle = _compute_loops_per_angle(nloops, edge_angles)
373
-
374
- idx = 0
375
- for theta1, theta2, nloops in nloops_per_angle:
376
- # Angular size of each loop in this wedge
377
- delta = (theta2 - theta1) / nloops
378
-
379
- # Iterate over individual loops
380
- for j in range(nloops):
381
- thetaj1 = theta1 + j * delta + max(delta - loopmaxangle, 0) / 2
382
- thetaj2 = thetaj1 + min(delta, loopmaxangle)
383
-
384
- # Get the path for this loop
385
- path = _compute_loop_path(
386
- vcoord_fig,
387
- vpath,
388
- vsize,
389
- thetaj1,
390
- thetaj2,
391
- trans_inv,
392
- looptension=self._style.get("looptension", 2.5),
393
- )
394
- paths[ldict["indices"][idx]] = path
395
- idx += 1
366
+ if vinfo.get("loops", True):
367
+ for vid, ldict in loop_vertex_dict.items():
368
+ vpath = vpaths[ldict["indices"][0]][0]
369
+ vsize = vsizes[ldict["indices"][0]][0]
370
+ vcoord_fig = trans(vcenters[ldict["indices"][0]][0])
371
+ nloops = len(ldict["indices"])
372
+ edge_angles = ldict["edge_angles"]
373
+
374
+ # The space between the existing angles is where we can fit the loops
375
+ # One loop we can fit in the largest wedge, multiple loops we need
376
+ nloops_per_angle = _compute_loops_per_angle(nloops, edge_angles)
377
+
378
+ idx = 0
379
+ for theta1, theta2, nloops in nloops_per_angle:
380
+ # Angular size of each loop in this wedge
381
+ delta = (theta2 - theta1) / nloops
382
+
383
+ # Iterate over individual loops
384
+ for j in range(nloops):
385
+ thetaj1 = theta1 + j * delta + max(delta - loopmaxangle, 0) / 2
386
+ thetaj2 = thetaj1 + min(delta, loopmaxangle)
387
+
388
+ # Get the path for this loop
389
+ path = _compute_loop_path(
390
+ vcoord_fig,
391
+ vpath,
392
+ vsize,
393
+ thetaj1,
394
+ thetaj2,
395
+ trans_inv,
396
+ looptension=self._style.get("looptension", 2.5),
397
+ )
398
+ paths[ldict["indices"][idx]] = path
399
+ idx += 1
396
400
 
397
401
  self._paths = paths
398
402
 
@@ -629,6 +633,7 @@ def make_stub_patch(**kwargs):
629
633
  "offset",
630
634
  "paralleloffset",
631
635
  "cmap",
636
+ "norm",
632
637
  ]
633
638
  for prop in forbidden_props:
634
639
  if prop in kwargs:
iplotx/edge/arrow.py CHANGED
@@ -145,6 +145,8 @@ class EdgeArrowCollection(mpl.collections.PatchCollection):
145
145
  def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
146
146
  """Make a patch of the given marker shape and size."""
147
147
  height = kwargs.pop("height", width * 1.3)
148
+ if height == "width":
149
+ height = width
148
150
 
149
151
  # Normalise by the max size, this is taken care of in _transforms
150
152
  # subsequently in a way that is nice to dpi scaling
@@ -169,6 +171,38 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
169
171
  codes=[getattr(mpl.path.Path, x) for x in codes],
170
172
  closed=True,
171
173
  )
174
+ elif marker == "|\\":
175
+ codes = ["MOVETO", "LINETO", "LINETO", "CLOSEPOLY"]
176
+ if "color" in kwargs:
177
+ kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
178
+ path = mpl.path.Path(
179
+ np.array(
180
+ [
181
+ [-height, width * 0.5],
182
+ [-height, 0],
183
+ [0, 0],
184
+ [-height, width * 0.5],
185
+ ]
186
+ ),
187
+ codes=[getattr(mpl.path.Path, x) for x in codes],
188
+ closed=True,
189
+ )
190
+ elif marker == "|/":
191
+ codes = ["MOVETO", "LINETO", "LINETO", "CLOSEPOLY"]
192
+ if "color" in kwargs:
193
+ kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
194
+ path = mpl.path.Path(
195
+ np.array(
196
+ [
197
+ [-height, 0],
198
+ [-height, -width * 0.5],
199
+ [0, 0],
200
+ [-height, 0],
201
+ ]
202
+ ),
203
+ codes=[getattr(mpl.path.Path, x) for x in codes],
204
+ closed=True,
205
+ )
172
206
  elif marker == ">":
173
207
  kwargs["facecolor"] = "none"
174
208
  if "color" in kwargs:
iplotx/edge/geometry.py CHANGED
@@ -171,11 +171,28 @@ def _compute_edge_path_straight(
171
171
  vsize_fig,
172
172
  trans,
173
173
  trans_inv,
174
+ layout_coordinate_system: str = "cartesian",
174
175
  **kwargs,
175
176
  ):
177
+ if layout_coordinate_system not in ("cartesian", "polar"):
178
+ raise ValueError(
179
+ f"Layout coordinate system not supported for straight edges: {layout_coordinate_system}.",
180
+ )
181
+
182
+ if layout_coordinate_system == "polar":
183
+ r0, theta0 = vcoord_data[0]
184
+ r1, theta1 = vcoord_data[1]
185
+ vcoord_data_cart = np.array(
186
+ [
187
+ [r0 * np.cos(theta0), r0 * np.sin(theta0)],
188
+ [r1 * np.cos(theta1), r1 * np.sin(theta1)],
189
+ ]
190
+ )
191
+ else:
192
+ vcoord_data_cart = vcoord_data
176
193
 
177
194
  # Coordinates in figure (default) coords
178
- vcoord_fig = trans(vcoord_data)
195
+ vcoord_fig = trans(vcoord_data_cart)
179
196
 
180
197
  points = []
181
198
 
@@ -305,7 +322,6 @@ def _compute_edge_path_waypoints(
305
322
  idx_outer = 1 - idx_inner
306
323
  alpha_outer = [alpha0, alpha1][idx_outer]
307
324
 
308
- # FIXME: this is aware of chirality as stored by the layout function
309
325
  betas = np.linspace(alpha0, alpha1, points_per_curve)
310
326
  waypoints = [r0, r1][idx_inner] * np.vstack([np.cos(betas), np.sin(betas)]).T
311
327
  endpoint = [r0, r1][idx_outer] * np.array(
@@ -314,7 +330,6 @@ def _compute_edge_path_waypoints(
314
330
  points = np.array(list(waypoints) + [endpoint])
315
331
  points = trans(points)
316
332
  codes = ["MOVETO"] + ["LINETO"] * len(waypoints)
317
- # FIXME: same as previus comment
318
333
  angles = (alpha0 + pi / 2, alpha1)
319
334
 
320
335
  else:
@@ -438,7 +453,11 @@ def _compute_edge_path(
438
453
  )
439
454
 
440
455
  if tension == 0:
441
- return _compute_edge_path_straight(*args, **kwargs)
456
+ return _compute_edge_path_straight(
457
+ *args,
458
+ layout_coordinate_system=layout_coordinate_system,
459
+ **kwargs,
460
+ )
442
461
 
443
462
  return _compute_edge_path_curved(
444
463
  tension,
iplotx/edge/leaf.py ADDED
@@ -0,0 +1,117 @@
1
+ """
2
+ Module containing leaf edges, i.e. special edges of tree visualisations
3
+ that connect leaf vertices to the deepest leaf (typically for labeling).
4
+ """
5
+
6
+ from typing import (
7
+ Sequence,
8
+ Optional,
9
+ Any,
10
+ )
11
+ import numpy as np
12
+ import pandas as pd
13
+ import matplotlib as mpl
14
+
15
+ from ..utils.matplotlib import (
16
+ _forwarder,
17
+ )
18
+ from ..vertex import VertexCollection
19
+ from iplotx.edge import EdgeCollection
20
+
21
+
22
+ @_forwarder(
23
+ (
24
+ "set_clip_path",
25
+ "set_clip_box",
26
+ "set_snap",
27
+ "set_sketch_params",
28
+ "set_animated",
29
+ "set_picker",
30
+ )
31
+ )
32
+ class LeafEdgeCollection(EdgeCollection):
33
+ """Artist for leaf edges in tree visualisations."""
34
+
35
+ def __init__(
36
+ self,
37
+ patches: Sequence[mpl.patches.Patch],
38
+ vertex_leaf_ids: Sequence[tuple],
39
+ vertex_collection: VertexCollection,
40
+ leaf_collection: VertexCollection,
41
+ *args,
42
+ transform: mpl.transforms.Transform = mpl.transforms.IdentityTransform(),
43
+ arrow_transform: mpl.transforms.Transform = mpl.transforms.IdentityTransform(),
44
+ directed: bool = False,
45
+ style: Optional[dict[str, Any]] = None,
46
+ **kwargs,
47
+ ) -> None:
48
+ """Initialise a LeafEdgeCollection.
49
+
50
+ Parameters:
51
+ patches: A sequence (usually, list) of matplotlib `Patch`es describing the edges.
52
+ vertex_ids: A sequence of pairs `(v1, v2)`, each defining the ids of vertices at the
53
+ end of an edge.
54
+ vertex_collection: The VertexCollection instance containing the Artist for the
55
+ vertices. This is needed to compute vertex borders and adjust edges accordingly.
56
+ transform: The matplotlib transform for the edges, usually transData.
57
+ arrow_transform: The matplotlib transform for the arrow patches. This is not the
58
+ *offset_transform* of arrows, which is set equal to the edge transform (previous
59
+ parameter). Instead, it specifies how arrow size scales, similar to vertex size.
60
+ This is usually the identity transform.
61
+ directed: Whether the graph is directed (in which case arrows are drawn, possibly
62
+ with zero size or opacity to obtain an "arrowless" effect).
63
+ style: The edge style (subdictionary: "edge") to use at creation.
64
+ """
65
+ self._leaf_collection = leaf_collection
66
+ super().__init__(
67
+ patches=patches,
68
+ vertex_ids=vertex_leaf_ids,
69
+ vertex_collection=vertex_collection,
70
+ *args,
71
+ transform=transform,
72
+ arrow_transform=arrow_transform,
73
+ directed=directed,
74
+ style=style,
75
+ **kwargs,
76
+ )
77
+
78
+ def _get_adjacent_vertices_info(self):
79
+ lindex = self._leaf_collection.get_index()
80
+ lindex = pd.Series(
81
+ np.arange(len(lindex)),
82
+ index=lindex,
83
+ )
84
+ vindex = self._vertex_collection.get_index()
85
+ vindex = pd.Series(
86
+ np.arange(len(vindex)),
87
+ index=vindex,
88
+ ).loc[lindex.index]
89
+
90
+ voffsets = []
91
+ vpaths = []
92
+ vsizes = []
93
+ for vid in self._vertex_ids:
94
+ # NOTE: these are in the original layout coordinate system
95
+ # not cartesianised yet.
96
+ offset1 = self._vertex_collection.get_layout().values[vindex[vid]]
97
+ offset2 = self._leaf_collection.get_layout().values[lindex[vid]]
98
+ voffsets.append((offset1, offset2))
99
+
100
+ path1 = self._vertex_collection.get_paths()[vindex[vid]]
101
+ path2 = self._leaf_collection.get_paths()[lindex[vid]]
102
+ vpaths.append((path1, path2))
103
+
104
+ # NOTE: This needs to be computed here because the
105
+ # VertexCollection._transforms are reset each draw in order to
106
+ # accomodate for DPI changes on the canvas
107
+ size1 = self._vertex_collection.get_sizes_dpi()[vindex[vid]]
108
+ size2 = self._leaf_collection.get_sizes_dpi()[lindex[vid]]
109
+ vsizes.append((size1, size2))
110
+
111
+ return {
112
+ "ids": [(vid, vid) for vid in self._vertex_ids],
113
+ "offsets": voffsets,
114
+ "paths": vpaths,
115
+ "sizes": vsizes,
116
+ "loops": False,
117
+ }
iplotx/ingest/__init__.py CHANGED
@@ -95,8 +95,7 @@ def ingest_network_data(
95
95
  f"Currently installed supported libraries: {sup}."
96
96
  )
97
97
 
98
- result = provider()(
99
- network=network,
98
+ result = provider(network)(
100
99
  layout=layout,
101
100
  vertex_labels=vertex_labels,
102
101
  edge_labels=edge_labels,
@@ -108,7 +107,6 @@ def ingest_network_data(
108
107
  def ingest_tree_data(
109
108
  tree: TreeType,
110
109
  layout: Optional[str] = "horizontal",
111
- orientation: Optional[str] = None,
112
110
  directed: bool | str = False,
113
111
  layout_style: Optional[dict[str, str | int | float]] = None,
114
112
  vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
@@ -133,7 +131,6 @@ def ingest_tree_data(
133
131
  tree=tree,
134
132
  )(
135
133
  layout=layout,
136
- orientation=orientation,
137
134
  directed=directed,
138
135
  layout_style=layout_style,
139
136
  vertex_labels=vertex_labels,
@@ -142,8 +139,6 @@ def ingest_tree_data(
142
139
  )
143
140
  result["tree_library"] = tl
144
141
 
145
- # TODO: cascading thing here
146
-
147
142
  return result
148
143
 
149
144
 
@@ -13,42 +13,13 @@ import pandas as pd
13
13
 
14
14
  from ..layout import compute_tree_layout
15
15
  from ..typing import (
16
- GraphType,
17
16
  GroupingType,
18
17
  LayoutType,
19
18
  )
20
19
 
21
20
 
22
- def number_of_vertices(network: GraphType) -> int:
23
- """Get the number of vertices in the network."""
24
- from . import network_library
25
-
26
- if network_library(network) == "igraph":
27
- return network.vcount()
28
- if network_library(network) == "networkx":
29
- return network.number_of_nodes()
30
- raise TypeError("Unsupported graph type. Supported types are igraph and networkx.")
31
-
32
-
33
- def detect_directedness(
34
- network: GraphType,
35
- ) -> bool:
36
- """Detect if the network is directed or not."""
37
- from . import network_library
38
-
39
- nl = network_library(network)
40
-
41
- if nl == "igraph":
42
- return network.is_directed()
43
- if nl == "networkx":
44
- import networkx as nx
45
-
46
- if isinstance(network, (nx.DiGraph, nx.MultiDiGraph)):
47
- return True
48
- return False
49
-
50
-
51
- def normalise_layout(layout, network=None):
21
+ # TODO: some of this logic should be moved into individual providers
22
+ def normalise_layout(layout, network=None, nvertices=None):
52
23
  """Normalise the layout to a pandas.DataFrame."""
53
24
  from . import network_library
54
25
 
@@ -58,7 +29,7 @@ def normalise_layout(layout, network=None):
58
29
  ig = None
59
30
 
60
31
  if layout is None:
61
- if (network is not None) and (number_of_vertices(network) == 0):
32
+ if (network is not None) and (nvertices == 0):
62
33
  return pd.DataFrame(np.zeros((0, 2)))
63
34
  return None
64
35
  if (network is not None) and isinstance(layout, str):
@@ -12,7 +12,6 @@ from ....typing import (
12
12
  )
13
13
  from ...heuristics import (
14
14
  normalise_layout,
15
- detect_directedness,
16
15
  )
17
16
  from ...typing import (
18
17
  NetworkDataProvider,
@@ -26,21 +25,24 @@ from ....utils.internal import (
26
25
  class IGraphDataProvider(NetworkDataProvider):
27
26
  def __call__(
28
27
  self,
29
- network: GraphType,
30
28
  layout: Optional[LayoutType] = None,
31
29
  vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
32
30
  edge_labels: Optional[Sequence[str] | dict[str]] = None,
33
31
  ) -> NetworkData:
34
- """Create network data object for iplotx from any provider."""
35
-
36
- directed = detect_directedness(network)
32
+ """Create network data object for iplotx from an igraph object."""
33
+ network = self.network
34
+ directed = self.is_directed()
37
35
 
38
36
  # Recast vertex_labels=False as vertex_labels=None
39
37
  if np.isscalar(vertex_labels) and (not vertex_labels):
40
38
  vertex_labels = None
41
39
 
42
40
  # Vertices are ordered integers, no gaps
43
- vertex_df = normalise_layout(layout, network=network)
41
+ vertex_df = normalise_layout(
42
+ layout,
43
+ network=network,
44
+ nvertices=self.number_of_vertices(),
45
+ )
44
46
  ndim = vertex_df.shape[1]
45
47
  vertex_df.columns = _make_layout_columns(ndim)
46
48
 
@@ -96,3 +98,11 @@ class IGraphDataProvider(NetworkDataProvider):
96
98
  import igraph as ig
97
99
 
98
100
  return ig.Graph
101
+
102
+ def is_directed(self):
103
+ """Whether the network is directed."""
104
+ return self.network.is_directed()
105
+
106
+ def number_of_vertices(self):
107
+ """The number of vertices/nodes in the network."""
108
+ return self.network.vcount()
@@ -12,7 +12,6 @@ from ....typing import (
12
12
  )
13
13
  from ...heuristics import (
14
14
  normalise_layout,
15
- detect_directedness,
16
15
  )
17
16
  from ...typing import (
18
17
  NetworkDataProvider,
@@ -26,16 +25,17 @@ from ....utils.internal import (
26
25
  class NetworkXDataProvider(NetworkDataProvider):
27
26
  def __call__(
28
27
  self,
29
- network: GraphType,
30
28
  layout: Optional[LayoutType] = None,
31
29
  vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
32
30
  edge_labels: Optional[Sequence[str] | dict[str]] = None,
33
31
  ) -> NetworkData:
34
- """Create network data object for iplotx from any provider."""
32
+ """Create network data object for iplotx from a networkx object."""
35
33
 
36
34
  import networkx as nx
37
35
 
38
- directed = detect_directedness(network)
36
+ network = self.network
37
+
38
+ directed = self.is_directed()
39
39
 
40
40
  # Recast vertex_labels=False as vertex_labels=None
41
41
  if np.isscalar(vertex_labels) and (not vertex_labels):
@@ -45,6 +45,7 @@ class NetworkXDataProvider(NetworkDataProvider):
45
45
  vertex_df = normalise_layout(
46
46
  layout,
47
47
  network=network,
48
+ nvertices=self.number_of_vertices(),
48
49
  ).loc[pd.Index(network.nodes)]
49
50
  ndim = vertex_df.shape[1]
50
51
  vertex_df.columns = _make_layout_columns(ndim)
@@ -133,3 +134,12 @@ class NetworkXDataProvider(NetworkDataProvider):
133
134
  from networkx import Graph
134
135
 
135
136
  return Graph
137
+
138
+ def is_directed(self):
139
+ import networkx as nx
140
+
141
+ return isinstance(self.network, (nx.DiGraph, nx.MultiDiGraph))
142
+
143
+ def number_of_vertices(self):
144
+ """The number of vertices/nodes in the network."""
145
+ return self.network.number_of_nodes()