iplotx 1.2.0__py3-none-any.whl → 1.3.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/edge/arrow.py +6 -1
- iplotx/ingest/__init__.py +19 -8
- iplotx/ingest/providers/tree/biopython.py +2 -1
- iplotx/ingest/providers/tree/cogent3.py +4 -1
- iplotx/ingest/providers/tree/dendropy.py +10 -1
- iplotx/ingest/providers/tree/ete4.py +2 -1
- iplotx/ingest/providers/tree/simple.py +13 -4
- iplotx/ingest/providers/tree/skbio.py +4 -1
- iplotx/ingest/typing.py +62 -10
- iplotx/layout/__init__.py +9 -0
- iplotx/layout/tree/__init__.py +72 -0
- iplotx/{layout.py → layout/tree/rooted.py} +3 -45
- iplotx/layout/tree/unrooted.py +383 -0
- iplotx/network/__init__.py +3 -4
- iplotx/tree/__init__.py +5 -2
- iplotx/version.py +1 -1
- {iplotx-1.2.0.dist-info → iplotx-1.3.0.dist-info}/METADATA +13 -6
- {iplotx-1.2.0.dist-info → iplotx-1.3.0.dist-info}/RECORD +19 -16
- {iplotx-1.2.0.dist-info → iplotx-1.3.0.dist-info}/WHEEL +0 -0
iplotx/edge/arrow.py
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
Module for edge arrows in iplotx.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import sys
|
|
6
|
+
if sys.version_info < (3, 11):
|
|
7
|
+
from typing_extensions import Never
|
|
8
|
+
else:
|
|
9
|
+
from typing import Never
|
|
10
|
+
from typing import Optional
|
|
6
11
|
|
|
7
12
|
from math import atan2, cos, sin
|
|
8
13
|
import numpy as np
|
iplotx/ingest/__init__.py
CHANGED
|
@@ -2,16 +2,24 @@
|
|
|
2
2
|
This module focuses on how to ingest network/tree data into standard data structures no matter what library they come from.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import pkgutil
|
|
7
|
-
import importlib
|
|
8
|
-
import warnings
|
|
5
|
+
import sys
|
|
9
6
|
from typing import (
|
|
10
7
|
Optional,
|
|
11
8
|
Sequence,
|
|
12
|
-
Protocol,
|
|
13
9
|
)
|
|
10
|
+
|
|
11
|
+
# NOTE: __init__ in Protocols has had a difficult gestation
|
|
12
|
+
# https://github.com/python/cpython/issues/88970
|
|
13
|
+
if sys.version_info < (3, 11):
|
|
14
|
+
Protocol = object
|
|
15
|
+
else:
|
|
16
|
+
from typing import Protocol
|
|
17
|
+
|
|
14
18
|
from collections.abc import Hashable
|
|
19
|
+
import pathlib
|
|
20
|
+
import pkgutil
|
|
21
|
+
import importlib
|
|
22
|
+
import warnings
|
|
15
23
|
import pandas as pd
|
|
16
24
|
|
|
17
25
|
from ..typing import (
|
|
@@ -35,6 +43,9 @@ provider_protocols = {
|
|
|
35
43
|
data_providers: dict[str, dict[str, Protocol]] = {kind: {} for kind in provider_protocols}
|
|
36
44
|
for kind in data_providers:
|
|
37
45
|
providers_path = pathlib.Path(__file__).parent.joinpath("providers").joinpath(kind)
|
|
46
|
+
if sys.version_info < (3, 11):
|
|
47
|
+
providers_path = str(providers_path)
|
|
48
|
+
|
|
38
49
|
for importer, module_name, _ in pkgutil.iter_modules([providers_path]):
|
|
39
50
|
module = importlib.import_module(f"iplotx.ingest.providers.{kind}.{module_name}")
|
|
40
51
|
for key, val in module.__dict__.items():
|
|
@@ -138,11 +149,11 @@ def ingest_tree_data(
|
|
|
138
149
|
|
|
139
150
|
|
|
140
151
|
# INTERNAL FUNCTIONS
|
|
141
|
-
def _update_data_providers(kind):
|
|
142
|
-
"""Update data
|
|
152
|
+
def _update_data_providers(kind: str):
|
|
153
|
+
"""Update data providers dynamically from external packages."""
|
|
143
154
|
discovered_providers = importlib.metadata.entry_points(group=f"iplotx.{kind}_data_providers")
|
|
144
155
|
for entry_point in discovered_providers:
|
|
145
|
-
if entry_point.name not in data_providers[
|
|
156
|
+
if entry_point.name not in data_providers[kind]:
|
|
146
157
|
try:
|
|
147
158
|
data_providers[kind][entry_point.name] = entry_point.load()
|
|
148
159
|
except Exception as e:
|
|
@@ -21,8 +21,9 @@ class BiopythonDataProvider(TreeDataProvider):
|
|
|
21
21
|
|
|
22
22
|
preorder = partialmethod(_traverse, order="preorder")
|
|
23
23
|
postorder = partialmethod(_traverse, order="postorder")
|
|
24
|
+
levelorder = partialmethod(_traverse, order="level")
|
|
24
25
|
|
|
25
|
-
def
|
|
26
|
+
def _get_leaves(self) -> Sequence[Any]:
|
|
26
27
|
return self.tree.get_terminals()
|
|
27
28
|
|
|
28
29
|
@staticmethod
|
|
@@ -16,7 +16,10 @@ class Cogent3DataProvider(TreeDataProvider):
|
|
|
16
16
|
def postorder(self) -> Sequence[Any]:
|
|
17
17
|
return self.tree.postorder()
|
|
18
18
|
|
|
19
|
-
def
|
|
19
|
+
def levelorder(self) -> Sequence[Any]:
|
|
20
|
+
return self.tree.levelorder()
|
|
21
|
+
|
|
22
|
+
def _get_leaves(self) -> Sequence[Any]:
|
|
20
23
|
return self.tree.tips()
|
|
21
24
|
|
|
22
25
|
@staticmethod
|
|
@@ -36,7 +36,16 @@ class DendropyDataProvider(TreeDataProvider):
|
|
|
36
36
|
return self.tree.postorder_node_iter()
|
|
37
37
|
return self.tree.postorder_iter()
|
|
38
38
|
|
|
39
|
-
def
|
|
39
|
+
def levelorder(self) -> Any:
|
|
40
|
+
"""Levelorder traversal of the tree.
|
|
41
|
+
|
|
42
|
+
NOTE: This will work on both entire Trees and Nodes (which means a subtree including self).
|
|
43
|
+
"""
|
|
44
|
+
if hasattr(self.tree, "levelorder_node_iter"):
|
|
45
|
+
return self.tree.levelorder_node_iter()
|
|
46
|
+
return self.tree.levelorder_iter()
|
|
47
|
+
|
|
48
|
+
def _get_leaves(self) -> Sequence[Any]:
|
|
40
49
|
"""Get a list of leaves."""
|
|
41
50
|
return self.tree.leaf_nodes()
|
|
42
51
|
|
|
@@ -18,8 +18,9 @@ class Ete4DataProvider(TreeDataProvider):
|
|
|
18
18
|
|
|
19
19
|
preorder = partialmethod(_traverse, order="preorder")
|
|
20
20
|
postorder = partialmethod(_traverse, order="postorder")
|
|
21
|
+
levelorder = partialmethod(_traverse, order="levelorder")
|
|
21
22
|
|
|
22
|
-
def
|
|
23
|
+
def _get_leaves(self) -> Sequence[Any]:
|
|
23
24
|
return self.tree.leaves()
|
|
24
25
|
|
|
25
26
|
@staticmethod
|
|
@@ -3,7 +3,6 @@ from typing import (
|
|
|
3
3
|
Optional,
|
|
4
4
|
Sequence,
|
|
5
5
|
Iterable,
|
|
6
|
-
Self,
|
|
7
6
|
)
|
|
8
7
|
|
|
9
8
|
from ...typing import (
|
|
@@ -19,12 +18,12 @@ class SimpleTree:
|
|
|
19
18
|
branch_length: Length of the branch leading to this node/tree.
|
|
20
19
|
"""
|
|
21
20
|
|
|
22
|
-
children: Sequence
|
|
21
|
+
children: Sequence = []
|
|
23
22
|
branch_length: float = 1
|
|
24
23
|
name: str = ""
|
|
25
24
|
|
|
26
25
|
@classmethod
|
|
27
|
-
def from_dict(cls, data: dict)
|
|
26
|
+
def from_dict(cls, data: dict):
|
|
28
27
|
"""Create a SimpleTree from a dictionary.
|
|
29
28
|
|
|
30
29
|
Parameters:
|
|
@@ -65,7 +64,17 @@ class SimpleTreeDataProvider(TreeDataProvider):
|
|
|
65
64
|
|
|
66
65
|
yield from _recur(self.tree)
|
|
67
66
|
|
|
68
|
-
def
|
|
67
|
+
def levelorder(self) -> Iterable[dict[dict | str, Any]]:
|
|
68
|
+
from collections import deque
|
|
69
|
+
|
|
70
|
+
queue = deque([self.get_root()])
|
|
71
|
+
while queue:
|
|
72
|
+
node = queue.popleft()
|
|
73
|
+
for child in self.get_children(node):
|
|
74
|
+
queue.append(child)
|
|
75
|
+
yield node
|
|
76
|
+
|
|
77
|
+
def _get_leaves(self) -> Sequence[Any]:
|
|
69
78
|
def _recur(node):
|
|
70
79
|
if len(node.children) == 0:
|
|
71
80
|
yield node
|
|
@@ -16,7 +16,10 @@ class SkbioDataProvider(TreeDataProvider):
|
|
|
16
16
|
def postorder(self) -> Sequence[Any]:
|
|
17
17
|
return self.tree.postorder()
|
|
18
18
|
|
|
19
|
-
def
|
|
19
|
+
def levelorder(self) -> Sequence[Any]:
|
|
20
|
+
return self.tree.levelorder()
|
|
21
|
+
|
|
22
|
+
def _get_leaves(self) -> Sequence[Any]:
|
|
20
23
|
return self.tree.tips()
|
|
21
24
|
|
|
22
25
|
@staticmethod
|
iplotx/ingest/typing.py
CHANGED
|
@@ -5,15 +5,21 @@ Networkx and trees are treated separately for practical reasons: many tree analy
|
|
|
5
5
|
work as well on general networks.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import sys
|
|
8
9
|
from typing import (
|
|
9
|
-
NotRequired,
|
|
10
|
-
TypedDict,
|
|
11
|
-
Protocol,
|
|
12
10
|
Optional,
|
|
13
11
|
Sequence,
|
|
14
12
|
Any,
|
|
15
13
|
Iterable,
|
|
16
14
|
)
|
|
15
|
+
|
|
16
|
+
# NOTE: __init__ in Protocols has had a difficult gestation
|
|
17
|
+
# https://github.com/python/cpython/issues/88970
|
|
18
|
+
if sys.version_info < (3, 11):
|
|
19
|
+
Protocol = object
|
|
20
|
+
else:
|
|
21
|
+
from typing import Protocol
|
|
22
|
+
|
|
17
23
|
from collections.abc import Hashable
|
|
18
24
|
import numpy as np
|
|
19
25
|
import pandas as pd
|
|
@@ -26,6 +32,11 @@ from .heuristics import (
|
|
|
26
32
|
normalise_tree_layout,
|
|
27
33
|
)
|
|
28
34
|
|
|
35
|
+
if sys.version_info < (3, 11):
|
|
36
|
+
from typing_extensions import TypedDict, NotRequired
|
|
37
|
+
else:
|
|
38
|
+
from typing import TypedDict, NotRequired
|
|
39
|
+
|
|
29
40
|
|
|
30
41
|
class NetworkData(TypedDict):
|
|
31
42
|
"""Network data structure for iplotx."""
|
|
@@ -146,8 +157,32 @@ class TreeDataProvider(Protocol):
|
|
|
146
157
|
return root_attr
|
|
147
158
|
return self.tree.get_root()
|
|
148
159
|
|
|
149
|
-
def
|
|
150
|
-
"""Get the
|
|
160
|
+
def get_subtree(self, node: TreeType):
|
|
161
|
+
"""Get the subtree rooted at the given node.
|
|
162
|
+
|
|
163
|
+
Parameters:
|
|
164
|
+
node: The node to get the subtree from.
|
|
165
|
+
Returns:
|
|
166
|
+
The subtree rooted at the given node.
|
|
167
|
+
"""
|
|
168
|
+
return self.__class__(node)
|
|
169
|
+
|
|
170
|
+
def get_leaves(self, node: Optional[TreeType] = None) -> Sequence[Any]:
|
|
171
|
+
"""Get the leaves of the entire tree or a subtree.
|
|
172
|
+
|
|
173
|
+
Parameters:
|
|
174
|
+
node: The node to get the leaves from. If None, get from the entire
|
|
175
|
+
tree.
|
|
176
|
+
Returns:
|
|
177
|
+
The leaves or tips of the tree or node-anchored subtree.
|
|
178
|
+
"""
|
|
179
|
+
if node is None:
|
|
180
|
+
return self._get_leaves()
|
|
181
|
+
else:
|
|
182
|
+
return self.get_subtree(node)._get_leaves()
|
|
183
|
+
|
|
184
|
+
def _get_leaves(self) -> Sequence[Any]:
|
|
185
|
+
"""Get the whole tree leaves/tips in a provider-specific data structure.
|
|
151
186
|
|
|
152
187
|
Returns:
|
|
153
188
|
The leaves or tips of the tree.
|
|
@@ -225,8 +260,6 @@ class TreeDataProvider(Protocol):
|
|
|
225
260
|
NOTE: individual providers may implement more efficient versions of
|
|
226
261
|
this function if desired.
|
|
227
262
|
"""
|
|
228
|
-
provider = self.__class__
|
|
229
|
-
|
|
230
263
|
# Find leaves of the selected nodes
|
|
231
264
|
leaves = set()
|
|
232
265
|
for node in nodes:
|
|
@@ -234,7 +267,7 @@ class TreeDataProvider(Protocol):
|
|
|
234
267
|
if len(self.get_children(node)) == 0:
|
|
235
268
|
leaves.add(node)
|
|
236
269
|
else:
|
|
237
|
-
leaves |= set(
|
|
270
|
+
leaves |= set(self.get_leaves(node))
|
|
238
271
|
|
|
239
272
|
# Look for nodes with the same set of leaves, starting from the bottom
|
|
240
273
|
# and stopping at the first (i.e. lowest) hit.
|
|
@@ -243,7 +276,7 @@ class TreeDataProvider(Protocol):
|
|
|
243
276
|
if len(self.get_children(node)) == 0:
|
|
244
277
|
leaves_node = {node}
|
|
245
278
|
else:
|
|
246
|
-
leaves_node = set(
|
|
279
|
+
leaves_node = set(self.get_leaves(node))
|
|
247
280
|
if leaves <= leaves_node:
|
|
248
281
|
root = node
|
|
249
282
|
break
|
|
@@ -275,9 +308,26 @@ class TreeDataProvider(Protocol):
|
|
|
275
308
|
orientation = "right"
|
|
276
309
|
elif layout == "vertical":
|
|
277
310
|
orientation = "descending"
|
|
278
|
-
elif layout
|
|
311
|
+
elif layout in ("radial", "equalangle", "daylight"):
|
|
279
312
|
orientation = "clockwise"
|
|
280
313
|
|
|
314
|
+
# Validate orientation
|
|
315
|
+
valid = (layout == "horizontal") and (orientation in ("right", "left"))
|
|
316
|
+
valid |= (layout == "vertical") and (orientation in ("ascending", "descending"))
|
|
317
|
+
valid |= (layout == "radial") and (
|
|
318
|
+
orientation in ("clockwise", "counterclockwise", "left", "right")
|
|
319
|
+
)
|
|
320
|
+
valid |= (layout == "equalangle") and (
|
|
321
|
+
orientation in ("clockwise", "counterclockwise", "left", "right")
|
|
322
|
+
)
|
|
323
|
+
valid |= (layout == "daylight") and (
|
|
324
|
+
orientation in ("clockwise", "counterclockwise", "left", "right")
|
|
325
|
+
)
|
|
326
|
+
if not valid:
|
|
327
|
+
raise ValueError(
|
|
328
|
+
f"Orientation '{orientation}' is not valid for layout '{layout}'.",
|
|
329
|
+
)
|
|
330
|
+
|
|
281
331
|
tree_data = {
|
|
282
332
|
"root": self.get_root(),
|
|
283
333
|
"rooted": self.is_rooted(),
|
|
@@ -294,8 +344,10 @@ class TreeDataProvider(Protocol):
|
|
|
294
344
|
root=tree_data["root"],
|
|
295
345
|
preorder_fun=self.preorder,
|
|
296
346
|
postorder_fun=self.postorder,
|
|
347
|
+
levelorder_fun=self.levelorder,
|
|
297
348
|
children_fun=self.get_children,
|
|
298
349
|
branch_length_fun=self.get_branch_length_default_to_one,
|
|
350
|
+
leaves_fun=self.get_leaves,
|
|
299
351
|
**layout_style,
|
|
300
352
|
)
|
|
301
353
|
if layout in ("radial",):
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tree layout algorithms.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
)
|
|
8
|
+
from collections.abc import (
|
|
9
|
+
Hashable,
|
|
10
|
+
Callable,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from .rooted import (
|
|
14
|
+
_horizontal_tree_layout,
|
|
15
|
+
_vertical_tree_layout,
|
|
16
|
+
_radial_tree_layout,
|
|
17
|
+
)
|
|
18
|
+
from .unrooted import (
|
|
19
|
+
_equalangle_tree_layout,
|
|
20
|
+
_daylight_tree_layout,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def compute_tree_layout(
|
|
25
|
+
layout: str,
|
|
26
|
+
orientation: str,
|
|
27
|
+
root: Any,
|
|
28
|
+
preorder_fun: Callable,
|
|
29
|
+
postorder_fun: Callable,
|
|
30
|
+
levelorder_fun: Callable,
|
|
31
|
+
children_fun: Callable,
|
|
32
|
+
branch_length_fun: Callable,
|
|
33
|
+
leaves_fun: Callable,
|
|
34
|
+
**kwargs,
|
|
35
|
+
) -> dict[Hashable, list[float]]:
|
|
36
|
+
"""Compute the layout for a tree.
|
|
37
|
+
|
|
38
|
+
Parameters:
|
|
39
|
+
layout: The name of the layout, e.g. "horizontal", "vertical", or "radial".
|
|
40
|
+
orientation: The orientation of the layout, e.g. "right", "left", "descending",
|
|
41
|
+
"ascending", "clockwise", "anticlockwise".
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
A layout dictionary with node positions.
|
|
45
|
+
"""
|
|
46
|
+
kwargs["root"] = root
|
|
47
|
+
kwargs["preorder_fun"] = preorder_fun
|
|
48
|
+
kwargs["postorder_fun"] = postorder_fun
|
|
49
|
+
kwargs["levelorder_fun"] = levelorder_fun
|
|
50
|
+
kwargs["children_fun"] = children_fun
|
|
51
|
+
kwargs["orientation"] = orientation
|
|
52
|
+
kwargs["branch_length_fun"] = branch_length_fun
|
|
53
|
+
kwargs["leaves_fun"] = leaves_fun
|
|
54
|
+
|
|
55
|
+
# Angular or not, the vertex layout is unchanged. Since we do not
|
|
56
|
+
# currently compute an edge layout here, we can ignore the option.
|
|
57
|
+
kwargs.pop("angular", None)
|
|
58
|
+
|
|
59
|
+
if layout == "radial":
|
|
60
|
+
layout_dict = _radial_tree_layout(**kwargs)
|
|
61
|
+
elif layout == "horizontal":
|
|
62
|
+
layout_dict = _horizontal_tree_layout(**kwargs)
|
|
63
|
+
elif layout == "vertical":
|
|
64
|
+
layout_dict = _vertical_tree_layout(**kwargs)
|
|
65
|
+
elif layout == "equalangle":
|
|
66
|
+
layout_dict = _equalangle_tree_layout(**kwargs)
|
|
67
|
+
elif layout == "daylight":
|
|
68
|
+
layout_dict = _daylight_tree_layout(**kwargs)
|
|
69
|
+
else:
|
|
70
|
+
raise ValueError(f"Tree layout not available: {layout}")
|
|
71
|
+
|
|
72
|
+
return layout_dict
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Rooted tree layout for iplotx library.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
5
|
from typing import (
|
|
@@ -14,55 +14,13 @@ from collections.abc import (
|
|
|
14
14
|
import numpy as np
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def compute_tree_layout(
|
|
18
|
-
layout: str,
|
|
19
|
-
orientation: str,
|
|
20
|
-
root: Any,
|
|
21
|
-
preorder_fun: Callable,
|
|
22
|
-
postorder_fun: Callable,
|
|
23
|
-
children_fun: Callable,
|
|
24
|
-
branch_length_fun: Callable,
|
|
25
|
-
**kwargs,
|
|
26
|
-
) -> dict[Hashable, list[float]]:
|
|
27
|
-
"""Compute the layout for a tree.
|
|
28
|
-
|
|
29
|
-
Parameters:
|
|
30
|
-
layout: The name of the layout, e.g. "horizontal", "vertical", or "radial".
|
|
31
|
-
orientation: The orientation of the layout, e.g. "right", "left", "descending",
|
|
32
|
-
"ascending", "clockwise", "anticlockwise".
|
|
33
|
-
|
|
34
|
-
Returns:
|
|
35
|
-
A layout dictionary with node positions.
|
|
36
|
-
"""
|
|
37
|
-
kwargs["root"] = root
|
|
38
|
-
kwargs["preorder_fun"] = preorder_fun
|
|
39
|
-
kwargs["postorder_fun"] = postorder_fun
|
|
40
|
-
kwargs["children_fun"] = children_fun
|
|
41
|
-
kwargs["branch_length_fun"] = branch_length_fun
|
|
42
|
-
kwargs["orientation"] = orientation
|
|
43
|
-
|
|
44
|
-
# Angular or not, the vertex layout is unchanged. Since we do not
|
|
45
|
-
# currently compute an edge layout here, we can ignore the option.
|
|
46
|
-
kwargs.pop("angular", None)
|
|
47
|
-
|
|
48
|
-
if layout == "radial":
|
|
49
|
-
layout_dict = _radial_tree_layout(**kwargs)
|
|
50
|
-
elif layout == "horizontal":
|
|
51
|
-
layout_dict = _horizontal_tree_layout(**kwargs)
|
|
52
|
-
elif layout == "vertical":
|
|
53
|
-
layout_dict = _vertical_tree_layout(**kwargs)
|
|
54
|
-
else:
|
|
55
|
-
raise ValueError(f"Tree layout not available: {layout}")
|
|
56
|
-
|
|
57
|
-
return layout_dict
|
|
58
|
-
|
|
59
|
-
|
|
60
17
|
def _horizontal_tree_layout_right(
|
|
61
18
|
root: Any,
|
|
62
19
|
preorder_fun: Callable,
|
|
63
20
|
postorder_fun: Callable,
|
|
64
21
|
children_fun: Callable,
|
|
65
22
|
branch_length_fun: Callable,
|
|
23
|
+
**kwargs,
|
|
66
24
|
) -> dict[Hashable, list[float]]:
|
|
67
25
|
"""Build a tree layout horizontally, left to right.
|
|
68
26
|
|
|
@@ -173,7 +131,7 @@ def _radial_tree_layout(
|
|
|
173
131
|
360, it leaves a small gap at the end to ensure the first and last leaf
|
|
174
132
|
are not overlapping.
|
|
175
133
|
Returns:
|
|
176
|
-
A dictionary with the
|
|
134
|
+
A dictionary with the layout.
|
|
177
135
|
"""
|
|
178
136
|
# Short form
|
|
179
137
|
th = start * np.pi / 180
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unrooted tree layout for iplotx.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import (
|
|
6
|
+
Any,
|
|
7
|
+
)
|
|
8
|
+
from collections.abc import (
|
|
9
|
+
Hashable,
|
|
10
|
+
Callable,
|
|
11
|
+
)
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _equalangle_tree_layout(
|
|
16
|
+
root: Any,
|
|
17
|
+
preorder_fun: Callable,
|
|
18
|
+
postorder_fun: Callable,
|
|
19
|
+
children_fun: Callable,
|
|
20
|
+
branch_length_fun: Callable,
|
|
21
|
+
leaves_fun: Callable,
|
|
22
|
+
orientation: str = "right",
|
|
23
|
+
start: float = 180,
|
|
24
|
+
span: float = 360,
|
|
25
|
+
**kwargs,
|
|
26
|
+
) -> dict[Hashable, list[float]]:
|
|
27
|
+
"""Equal angle unrooted tree layout.
|
|
28
|
+
|
|
29
|
+
Parameters:
|
|
30
|
+
orientation: Whether the layout fans out towards the right (clockwise) or left
|
|
31
|
+
(anticlockwise).
|
|
32
|
+
start: The starting angle in degrees, default is -180 (left).
|
|
33
|
+
span: The angular span in degrees, default is 360 (full circle). When this is
|
|
34
|
+
360, it leaves a small gap at the end to ensure the first and last leaf
|
|
35
|
+
are not overlapping.
|
|
36
|
+
Returns:
|
|
37
|
+
A dictionary with the layout.
|
|
38
|
+
|
|
39
|
+
Reference: "Inferring Phylogenies" by Joseph Felsenstein, ggtree.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
props = {
|
|
43
|
+
"layout": {},
|
|
44
|
+
"nleaves": {},
|
|
45
|
+
"start": {},
|
|
46
|
+
"end": {},
|
|
47
|
+
"angle": {},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
props["layout"][root] = [0.0, 0.0]
|
|
51
|
+
props["start"][root] = 0.0
|
|
52
|
+
props["end"][root] = 360.0
|
|
53
|
+
props["angle"][root] = 0.0
|
|
54
|
+
|
|
55
|
+
# Count the number of leaves in each subtree
|
|
56
|
+
for node in postorder_fun():
|
|
57
|
+
props["nleaves"][node] = sum(props["nleaves"][child] for child in children_fun(node)) or 1
|
|
58
|
+
|
|
59
|
+
# Set the layout of everyone except the root
|
|
60
|
+
# NOTE: In ggtree, it says "postorder", but I cannot quite imagine how that would work,
|
|
61
|
+
# given that in postorder the root is visited last but it's also the only node about
|
|
62
|
+
# which we know anything at this point.
|
|
63
|
+
for node in preorder_fun():
|
|
64
|
+
nleaves = props["nleaves"][node]
|
|
65
|
+
children = children_fun(node)
|
|
66
|
+
|
|
67
|
+
# Get current node props
|
|
68
|
+
start = props["start"].get(node, 0)
|
|
69
|
+
end = props["end"].get(node, 0)
|
|
70
|
+
cur_x, cur_y = props["layout"].get(node, [0.0, 0.0])
|
|
71
|
+
|
|
72
|
+
total_angle = end - start
|
|
73
|
+
|
|
74
|
+
for child in children:
|
|
75
|
+
nleaves_child = props["nleaves"][child]
|
|
76
|
+
alpha = nleaves_child / nleaves * total_angle
|
|
77
|
+
beta = start + alpha / 2
|
|
78
|
+
|
|
79
|
+
props["layout"][child] = [
|
|
80
|
+
cur_x + branch_length_fun(child) * np.cos(np.radians(beta)),
|
|
81
|
+
cur_y + branch_length_fun(child) * np.sin(np.radians(beta)),
|
|
82
|
+
]
|
|
83
|
+
props["angle"][child] = -90 - beta * np.sign(beta - 180)
|
|
84
|
+
props["start"][child] = start
|
|
85
|
+
props["end"][child] = start + alpha
|
|
86
|
+
start += alpha
|
|
87
|
+
|
|
88
|
+
# FIXME: figure out how to tell the caller about "angle"
|
|
89
|
+
return props["layout"]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _daylight_tree_layout(
|
|
93
|
+
root: Any,
|
|
94
|
+
preorder_fun: Callable,
|
|
95
|
+
postorder_fun: Callable,
|
|
96
|
+
levelorder_fun: Callable,
|
|
97
|
+
children_fun: Callable,
|
|
98
|
+
branch_length_fun: Callable,
|
|
99
|
+
leaves_fun: Callable,
|
|
100
|
+
orientation: str = "right",
|
|
101
|
+
start: float = 180,
|
|
102
|
+
span: float = 360,
|
|
103
|
+
max_iter: int = 5,
|
|
104
|
+
dampening: float = 0.3,
|
|
105
|
+
**kwargs,
|
|
106
|
+
) -> dict[Hashable, list[float]]:
|
|
107
|
+
"""Daylight unrooted tree layout.
|
|
108
|
+
|
|
109
|
+
Parameters:
|
|
110
|
+
orientation: Whether the layout fans out towards the right (clockwise) or left
|
|
111
|
+
(anticlockwise).
|
|
112
|
+
start: The starting angle in degrees, default is -180 (left).
|
|
113
|
+
span: The angular span in degrees, default is 360 (full circle). When this is
|
|
114
|
+
360, it leaves a small gap at the end to ensure the first and last leaf
|
|
115
|
+
are not overlapping.
|
|
116
|
+
max_iter: Maximum number of iterations to perform.
|
|
117
|
+
dampening: Dampening factor for angle adjustments. 1.0 means full adjustment.
|
|
118
|
+
The number must be strictily positive (usually between 0 excluded and 1
|
|
119
|
+
included).
|
|
120
|
+
Returns:
|
|
121
|
+
A dictionary with the layout.
|
|
122
|
+
|
|
123
|
+
Reference: "Inferring Phylogenies" by Joseph Felsenstein, ggtree.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
delta_angle_min = 9.0
|
|
127
|
+
|
|
128
|
+
layout = _equalangle_tree_layout(
|
|
129
|
+
root,
|
|
130
|
+
preorder_fun,
|
|
131
|
+
postorder_fun,
|
|
132
|
+
children_fun,
|
|
133
|
+
branch_length_fun,
|
|
134
|
+
leaves_fun,
|
|
135
|
+
orientation,
|
|
136
|
+
start,
|
|
137
|
+
span,
|
|
138
|
+
**kwargs,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if len(layout) <= 2:
|
|
142
|
+
return layout
|
|
143
|
+
|
|
144
|
+
# Make all arrays for easier manipulation
|
|
145
|
+
orig_class = next(iter(layout.values())).__class__
|
|
146
|
+
for key, value in layout.items():
|
|
147
|
+
layout[key] = np.asarray(value)
|
|
148
|
+
|
|
149
|
+
all_leaves = list(leaves_fun(root))
|
|
150
|
+
|
|
151
|
+
change_avg = 1.0
|
|
152
|
+
for it in range(max_iter):
|
|
153
|
+
change_sum = 0
|
|
154
|
+
ninternal = 0
|
|
155
|
+
parents = [None] + list(levelorder_fun())
|
|
156
|
+
for parent in parents:
|
|
157
|
+
children = children_fun(parent) if parent is not None else [root]
|
|
158
|
+
for node in children:
|
|
159
|
+
res = _apply_daylight_single_node(
|
|
160
|
+
node,
|
|
161
|
+
parent,
|
|
162
|
+
all_leaves,
|
|
163
|
+
layout,
|
|
164
|
+
leaves_fun,
|
|
165
|
+
children_fun,
|
|
166
|
+
dampening,
|
|
167
|
+
)
|
|
168
|
+
change_sum += res
|
|
169
|
+
ninternal += 1
|
|
170
|
+
|
|
171
|
+
change_avg = change_sum / ninternal
|
|
172
|
+
if change_avg < delta_angle_min:
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
# Make all lists again
|
|
176
|
+
for key, value in layout.items():
|
|
177
|
+
layout[key] = orig_class(value)
|
|
178
|
+
|
|
179
|
+
return layout
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _apply_daylight_single_node(
|
|
183
|
+
node: Any,
|
|
184
|
+
parent: Any,
|
|
185
|
+
all_leaves: list[Any],
|
|
186
|
+
layout: dict[Hashable, np.ndarray],
|
|
187
|
+
leaves_fun: Callable,
|
|
188
|
+
children_fun: Callable,
|
|
189
|
+
dampening: float,
|
|
190
|
+
) -> float:
|
|
191
|
+
"""Apply daylight adjustment to a single internal node.
|
|
192
|
+
|
|
193
|
+
Parameters:
|
|
194
|
+
node: The internal node to adjust.
|
|
195
|
+
Returns:
|
|
196
|
+
The total change in angle applied.
|
|
197
|
+
|
|
198
|
+
NOTE: The layout is also changed in place.
|
|
199
|
+
|
|
200
|
+
# Inspired from:
|
|
201
|
+
# https://github.com/thomasp85/ggraph/blob/6c4ce81e460c50a16f9cd97e0b3a089f36901316/src/unrooted.cpp#L122
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
import os
|
|
205
|
+
from builtins import print as _print
|
|
206
|
+
|
|
207
|
+
DEBUG_DAYLIGHT = os.getenv("IPLOTX_DEBUG_DAYLIGHT", "0") == "1"
|
|
208
|
+
|
|
209
|
+
print = _print if DEBUG_DAYLIGHT else lambda *a, **k: None
|
|
210
|
+
|
|
211
|
+
children = children_fun(node)
|
|
212
|
+
|
|
213
|
+
# 1. Find boundary leaves for each child and for the parent
|
|
214
|
+
p0 = layout[node]
|
|
215
|
+
bounds = {}
|
|
216
|
+
|
|
217
|
+
print("node")
|
|
218
|
+
print(node)
|
|
219
|
+
print(float(p0[0]), float(p0[1]))
|
|
220
|
+
|
|
221
|
+
# To find the parent side leaves, we take all leaves and skip the ones
|
|
222
|
+
# downstream of this node
|
|
223
|
+
leaves_below = leaves_fun(node)
|
|
224
|
+
|
|
225
|
+
# Check the parent first if there is one
|
|
226
|
+
if parent is not None:
|
|
227
|
+
vec1 = layout[parent] - p0
|
|
228
|
+
print("parent side leaves:")
|
|
229
|
+
print(parent)
|
|
230
|
+
print(f" node to parent vector: {vec1[0]:.2f}, {vec1[1]:.2f}")
|
|
231
|
+
lower_angle, upper_angle = 2 * np.pi, -2 * np.pi
|
|
232
|
+
for leaf in all_leaves:
|
|
233
|
+
# Skip subtree leaves
|
|
234
|
+
if leaf in leaves_below:
|
|
235
|
+
continue
|
|
236
|
+
vec2 = layout[leaf] - p0
|
|
237
|
+
angle = _anticlockwise_angle(vec1, vec2)
|
|
238
|
+
print(" parent side leaf:")
|
|
239
|
+
print(leaf)
|
|
240
|
+
print(f" node to leaf vector: {vec2[0]:.2f}, {vec2[1]:.2f}")
|
|
241
|
+
print(f" angle: {angle:.2f}")
|
|
242
|
+
if angle < lower_angle:
|
|
243
|
+
lower_angle = angle
|
|
244
|
+
lower = leaf
|
|
245
|
+
if angle > upper_angle:
|
|
246
|
+
upper_angle = angle
|
|
247
|
+
upper = leaf
|
|
248
|
+
bounds[parent] = (lower, upper, lower_angle, upper_angle)
|
|
249
|
+
|
|
250
|
+
# Repeat the exact same thing for each child rather than the parent
|
|
251
|
+
print("subtree leaves:")
|
|
252
|
+
for child in children:
|
|
253
|
+
vec1 = layout[child] - p0
|
|
254
|
+
print(f" node to child vector: {vec1[0]:.2f}, {vec1[1]:.2f}")
|
|
255
|
+
lower_angle, upper_angle = 2 * np.pi, -2 * np.pi
|
|
256
|
+
|
|
257
|
+
for leaf in leaves_fun(child):
|
|
258
|
+
vec2 = layout[leaf] - p0
|
|
259
|
+
angle = _anticlockwise_angle(vec1, vec2)
|
|
260
|
+
print(f" node to leaf vector: {vec2[0]:.2f}, {vec2[1]:.2f}")
|
|
261
|
+
print(leaf)
|
|
262
|
+
print(f" angle: {angle:.2f}")
|
|
263
|
+
if angle < lower_angle:
|
|
264
|
+
lower_angle = angle
|
|
265
|
+
lower = leaf
|
|
266
|
+
if angle > upper_angle:
|
|
267
|
+
upper_angle = angle
|
|
268
|
+
upper = leaf
|
|
269
|
+
bounds[child] = (lower, upper, lower_angle, upper_angle)
|
|
270
|
+
|
|
271
|
+
print("final boundary leaves:")
|
|
272
|
+
print(bounds)
|
|
273
|
+
|
|
274
|
+
# 2. Compute daylight angles
|
|
275
|
+
# NOTE: Since Python 3.6, python keys are ordered by insertion order.
|
|
276
|
+
daylight = {}
|
|
277
|
+
daylight_sum = 0.0
|
|
278
|
+
# TODO: Mayvbe optimise this by avoiding creating all these lists
|
|
279
|
+
prev_leaves = [bound[1] for bound in bounds.values()]
|
|
280
|
+
leaves = [bound[0] for bound in bounds.values()]
|
|
281
|
+
leaves = leaves[1:] + [leaves[0]] # cycle left
|
|
282
|
+
subtrees = children + ([parent] if parent is not None else [])
|
|
283
|
+
for subtree, prev_leaf, leaf in zip(subtrees, prev_leaves, leaves):
|
|
284
|
+
vec1 = layout[prev_leaf] - p0
|
|
285
|
+
vec2 = layout[leaf] - p0
|
|
286
|
+
angle = _anticlockwise_angle(vec1, vec2)
|
|
287
|
+
|
|
288
|
+
daylight_sum += angle
|
|
289
|
+
if leaf != parent:
|
|
290
|
+
# daylight[leaf] = float(angle)
|
|
291
|
+
daylight[subtree] = float(angle)
|
|
292
|
+
|
|
293
|
+
print("daylight")
|
|
294
|
+
print(daylight)
|
|
295
|
+
print(f"daylight sum: {daylight_sum:.2f}")
|
|
296
|
+
|
|
297
|
+
# 3. Compute *excess* daylight, and correct it
|
|
298
|
+
# NOTE: There seems to be this notion that you rotate a node by the accumulated daylight
|
|
299
|
+
# correction (to fill the space on the left) plus its own correction (to fill the space on the right).
|
|
300
|
+
# It reads funny and this is within a BFS iteration anyway so it's probably ok, but it's
|
|
301
|
+
# curious.
|
|
302
|
+
# NOTE: The average adjustment is divided by children + 1 (the parent) bc this is unrooted.
|
|
303
|
+
daylight_avg = daylight_sum / (len(children) + 1)
|
|
304
|
+
daylight_cum_corr = 0.0
|
|
305
|
+
daylight_changes = 0
|
|
306
|
+
for leaf in daylight:
|
|
307
|
+
daylight_cum_corr += daylight_avg - daylight[leaf]
|
|
308
|
+
_rotate_subtree_around_point(
|
|
309
|
+
leaf,
|
|
310
|
+
children_fun,
|
|
311
|
+
layout,
|
|
312
|
+
p0,
|
|
313
|
+
dampening * daylight_cum_corr,
|
|
314
|
+
recur=True,
|
|
315
|
+
)
|
|
316
|
+
daylight_changes += abs(dampening * daylight_cum_corr)
|
|
317
|
+
|
|
318
|
+
# Caller wants degrees
|
|
319
|
+
return np.degrees(daylight_changes / len(leaves))
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# see: https://stackoverflow.com/questions/14066933/direct-way-of-computing-the-clockwise-angle-between-two-vectors
|
|
323
|
+
def _anticlockwise_angle(v1, v2):
|
|
324
|
+
"""Compute the anticlockwise angle between two 2D vectors.
|
|
325
|
+
|
|
326
|
+
Parameters:
|
|
327
|
+
v1: First vector.
|
|
328
|
+
v2: Second vector.
|
|
329
|
+
Returns:
|
|
330
|
+
The angle in radians.
|
|
331
|
+
"""
|
|
332
|
+
dot = v1[0] * v2[0] + v1[1] * v2[1]
|
|
333
|
+
determinant = v1[0] * v2[1] - v1[1] * v2[0]
|
|
334
|
+
return np.arctan2(determinant, dot)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _rotate_subtree_around_point(
|
|
338
|
+
node,
|
|
339
|
+
children_fun: Callable,
|
|
340
|
+
layout: dict[Hashable, list[float]],
|
|
341
|
+
pivot,
|
|
342
|
+
angle,
|
|
343
|
+
recur: bool = True,
|
|
344
|
+
):
|
|
345
|
+
point = np.asarray(layout[node])
|
|
346
|
+
pivot = np.asarray(pivot)
|
|
347
|
+
layout[node] = _rotate_around_point(
|
|
348
|
+
point,
|
|
349
|
+
pivot,
|
|
350
|
+
angle,
|
|
351
|
+
)
|
|
352
|
+
if not recur:
|
|
353
|
+
return
|
|
354
|
+
for child in children_fun(node):
|
|
355
|
+
_rotate_subtree_around_point(
|
|
356
|
+
child,
|
|
357
|
+
children_fun,
|
|
358
|
+
layout,
|
|
359
|
+
pivot,
|
|
360
|
+
angle,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _rotate_around_point(
|
|
365
|
+
point,
|
|
366
|
+
pivot,
|
|
367
|
+
angle,
|
|
368
|
+
):
|
|
369
|
+
"""Rotate a point around a piviot by angle (in radians).
|
|
370
|
+
|
|
371
|
+
Parameters:
|
|
372
|
+
point: The point to rotate.
|
|
373
|
+
pivot: The piviot point.
|
|
374
|
+
angle: The angle in radians.
|
|
375
|
+
Returns:
|
|
376
|
+
The rotated point.
|
|
377
|
+
"""
|
|
378
|
+
point = np.asarray(point)
|
|
379
|
+
pivot = np.asarray(pivot)
|
|
380
|
+
cos = np.cos(angle)
|
|
381
|
+
sin = np.sin(angle)
|
|
382
|
+
rot = np.array([[cos, sin], [-sin, cos]])
|
|
383
|
+
return pivot + (point - pivot) @ rot
|
iplotx/network/__init__.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from typing import (
|
|
2
2
|
Optional,
|
|
3
3
|
Sequence,
|
|
4
|
-
Self,
|
|
5
4
|
)
|
|
6
5
|
import numpy as np
|
|
7
6
|
import pandas as pd
|
|
@@ -103,8 +102,8 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
103
102
|
@classmethod
|
|
104
103
|
def from_other(
|
|
105
104
|
cls: "NetworkArtist", # NOTE: This is fixed in Python 3.14
|
|
106
|
-
other
|
|
107
|
-
)
|
|
105
|
+
other,
|
|
106
|
+
):
|
|
108
107
|
"""Create a NetworkArtist as a copy of another one.
|
|
109
108
|
|
|
110
109
|
Parameters:
|
|
@@ -123,7 +122,7 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
123
122
|
def from_edgecollection(
|
|
124
123
|
cls: "NetworkArtist", # NOTE: This is fixed in Python 3.14
|
|
125
124
|
edge_collection: EdgeCollection | Edge3DCollection,
|
|
126
|
-
)
|
|
125
|
+
):
|
|
127
126
|
"""Create a NetworkArtist from iplotx artists.
|
|
128
127
|
|
|
129
128
|
Parameters:
|
iplotx/tree/__init__.py
CHANGED
|
@@ -445,7 +445,7 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
445
445
|
leaf_layout = self.get_layout("leaf").copy()
|
|
446
446
|
|
|
447
447
|
# Set all to max depth
|
|
448
|
-
if user_leaf_style.get("deep",
|
|
448
|
+
if user_leaf_style.get("deep", False):
|
|
449
449
|
if layout_name == "radial":
|
|
450
450
|
leaf_layout.iloc[:, 0] = leaf_layout.iloc[:, 0].max()
|
|
451
451
|
elif (layout_name, orientation) == ("horizontal", "right"):
|
|
@@ -666,7 +666,10 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
666
666
|
norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
|
|
667
667
|
edge_style["norm"] = norm
|
|
668
668
|
|
|
669
|
-
|
|
669
|
+
angular_layout = get_style(".layout", {}).get("angular", False)
|
|
670
|
+
if self._ipx_internal_data["layout_name"] in ("equalangle", "daylight"):
|
|
671
|
+
angular_layout = True
|
|
672
|
+
if angular_layout:
|
|
670
673
|
edge_style.pop("waypoints", None)
|
|
671
674
|
else:
|
|
672
675
|
edge_style["waypoints"] = waypoints
|
iplotx/version.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iplotx
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Plot networkx from igraph and networkx.
|
|
5
5
|
Project-URL: Homepage, https://github.com/fabilab/iplotx
|
|
6
6
|
Project-URL: Documentation, https://readthedocs.org/iplotx
|
|
@@ -26,8 +26,8 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
26
26
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
27
27
|
Classifier: Topic :: System :: Networking
|
|
28
28
|
Classifier: Typing :: Typed
|
|
29
|
-
Requires-Python: >=3.
|
|
30
|
-
Requires-Dist: matplotlib>=
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Requires-Dist: matplotlib>=3.10.0
|
|
31
31
|
Requires-Dist: numpy>=2.0.0
|
|
32
32
|
Requires-Dist: pandas>=2.0.0
|
|
33
33
|
Provides-Extra: igraph
|
|
@@ -36,9 +36,9 @@ Provides-Extra: networkx
|
|
|
36
36
|
Requires-Dist: networkx>=2.0.0; extra == 'networkx'
|
|
37
37
|
Description-Content-Type: text/markdown
|
|
38
38
|
|
|
39
|
-

|
|
40
|
-

|
|
41
|
-

|
|
39
|
+
[](https://github.com/fabilab/iplotx/actions/workflows/test.yml)
|
|
40
|
+
[](https://pypi.org/project/iplotx/)
|
|
41
|
+
[](https://iplotx.readthedocs.io/en/latest/)
|
|
42
42
|
[](https://coveralls.io/github/fabilab/iplotx?branch=main)
|
|
43
43
|

|
|
44
44
|
[](https://doi.org/10.5281/zenodo.16599333)
|
|
@@ -89,6 +89,13 @@ See [readthedocs](https://iplotx.readthedocs.io/en/latest/) for the full documen
|
|
|
89
89
|
## Gallery
|
|
90
90
|
See [gallery](https://iplotx.readthedocs.io/en/latest/gallery/index.html).
|
|
91
91
|
|
|
92
|
+
## Citation
|
|
93
|
+
If you use `iplotx` for publication figures, please cite the [zenodo preprint](https://doi.org/10.5281/zenodo.16599333):
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
F. Zanini. (2025). Unified network visualisation in Python. Zenodo [PREPRINT]. https://doi.org/10.5281/zenodo.16599333
|
|
97
|
+
```
|
|
98
|
+
|
|
92
99
|
## Features
|
|
93
100
|
- Plot networks from multiple libraries including networkx, igraph and graph-tool, using Matplotlib. ✅
|
|
94
101
|
- Plot trees from multiple libraries such as cogent3, ETE4, skbio, biopython, and dendropy. ✅
|
|
@@ -1,45 +1,48 @@
|
|
|
1
1
|
iplotx/__init__.py,sha256=RKlRSSEAv2qECd6rCiovdLDu-4k1eXMGCOCPt0xwpFA,523
|
|
2
2
|
iplotx/artists.py,sha256=2dBDT240zGwKb6tIc_y9pXeyU3LuYeF9wjj2tvi4KJo,730
|
|
3
3
|
iplotx/label.py,sha256=7eS8ByadrhdIFOZz19U4VrS-oXY_ndFYNB-D4RZbFqI,9573
|
|
4
|
-
iplotx/layout.py,sha256=S-iFxHaIOzhBDG2JUzl9_oDBRP5TYY1hXnEOs0h1Rck,5588
|
|
5
4
|
iplotx/plotting.py,sha256=RyAdvaHSpuyJkf8DF3SJBvEXBrPmJEdovUyAlBWQvqU,16228
|
|
6
5
|
iplotx/typing.py,sha256=QLdzV358IiD1CFe88MVp0D77FSx5sSAVUmM_2WPPE8I,1463
|
|
7
|
-
iplotx/version.py,sha256=
|
|
6
|
+
iplotx/version.py,sha256=k2KjblHRw7cpH2h7LRTJqlcSXCjDH-WN6aCDQlzMEH4,66
|
|
8
7
|
iplotx/vertex.py,sha256=_yYyvusn4vYvi6RBEW6CHa3vnbv43GnZylnMIaK4bG0,16040
|
|
9
8
|
iplotx/art3d/vertex.py,sha256=Xf8Um30X2doCd8KdNN7332F6BxC4k72Mb_GeRAuzQfQ,2545
|
|
10
9
|
iplotx/art3d/edge/__init__.py,sha256=uw1U_mMXqcZAvea-7JbU1PUKULQD1CMMrbwY02tiWRQ,8529
|
|
11
10
|
iplotx/art3d/edge/arrow.py,sha256=14BFXY9kDOUGPZl2fMD9gRVGyaaN5kyd-l6ikBg6WHU,3601
|
|
12
11
|
iplotx/art3d/edge/geometry.py,sha256=76VUmpPG-4Mls7x_994dMwdDPrWWnjT7nHJsHfwK_hA,2467
|
|
13
12
|
iplotx/edge/__init__.py,sha256=GlY_CmlMVD1DWJeOb5cFQEP_4K1pPneS_fRLqAxNGMk,26573
|
|
14
|
-
iplotx/edge/arrow.py,sha256=
|
|
13
|
+
iplotx/edge/arrow.py,sha256=ymup2YT_0GVYMtZw_DSKrZqFHG_ysYteEhmoL6T8Mu4,17563
|
|
15
14
|
iplotx/edge/geometry.py,sha256=jkTMvQC5425GjB_fmGLIPJeSDAr_7NZF8zZDLTrSj34,15541
|
|
16
15
|
iplotx/edge/leaf.py,sha256=SyGMv2PIOoH0pey8-aMVaZheK3hNe1Qz_okcyWbc4E4,4268
|
|
17
16
|
iplotx/edge/ports.py,sha256=BpkbiEhX4mPBBAhOv4jcKFG4Y8hxXz5GRtVLCC0jbtI,1235
|
|
18
|
-
iplotx/ingest/__init__.py,sha256=
|
|
17
|
+
iplotx/ingest/__init__.py,sha256=k1Q-7lSdotMR4RkF1x0t19RFsTknohX0L507Dw69WyU,5035
|
|
19
18
|
iplotx/ingest/heuristics.py,sha256=715VqgfKek5LOJnu1vTo7RqPgCl-Bb8Cf6o7_Tt57fA,5797
|
|
20
|
-
iplotx/ingest/typing.py,sha256=
|
|
19
|
+
iplotx/ingest/typing.py,sha256=VYWlNGwn047J0i10vC_F3Lfnx2CSiX6IeH_NtAgHLQg,16089
|
|
21
20
|
iplotx/ingest/providers/network/graph_tool.py,sha256=iTCf4zHe4Zmdd8Tlz6j7Xfo_FwfsIiK5JkQfH3uq7TM,3028
|
|
22
21
|
iplotx/ingest/providers/network/igraph.py,sha256=WL9Yx2IF5QhUIoKMlozdyq5HWIZ-IJmNoeS8GOhL0KU,2945
|
|
23
22
|
iplotx/ingest/providers/network/networkx.py,sha256=ehCg4npL073HX-eAG-VoP6refLPsMb3lYG51xt_rNjA,4636
|
|
24
23
|
iplotx/ingest/providers/network/simple.py,sha256=e_aHhiHhN9DrMoNrt7tEMPURXGhQ1TYRPzsxDEptUlc,3766
|
|
25
|
-
iplotx/ingest/providers/tree/biopython.py,sha256=
|
|
26
|
-
iplotx/ingest/providers/tree/cogent3.py,sha256=
|
|
27
|
-
iplotx/ingest/providers/tree/dendropy.py,sha256=
|
|
28
|
-
iplotx/ingest/providers/tree/ete4.py,sha256=
|
|
29
|
-
iplotx/ingest/providers/tree/simple.py,sha256=
|
|
30
|
-
iplotx/ingest/providers/tree/skbio.py,sha256=
|
|
31
|
-
iplotx/
|
|
24
|
+
iplotx/ingest/providers/tree/biopython.py,sha256=si-ncMVHrdbwDVspznFbO7ajTaa37gqSfYGKdx9eoQ8,1538
|
|
25
|
+
iplotx/ingest/providers/tree/cogent3.py,sha256=6omk0cDSmb2k2J1BQ9depcSPWcfamSyS0eIlRpLRgoM,1156
|
|
26
|
+
iplotx/ingest/providers/tree/dendropy.py,sha256=MRmPdlrPwqCQWLkKM4vJLN4hpqLY7qGtNrBYs5pLP9E,1891
|
|
27
|
+
iplotx/ingest/providers/tree/ete4.py,sha256=JT0Zv-nzHKTSJ0h4I7SxkwFafww_siDZ06Krvz-BT1U,991
|
|
28
|
+
iplotx/ingest/providers/tree/simple.py,sha256=hin38l9ZAnuDGwZAPctxNhIBrmIkQYIkBMFPt9uALuE,2877
|
|
29
|
+
iplotx/ingest/providers/tree/skbio.py,sha256=T3IbOBut98A2GoGJzo6Tzp108uFa9n485mV4M9J5xFk,905
|
|
30
|
+
iplotx/layout/__init__.py,sha256=7on7I9CcbByz4X4hNJGsCc0GDFr3ul1K2UvTyvcGAWo,107
|
|
31
|
+
iplotx/layout/tree/__init__.py,sha256=hxASc8uXMWbpxnEHnChMzb3VQTTIyU4ww7SQRez1hK0,2000
|
|
32
|
+
iplotx/layout/tree/rooted.py,sha256=j3Y_Yd3YCWJRnhfE1qwEX5xCyUgvOJCX9Qfkc81B5BM,4215
|
|
33
|
+
iplotx/layout/tree/unrooted.py,sha256=AH_8f4runflAs1bqXECeTDjtGDmFfO4afJf5JllS1lA,11716
|
|
34
|
+
iplotx/network/__init__.py,sha256=cJ6m6s157AOCqg-znUAlsumuZ2jiE9QsVQ3-GCK01wo,13543
|
|
32
35
|
iplotx/network/groups.py,sha256=E_eYVXRHjv1DcyA4RupTkMa-rRFrIKkt9Rxn_Elw9Nc,6796
|
|
33
36
|
iplotx/style/__init__.py,sha256=rf1GutrE8hHUhCoe4FGKYX-aNtHuu_U-kYQnqUxZNrY,10282
|
|
34
37
|
iplotx/style/leaf_info.py,sha256=3xBn7xv9Uy2KAqdhM9S6ew5ZBJrGRTXRL3xXb8atfLw,1018
|
|
35
38
|
iplotx/style/library.py,sha256=58Y8BlllGLsR4pQM7_PVCP5tH6_4GkchXZvJpqGHlcg,8534
|
|
36
|
-
iplotx/tree/__init__.py,sha256=
|
|
39
|
+
iplotx/tree/__init__.py,sha256=sxVrxZcsDIZddBdtI-TxOs37FyeYbHBLYqkLPWs6c8M,31199
|
|
37
40
|
iplotx/tree/cascades.py,sha256=Wwqhy46QGeb4LNGUuz_-bgNWUMz6PFzs_dIxIb1dtqc,8394
|
|
38
41
|
iplotx/tree/scalebar.py,sha256=Yxt_kF8JdTwKGa8Jzqt3qVePPK5ZBG8P0EiONrsh3E8,11863
|
|
39
42
|
iplotx/utils/geometry.py,sha256=6RrC6qaB0-1vIk1LhGA4CfsiMd-9JNniSPyL_l9mshE,9245
|
|
40
43
|
iplotx/utils/internal.py,sha256=WWfcZDGK8Ut1y_tOHRGg9wSqY1bwSeLQO7dHM_8Tvwo,107
|
|
41
44
|
iplotx/utils/matplotlib.py,sha256=p_53Oamof0RI4mtV8HrdDtZbgVqUxeUZ_KDvLZSiBUQ,8604
|
|
42
45
|
iplotx/utils/style.py,sha256=vyNP80nDYVinqm6_9ltCJCtjK35ZcGlHvOskNv3eQBc,4225
|
|
43
|
-
iplotx-1.
|
|
44
|
-
iplotx-1.
|
|
45
|
-
iplotx-1.
|
|
46
|
+
iplotx-1.3.0.dist-info/METADATA,sha256=laDlBsyQ5RwK1WeA4Hm_3uusbpJGxNifWJZu_0JHYYI,5407
|
|
47
|
+
iplotx-1.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
48
|
+
iplotx-1.3.0.dist-info/RECORD,,
|
|
File without changes
|