iplotx 1.2.1__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/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 +11 -1
- iplotx/ingest/providers/tree/skbio.py +4 -1
- iplotx/ingest/typing.py +49 -7
- 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/tree/__init__.py +5 -2
- iplotx/version.py +1 -1
- {iplotx-1.2.1.dist-info → iplotx-1.3.0.dist-info}/METADATA +11 -4
- {iplotx-1.2.1.dist-info → iplotx-1.3.0.dist-info}/RECORD +16 -13
- {iplotx-1.2.1.dist-info → iplotx-1.3.0.dist-info}/WHEEL +0 -0
|
@@ -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
|
|
@@ -64,7 +64,17 @@ class SimpleTreeDataProvider(TreeDataProvider):
|
|
|
64
64
|
|
|
65
65
|
yield from _recur(self.tree)
|
|
66
66
|
|
|
67
|
-
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]:
|
|
68
78
|
def _recur(node):
|
|
69
79
|
if len(node.children) == 0:
|
|
70
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
|
@@ -12,6 +12,7 @@ from typing import (
|
|
|
12
12
|
Any,
|
|
13
13
|
Iterable,
|
|
14
14
|
)
|
|
15
|
+
|
|
15
16
|
# NOTE: __init__ in Protocols has had a difficult gestation
|
|
16
17
|
# https://github.com/python/cpython/issues/88970
|
|
17
18
|
if sys.version_info < (3, 11):
|
|
@@ -156,8 +157,32 @@ class TreeDataProvider(Protocol):
|
|
|
156
157
|
return root_attr
|
|
157
158
|
return self.tree.get_root()
|
|
158
159
|
|
|
159
|
-
def
|
|
160
|
-
"""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.
|
|
161
186
|
|
|
162
187
|
Returns:
|
|
163
188
|
The leaves or tips of the tree.
|
|
@@ -235,8 +260,6 @@ class TreeDataProvider(Protocol):
|
|
|
235
260
|
NOTE: individual providers may implement more efficient versions of
|
|
236
261
|
this function if desired.
|
|
237
262
|
"""
|
|
238
|
-
provider = self.__class__
|
|
239
|
-
|
|
240
263
|
# Find leaves of the selected nodes
|
|
241
264
|
leaves = set()
|
|
242
265
|
for node in nodes:
|
|
@@ -244,7 +267,7 @@ class TreeDataProvider(Protocol):
|
|
|
244
267
|
if len(self.get_children(node)) == 0:
|
|
245
268
|
leaves.add(node)
|
|
246
269
|
else:
|
|
247
|
-
leaves |= set(
|
|
270
|
+
leaves |= set(self.get_leaves(node))
|
|
248
271
|
|
|
249
272
|
# Look for nodes with the same set of leaves, starting from the bottom
|
|
250
273
|
# and stopping at the first (i.e. lowest) hit.
|
|
@@ -253,7 +276,7 @@ class TreeDataProvider(Protocol):
|
|
|
253
276
|
if len(self.get_children(node)) == 0:
|
|
254
277
|
leaves_node = {node}
|
|
255
278
|
else:
|
|
256
|
-
leaves_node = set(
|
|
279
|
+
leaves_node = set(self.get_leaves(node))
|
|
257
280
|
if leaves <= leaves_node:
|
|
258
281
|
root = node
|
|
259
282
|
break
|
|
@@ -285,9 +308,26 @@ class TreeDataProvider(Protocol):
|
|
|
285
308
|
orientation = "right"
|
|
286
309
|
elif layout == "vertical":
|
|
287
310
|
orientation = "descending"
|
|
288
|
-
elif layout
|
|
311
|
+
elif layout in ("radial", "equalangle", "daylight"):
|
|
289
312
|
orientation = "clockwise"
|
|
290
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
|
+
|
|
291
331
|
tree_data = {
|
|
292
332
|
"root": self.get_root(),
|
|
293
333
|
"rooted": self.is_rooted(),
|
|
@@ -304,8 +344,10 @@ class TreeDataProvider(Protocol):
|
|
|
304
344
|
root=tree_data["root"],
|
|
305
345
|
preorder_fun=self.preorder,
|
|
306
346
|
postorder_fun=self.postorder,
|
|
347
|
+
levelorder_fun=self.levelorder,
|
|
307
348
|
children_fun=self.get_children,
|
|
308
349
|
branch_length_fun=self.get_branch_length_default_to_one,
|
|
350
|
+
leaves_fun=self.get_leaves,
|
|
309
351
|
**layout_style,
|
|
310
352
|
)
|
|
311
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/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
|
|
@@ -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,10 +1,9 @@
|
|
|
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
|
|
@@ -17,29 +16,33 @@ iplotx/edge/leaf.py,sha256=SyGMv2PIOoH0pey8-aMVaZheK3hNe1Qz_okcyWbc4E4,4268
|
|
|
17
16
|
iplotx/edge/ports.py,sha256=BpkbiEhX4mPBBAhOv4jcKFG4Y8hxXz5GRtVLCC0jbtI,1235
|
|
18
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=
|
|
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
|
|
31
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
|