iplotx 0.3.1__py3-none-any.whl → 0.5.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iplotx/__init__.py +5 -0
- iplotx/artists.py +24 -0
- iplotx/cascades.py +22 -31
- iplotx/edge/__init__.py +122 -49
- iplotx/edge/arrow.py +44 -3
- iplotx/edge/geometry.py +30 -21
- iplotx/edge/leaf.py +117 -0
- iplotx/edge/ports.py +3 -2
- iplotx/groups.py +1 -3
- iplotx/ingest/__init__.py +6 -20
- iplotx/ingest/heuristics.py +4 -36
- iplotx/ingest/providers/network/igraph.py +20 -18
- iplotx/ingest/providers/network/networkx.py +20 -24
- iplotx/ingest/providers/network/simple.py +114 -0
- iplotx/ingest/providers/tree/biopython.py +15 -5
- iplotx/ingest/providers/tree/cogent3.py +9 -5
- iplotx/ingest/providers/tree/ete4.py +2 -5
- iplotx/ingest/providers/tree/simple.py +97 -0
- iplotx/ingest/providers/tree/skbio.py +2 -5
- iplotx/ingest/typing.py +109 -19
- iplotx/label.py +42 -12
- iplotx/layout.py +5 -1
- iplotx/network.py +69 -18
- iplotx/plotting.py +9 -9
- iplotx/{style.py → style/__init__.py} +150 -187
- iplotx/style/leaf_info.py +44 -0
- iplotx/style/library.py +324 -0
- iplotx/tree.py +279 -51
- iplotx/typing.py +2 -0
- iplotx/utils/geometry.py +32 -40
- iplotx/utils/matplotlib.py +43 -22
- iplotx/utils/style.py +17 -1
- iplotx/version.py +1 -1
- iplotx/vertex.py +63 -15
- {iplotx-0.3.1.dist-info → iplotx-0.5.0.dist-info}/METADATA +2 -1
- iplotx-0.5.0.dist-info/RECORD +38 -0
- iplotx-0.3.1.dist-info/RECORD +0 -32
- {iplotx-0.3.1.dist-info → iplotx-0.5.0.dist-info}/WHEEL +0 -0
|
@@ -3,6 +3,7 @@ from typing import (
|
|
|
3
3
|
Optional,
|
|
4
4
|
Sequence,
|
|
5
5
|
)
|
|
6
|
+
import importlib
|
|
6
7
|
from functools import partialmethod
|
|
7
8
|
|
|
8
9
|
from ...typing import (
|
|
@@ -31,11 +32,7 @@ class Ete4DataProvider(TreeDataProvider):
|
|
|
31
32
|
|
|
32
33
|
@staticmethod
|
|
33
34
|
def check_dependencies() -> bool:
|
|
34
|
-
|
|
35
|
-
from ete4 import Tree
|
|
36
|
-
except ImportError:
|
|
37
|
-
return False
|
|
38
|
-
return True
|
|
35
|
+
return importlib.util.find_spec("ete4") is not None
|
|
39
36
|
|
|
40
37
|
@staticmethod
|
|
41
38
|
def tree_type():
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Any,
|
|
3
|
+
Optional,
|
|
4
|
+
Sequence,
|
|
5
|
+
Iterable,
|
|
6
|
+
Self,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from ...typing import (
|
|
10
|
+
TreeDataProvider,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SimpleTree:
|
|
15
|
+
"""A simple tree class for educational purposes.
|
|
16
|
+
|
|
17
|
+
Properties:
|
|
18
|
+
children: Children SimpleTree objects.
|
|
19
|
+
branch_length: Length of the branch leading to this node/tree.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
children: Sequence[Self] = []
|
|
23
|
+
branch_length: float = 1
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_dict(cls, data: dict) -> Self:
|
|
27
|
+
"""Create a SimpleTree from a dictionary.
|
|
28
|
+
|
|
29
|
+
Parameters:
|
|
30
|
+
data: A dictionary representation of the tree, with "children" as a list offset_transform
|
|
31
|
+
child nodes and an optional "branch_length" property (float).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
An instance of SimpleTree constructed from the provided dictionary.
|
|
35
|
+
"""
|
|
36
|
+
tree = cls()
|
|
37
|
+
tree.branch_length = data.get("branch_length", 1)
|
|
38
|
+
tree.children = [cls.from_dict(child) for child in data.get("children", [])]
|
|
39
|
+
return tree
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SimpleTreeDataProvider(TreeDataProvider):
|
|
43
|
+
def is_rooted(self) -> bool:
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
def get_root(self) -> Any:
|
|
47
|
+
"""Get the root node of the tree."""
|
|
48
|
+
return self.tree
|
|
49
|
+
|
|
50
|
+
def preorder(self) -> Iterable[dict[dict | str, Any]]:
|
|
51
|
+
def _recur(node):
|
|
52
|
+
yield node
|
|
53
|
+
for child in node.children:
|
|
54
|
+
yield from _recur(child)
|
|
55
|
+
|
|
56
|
+
yield from _recur(self.tree)
|
|
57
|
+
|
|
58
|
+
def postorder(self) -> Iterable[dict[dict | str, Any]]:
|
|
59
|
+
def _recur(node):
|
|
60
|
+
for child in node.children:
|
|
61
|
+
yield from _recur(child)
|
|
62
|
+
yield node
|
|
63
|
+
|
|
64
|
+
yield from _recur(self.tree)
|
|
65
|
+
|
|
66
|
+
def get_leaves(self) -> Sequence[Any]:
|
|
67
|
+
def _recur(node):
|
|
68
|
+
if len(node.children) == 0:
|
|
69
|
+
yield node
|
|
70
|
+
else:
|
|
71
|
+
for child in node.children:
|
|
72
|
+
yield from _recur(child)
|
|
73
|
+
|
|
74
|
+
return list(_recur(self.tree))
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def get_children(node: Any) -> Sequence[Any]:
|
|
78
|
+
return node.children
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def get_branch_length(node: Any) -> Optional[float]:
|
|
82
|
+
return node.branch_length
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def check_dependencies() -> bool:
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def tree_type():
|
|
90
|
+
return SimpleTree
|
|
91
|
+
|
|
92
|
+
def get_support(self):
|
|
93
|
+
"""Get support/confidence values for all nodes."""
|
|
94
|
+
support_dict = {}
|
|
95
|
+
for node in self.preorder():
|
|
96
|
+
support_dict[node] = None
|
|
97
|
+
return support_dict
|
|
@@ -3,6 +3,7 @@ from typing import (
|
|
|
3
3
|
Optional,
|
|
4
4
|
Sequence,
|
|
5
5
|
)
|
|
6
|
+
import importlib
|
|
6
7
|
from ...typing import (
|
|
7
8
|
TreeDataProvider,
|
|
8
9
|
)
|
|
@@ -28,11 +29,7 @@ class SkbioDataProvider(TreeDataProvider):
|
|
|
28
29
|
|
|
29
30
|
@staticmethod
|
|
30
31
|
def check_dependencies() -> bool:
|
|
31
|
-
|
|
32
|
-
from skbio import TreeNode
|
|
33
|
-
except ImportError:
|
|
34
|
-
return False
|
|
35
|
-
return True
|
|
32
|
+
return importlib.util.find_spec("skbio") is not None
|
|
36
33
|
|
|
37
34
|
@staticmethod
|
|
38
35
|
def tree_type():
|
iplotx/ingest/typing.py
CHANGED
|
@@ -40,9 +40,19 @@ class NetworkData(TypedDict):
|
|
|
40
40
|
class NetworkDataProvider(Protocol):
|
|
41
41
|
"""Protocol for network data ingestion provider for iplotx."""
|
|
42
42
|
|
|
43
|
-
def
|
|
43
|
+
def __init__(
|
|
44
44
|
self,
|
|
45
45
|
network: GraphType,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Initialise network data provider.
|
|
48
|
+
|
|
49
|
+
Parameters:
|
|
50
|
+
network: The network to ingest.
|
|
51
|
+
"""
|
|
52
|
+
self.network = network
|
|
53
|
+
|
|
54
|
+
def __call__(
|
|
55
|
+
self,
|
|
46
56
|
layout: Optional[LayoutType] = None,
|
|
47
57
|
vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
|
|
48
58
|
edge_labels: Optional[Sequence[str] | dict] = None,
|
|
@@ -60,6 +70,14 @@ class NetworkDataProvider(Protocol):
|
|
|
60
70
|
"""Return the graph type from this provider to check for instances."""
|
|
61
71
|
raise NotImplementedError("Network data providers must implement this method.")
|
|
62
72
|
|
|
73
|
+
def is_directed(self):
|
|
74
|
+
"""Check whether the network is directed."""
|
|
75
|
+
raise NotImplementedError("Network data providers must implement this method.")
|
|
76
|
+
|
|
77
|
+
def number_of_vertices(self):
|
|
78
|
+
"""The number of vertices/nodes in the network."""
|
|
79
|
+
raise NotImplementedError("Network data providers must implement this method.")
|
|
80
|
+
|
|
63
81
|
|
|
64
82
|
class TreeData(TypedDict):
|
|
65
83
|
"""Tree data structure for iplotx."""
|
|
@@ -190,23 +208,66 @@ class TreeDataProvider(Protocol):
|
|
|
190
208
|
branch_length = self.get_branch_length(node)
|
|
191
209
|
return branch_length if branch_length is not None else 1.0
|
|
192
210
|
|
|
211
|
+
def get_lca(
|
|
212
|
+
self,
|
|
213
|
+
nodes: Sequence[Hashable],
|
|
214
|
+
) -> Hashable:
|
|
215
|
+
"""Find the last common ancestor of a sequence of nodes.
|
|
216
|
+
|
|
217
|
+
Parameters:
|
|
218
|
+
nodes: The nodes to find a common ancestor for.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
The node that is the last (deepest) common ancestor of the nodes.
|
|
222
|
+
|
|
223
|
+
NOTE: individual providers may implement more efficient versions of
|
|
224
|
+
this function if desired.
|
|
225
|
+
"""
|
|
226
|
+
provider = self.__class__
|
|
227
|
+
|
|
228
|
+
# Find leaves of the selected nodes
|
|
229
|
+
leaves = set()
|
|
230
|
+
for node in nodes:
|
|
231
|
+
# NOTE: get_leaves excludes the node itself...
|
|
232
|
+
if len(self.get_children(node)) == 0:
|
|
233
|
+
leaves.add(node)
|
|
234
|
+
else:
|
|
235
|
+
leaves |= set(provider(node).get_leaves())
|
|
236
|
+
|
|
237
|
+
# Look for nodes with the same set of leaves, starting from the bottom
|
|
238
|
+
# and stopping at the first (i.e. lowest) hit.
|
|
239
|
+
for node in self.postorder():
|
|
240
|
+
# NOTE: As above, get_leaves excludes the node itself
|
|
241
|
+
if len(self.get_children(node)) == 0:
|
|
242
|
+
leaves_node = {node}
|
|
243
|
+
else:
|
|
244
|
+
leaves_node = set(provider(node).get_leaves())
|
|
245
|
+
if leaves <= leaves_node:
|
|
246
|
+
root = node
|
|
247
|
+
break
|
|
248
|
+
else:
|
|
249
|
+
raise ValueError(f"Common ancestor not found for nodes: {nodes}")
|
|
250
|
+
|
|
251
|
+
return root
|
|
252
|
+
|
|
193
253
|
def __call__(
|
|
194
254
|
self,
|
|
195
255
|
layout: str | LayoutType,
|
|
196
|
-
orientation: Optional[str],
|
|
197
256
|
layout_style: Optional[dict[str, int | float | str]] = None,
|
|
198
257
|
directed: bool | str = False,
|
|
199
|
-
vertex_labels: Optional[
|
|
200
|
-
Sequence[str] | dict[Hashable, str] | pd.Series | bool
|
|
201
|
-
] = None,
|
|
258
|
+
vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series | bool] = None,
|
|
202
259
|
edge_labels: Optional[Sequence[str] | dict] = None,
|
|
203
|
-
leaf_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
|
|
260
|
+
leaf_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series | bool] = None,
|
|
204
261
|
) -> TreeData:
|
|
205
|
-
"""Create tree data object for iplotx from ete4.core.tre.Tree classes.
|
|
262
|
+
"""Create tree data object for iplotx from ete4.core.tre.Tree classes.
|
|
263
|
+
|
|
264
|
+
NOTE: This function needs NOT be implemented by individual providers.
|
|
265
|
+
"""
|
|
206
266
|
|
|
207
267
|
if layout_style is None:
|
|
208
268
|
layout_style = {}
|
|
209
269
|
|
|
270
|
+
orientation = layout_style.pop("orientation", None)
|
|
210
271
|
if orientation is None:
|
|
211
272
|
if layout == "horizontal":
|
|
212
273
|
orientation = "right"
|
|
@@ -240,6 +301,15 @@ class TreeDataProvider(Protocol):
|
|
|
240
301
|
else:
|
|
241
302
|
tree_data["layout_coordinate_system"] = "cartesian"
|
|
242
303
|
|
|
304
|
+
# Add leaf_df
|
|
305
|
+
# NOTE: Sometimes (e.g. cogent3) the leaves convert into a pd.Index
|
|
306
|
+
# in a strange way, whereby their name disappears upon printing the
|
|
307
|
+
# index but is actually visible (and kept) when inspecting the
|
|
308
|
+
# individual elements (leaves). Seems ok functionally, though a little
|
|
309
|
+
# awkward visually during debugging.
|
|
310
|
+
tree_data["leaf_df"] = pd.DataFrame(index=self.get_leaves())
|
|
311
|
+
leaf_name_attrs = ("name",)
|
|
312
|
+
|
|
243
313
|
# Add edge_df
|
|
244
314
|
edge_data = {"_ipx_source": [], "_ipx_target": []}
|
|
245
315
|
for node in self.preorder():
|
|
@@ -253,16 +323,30 @@ class TreeDataProvider(Protocol):
|
|
|
253
323
|
edge_df = pd.DataFrame(edge_data)
|
|
254
324
|
tree_data["edge_df"] = edge_df
|
|
255
325
|
|
|
256
|
-
# Add
|
|
257
|
-
|
|
326
|
+
# Add branch support
|
|
327
|
+
if hasattr(self, "get_support"):
|
|
328
|
+
support = self.get_support()
|
|
329
|
+
|
|
330
|
+
for key, value in support.items():
|
|
331
|
+
# Leaves never show support, it's not a branching point
|
|
332
|
+
if key in tree_data["leaf_df"].index:
|
|
333
|
+
support[key] = ""
|
|
334
|
+
elif value is None:
|
|
335
|
+
support[key] = ""
|
|
336
|
+
elif np.isscalar(value):
|
|
337
|
+
# Assume support is in percentage and round it to nearest integer.
|
|
338
|
+
support[key] = str(int(np.round(value, 0)))
|
|
339
|
+
else:
|
|
340
|
+
# Apparently multiple supports are accepted in some XML format
|
|
341
|
+
support[key] = "/".join(str(int(np.round(v, 0))) for v in value)
|
|
342
|
+
|
|
343
|
+
tree_data["vertex_df"]["support"] = pd.Series(support).loc[tree_data["vertex_df"].index]
|
|
258
344
|
|
|
259
345
|
# Add vertex labels
|
|
260
346
|
if vertex_labels is None:
|
|
261
347
|
vertex_labels = False
|
|
262
348
|
if np.isscalar(vertex_labels) and vertex_labels:
|
|
263
|
-
tree_data["vertex_df"]["label"] = [
|
|
264
|
-
x.name for x in tree_data["vertex_df"].index
|
|
265
|
-
]
|
|
349
|
+
tree_data["vertex_df"]["label"] = [x.name for x in tree_data["vertex_df"].index]
|
|
266
350
|
elif not np.isscalar(vertex_labels):
|
|
267
351
|
# If a dict-like object is passed, it can be incomplete (e.g. only the leaves):
|
|
268
352
|
# we fill the rest with empty strings which are not going to show up in the plot.
|
|
@@ -278,17 +362,23 @@ class TreeDataProvider(Protocol):
|
|
|
278
362
|
if leaf_labels is None:
|
|
279
363
|
leaf_labels = False
|
|
280
364
|
if np.isscalar(leaf_labels) and leaf_labels:
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
365
|
+
leaf_labels = []
|
|
366
|
+
for leaf in tree_data["leaf_df"].index:
|
|
367
|
+
for name_attr in leaf_name_attrs:
|
|
368
|
+
if hasattr(leaf, name_attr):
|
|
369
|
+
label = getattr(leaf, name_attr)
|
|
370
|
+
break
|
|
371
|
+
else:
|
|
372
|
+
raise ValueError(
|
|
373
|
+
"Could not find leaf name attribute.",
|
|
374
|
+
)
|
|
375
|
+
leaf_labels.append(label)
|
|
376
|
+
tree_data["leaf_df"]["label"] = leaf_labels
|
|
286
377
|
elif not np.isscalar(leaf_labels):
|
|
287
378
|
# Leaves are already in the dataframe in a certain order, so sequences are allowed
|
|
288
379
|
if isinstance(leaf_labels, (list, tuple, np.ndarray)):
|
|
289
380
|
leaf_labels = {
|
|
290
|
-
leaf: label
|
|
291
|
-
for leaf, label in zip(tree_data["leaf_df"].index, leaf_labels)
|
|
381
|
+
leaf: label for leaf, label in zip(tree_data["leaf_df"].index, leaf_labels)
|
|
292
382
|
}
|
|
293
383
|
# If a dict-like object is passed, it can be incomplete (e.g. only the leaves):
|
|
294
384
|
# we fill the rest with empty strings which are not going to show up in the plot.
|
iplotx/label.py
CHANGED
|
@@ -17,6 +17,7 @@ from .style import (
|
|
|
17
17
|
from .utils.matplotlib import (
|
|
18
18
|
_stale_wrapper,
|
|
19
19
|
_forwarder,
|
|
20
|
+
_get_label_width_height,
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
|
|
@@ -108,12 +109,13 @@ class LabelCollection(mpl.artist.Artist):
|
|
|
108
109
|
self._offsets[i][1],
|
|
109
110
|
label,
|
|
110
111
|
transform=transform,
|
|
112
|
+
rotation_mode="anchor",
|
|
111
113
|
**stylei,
|
|
112
114
|
)
|
|
113
115
|
arts.append(art)
|
|
114
116
|
self._labelartists = arts
|
|
115
117
|
self._margins = np.array(margins)
|
|
116
|
-
self._rotations = np.
|
|
118
|
+
self._rotations = np.array([np.pi / 180 * art.get_rotation() for art in arts])
|
|
117
119
|
|
|
118
120
|
def _update_offsets(self, dpi: float = 72.0) -> None:
|
|
119
121
|
"""Update offsets including margins."""
|
|
@@ -134,10 +136,11 @@ class LabelCollection(mpl.artist.Artist):
|
|
|
134
136
|
transform = self.get_transform()
|
|
135
137
|
trans = transform.transform
|
|
136
138
|
trans_inv = transform.inverted().transform
|
|
137
|
-
rotations = self.get_rotations()
|
|
138
|
-
vrot = [np.cos(rotations), np.sin(rotations)]
|
|
139
139
|
|
|
140
|
+
# Add margins *before* applying the rotation
|
|
140
141
|
margins_rot = np.empty_like(margins)
|
|
142
|
+
rotations = self.get_rotations()
|
|
143
|
+
vrot = [np.cos(rotations), np.sin(rotations)]
|
|
141
144
|
margins_rot[:, 0] = margins[:, 0] * vrot[0] - margins[:, 1] * vrot[1]
|
|
142
145
|
margins_rot[:, 1] = margins[:, 0] * vrot[1] + margins[:, 1] * vrot[0]
|
|
143
146
|
offsets = trans_inv(trans(offsets) + margins_rot)
|
|
@@ -170,12 +173,12 @@ class LabelCollection(mpl.artist.Artist):
|
|
|
170
173
|
rot_deg = 180.0 / np.pi * rotation
|
|
171
174
|
# Force the font size to be upwards
|
|
172
175
|
if ha == "auto":
|
|
173
|
-
if -90 <= rot_deg < 90:
|
|
176
|
+
if -90 <= np.round(rot_deg, 3) < 90:
|
|
174
177
|
art.set_horizontalalignment("left")
|
|
175
178
|
else:
|
|
176
179
|
art.set_horizontalalignment("right")
|
|
177
|
-
|
|
178
|
-
art.set_rotation(
|
|
180
|
+
rot_deg_new = ((rot_deg + 90) % 180) - 90
|
|
181
|
+
art.set_rotation(rot_deg_new)
|
|
179
182
|
|
|
180
183
|
def get_datalim(self, transData=None) -> mpl.transforms.Bbox:
|
|
181
184
|
"""Get the data limits of the labels."""
|
|
@@ -184,15 +187,42 @@ class LabelCollection(mpl.artist.Artist):
|
|
|
184
187
|
return bbox
|
|
185
188
|
|
|
186
189
|
def get_datalims_children(self, transData=None) -> Sequence[mpl.transforms.Bbox]:
|
|
187
|
-
"""Get the data limits
|
|
190
|
+
"""Get the data limits of the children of this artist."""
|
|
191
|
+
dpi = self.figure.dpi if self.figure is not None else 72.0
|
|
188
192
|
if transData is None:
|
|
189
193
|
transData = self.get_transform()
|
|
190
|
-
|
|
194
|
+
trans = transData.transform
|
|
195
|
+
trans_inv = transData.inverted().transform
|
|
191
196
|
bboxes = []
|
|
192
|
-
for art in self._labelartists:
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
197
|
+
for art, rot in zip(self._labelartists, self.get_rotations()):
|
|
198
|
+
# These are in figure points
|
|
199
|
+
textprops = ("text", "fontsize")
|
|
200
|
+
props = art.properties()
|
|
201
|
+
props = {key: props[key] for key in textprops}
|
|
202
|
+
props["dpi"] = dpi
|
|
203
|
+
props["hpadding"] = 12
|
|
204
|
+
props["vpadding"] = 12
|
|
205
|
+
width, height = _get_label_width_height(**props)
|
|
206
|
+
|
|
207
|
+
# Positions are in data coordinates
|
|
208
|
+
# and include the margins (it's the actual position of the
|
|
209
|
+
# text anchor)
|
|
210
|
+
pos_data = art.get_position()
|
|
211
|
+
|
|
212
|
+
# Four corners
|
|
213
|
+
dh = width / 2 * np.array([-np.sin(rot), np.cos(rot)])
|
|
214
|
+
dw = height * np.array([np.cos(rot), np.sin(rot)])
|
|
215
|
+
c1 = trans_inv(trans(pos_data) + dh)
|
|
216
|
+
c2 = trans_inv(trans(pos_data) - dh)
|
|
217
|
+
if art.get_horizontalalignment() == "right":
|
|
218
|
+
c3 = trans_inv(trans(pos_data) - dw + dh)
|
|
219
|
+
c4 = trans_inv(trans(pos_data) - dw - dh)
|
|
220
|
+
else:
|
|
221
|
+
c3 = trans_inv(trans(pos_data) + dw + dh)
|
|
222
|
+
c4 = trans_inv(trans(pos_data) + dw - dh)
|
|
223
|
+
bbox = mpl.transforms.Bbox.null()
|
|
224
|
+
bbox.update_from_data_xy([c1, c2, c3, c4], ignore=True)
|
|
225
|
+
bboxes.append(bbox)
|
|
196
226
|
return bboxes
|
|
197
227
|
|
|
198
228
|
@_stale_wrapper
|
iplotx/layout.py
CHANGED
|
@@ -24,7 +24,7 @@ def compute_tree_layout(
|
|
|
24
24
|
"""Compute the layout for a tree.
|
|
25
25
|
|
|
26
26
|
Parameters:
|
|
27
|
-
layout: The name of the layout, e.g. "horizontal", "
|
|
27
|
+
layout: The name of the layout, e.g. "horizontal", "vertical", or "radial".
|
|
28
28
|
orientation: The orientation of the layout, e.g. "right", "left", "descending",
|
|
29
29
|
"ascending", "clockwise", "anticlockwise".
|
|
30
30
|
|
|
@@ -38,6 +38,10 @@ def compute_tree_layout(
|
|
|
38
38
|
kwargs["branch_length_fun"] = branch_length_fun
|
|
39
39
|
kwargs["orientation"] = orientation
|
|
40
40
|
|
|
41
|
+
# Angular or not, the vertex layout is unchanged. Since we do not
|
|
42
|
+
# currently compute an edge layout here, we can ignore the option.
|
|
43
|
+
kwargs.pop("angular", None)
|
|
44
|
+
|
|
41
45
|
if layout == "radial":
|
|
42
46
|
layout_dict = _radial_tree_layout(**kwargs)
|
|
43
47
|
elif layout == "horizontal":
|
iplotx/network.py
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import (
|
|
2
|
+
Optional,
|
|
3
|
+
Sequence,
|
|
4
|
+
Self,
|
|
5
|
+
)
|
|
2
6
|
import numpy as np
|
|
3
7
|
import pandas as pd
|
|
4
8
|
import matplotlib as mpl
|
|
@@ -62,12 +66,6 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
62
66
|
|
|
63
67
|
"""
|
|
64
68
|
self.network = network
|
|
65
|
-
self._ipx_internal_data = ingest_network_data(
|
|
66
|
-
network,
|
|
67
|
-
layout,
|
|
68
|
-
vertex_labels=vertex_labels,
|
|
69
|
-
edge_labels=edge_labels,
|
|
70
|
-
)
|
|
71
69
|
|
|
72
70
|
super().__init__()
|
|
73
71
|
|
|
@@ -80,8 +78,66 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
80
78
|
zorder = get_style(".network").get("zorder", 1)
|
|
81
79
|
self.set_zorder(zorder)
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
if network is not None:
|
|
82
|
+
self._ipx_internal_data = ingest_network_data(
|
|
83
|
+
network,
|
|
84
|
+
layout,
|
|
85
|
+
vertex_labels=vertex_labels,
|
|
86
|
+
edge_labels=edge_labels,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
self._add_vertices()
|
|
90
|
+
self._add_edges()
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def from_other(
|
|
94
|
+
cls: "NetworkArtist", # NOTE: This is fixed in Python 3.14
|
|
95
|
+
other: Self,
|
|
96
|
+
) -> Self:
|
|
97
|
+
"""Create a NetworkArtist as a copy of another one.
|
|
98
|
+
|
|
99
|
+
Parameters:
|
|
100
|
+
other: The other NetworkArtist.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
An instantiated NetworkArtist.
|
|
104
|
+
"""
|
|
105
|
+
self = cls.from_edgecollection(other._edges)
|
|
106
|
+
self.network = other.network
|
|
107
|
+
if hasattr(other, "_ipx_internal_data"):
|
|
108
|
+
self._ipx_internal_data = other._ipx_internal_data
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def from_edgecollection(
|
|
113
|
+
cls: "NetworkArtist", # NOTE: This is fixed in Python 3.14
|
|
114
|
+
edge_collection: EdgeCollection,
|
|
115
|
+
) -> Self:
|
|
116
|
+
"""Create a NetworkArtist from iplotx artists.
|
|
117
|
+
|
|
118
|
+
Parameters:
|
|
119
|
+
edge_collection: The edge collection to use to initialise the artist. Vertices will
|
|
120
|
+
be obtained automatically.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
The initialised NetworkArtist.
|
|
124
|
+
"""
|
|
125
|
+
vertex_collection = edge_collection._vertex_collection
|
|
126
|
+
layout = vertex_collection._layout
|
|
127
|
+
transform = vertex_collection.get_transform()
|
|
128
|
+
offset_transform = edge_collection.get_transform()
|
|
129
|
+
|
|
130
|
+
# Follow the steps in the normal constructor
|
|
131
|
+
self = cls(
|
|
132
|
+
network=None,
|
|
133
|
+
layout=layout,
|
|
134
|
+
transform=transform,
|
|
135
|
+
offset_transform=offset_transform,
|
|
136
|
+
)
|
|
137
|
+
self._vertices = vertex_collection
|
|
138
|
+
self._edges = edge_collection
|
|
139
|
+
|
|
140
|
+
return self
|
|
85
141
|
|
|
86
142
|
def get_children(self):
|
|
87
143
|
return (self._vertices, self._edges)
|
|
@@ -145,9 +201,7 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
145
201
|
self.axes.autoscale_view(tight=tight)
|
|
146
202
|
|
|
147
203
|
def get_layout(self):
|
|
148
|
-
layout_columns = [
|
|
149
|
-
f"_ipx_layout_{i}" for i in range(self._ipx_internal_data["ndim"])
|
|
150
|
-
]
|
|
204
|
+
layout_columns = [f"_ipx_layout_{i}" for i in range(self._ipx_internal_data["ndim"])]
|
|
151
205
|
vertex_layout_df = self._ipx_internal_data["vertex_df"][layout_columns]
|
|
152
206
|
return vertex_layout_df
|
|
153
207
|
|
|
@@ -189,13 +243,12 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
189
243
|
cmap_fun = _build_cmap_fun(
|
|
190
244
|
edge_style["color"],
|
|
191
245
|
edge_style["cmap"],
|
|
246
|
+
edge_style.get("norm", None),
|
|
192
247
|
)
|
|
193
248
|
else:
|
|
194
249
|
cmap_fun = None
|
|
195
250
|
|
|
196
|
-
edge_df = self._ipx_internal_data["edge_df"].set_index(
|
|
197
|
-
["_ipx_source", "_ipx_target"]
|
|
198
|
-
)
|
|
251
|
+
edge_df = self._ipx_internal_data["edge_df"].set_index(["_ipx_source", "_ipx_target"])
|
|
199
252
|
|
|
200
253
|
if "cmap" in edge_style:
|
|
201
254
|
colorarray = []
|
|
@@ -231,7 +284,7 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
231
284
|
edgepatches.append(patch)
|
|
232
285
|
adjacent_vertex_ids.append((vid1, vid2))
|
|
233
286
|
|
|
234
|
-
if "cmap" in edge_style:
|
|
287
|
+
if ("cmap" in edge_style) and ("norm" not in edge_style):
|
|
235
288
|
vmin = np.min(colorarray)
|
|
236
289
|
vmax = np.max(colorarray)
|
|
237
290
|
norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
|
|
@@ -259,8 +312,6 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
259
312
|
if not self.get_visible():
|
|
260
313
|
return
|
|
261
314
|
|
|
262
|
-
# FIXME: Callbacks on stale vertices/edges??
|
|
263
|
-
|
|
264
315
|
# NOTE: looks like we have to manage the zorder ourselves
|
|
265
316
|
# this is kind of funny actually
|
|
266
317
|
children = list(self.get_children())
|
iplotx/plotting.py
CHANGED
|
@@ -21,7 +21,7 @@ def network(
|
|
|
21
21
|
network: Optional[GraphType] = None,
|
|
22
22
|
layout: Optional[LayoutType] = None,
|
|
23
23
|
grouping: Optional[GroupingType] = None,
|
|
24
|
-
vertex_labels: Optional[list | dict | pd.Series] = None,
|
|
24
|
+
vertex_labels: Optional[list | dict | pd.Series | bool] = None,
|
|
25
25
|
edge_labels: Optional[Sequence] = None,
|
|
26
26
|
ax: Optional[mpl.axes.Axes] = None,
|
|
27
27
|
style: str | dict | Sequence[str | dict] = (),
|
|
@@ -53,7 +53,7 @@ def network(
|
|
|
53
53
|
used as a quick fix when some vertex shapes reach beyond the plot edge. This is
|
|
54
54
|
a fraction of the data limits, so 0.1 means 10% of the data limits will be left
|
|
55
55
|
as margin.
|
|
56
|
-
|
|
56
|
+
kwargs: Additional arguments are treated as an alternate way to specify style. If
|
|
57
57
|
both "style" and additional **kwargs are provided, they are both applied in that
|
|
58
58
|
order (style, then **kwargs).
|
|
59
59
|
|
|
@@ -123,10 +123,10 @@ def network(
|
|
|
123
123
|
def tree(
|
|
124
124
|
tree: Optional[TreeType] = None,
|
|
125
125
|
layout: str | LayoutType = "horizontal",
|
|
126
|
-
orientation: Optional[str] = None,
|
|
127
126
|
directed: bool | str = False,
|
|
128
|
-
vertex_labels: Optional[list | dict | pd.Series] = None,
|
|
129
|
-
leaf_labels: Optional[list | dict | pd.Series] = None,
|
|
127
|
+
vertex_labels: Optional[list | dict | pd.Series | bool] = None,
|
|
128
|
+
leaf_labels: Optional[list | dict | pd.Series | bool] = None,
|
|
129
|
+
show_support: bool = False,
|
|
130
130
|
ax: Optional[mpl.axes.Axes] = None,
|
|
131
131
|
style: str | dict | Sequence[str | dict] = "tree",
|
|
132
132
|
title: Optional[str] = None,
|
|
@@ -139,11 +139,11 @@ def tree(
|
|
|
139
139
|
Parameters:
|
|
140
140
|
tree: The tree to plot. Can be a BioPython.Phylo.Tree object.
|
|
141
141
|
layout: The layout to use for plotting.
|
|
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.
|
|
145
142
|
directed: If False, donot draw arrows. If True or "child", draw arrows from parent to child
|
|
146
143
|
node. If "parent", draw arrows the other way around.
|
|
144
|
+
show_support: If True, show the support values for the nodes (assumed to be from 0 to 100,
|
|
145
|
+
rounded to nearest integer). If both this parameter and vertex_labels are set,
|
|
146
|
+
show_support takes precedence and hides the vertex labels.
|
|
147
147
|
|
|
148
148
|
Returns:
|
|
149
149
|
A TreeArtist object, set as a direct child of the matplotlib Axes.
|
|
@@ -157,12 +157,12 @@ def tree(
|
|
|
157
157
|
artist = TreeArtist(
|
|
158
158
|
tree=tree,
|
|
159
159
|
layout=layout,
|
|
160
|
-
orientation=orientation,
|
|
161
160
|
directed=directed,
|
|
162
161
|
transform=mpl.transforms.IdentityTransform(),
|
|
163
162
|
offset_transform=ax.transData,
|
|
164
163
|
vertex_labels=vertex_labels,
|
|
165
164
|
leaf_labels=leaf_labels,
|
|
165
|
+
show_support=show_support,
|
|
166
166
|
)
|
|
167
167
|
ax.add_artist(artist)
|
|
168
168
|
|