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/__init__.py +22 -1
- iplotx/edge/__init__.py +623 -0
- iplotx/edge/arrow.py +220 -10
- iplotx/edge/geometry.py +392 -0
- iplotx/edge/ports.py +47 -0
- iplotx/groups.py +93 -45
- 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 +162 -0
- iplotx/layout.py +139 -0
- iplotx/network.py +161 -379
- iplotx/plotting.py +157 -56
- iplotx/style.py +391 -0
- iplotx/tree.py +312 -0
- iplotx/typing.py +55 -41
- 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 +305 -55
- iplotx-0.2.1.dist-info/METADATA +88 -0
- iplotx-0.2.1.dist-info/RECORD +31 -0
- 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.1.0.dist-info/METADATA +0 -47
- iplotx-0.1.0.dist-info/RECORD +0 -20
- {iplotx-0.1.0.dist-info → iplotx-0.2.1.dist-info}/WHEEL +0 -0
|
@@ -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
|
|
@@ -0,0 +1,112 @@
|
|
|
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 Cogent3DataProvider(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 cogent3.core.tree.PhyloNode classes."""
|
|
36
|
+
|
|
37
|
+
root_fun = lambda tree: tree.root()
|
|
38
|
+
preorder_fun = lambda tree: tree.preorder()
|
|
39
|
+
postorder_fun = lambda tree: tree.postorder()
|
|
40
|
+
children_fun = attrgetter("children")
|
|
41
|
+
branch_length_fun = attrgetter("length")
|
|
42
|
+
leaves_fun = lambda tree: tree.tips()
|
|
43
|
+
|
|
44
|
+
tree_data = {
|
|
45
|
+
"root": root_fun(tree),
|
|
46
|
+
"leaves": leaves_fun(tree),
|
|
47
|
+
"rooted": True,
|
|
48
|
+
"directed": directed,
|
|
49
|
+
"ndim": 2,
|
|
50
|
+
"layout_name": layout,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Add vertex_df including layout
|
|
54
|
+
tree_data["vertex_df"] = normalise_tree_layout(
|
|
55
|
+
layout,
|
|
56
|
+
tree=tree,
|
|
57
|
+
orientation=orientation,
|
|
58
|
+
root_fun=root_fun,
|
|
59
|
+
preorder_fun=preorder_fun,
|
|
60
|
+
postorder_fun=postorder_fun,
|
|
61
|
+
children_fun=children_fun,
|
|
62
|
+
branch_length_fun=branch_length_fun,
|
|
63
|
+
)
|
|
64
|
+
if layout in ("radial",):
|
|
65
|
+
tree_data["layout_coordinate_system"] = "polar"
|
|
66
|
+
else:
|
|
67
|
+
tree_data["layout_coordinate_system"] = "cartesian"
|
|
68
|
+
|
|
69
|
+
# Add edge_df
|
|
70
|
+
edge_data = {"_ipx_source": [], "_ipx_target": []}
|
|
71
|
+
for node in preorder_fun(tree):
|
|
72
|
+
for child in node.children:
|
|
73
|
+
if directed == "parent":
|
|
74
|
+
edge_data["_ipx_source"].append(child)
|
|
75
|
+
edge_data["_ipx_target"].append(node)
|
|
76
|
+
else:
|
|
77
|
+
edge_data["_ipx_source"].append(node)
|
|
78
|
+
edge_data["_ipx_target"].append(child)
|
|
79
|
+
edge_df = pd.DataFrame(edge_data)
|
|
80
|
+
tree_data["edge_df"] = edge_df
|
|
81
|
+
|
|
82
|
+
# Add vertex labels
|
|
83
|
+
if vertex_labels is None:
|
|
84
|
+
vertex_labels = False
|
|
85
|
+
if np.isscalar(vertex_labels) and vertex_labels:
|
|
86
|
+
tree_data["vertex_df"]["label"] = [
|
|
87
|
+
x.name for x in tree_data["vertices"].index
|
|
88
|
+
]
|
|
89
|
+
elif not np.isscalar(vertex_labels):
|
|
90
|
+
# If a dict-like object is passed, it can be incomplete (e.g. only the leaves):
|
|
91
|
+
# we fill the rest with empty strings which are not going to show up in the plot.
|
|
92
|
+
if isinstance(vertex_labels, pd.Series):
|
|
93
|
+
vertex_labels = dict(vertex_labels)
|
|
94
|
+
if isinstance(vertex_labels, dict):
|
|
95
|
+
for vertex in tree_data["vertex_df"].index:
|
|
96
|
+
if vertex not in vertex_labels:
|
|
97
|
+
vertex_labels[vertex] = ""
|
|
98
|
+
tree_data["vertex_df"]["label"] = pd.Series(vertex_labels)
|
|
99
|
+
|
|
100
|
+
return tree_data
|
|
101
|
+
|
|
102
|
+
def check_dependencies(self) -> bool:
|
|
103
|
+
try:
|
|
104
|
+
import cogent3
|
|
105
|
+
except ImportError:
|
|
106
|
+
return False
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
def tree_type(self):
|
|
110
|
+
from cogent3.core.tree import PhyloNode
|
|
111
|
+
|
|
112
|
+
return PhyloNode
|
|
@@ -0,0 +1,112 @@
|
|
|
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 Ete4DataProvider(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 ete4.core.tre.Tree classes."""
|
|
36
|
+
|
|
37
|
+
root_fun = attrgetter("root")
|
|
38
|
+
preorder_fun = lambda tree: tree.traverse("preorder")
|
|
39
|
+
postorder_fun = lambda tree: tree.traverse("postorder")
|
|
40
|
+
children_fun = attrgetter("children")
|
|
41
|
+
branch_length_fun = lambda node: node.dist if node.dist is not None else 1.0
|
|
42
|
+
leaves_fun = lambda tree: tree.leaves()
|
|
43
|
+
|
|
44
|
+
tree_data = {
|
|
45
|
+
"root": tree.root,
|
|
46
|
+
"leaves": leaves_fun(tree),
|
|
47
|
+
"rooted": True,
|
|
48
|
+
"directed": directed,
|
|
49
|
+
"ndim": 2,
|
|
50
|
+
"layout_name": layout,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Add vertex_df including layout
|
|
54
|
+
tree_data["vertex_df"] = normalise_tree_layout(
|
|
55
|
+
layout,
|
|
56
|
+
tree=tree,
|
|
57
|
+
orientation=orientation,
|
|
58
|
+
root_fun=root_fun,
|
|
59
|
+
preorder_fun=preorder_fun,
|
|
60
|
+
postorder_fun=postorder_fun,
|
|
61
|
+
children_fun=children_fun,
|
|
62
|
+
branch_length_fun=branch_length_fun,
|
|
63
|
+
)
|
|
64
|
+
if layout in ("radial",):
|
|
65
|
+
tree_data["layout_coordinate_system"] = "polar"
|
|
66
|
+
else:
|
|
67
|
+
tree_data["layout_coordinate_system"] = "cartesian"
|
|
68
|
+
|
|
69
|
+
# Add edge_df
|
|
70
|
+
edge_data = {"_ipx_source": [], "_ipx_target": []}
|
|
71
|
+
for node in preorder_fun(tree):
|
|
72
|
+
for child in children_fun(node):
|
|
73
|
+
if directed == "parent":
|
|
74
|
+
edge_data["_ipx_source"].append(child)
|
|
75
|
+
edge_data["_ipx_target"].append(node)
|
|
76
|
+
else:
|
|
77
|
+
edge_data["_ipx_source"].append(node)
|
|
78
|
+
edge_data["_ipx_target"].append(child)
|
|
79
|
+
edge_df = pd.DataFrame(edge_data)
|
|
80
|
+
tree_data["edge_df"] = edge_df
|
|
81
|
+
|
|
82
|
+
# Add vertex labels
|
|
83
|
+
if vertex_labels is None:
|
|
84
|
+
vertex_labels = False
|
|
85
|
+
if np.isscalar(vertex_labels) and vertex_labels:
|
|
86
|
+
tree_data["vertex_df"]["label"] = [
|
|
87
|
+
x.name for x in tree_data["vertices"].index
|
|
88
|
+
]
|
|
89
|
+
elif not np.isscalar(vertex_labels):
|
|
90
|
+
# If a dict-like object is passed, it can be incomplete (e.g. only the leaves):
|
|
91
|
+
# we fill the rest with empty strings which are not going to show up in the plot.
|
|
92
|
+
if isinstance(vertex_labels, pd.Series):
|
|
93
|
+
vertex_labels = dict(vertex_labels)
|
|
94
|
+
if isinstance(vertex_labels, dict):
|
|
95
|
+
for vertex in tree_data["vertex_df"].index:
|
|
96
|
+
if vertex not in vertex_labels:
|
|
97
|
+
vertex_labels[vertex] = ""
|
|
98
|
+
tree_data["vertex_df"]["label"] = pd.Series(vertex_labels)
|
|
99
|
+
|
|
100
|
+
return tree_data
|
|
101
|
+
|
|
102
|
+
def check_dependencies(self) -> bool:
|
|
103
|
+
try:
|
|
104
|
+
from ete4 import Tree
|
|
105
|
+
except ImportError:
|
|
106
|
+
return False
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
def tree_type(self):
|
|
110
|
+
from ete4 import Tree
|
|
111
|
+
|
|
112
|
+
return Tree
|
|
@@ -0,0 +1,112 @@
|
|
|
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 SkbioDataProvider(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 skbio.tree.TreeNode classes."""
|
|
36
|
+
|
|
37
|
+
root_fun = lambda tree: tree.root()
|
|
38
|
+
preorder_fun = lambda tree: tree.preorder()
|
|
39
|
+
postorder_fun = lambda tree: tree.postorder()
|
|
40
|
+
children_fun = attrgetter("children")
|
|
41
|
+
branch_length_fun = attrgetter("length")
|
|
42
|
+
leaves_fun = lambda tree: tree.tips()
|
|
43
|
+
|
|
44
|
+
tree_data = {
|
|
45
|
+
"root": root_fun(tree),
|
|
46
|
+
"leaves": leaves_fun(tree),
|
|
47
|
+
"rooted": True,
|
|
48
|
+
"directed": directed,
|
|
49
|
+
"ndim": 2,
|
|
50
|
+
"layout_name": layout,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Add vertex_df including layout
|
|
54
|
+
tree_data["vertex_df"] = normalise_tree_layout(
|
|
55
|
+
layout,
|
|
56
|
+
tree=tree,
|
|
57
|
+
orientation=orientation,
|
|
58
|
+
root_fun=root_fun,
|
|
59
|
+
preorder_fun=preorder_fun,
|
|
60
|
+
postorder_fun=postorder_fun,
|
|
61
|
+
children_fun=children_fun,
|
|
62
|
+
branch_length_fun=branch_length_fun,
|
|
63
|
+
)
|
|
64
|
+
if layout in ("radial",):
|
|
65
|
+
tree_data["layout_coordinate_system"] = "polar"
|
|
66
|
+
else:
|
|
67
|
+
tree_data["layout_coordinate_system"] = "cartesian"
|
|
68
|
+
|
|
69
|
+
# Add edge_df
|
|
70
|
+
edge_data = {"_ipx_source": [], "_ipx_target": []}
|
|
71
|
+
for node in preorder_fun(tree):
|
|
72
|
+
for child in children_fun(node):
|
|
73
|
+
if directed == "parent":
|
|
74
|
+
edge_data["_ipx_source"].append(child)
|
|
75
|
+
edge_data["_ipx_target"].append(node)
|
|
76
|
+
else:
|
|
77
|
+
edge_data["_ipx_source"].append(node)
|
|
78
|
+
edge_data["_ipx_target"].append(child)
|
|
79
|
+
edge_df = pd.DataFrame(edge_data)
|
|
80
|
+
tree_data["edge_df"] = edge_df
|
|
81
|
+
|
|
82
|
+
# Add vertex labels
|
|
83
|
+
if vertex_labels is None:
|
|
84
|
+
vertex_labels = False
|
|
85
|
+
if np.isscalar(vertex_labels) and vertex_labels:
|
|
86
|
+
tree_data["vertex_df"]["label"] = [
|
|
87
|
+
x.name for x in tree_data["vertices"].index
|
|
88
|
+
]
|
|
89
|
+
elif not np.isscalar(vertex_labels):
|
|
90
|
+
# If a dict-like object is passed, it can be incomplete (e.g. only the leaves):
|
|
91
|
+
# we fill the rest with empty strings which are not going to show up in the plot.
|
|
92
|
+
if isinstance(vertex_labels, pd.Series):
|
|
93
|
+
vertex_labels = dict(vertex_labels)
|
|
94
|
+
if isinstance(vertex_labels, dict):
|
|
95
|
+
for vertex in tree_data["vertex_df"].index:
|
|
96
|
+
if vertex not in vertex_labels:
|
|
97
|
+
vertex_labels[vertex] = ""
|
|
98
|
+
tree_data["vertex_df"]["label"] = pd.Series(vertex_labels)
|
|
99
|
+
|
|
100
|
+
return tree_data
|
|
101
|
+
|
|
102
|
+
def check_dependencies(self) -> bool:
|
|
103
|
+
try:
|
|
104
|
+
from skbio import TreeNode
|
|
105
|
+
except ImportError:
|
|
106
|
+
return False
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
def tree_type(self):
|
|
110
|
+
from skbio import TreeNode
|
|
111
|
+
|
|
112
|
+
return TreeNode
|