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 +5 -0
- iplotx/artists.py +24 -0
- iplotx/cascades.py +3 -3
- iplotx/edge/__init__.py +48 -43
- iplotx/edge/arrow.py +34 -0
- iplotx/edge/geometry.py +23 -4
- iplotx/edge/leaf.py +117 -0
- iplotx/ingest/__init__.py +1 -6
- iplotx/ingest/heuristics.py +3 -32
- iplotx/ingest/providers/network/igraph.py +16 -6
- iplotx/ingest/providers/network/networkx.py +14 -4
- iplotx/ingest/providers/network/simple.py +121 -0
- iplotx/ingest/providers/tree/biopython.py +13 -0
- iplotx/ingest/providers/tree/cogent3.py +7 -0
- iplotx/ingest/typing.py +110 -11
- iplotx/label.py +42 -12
- iplotx/layout.py +5 -1
- iplotx/network.py +66 -12
- iplotx/plotting.py +8 -8
- iplotx/{style.py → style/__init__.py} +23 -85
- iplotx/style/leaf_info.py +41 -0
- iplotx/style/library.py +230 -0
- iplotx/tree.py +244 -29
- iplotx/utils/matplotlib.py +30 -12
- iplotx/utils/style.py +12 -1
- iplotx/version.py +1 -1
- iplotx/vertex.py +62 -7
- {iplotx-0.3.0.dist-info → iplotx-0.4.0.dist-info}/METADATA +1 -1
- iplotx-0.4.0.dist-info/RECORD +37 -0
- iplotx-0.3.0.dist-info/RECORD +0 -32
- {iplotx-0.3.0.dist-info → iplotx-0.4.0.dist-info}/WHEEL +0 -0
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", "
|
|
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[[
|
|
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[[
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
|
334
|
-
loop_vertex_dict
|
|
335
|
-
|
|
336
|
-
loop_vertex_dict
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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(
|
|
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(
|
|
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
|
|
iplotx/ingest/heuristics.py
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
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 (
|
|
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
|
|
35
|
-
|
|
36
|
-
directed =
|
|
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(
|
|
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
|
|
32
|
+
"""Create network data object for iplotx from a networkx object."""
|
|
35
33
|
|
|
36
34
|
import networkx as nx
|
|
37
35
|
|
|
38
|
-
|
|
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()
|