iplotx 0.2.1__py3-none-any.whl → 0.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/cascades.py +223 -0
- iplotx/edge/__init__.py +20 -1
- iplotx/edge/geometry.py +72 -16
- iplotx/ingest/__init__.py +12 -4
- iplotx/ingest/heuristics.py +1 -3
- iplotx/ingest/providers/network/igraph.py +4 -2
- iplotx/ingest/providers/network/networkx.py +4 -2
- iplotx/ingest/providers/tree/biopython.py +21 -79
- iplotx/ingest/providers/tree/cogent3.py +17 -88
- iplotx/ingest/providers/tree/ete4.py +19 -87
- iplotx/ingest/providers/tree/skbio.py +17 -88
- iplotx/ingest/typing.py +225 -22
- iplotx/label.py +55 -8
- iplotx/layout.py +56 -35
- iplotx/plotting.py +6 -3
- iplotx/style.py +19 -5
- iplotx/tree.py +189 -8
- iplotx/version.py +1 -1
- iplotx/vertex.py +39 -7
- {iplotx-0.2.1.dist-info → iplotx-0.3.0.dist-info}/METADATA +2 -1
- iplotx-0.3.0.dist-info/RECORD +32 -0
- iplotx-0.2.1.dist-info/RECORD +0 -31
- {iplotx-0.2.1.dist-info → iplotx-0.3.0.dist-info}/WHEEL +0 -0
iplotx/label.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import (
|
|
|
7
7
|
Sequence,
|
|
8
8
|
)
|
|
9
9
|
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
10
11
|
import matplotlib as mpl
|
|
11
12
|
|
|
12
13
|
from .style import (
|
|
@@ -39,7 +40,7 @@ class LabelCollection(mpl.artist.Artist):
|
|
|
39
40
|
|
|
40
41
|
def __init__(
|
|
41
42
|
self,
|
|
42
|
-
labels:
|
|
43
|
+
labels: pd.Series,
|
|
43
44
|
style: Optional[dict[str, dict]] = None,
|
|
44
45
|
offsets: Optional[np.ndarray] = None,
|
|
45
46
|
transform: mpl.transforms.Transform = mpl.transforms.IdentityTransform(),
|
|
@@ -97,6 +98,11 @@ class LabelCollection(mpl.artist.Artist):
|
|
|
97
98
|
vmargin = stylei.pop("vmargin", 0.0)
|
|
98
99
|
margins.append((hmargin, vmargin))
|
|
99
100
|
|
|
101
|
+
# Initially, ignore autoalignment since we do not know the
|
|
102
|
+
# rotations
|
|
103
|
+
if stylei.get("horizontalalignment") == "auto":
|
|
104
|
+
stylei["horizontalalignment"] = "center"
|
|
105
|
+
|
|
100
106
|
art = mpl.text.Text(
|
|
101
107
|
self._offsets[i][0],
|
|
102
108
|
self._offsets[i][1],
|
|
@@ -107,15 +113,20 @@ class LabelCollection(mpl.artist.Artist):
|
|
|
107
113
|
arts.append(art)
|
|
108
114
|
self._labelartists = arts
|
|
109
115
|
self._margins = np.array(margins)
|
|
116
|
+
self._rotations = np.zeros(len(self._labels))
|
|
110
117
|
|
|
111
118
|
def _update_offsets(self, dpi: float = 72.0) -> None:
|
|
112
119
|
"""Update offsets including margins."""
|
|
113
|
-
|
|
114
|
-
self.set_offsets(offsets)
|
|
120
|
+
self.set_offsets(self._offsets, dpi=dpi)
|
|
115
121
|
|
|
116
|
-
def get_offsets(self) -> np.ndarray:
|
|
122
|
+
def get_offsets(self, with_margins: bool = False) -> np.ndarray:
|
|
117
123
|
"""Get the positions (offsets) of the labels."""
|
|
118
|
-
|
|
124
|
+
if not with_margins:
|
|
125
|
+
return self._offsets
|
|
126
|
+
else:
|
|
127
|
+
return np.array(
|
|
128
|
+
[art.get_position() for art in self._labelartists],
|
|
129
|
+
)
|
|
119
130
|
|
|
120
131
|
def _adjust_offsets_for_margins(self, offsets, dpi=72.0):
|
|
121
132
|
margins = self._get_margins_with_dpi(dpi=dpi)
|
|
@@ -123,31 +134,67 @@ class LabelCollection(mpl.artist.Artist):
|
|
|
123
134
|
transform = self.get_transform()
|
|
124
135
|
trans = transform.transform
|
|
125
136
|
trans_inv = transform.inverted().transform
|
|
126
|
-
|
|
137
|
+
rotations = self.get_rotations()
|
|
138
|
+
vrot = [np.cos(rotations), np.sin(rotations)]
|
|
139
|
+
|
|
140
|
+
margins_rot = np.empty_like(margins)
|
|
141
|
+
margins_rot[:, 0] = margins[:, 0] * vrot[0] - margins[:, 1] * vrot[1]
|
|
142
|
+
margins_rot[:, 1] = margins[:, 0] * vrot[1] + margins[:, 1] * vrot[0]
|
|
143
|
+
offsets = trans_inv(trans(offsets) + margins_rot)
|
|
127
144
|
return offsets
|
|
128
145
|
|
|
129
|
-
def set_offsets(self, offsets) -> None:
|
|
146
|
+
def set_offsets(self, offsets, dpi: float = 72.0) -> None:
|
|
130
147
|
"""Set positions (offsets) of the labels.
|
|
131
148
|
|
|
132
149
|
Parameters:
|
|
133
150
|
offsets: A sequence of offsets for each label, specifying the position of the label.
|
|
134
151
|
"""
|
|
135
152
|
self._offsets = np.asarray(offsets)
|
|
136
|
-
|
|
153
|
+
offsets_with_margins = self._adjust_offsets_for_margins(offsets, dpi=dpi)
|
|
154
|
+
for art, offset in zip(self._labelartists, offsets_with_margins):
|
|
137
155
|
art.set_position((offset[0], offset[1]))
|
|
138
156
|
|
|
157
|
+
def get_rotations(self) -> np.ndarray:
|
|
158
|
+
"""Get the rotations of the labels in radians."""
|
|
159
|
+
return self._rotations
|
|
160
|
+
|
|
139
161
|
def set_rotations(self, rotations: Sequence[float]) -> None:
|
|
140
162
|
"""Set the rotations of the labels.
|
|
141
163
|
|
|
142
164
|
Parameters:
|
|
143
165
|
rotations: A sequence of rotations in radians for each label.
|
|
144
166
|
"""
|
|
167
|
+
self._rotations = np.asarray(rotations)
|
|
168
|
+
ha = self._style.get("horizontalalignment", "center")
|
|
145
169
|
for art, rotation in zip(self._labelartists, rotations):
|
|
146
170
|
rot_deg = 180.0 / np.pi * rotation
|
|
147
171
|
# Force the font size to be upwards
|
|
172
|
+
if ha == "auto":
|
|
173
|
+
if -90 <= rot_deg < 90:
|
|
174
|
+
art.set_horizontalalignment("left")
|
|
175
|
+
else:
|
|
176
|
+
art.set_horizontalalignment("right")
|
|
148
177
|
rot_deg = ((rot_deg + 90) % 180) - 90
|
|
149
178
|
art.set_rotation(rot_deg)
|
|
150
179
|
|
|
180
|
+
def get_datalim(self, transData=None) -> mpl.transforms.Bbox:
|
|
181
|
+
"""Get the data limits of the labels."""
|
|
182
|
+
bboxes = self.get_datalims_children(transData=transData)
|
|
183
|
+
bbox = mpl.transforms.Bbox.union(bboxes)
|
|
184
|
+
return bbox
|
|
185
|
+
|
|
186
|
+
def get_datalims_children(self, transData=None) -> Sequence[mpl.transforms.Bbox]:
|
|
187
|
+
"""Get the data limits of the children of this artist."""
|
|
188
|
+
if transData is None:
|
|
189
|
+
transData = self.get_transform()
|
|
190
|
+
trans_inv = transData.inverted().transform_bbox
|
|
191
|
+
bboxes = []
|
|
192
|
+
for art in self._labelartists:
|
|
193
|
+
bbox_fig = art.get_bbox_patch().get_extents()
|
|
194
|
+
bbox_data = trans_inv(bbox_fig)
|
|
195
|
+
bboxes.append(bbox_data)
|
|
196
|
+
return bboxes
|
|
197
|
+
|
|
151
198
|
@_stale_wrapper
|
|
152
199
|
def draw(self, renderer) -> None:
|
|
153
200
|
"""Draw each of the children, with some buffering mechanism."""
|
iplotx/layout.py
CHANGED
|
@@ -2,35 +2,48 @@
|
|
|
2
2
|
Layout functions, currently limited to trees.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from typing import Any
|
|
6
|
+
from collections.abc import (
|
|
7
|
+
Hashable,
|
|
8
|
+
Callable,
|
|
9
|
+
)
|
|
6
10
|
|
|
7
11
|
import numpy as np
|
|
8
12
|
|
|
9
13
|
|
|
10
14
|
def compute_tree_layout(
|
|
11
|
-
tree,
|
|
12
15
|
layout: str,
|
|
13
16
|
orientation: str,
|
|
17
|
+
root: Any,
|
|
18
|
+
preorder_fun: Callable,
|
|
19
|
+
postorder_fun: Callable,
|
|
20
|
+
children_fun: Callable,
|
|
21
|
+
branch_length_fun: Callable,
|
|
14
22
|
**kwargs,
|
|
15
23
|
) -> dict[Hashable, list[float]]:
|
|
16
24
|
"""Compute the layout for a tree.
|
|
17
25
|
|
|
18
26
|
Parameters:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"ascending".
|
|
27
|
+
layout: The name of the layout, e.g. "horizontal", "vertial", or "radial".
|
|
28
|
+
orientation: The orientation of the layout, e.g. "right", "left", "descending",
|
|
29
|
+
"ascending", "clockwise", "anticlockwise".
|
|
23
30
|
|
|
24
31
|
Returns:
|
|
25
32
|
A layout dictionary with node positions.
|
|
26
33
|
"""
|
|
34
|
+
kwargs["root"] = root
|
|
35
|
+
kwargs["preorder_fun"] = preorder_fun
|
|
36
|
+
kwargs["postorder_fun"] = postorder_fun
|
|
37
|
+
kwargs["children_fun"] = children_fun
|
|
38
|
+
kwargs["branch_length_fun"] = branch_length_fun
|
|
39
|
+
kwargs["orientation"] = orientation
|
|
27
40
|
|
|
28
41
|
if layout == "radial":
|
|
29
|
-
layout_dict =
|
|
42
|
+
layout_dict = _radial_tree_layout(**kwargs)
|
|
30
43
|
elif layout == "horizontal":
|
|
31
|
-
layout_dict = _horizontal_tree_layout(
|
|
44
|
+
layout_dict = _horizontal_tree_layout(**kwargs)
|
|
32
45
|
elif layout == "vertical":
|
|
33
|
-
layout_dict = _vertical_tree_layout(
|
|
46
|
+
layout_dict = _vertical_tree_layout(**kwargs)
|
|
34
47
|
else:
|
|
35
48
|
raise ValueError(f"Tree layout not available: {layout}")
|
|
36
49
|
|
|
@@ -38,12 +51,11 @@ def compute_tree_layout(
|
|
|
38
51
|
|
|
39
52
|
|
|
40
53
|
def _horizontal_tree_layout_right(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
branch_length_fun: callable,
|
|
54
|
+
root: Any,
|
|
55
|
+
preorder_fun: Callable,
|
|
56
|
+
postorder_fun: Callable,
|
|
57
|
+
children_fun: Callable,
|
|
58
|
+
branch_length_fun: Callable,
|
|
47
59
|
) -> dict[Hashable, list[float]]:
|
|
48
60
|
"""Build a tree layout horizontally, left to right.
|
|
49
61
|
|
|
@@ -58,7 +70,7 @@ def _horizontal_tree_layout_right(
|
|
|
58
70
|
|
|
59
71
|
# Set the y values for vertices
|
|
60
72
|
i = 0
|
|
61
|
-
for node in postorder_fun(
|
|
73
|
+
for node in postorder_fun():
|
|
62
74
|
children = children_fun(node)
|
|
63
75
|
if len(children) == 0:
|
|
64
76
|
layout[node] = [None, i]
|
|
@@ -70,8 +82,8 @@ def _horizontal_tree_layout_right(
|
|
|
70
82
|
]
|
|
71
83
|
|
|
72
84
|
# Set the x values for vertices
|
|
73
|
-
layout[
|
|
74
|
-
for node in preorder_fun(
|
|
85
|
+
layout[root][0] = 0
|
|
86
|
+
for node in preorder_fun():
|
|
75
87
|
for child in children_fun(node):
|
|
76
88
|
bl = branch_length_fun(child)
|
|
77
89
|
if bl is None:
|
|
@@ -82,7 +94,6 @@ def _horizontal_tree_layout_right(
|
|
|
82
94
|
|
|
83
95
|
|
|
84
96
|
def _horizontal_tree_layout(
|
|
85
|
-
tree,
|
|
86
97
|
orientation="right",
|
|
87
98
|
**kwargs,
|
|
88
99
|
) -> dict[Hashable, list[float]]:
|
|
@@ -90,7 +101,7 @@ def _horizontal_tree_layout(
|
|
|
90
101
|
if orientation not in ("right", "left"):
|
|
91
102
|
raise ValueError("Orientation must be 'right' or 'left'.")
|
|
92
103
|
|
|
93
|
-
layout = _horizontal_tree_layout_right(
|
|
104
|
+
layout = _horizontal_tree_layout_right(**kwargs)
|
|
94
105
|
|
|
95
106
|
if orientation == "left":
|
|
96
107
|
for key in layout:
|
|
@@ -99,13 +110,12 @@ def _horizontal_tree_layout(
|
|
|
99
110
|
|
|
100
111
|
|
|
101
112
|
def _vertical_tree_layout(
|
|
102
|
-
tree,
|
|
103
113
|
orientation="descending",
|
|
104
114
|
**kwargs,
|
|
105
115
|
) -> dict[Hashable, list[float]]:
|
|
106
116
|
"""Vertical tree layout."""
|
|
107
|
-
sign = 1 if orientation == "descending" else
|
|
108
|
-
layout = _horizontal_tree_layout(
|
|
117
|
+
sign = -1 if orientation == "descending" else 1
|
|
118
|
+
layout = _horizontal_tree_layout(**kwargs)
|
|
109
119
|
for key, value in layout.items():
|
|
110
120
|
# Invert x and y
|
|
111
121
|
layout[key] = value[::-1]
|
|
@@ -114,24 +124,35 @@ def _vertical_tree_layout(
|
|
|
114
124
|
return layout
|
|
115
125
|
|
|
116
126
|
|
|
117
|
-
def
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
angular_span=360,
|
|
127
|
+
def _radial_tree_layout(
|
|
128
|
+
orientation: str = "right",
|
|
129
|
+
start: float = 180,
|
|
130
|
+
span: float = 360,
|
|
122
131
|
**kwargs,
|
|
123
|
-
) -> dict[Hashable,
|
|
124
|
-
"""
|
|
132
|
+
) -> dict[Hashable, tuple[float, float]]:
|
|
133
|
+
"""Radial tree layout.
|
|
134
|
+
|
|
135
|
+
Parameters:
|
|
136
|
+
orientation: Whether the layout fans out towards the right (clockwise) or left
|
|
137
|
+
(anticlockwise).
|
|
138
|
+
start: The starting angle in degrees, default is -180 (left).
|
|
139
|
+
span: The angular span in degrees, default is 360 (full circle). When this is
|
|
140
|
+
360, it leaves a small gap at the end to ensure the first and last leaf
|
|
141
|
+
are not overlapping.
|
|
142
|
+
Returns:
|
|
143
|
+
A dictionary with the radial layout.
|
|
144
|
+
"""
|
|
125
145
|
# Short form
|
|
126
|
-
th =
|
|
127
|
-
th_span =
|
|
128
|
-
|
|
146
|
+
th = start * np.pi / 180
|
|
147
|
+
th_span = span * np.pi / 180
|
|
148
|
+
pad = int(span == 360)
|
|
149
|
+
sign = -1 if orientation in ("right", "clockwise") else 1
|
|
129
150
|
|
|
130
|
-
layout = _horizontal_tree_layout_right(
|
|
151
|
+
layout = _horizontal_tree_layout_right(**kwargs)
|
|
131
152
|
ymax = max(point[1] for point in layout.values())
|
|
132
153
|
for key, (x, y) in layout.items():
|
|
133
154
|
r = x
|
|
134
|
-
theta = sign * th_span * y / (ymax +
|
|
155
|
+
theta = sign * th_span * y / (ymax + pad) + th
|
|
135
156
|
# We export r and theta to ensure theta does not
|
|
136
157
|
# modulo 2pi if we take the tan and then arctan later.
|
|
137
158
|
layout[key] = (r, theta)
|
iplotx/plotting.py
CHANGED
|
@@ -123,9 +123,10 @@ def network(
|
|
|
123
123
|
def tree(
|
|
124
124
|
tree: Optional[TreeType] = None,
|
|
125
125
|
layout: str | LayoutType = "horizontal",
|
|
126
|
-
orientation: str =
|
|
126
|
+
orientation: Optional[str] = None,
|
|
127
127
|
directed: bool | str = False,
|
|
128
128
|
vertex_labels: Optional[list | dict | pd.Series] = None,
|
|
129
|
+
leaf_labels: Optional[list | dict | pd.Series] = None,
|
|
129
130
|
ax: Optional[mpl.axes.Axes] = None,
|
|
130
131
|
style: str | dict | Sequence[str | dict] = "tree",
|
|
131
132
|
title: Optional[str] = None,
|
|
@@ -138,8 +139,9 @@ def tree(
|
|
|
138
139
|
Parameters:
|
|
139
140
|
tree: The tree to plot. Can be a BioPython.Phylo.Tree object.
|
|
140
141
|
layout: The layout to use for plotting.
|
|
141
|
-
orientation: The orientation of the
|
|
142
|
-
"right"
|
|
142
|
+
orientation: The orientation of the layout. Can be "right" or "left". Defaults to
|
|
143
|
+
"right" for horizontal layout, "descending" or "ascending" for vertical layout,
|
|
144
|
+
and "clockwise" or "anticlockwise" for radial layout.
|
|
143
145
|
directed: If False, donot draw arrows. If True or "child", draw arrows from parent to child
|
|
144
146
|
node. If "parent", draw arrows the other way around.
|
|
145
147
|
|
|
@@ -160,6 +162,7 @@ def tree(
|
|
|
160
162
|
transform=mpl.transforms.IdentityTransform(),
|
|
161
163
|
offset_transform=ax.transData,
|
|
162
164
|
vertex_labels=vertex_labels,
|
|
165
|
+
leaf_labels=leaf_labels,
|
|
163
166
|
)
|
|
164
167
|
ax.add_artist(artist)
|
|
165
168
|
|
iplotx/style.py
CHANGED
|
@@ -23,6 +23,8 @@ style_leaves = (
|
|
|
23
23
|
"tension",
|
|
24
24
|
"looptension",
|
|
25
25
|
"loopmaxangle",
|
|
26
|
+
"paralleloffset",
|
|
27
|
+
"offset",
|
|
26
28
|
"rotate",
|
|
27
29
|
"marker",
|
|
28
30
|
"waypoints",
|
|
@@ -34,15 +36,17 @@ style_leaves = (
|
|
|
34
36
|
"hmargin",
|
|
35
37
|
"vmargin",
|
|
36
38
|
"ports",
|
|
39
|
+
"extend",
|
|
37
40
|
)
|
|
38
41
|
|
|
39
42
|
# These properties are not allowed to be rotated (global throughout the graph).
|
|
40
43
|
# This might change in the future as the API improves.
|
|
41
44
|
nonrotating_leaves = (
|
|
42
|
-
"
|
|
45
|
+
"paralleloffset",
|
|
43
46
|
"looptension",
|
|
44
47
|
"loopmaxangle",
|
|
45
48
|
"vertexpadding",
|
|
49
|
+
"extend",
|
|
46
50
|
)
|
|
47
51
|
|
|
48
52
|
|
|
@@ -64,7 +68,7 @@ default = {
|
|
|
64
68
|
"linestyle": "-",
|
|
65
69
|
"color": "black",
|
|
66
70
|
"curved": False,
|
|
67
|
-
"
|
|
71
|
+
"paralleloffset": 3,
|
|
68
72
|
"tension": 1,
|
|
69
73
|
"looptension": 4,
|
|
70
74
|
"loopmaxangle": 60,
|
|
@@ -111,6 +115,7 @@ hollow["vertex"]["edgecolor"] = "black"
|
|
|
111
115
|
hollow["vertex"]["linewidth"] = 1.5
|
|
112
116
|
hollow["vertex"]["marker"] = "r"
|
|
113
117
|
hollow["vertex"]["size"] = "label"
|
|
118
|
+
hollow["vertex"]["label"]["color"] = "black"
|
|
114
119
|
|
|
115
120
|
tree = copy_with_deep_values(default)
|
|
116
121
|
tree["vertex"]["size"] = 0
|
|
@@ -123,8 +128,8 @@ tree["vertex"]["label"]["bbox"] = {
|
|
|
123
128
|
}
|
|
124
129
|
tree["vertex"]["label"]["color"] = "black"
|
|
125
130
|
tree["vertex"]["label"]["size"] = 12
|
|
126
|
-
tree["vertex"]["label"]["
|
|
127
|
-
tree["vertex"]["label"]["hmargin"] =
|
|
131
|
+
tree["vertex"]["label"]["verticalalignment"] = "center"
|
|
132
|
+
tree["vertex"]["label"]["hmargin"] = 10
|
|
128
133
|
|
|
129
134
|
|
|
130
135
|
styles = {
|
|
@@ -145,13 +150,17 @@ def get_stylename():
|
|
|
145
150
|
return str(stylename)
|
|
146
151
|
|
|
147
152
|
|
|
148
|
-
def get_style(name: str = "") -> dict[str, Any]:
|
|
153
|
+
def get_style(name: str = "", *args) -> dict[str, Any]:
|
|
149
154
|
"""Get a *deep copy* of the chosen style.
|
|
150
155
|
|
|
151
156
|
Parameters:
|
|
152
157
|
name: The name of the style to get. If empty, the current style is returned.
|
|
153
158
|
Substyles can be obtained by using a dot notation, e.g. "default.vertex".
|
|
154
159
|
If "name" starts with a dot, it means a substyle of the current style.
|
|
160
|
+
*args: A single argument is accepted. If present, this value (usually a
|
|
161
|
+
dictionary) is returned if the queried style is not found. For example,
|
|
162
|
+
get_style(".nonexistent") raises an Exception but
|
|
163
|
+
get_style("nonexistent", {}) does not, returning an empty dict instead.
|
|
155
164
|
Returns:
|
|
156
165
|
The requected style or substyle.
|
|
157
166
|
|
|
@@ -160,6 +169,9 @@ def get_style(name: str = "") -> dict[str, Any]:
|
|
|
160
169
|
useful for hashables that change hash upon copying, such as Biopython's
|
|
161
170
|
tree nodes.
|
|
162
171
|
"""
|
|
172
|
+
if len(args) > 1:
|
|
173
|
+
raise ValueError("get_style() accepts at most one additional argument.")
|
|
174
|
+
|
|
163
175
|
namelist = name.split(".")
|
|
164
176
|
style = styles
|
|
165
177
|
for i, namei in enumerate(namelist):
|
|
@@ -173,6 +185,8 @@ def get_style(name: str = "") -> dict[str, Any]:
|
|
|
173
185
|
# which will not fail unless the uder tries to enter it
|
|
174
186
|
elif namei not in style_leaves:
|
|
175
187
|
style = {}
|
|
188
|
+
elif len(args) > 0:
|
|
189
|
+
return args[0]
|
|
176
190
|
else:
|
|
177
191
|
raise KeyError(f"Style not found: {name}")
|
|
178
192
|
|