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/__init__.py +22 -1
- iplotx/edge/__init__.py +882 -0
- iplotx/edge/arrow.py +220 -10
- iplotx/edge/ports.py +42 -0
- iplotx/groups.py +79 -41
- iplotx/ingest/__init__.py +155 -0
- iplotx/ingest/heuristics.py +209 -0
- iplotx/ingest/providers/network/igraph.py +96 -0
- iplotx/ingest/providers/network/networkx.py +133 -0
- iplotx/ingest/providers/tree/biopython.py +105 -0
- iplotx/ingest/providers/tree/cogent3.py +112 -0
- iplotx/ingest/providers/tree/ete4.py +112 -0
- iplotx/ingest/providers/tree/skbio.py +112 -0
- iplotx/ingest/typing.py +100 -0
- iplotx/label.py +127 -0
- iplotx/layout.py +139 -0
- iplotx/network.py +156 -375
- iplotx/plotting.py +157 -55
- iplotx/style.py +379 -0
- iplotx/tree.py +285 -0
- iplotx/typing.py +33 -38
- iplotx/utils/geometry.py +128 -81
- iplotx/utils/internal.py +3 -0
- iplotx/utils/matplotlib.py +58 -38
- iplotx/utils/style.py +1 -0
- iplotx/version.py +5 -1
- iplotx/vertex.py +250 -55
- iplotx-0.2.0.dist-info/METADATA +76 -0
- iplotx-0.2.0.dist-info/RECORD +30 -0
- {iplotx-0.0.1.dist-info → iplotx-0.2.0.dist-info}/WHEEL +0 -1
- iplotx/edge/common.py +0 -47
- iplotx/edge/directed.py +0 -149
- iplotx/edge/label.py +0 -50
- iplotx/edge/undirected.py +0 -447
- iplotx/heuristics.py +0 -114
- iplotx/importing.py +0 -13
- iplotx/styles.py +0 -186
- iplotx-0.0.1.dist-info/METADATA +0 -39
- iplotx-0.0.1.dist-info/RECORD +0 -20
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module focuses on how to ingest network data into standard data structures no matter what library they come from.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pathlib
|
|
6
|
+
import pkgutil
|
|
7
|
+
import importlib
|
|
8
|
+
import warnings
|
|
9
|
+
from typing import (
|
|
10
|
+
Optional,
|
|
11
|
+
Sequence,
|
|
12
|
+
Protocol,
|
|
13
|
+
)
|
|
14
|
+
from collections.abc import Hashable
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
from ..typing import (
|
|
18
|
+
GraphType,
|
|
19
|
+
LayoutType,
|
|
20
|
+
TreeType,
|
|
21
|
+
)
|
|
22
|
+
from .typing import (
|
|
23
|
+
NetworkDataProvider,
|
|
24
|
+
NetworkData,
|
|
25
|
+
TreeDataProvider,
|
|
26
|
+
TreeData,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
provider_protocols = {
|
|
30
|
+
"network": NetworkDataProvider,
|
|
31
|
+
"tree": TreeDataProvider,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Internally supported data providers
|
|
35
|
+
data_providers: dict[str, dict[str, Protocol]] = {
|
|
36
|
+
kind: {} for kind in provider_protocols
|
|
37
|
+
}
|
|
38
|
+
for kind in data_providers:
|
|
39
|
+
providers_path = pathlib.Path(__file__).parent.joinpath("providers").joinpath(kind)
|
|
40
|
+
for importer, module_name, _ in pkgutil.iter_modules([providers_path]):
|
|
41
|
+
module = importlib.import_module(
|
|
42
|
+
f"iplotx.ingest.providers.{kind}.{module_name}"
|
|
43
|
+
)
|
|
44
|
+
for key, val in module.__dict__.items():
|
|
45
|
+
if key == provider_protocols[kind].__name__:
|
|
46
|
+
continue
|
|
47
|
+
if key.endswith("DataProvider"):
|
|
48
|
+
data_providers[kind][module_name] = val()
|
|
49
|
+
break
|
|
50
|
+
del providers_path
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def network_library(network) -> str:
|
|
54
|
+
"""Guess the network library used to create the network."""
|
|
55
|
+
for name, provider in data_providers["network"].items():
|
|
56
|
+
if provider.check_dependencies():
|
|
57
|
+
graph_type = provider.graph_type()
|
|
58
|
+
if isinstance(network, graph_type):
|
|
59
|
+
return name
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"Network {network} did not match any available network library.",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def tree_library(tree) -> str:
|
|
66
|
+
"""Guess the tree library used to create the tree."""
|
|
67
|
+
for name, provider in data_providers["tree"].items():
|
|
68
|
+
if provider.check_dependencies():
|
|
69
|
+
tree_type = provider.tree_type()
|
|
70
|
+
if isinstance(tree, tree_type):
|
|
71
|
+
return name
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Tree {tree} did not match any available tree library.",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# Functions to ingest data from various libraries
|
|
78
|
+
def ingest_network_data(
|
|
79
|
+
network: GraphType,
|
|
80
|
+
layout: Optional[LayoutType] = None,
|
|
81
|
+
vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
|
|
82
|
+
edge_labels: Optional[Sequence[str] | dict[str,]] = None,
|
|
83
|
+
) -> NetworkData:
|
|
84
|
+
"""Create internal data for the network."""
|
|
85
|
+
_update_data_providers("network")
|
|
86
|
+
|
|
87
|
+
nl = network_library(network)
|
|
88
|
+
|
|
89
|
+
if nl in data_providers["network"]:
|
|
90
|
+
provider: NetworkDataProvider = data_providers["network"][nl]
|
|
91
|
+
else:
|
|
92
|
+
sup = ", ".join(data_providers["network"].keys())
|
|
93
|
+
raise ValueError(
|
|
94
|
+
f"Network library '{nl}' is not installed. "
|
|
95
|
+
f"Currently installed supported libraries: {sup}."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
result = provider(
|
|
99
|
+
network=network,
|
|
100
|
+
layout=layout,
|
|
101
|
+
vertex_labels=vertex_labels,
|
|
102
|
+
edge_labels=edge_labels,
|
|
103
|
+
)
|
|
104
|
+
result["network_library"] = nl
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def ingest_tree_data(
|
|
109
|
+
tree: TreeType,
|
|
110
|
+
layout: Optional[str] = "horizontal",
|
|
111
|
+
orientation: Optional[str] = "right",
|
|
112
|
+
directed: bool | str = False,
|
|
113
|
+
vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
|
|
114
|
+
edge_labels: Optional[Sequence[str] | dict[str,]] = None,
|
|
115
|
+
) -> TreeData:
|
|
116
|
+
"""Create internal data for the tree."""
|
|
117
|
+
_update_data_providers("tree")
|
|
118
|
+
|
|
119
|
+
tl = tree_library(tree)
|
|
120
|
+
|
|
121
|
+
if tl in data_providers["tree"]:
|
|
122
|
+
provider: TreeDataProvider = data_providers["tree"][tl]
|
|
123
|
+
else:
|
|
124
|
+
sup = ", ".join(data_providers["tree"].keys())
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"Tree library '{tl}' is not installed. "
|
|
127
|
+
f"Currently installed supported libraries: {sup}."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
result = provider(
|
|
131
|
+
tree=tree,
|
|
132
|
+
layout=layout,
|
|
133
|
+
orientation=orientation,
|
|
134
|
+
directed=directed,
|
|
135
|
+
vertex_labels=vertex_labels,
|
|
136
|
+
edge_labels=edge_labels,
|
|
137
|
+
)
|
|
138
|
+
result["tree_library"] = tl
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# INTERNAL FUNCTIONS
|
|
143
|
+
def _update_data_providers(kind):
|
|
144
|
+
"""Update data provieders dynamically from external packages."""
|
|
145
|
+
discovered_providers = importlib.metadata.entry_points(
|
|
146
|
+
group=f"iplotx.{kind}_data_providers"
|
|
147
|
+
)
|
|
148
|
+
for entry_point in discovered_providers:
|
|
149
|
+
if entry_point.name not in data_providers["network"]:
|
|
150
|
+
try:
|
|
151
|
+
data_providers[kind][entry_point.name] = entry_point.load()
|
|
152
|
+
except Exception as e:
|
|
153
|
+
warnings.warn(
|
|
154
|
+
f"Failed to load {kind} data provider '{entry_point.name}': {e}"
|
|
155
|
+
)
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Heuristics module to funnel certain variable inputs (e.g. layouts) into a standard format.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import (
|
|
6
|
+
Optional,
|
|
7
|
+
Any,
|
|
8
|
+
)
|
|
9
|
+
from collections.abc import Hashable
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pandas as pd
|
|
13
|
+
|
|
14
|
+
from ..layout import compute_tree_layout
|
|
15
|
+
from ..typing import (
|
|
16
|
+
GraphType,
|
|
17
|
+
GroupingType,
|
|
18
|
+
TreeType,
|
|
19
|
+
LayoutType,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def number_of_vertices(network: GraphType) -> int:
|
|
24
|
+
"""Get the number of vertices in the network."""
|
|
25
|
+
from . import network_library
|
|
26
|
+
|
|
27
|
+
if network_library(network) == "igraph":
|
|
28
|
+
return network.vcount()
|
|
29
|
+
if network_library(network) == "networkx":
|
|
30
|
+
return network.number_of_nodes()
|
|
31
|
+
raise TypeError("Unsupported graph type. Supported types are igraph and networkx.")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def detect_directedness(
|
|
35
|
+
network: GraphType,
|
|
36
|
+
) -> bool:
|
|
37
|
+
"""Detect if the network is directed or not."""
|
|
38
|
+
from . import network_library
|
|
39
|
+
|
|
40
|
+
nl = network_library(network)
|
|
41
|
+
|
|
42
|
+
if nl == "igraph":
|
|
43
|
+
return network.is_directed()
|
|
44
|
+
if nl == "networkx":
|
|
45
|
+
import networkx as nx
|
|
46
|
+
|
|
47
|
+
if isinstance(network, (nx.DiGraph, nx.MultiDiGraph)):
|
|
48
|
+
return True
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def normalise_layout(layout, network=None):
|
|
53
|
+
"""Normalise the layout to a pandas.DataFrame."""
|
|
54
|
+
from . import network_library
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
import igraph as ig
|
|
58
|
+
except ImportError:
|
|
59
|
+
ig = None
|
|
60
|
+
|
|
61
|
+
if layout is None:
|
|
62
|
+
if (network is not None) and (number_of_vertices(network) == 0):
|
|
63
|
+
return pd.DataFrame(np.zeros((0, 2)))
|
|
64
|
+
return None
|
|
65
|
+
if (network is not None) and isinstance(layout, str):
|
|
66
|
+
if network_library(network) == "igraph":
|
|
67
|
+
if hasattr(network, layout):
|
|
68
|
+
layout = network[layout]
|
|
69
|
+
else:
|
|
70
|
+
layout = network.layout(layout)
|
|
71
|
+
# NOTE: This seems like a legit bug in igraph
|
|
72
|
+
# Sometimes (e.g. sugiyama) the layout has more vertices than the network (?)
|
|
73
|
+
layout = np.asarray(layout.coords)[: network.vcount()]
|
|
74
|
+
if network_library(network) == "networkx":
|
|
75
|
+
layout = dict(network.nodes.data(layout))
|
|
76
|
+
|
|
77
|
+
if (ig is not None) and isinstance(layout, ig.layout.Layout):
|
|
78
|
+
return pd.DataFrame(layout.coords)
|
|
79
|
+
if isinstance(layout, dict):
|
|
80
|
+
return pd.DataFrame(layout).T
|
|
81
|
+
if isinstance(layout, str):
|
|
82
|
+
raise NotImplementedError("Layout as a string is not supported yet.")
|
|
83
|
+
if isinstance(layout, (list, tuple)):
|
|
84
|
+
return pd.DataFrame(np.array(layout))
|
|
85
|
+
if isinstance(layout, pd.DataFrame):
|
|
86
|
+
return layout
|
|
87
|
+
if isinstance(layout, np.ndarray):
|
|
88
|
+
return pd.DataFrame(layout)
|
|
89
|
+
raise TypeError("Layout could not be normalised.")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def normalise_tree_layout(
|
|
93
|
+
layout: str | Any,
|
|
94
|
+
tree: Optional[TreeType] = None,
|
|
95
|
+
**kwargs,
|
|
96
|
+
) -> pd.DataFrame:
|
|
97
|
+
"""Normalise tree layout from a variety of inputs.
|
|
98
|
+
|
|
99
|
+
Parameters:
|
|
100
|
+
layout: The tree layout to normalise.
|
|
101
|
+
tree: The correcponding tree object.
|
|
102
|
+
**kwargs: Additional arguments for the subroutines.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
A pandas DataFrame with the normalised tree layout.
|
|
106
|
+
|
|
107
|
+
NOTE: This function currently only accepts strings and computes
|
|
108
|
+
the layout internally. This might change in the future.
|
|
109
|
+
"""
|
|
110
|
+
if isinstance(layout, str):
|
|
111
|
+
layout = compute_tree_layout(tree, layout, **kwargs)
|
|
112
|
+
else:
|
|
113
|
+
raise NotImplementedError(
|
|
114
|
+
"Only internally computed tree layout currently accepted."
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if isinstance(layout, dict):
|
|
118
|
+
# Adjust vertex layout
|
|
119
|
+
index = []
|
|
120
|
+
coordinates = []
|
|
121
|
+
for key, coordinate in layout.items():
|
|
122
|
+
index.append(key)
|
|
123
|
+
coordinates.append(coordinate)
|
|
124
|
+
index = pd.Index(index)
|
|
125
|
+
coordinates = np.array(coordinates)
|
|
126
|
+
ndim = len(coordinates[0]) if len(coordinates) > 0 else 2
|
|
127
|
+
layout_columns = [f"_ipx_layout_{i}" for i in range(ndim)]
|
|
128
|
+
layout = pd.DataFrame(
|
|
129
|
+
coordinates,
|
|
130
|
+
index=index,
|
|
131
|
+
columns=layout_columns,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return layout
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def normalise_grouping(
|
|
138
|
+
grouping: GroupingType,
|
|
139
|
+
layout: LayoutType,
|
|
140
|
+
) -> dict[Hashable, set]:
|
|
141
|
+
"""Normalise network grouping from a variery of inputs.
|
|
142
|
+
|
|
143
|
+
Parameters:
|
|
144
|
+
grouping: Network grouping (e.g. vertex cover).
|
|
145
|
+
layout: Network layout.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
A dictionary of sets. Each key is the index of a group, each value is a set of vertices
|
|
149
|
+
included in that group. If all sets are mutually exclusive, this is a vertex clustering,
|
|
150
|
+
otherwise it's only a vertex cover.
|
|
151
|
+
"""
|
|
152
|
+
try:
|
|
153
|
+
import igraph as ig
|
|
154
|
+
except ImportError:
|
|
155
|
+
ig = None
|
|
156
|
+
|
|
157
|
+
if len(grouping) == 0:
|
|
158
|
+
return {}
|
|
159
|
+
|
|
160
|
+
if isinstance(grouping, dict):
|
|
161
|
+
val0 = next(iter(grouping.values()))
|
|
162
|
+
# If already the right data type or compatible, leave as is
|
|
163
|
+
if isinstance(val0, (set, frozenset)):
|
|
164
|
+
return grouping
|
|
165
|
+
|
|
166
|
+
# If a dict of integers or strings, assume each key is a vertex id and each value is a
|
|
167
|
+
# group, convert (i.e. invert the dict)
|
|
168
|
+
if isinstance(val0, (int, str)):
|
|
169
|
+
group_dic = defaultdict(set)
|
|
170
|
+
for key, val in grouping.items():
|
|
171
|
+
group_dic[val].add(key)
|
|
172
|
+
return group_dic
|
|
173
|
+
|
|
174
|
+
# If an igraph object, convert to a dict of sets
|
|
175
|
+
if ig is not None:
|
|
176
|
+
if isinstance(grouping, ig.clustering.Clustering):
|
|
177
|
+
layout = normalise_layout(layout)
|
|
178
|
+
group_dic = defaultdict(set)
|
|
179
|
+
for i, member in enumerate(grouping.membership):
|
|
180
|
+
group_dic[member].add(i)
|
|
181
|
+
return group_dic
|
|
182
|
+
|
|
183
|
+
if isinstance(grouping, ig.clustering.Cover):
|
|
184
|
+
layout = normalise_layout(layout)
|
|
185
|
+
group_dic = defaultdict(set)
|
|
186
|
+
for i, members in enumerate(grouping.membership):
|
|
187
|
+
for member in members:
|
|
188
|
+
group_dic[member].add(i)
|
|
189
|
+
return group_dic
|
|
190
|
+
|
|
191
|
+
# Assume it's a sequence, so convert to list
|
|
192
|
+
grouping = list(grouping)
|
|
193
|
+
|
|
194
|
+
# If the values are already sets, assume group indices are integers
|
|
195
|
+
# and values are as is
|
|
196
|
+
if isinstance(grouping[0], set):
|
|
197
|
+
return dict(enumerate(grouping))
|
|
198
|
+
|
|
199
|
+
# If the values are integers or strings, assume each key is a vertex id and each value is a
|
|
200
|
+
# group, convert to dict of sets
|
|
201
|
+
if isinstance(grouping[0], (int, str)):
|
|
202
|
+
group_dic = defaultdict(set)
|
|
203
|
+
for i, val in enumerate(grouping):
|
|
204
|
+
group_dic[val].add(i)
|
|
205
|
+
return group_dic
|
|
206
|
+
|
|
207
|
+
raise TypeError(
|
|
208
|
+
"Could not standardise grouping from object.",
|
|
209
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Optional,
|
|
3
|
+
Sequence,
|
|
4
|
+
)
|
|
5
|
+
from collections.abc import Hashable
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from ....typing import (
|
|
10
|
+
GraphType,
|
|
11
|
+
LayoutType,
|
|
12
|
+
)
|
|
13
|
+
from ...heuristics import (
|
|
14
|
+
normalise_layout,
|
|
15
|
+
detect_directedness,
|
|
16
|
+
)
|
|
17
|
+
from ...typing import (
|
|
18
|
+
NetworkDataProvider,
|
|
19
|
+
NetworkData,
|
|
20
|
+
)
|
|
21
|
+
from ....utils.internal import (
|
|
22
|
+
_make_layout_columns,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class IGraphDataProvider(NetworkDataProvider):
|
|
27
|
+
def __call__(
|
|
28
|
+
self,
|
|
29
|
+
network: GraphType,
|
|
30
|
+
layout: Optional[LayoutType] = None,
|
|
31
|
+
vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
|
|
32
|
+
edge_labels: Optional[Sequence[str] | dict[str]] = None,
|
|
33
|
+
) -> NetworkData:
|
|
34
|
+
"""Create network data object for iplotx from any provider."""
|
|
35
|
+
|
|
36
|
+
directed = detect_directedness(network)
|
|
37
|
+
|
|
38
|
+
# Recast vertex_labels=False as vertex_labels=None
|
|
39
|
+
if np.isscalar(vertex_labels) and (not vertex_labels):
|
|
40
|
+
vertex_labels = None
|
|
41
|
+
|
|
42
|
+
# Vertices are ordered integers, no gaps
|
|
43
|
+
vertex_df = normalise_layout(layout, network=network)
|
|
44
|
+
ndim = vertex_df.shape[1]
|
|
45
|
+
vertex_df.columns = _make_layout_columns(ndim)
|
|
46
|
+
|
|
47
|
+
# Vertex labels
|
|
48
|
+
if vertex_labels is not None:
|
|
49
|
+
if np.isscalar(vertex_labels):
|
|
50
|
+
vertex_df["label"] = vertex_df.index.astype(str)
|
|
51
|
+
elif len(vertex_labels) != len(vertex_df):
|
|
52
|
+
raise ValueError(
|
|
53
|
+
"Vertex labels must be the same length as the number of vertices."
|
|
54
|
+
)
|
|
55
|
+
else:
|
|
56
|
+
vertex_df["label"] = vertex_labels
|
|
57
|
+
|
|
58
|
+
# Edges are a list of tuples, because of multiedges
|
|
59
|
+
tmp = []
|
|
60
|
+
for edge in network.es:
|
|
61
|
+
row = {"_ipx_source": edge.source, "_ipx_target": edge.target}
|
|
62
|
+
row.update(edge.attributes())
|
|
63
|
+
tmp.append(row)
|
|
64
|
+
if len(tmp):
|
|
65
|
+
edge_df = pd.DataFrame(tmp)
|
|
66
|
+
else:
|
|
67
|
+
edge_df = pd.DataFrame(columns=["_ipx_source", "_ipx_target"])
|
|
68
|
+
del tmp
|
|
69
|
+
|
|
70
|
+
# Edge labels
|
|
71
|
+
if edge_labels is not None:
|
|
72
|
+
if len(edge_labels) != len(edge_df):
|
|
73
|
+
raise ValueError(
|
|
74
|
+
"Edge labels must be the same length as the number of edges."
|
|
75
|
+
)
|
|
76
|
+
edge_df["label"] = edge_labels
|
|
77
|
+
|
|
78
|
+
network_data = {
|
|
79
|
+
"vertex_df": vertex_df,
|
|
80
|
+
"edge_df": edge_df,
|
|
81
|
+
"directed": directed,
|
|
82
|
+
"ndim": ndim,
|
|
83
|
+
}
|
|
84
|
+
return network_data
|
|
85
|
+
|
|
86
|
+
def check_dependencies(self) -> bool:
|
|
87
|
+
try:
|
|
88
|
+
import igraph
|
|
89
|
+
except ImportError:
|
|
90
|
+
return False
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
def graph_type(self):
|
|
94
|
+
import igraph as ig
|
|
95
|
+
|
|
96
|
+
return ig.Graph
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Optional,
|
|
3
|
+
Sequence,
|
|
4
|
+
)
|
|
5
|
+
from collections.abc import Hashable
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from ....typing import (
|
|
10
|
+
GraphType,
|
|
11
|
+
LayoutType,
|
|
12
|
+
)
|
|
13
|
+
from ...heuristics import (
|
|
14
|
+
normalise_layout,
|
|
15
|
+
detect_directedness,
|
|
16
|
+
)
|
|
17
|
+
from ...typing import (
|
|
18
|
+
NetworkDataProvider,
|
|
19
|
+
NetworkData,
|
|
20
|
+
)
|
|
21
|
+
from ....utils.internal import (
|
|
22
|
+
_make_layout_columns,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NetworkXDataProvider(NetworkDataProvider):
|
|
27
|
+
def __call__(
|
|
28
|
+
self,
|
|
29
|
+
network: GraphType,
|
|
30
|
+
layout: Optional[LayoutType] = None,
|
|
31
|
+
vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
|
|
32
|
+
edge_labels: Optional[Sequence[str] | dict[str]] = None,
|
|
33
|
+
) -> NetworkData:
|
|
34
|
+
"""Create network data object for iplotx from any provider."""
|
|
35
|
+
|
|
36
|
+
import networkx as nx
|
|
37
|
+
|
|
38
|
+
directed = detect_directedness(network)
|
|
39
|
+
|
|
40
|
+
# Recast vertex_labels=False as vertex_labels=None
|
|
41
|
+
if np.isscalar(vertex_labels) and (not vertex_labels):
|
|
42
|
+
vertex_labels = None
|
|
43
|
+
|
|
44
|
+
# Vertices are indexed by node ID
|
|
45
|
+
vertex_df = normalise_layout(
|
|
46
|
+
layout,
|
|
47
|
+
network=network,
|
|
48
|
+
).loc[pd.Index(network.nodes)]
|
|
49
|
+
ndim = vertex_df.shape[1]
|
|
50
|
+
vertex_df.columns = _make_layout_columns(ndim)
|
|
51
|
+
|
|
52
|
+
# Vertex internal properties
|
|
53
|
+
tmp = pd.DataFrame(dict(network.nodes.data())).T
|
|
54
|
+
# Arrays become a single column, which we have already anyway
|
|
55
|
+
if isinstance(layout, str) and (layout in tmp.columns):
|
|
56
|
+
del tmp[layout]
|
|
57
|
+
for col in tmp.columns:
|
|
58
|
+
vertex_df[col] = tmp[col]
|
|
59
|
+
del tmp
|
|
60
|
+
|
|
61
|
+
# Vertex labels
|
|
62
|
+
if vertex_labels is None:
|
|
63
|
+
if "label" in vertex_df:
|
|
64
|
+
del vertex_df["label"]
|
|
65
|
+
else:
|
|
66
|
+
if (
|
|
67
|
+
np.isscalar(vertex_labels)
|
|
68
|
+
and (not vertex_labels)
|
|
69
|
+
and ("label" in vertex_df)
|
|
70
|
+
):
|
|
71
|
+
del vertex_df["label"]
|
|
72
|
+
elif vertex_labels is True:
|
|
73
|
+
if "label" not in vertex_df:
|
|
74
|
+
vertex_df["label"] = vertex_df.index
|
|
75
|
+
elif (not np.isscalar(vertex_labels)) and (
|
|
76
|
+
len(vertex_labels) != len(vertex_df)
|
|
77
|
+
):
|
|
78
|
+
raise ValueError(
|
|
79
|
+
"Vertex labels must be the same length as the number of vertices."
|
|
80
|
+
)
|
|
81
|
+
elif isinstance(vertex_labels, nx.classes.reportviews.NodeDataView):
|
|
82
|
+
vertex_df["label"] = pd.Series(dict(vertex_labels))
|
|
83
|
+
else:
|
|
84
|
+
vertex_df["label"] = vertex_labels
|
|
85
|
+
|
|
86
|
+
# Edges are a list of tuples, because of multiedges
|
|
87
|
+
tmp = []
|
|
88
|
+
for u, v, d in network.edges.data():
|
|
89
|
+
row = {"_ipx_source": u, "_ipx_target": v}
|
|
90
|
+
row.update(d)
|
|
91
|
+
tmp.append(row)
|
|
92
|
+
if len(tmp):
|
|
93
|
+
edge_df = pd.DataFrame(tmp)
|
|
94
|
+
else:
|
|
95
|
+
edge_df = pd.DataFrame(columns=["_ipx_source", "_ipx_target"])
|
|
96
|
+
del tmp
|
|
97
|
+
|
|
98
|
+
# Edge labels
|
|
99
|
+
# Even though they could exist in the dataframe, request the to be explicitely mentioned
|
|
100
|
+
if (edge_labels is None) and ("label" in edge_df):
|
|
101
|
+
del edge_df["label"]
|
|
102
|
+
elif edge_labels is not None:
|
|
103
|
+
if np.isscalar(edge_labels):
|
|
104
|
+
if (not edge_labels) and ("label" in edge_df):
|
|
105
|
+
del edge_df["label"]
|
|
106
|
+
if (edge_labels is True) and ("label" not in edge_df):
|
|
107
|
+
edge_df["label"] = [str(i) for i in edge_df.index]
|
|
108
|
+
else:
|
|
109
|
+
if len(edge_labels) != len(edge_df):
|
|
110
|
+
raise ValueError(
|
|
111
|
+
"Edge labels must be the same length as the number of edges."
|
|
112
|
+
)
|
|
113
|
+
edge_df["label"] = edge_labels
|
|
114
|
+
|
|
115
|
+
network_data = {
|
|
116
|
+
"vertex_df": vertex_df,
|
|
117
|
+
"edge_df": edge_df,
|
|
118
|
+
"directed": directed,
|
|
119
|
+
"ndim": ndim,
|
|
120
|
+
}
|
|
121
|
+
return network_data
|
|
122
|
+
|
|
123
|
+
def check_dependencies(self) -> bool:
|
|
124
|
+
try:
|
|
125
|
+
import networkx
|
|
126
|
+
except ImportError:
|
|
127
|
+
return False
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
def graph_type(self):
|
|
131
|
+
from networkx import Graph
|
|
132
|
+
|
|
133
|
+
return Graph
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Optional,
|
|
3
|
+
Sequence,
|
|
4
|
+
)
|
|
5
|
+
from collections.abc import Hashable
|
|
6
|
+
from operator import attrgetter
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from ....typing import (
|
|
11
|
+
TreeType,
|
|
12
|
+
LayoutType,
|
|
13
|
+
)
|
|
14
|
+
from ...typing import (
|
|
15
|
+
TreeDataProvider,
|
|
16
|
+
TreeData,
|
|
17
|
+
)
|
|
18
|
+
from ...heuristics import (
|
|
19
|
+
normalise_tree_layout,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BiopythonDataProvider(TreeDataProvider):
|
|
24
|
+
def __call__(
|
|
25
|
+
self,
|
|
26
|
+
tree: TreeType,
|
|
27
|
+
layout: str | LayoutType,
|
|
28
|
+
orientation: str = "horizontal",
|
|
29
|
+
directed: bool | str = False,
|
|
30
|
+
vertex_labels: Optional[
|
|
31
|
+
Sequence[str] | dict[Hashable, str] | pd.Series | bool
|
|
32
|
+
] = None,
|
|
33
|
+
edge_labels: Optional[Sequence[str] | dict] = None,
|
|
34
|
+
) -> TreeData:
|
|
35
|
+
"""Create tree data object for iplotx from BioPython.Phylo.Tree classes."""
|
|
36
|
+
|
|
37
|
+
tree_data = {
|
|
38
|
+
"root": tree.root,
|
|
39
|
+
"leaves": tree.get_terminals(),
|
|
40
|
+
"rooted": tree.rooted,
|
|
41
|
+
"directed": directed,
|
|
42
|
+
"ndim": 2,
|
|
43
|
+
"layout_name": layout,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Add vertex_df including layout
|
|
47
|
+
tree_data["vertex_df"] = normalise_tree_layout(
|
|
48
|
+
layout,
|
|
49
|
+
tree=tree,
|
|
50
|
+
orientation=orientation,
|
|
51
|
+
root_fun=attrgetter("root"),
|
|
52
|
+
preorder_fun=lambda tree: tree.find_clades(order="preorder"),
|
|
53
|
+
postorder_fun=lambda tree: tree.find_clades(order="postorder"),
|
|
54
|
+
children_fun=attrgetter("clades"),
|
|
55
|
+
branch_length_fun=attrgetter("branch_length"),
|
|
56
|
+
)
|
|
57
|
+
if layout in ("radial",):
|
|
58
|
+
tree_data["layout_coordinate_system"] = "polar"
|
|
59
|
+
else:
|
|
60
|
+
tree_data["layout_coordinate_system"] = "cartesian"
|
|
61
|
+
|
|
62
|
+
# Add edge_df
|
|
63
|
+
edge_data = {"_ipx_source": [], "_ipx_target": []}
|
|
64
|
+
for node in tree.find_clades(order="preorder"):
|
|
65
|
+
for child in node.clades:
|
|
66
|
+
if directed == "parent":
|
|
67
|
+
edge_data["_ipx_source"].append(child)
|
|
68
|
+
edge_data["_ipx_target"].append(node)
|
|
69
|
+
else:
|
|
70
|
+
edge_data["_ipx_source"].append(node)
|
|
71
|
+
edge_data["_ipx_target"].append(child)
|
|
72
|
+
edge_df = pd.DataFrame(edge_data)
|
|
73
|
+
tree_data["edge_df"] = edge_df
|
|
74
|
+
|
|
75
|
+
# Add vertex labels
|
|
76
|
+
if vertex_labels is None:
|
|
77
|
+
vertex_labels = False
|
|
78
|
+
if np.isscalar(vertex_labels) and vertex_labels:
|
|
79
|
+
tree_data["vertex_df"]["label"] = [
|
|
80
|
+
x.name for x in tree_data["vertices"].index
|
|
81
|
+
]
|
|
82
|
+
elif not np.isscalar(vertex_labels):
|
|
83
|
+
# If a dict-like object is passed, it can be incomplete (e.g. only the leaves):
|
|
84
|
+
# we fill the rest with empty strings which are not going to show up in the plot.
|
|
85
|
+
if isinstance(vertex_labels, pd.Series):
|
|
86
|
+
vertex_labels = dict(vertex_labels)
|
|
87
|
+
if isinstance(vertex_labels, dict):
|
|
88
|
+
for vertex in tree_data["vertex_df"].index:
|
|
89
|
+
if vertex not in vertex_labels:
|
|
90
|
+
vertex_labels[vertex] = ""
|
|
91
|
+
tree_data["vertex_df"]["label"] = pd.Series(vertex_labels)
|
|
92
|
+
|
|
93
|
+
return tree_data
|
|
94
|
+
|
|
95
|
+
def check_dependencies(self) -> bool:
|
|
96
|
+
try:
|
|
97
|
+
from Bio import Phylo
|
|
98
|
+
except ImportError:
|
|
99
|
+
return False
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
def tree_type(self):
|
|
103
|
+
from Bio import Phylo
|
|
104
|
+
|
|
105
|
+
return Phylo.BaseTree.Tree
|