iplotx 0.3.1__py3-none-any.whl → 0.4.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 +3 -3
- iplotx/edge/__init__.py +48 -43
- iplotx/edge/arrow.py +34 -0
- iplotx/edge/geometry.py +23 -4
- iplotx/edge/leaf.py +117 -0
- iplotx/ingest/__init__.py +1 -6
- iplotx/ingest/heuristics.py +3 -32
- iplotx/ingest/providers/network/igraph.py +16 -6
- iplotx/ingest/providers/network/networkx.py +14 -4
- iplotx/ingest/providers/network/simple.py +121 -0
- iplotx/ingest/providers/tree/biopython.py +13 -0
- iplotx/ingest/providers/tree/cogent3.py +7 -0
- iplotx/ingest/typing.py +110 -11
- iplotx/label.py +42 -12
- iplotx/layout.py +5 -1
- iplotx/network.py +66 -12
- iplotx/plotting.py +8 -8
- iplotx/{style.py → style/__init__.py} +23 -85
- iplotx/style/leaf_info.py +41 -0
- iplotx/style/library.py +230 -0
- iplotx/tree.py +244 -29
- iplotx/utils/matplotlib.py +30 -12
- iplotx/utils/style.py +12 -1
- iplotx/version.py +1 -1
- iplotx/vertex.py +62 -7
- {iplotx-0.3.1.dist-info → iplotx-0.4.0.dist-info}/METADATA +1 -1
- iplotx-0.4.0.dist-info/RECORD +37 -0
- iplotx-0.3.1.dist-info/RECORD +0 -32
- {iplotx-0.3.1.dist-info → iplotx-0.4.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Optional,
|
|
3
|
+
Sequence,
|
|
4
|
+
Any,
|
|
5
|
+
)
|
|
6
|
+
from collections.abc import Hashable
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from ....typing import (
|
|
11
|
+
GraphType,
|
|
12
|
+
LayoutType,
|
|
13
|
+
)
|
|
14
|
+
from ...heuristics import (
|
|
15
|
+
normalise_layout,
|
|
16
|
+
)
|
|
17
|
+
from ...typing import (
|
|
18
|
+
NetworkDataProvider,
|
|
19
|
+
NetworkData,
|
|
20
|
+
)
|
|
21
|
+
from ....utils.internal import (
|
|
22
|
+
_make_layout_columns,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SimpleDataProvider(NetworkDataProvider):
|
|
27
|
+
def __call__(
|
|
28
|
+
self,
|
|
29
|
+
layout: Optional[LayoutType] = None,
|
|
30
|
+
vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
|
|
31
|
+
edge_labels: Optional[Sequence[str] | dict[str]] = None,
|
|
32
|
+
) -> NetworkData:
|
|
33
|
+
"""Create network data object for iplotx from a simple Python object."""
|
|
34
|
+
network = self.network
|
|
35
|
+
directed = self.is_directed()
|
|
36
|
+
|
|
37
|
+
# Recast vertex_labels=False as vertex_labels=None
|
|
38
|
+
if np.isscalar(vertex_labels) and (not vertex_labels):
|
|
39
|
+
vertex_labels = None
|
|
40
|
+
|
|
41
|
+
# Vertices are ordered integers, no gaps
|
|
42
|
+
for key in ["nodes", "vertices"]:
|
|
43
|
+
if key in network:
|
|
44
|
+
vertices = network[key]
|
|
45
|
+
break
|
|
46
|
+
else:
|
|
47
|
+
# Infer from edge adjacent vertices, singletons will be missed
|
|
48
|
+
vertices = set()
|
|
49
|
+
for edge in self.network.get("edges", []):
|
|
50
|
+
vertices.add(edge[0])
|
|
51
|
+
vertices.add(edge[1])
|
|
52
|
+
vertices = list(vertices)
|
|
53
|
+
|
|
54
|
+
# NOTE: This is underpowered, but it's ok for a simple educational provider
|
|
55
|
+
if isinstance(layout, pd.DataFrame):
|
|
56
|
+
vertex_df = layout.loc[vertices].copy()
|
|
57
|
+
elif isinstance(layout, dict):
|
|
58
|
+
vertex_df = pd.DataFrame(layout).T.loc[vertices]
|
|
59
|
+
else:
|
|
60
|
+
vertex_df = pd.DataFrame(
|
|
61
|
+
index=vertices,
|
|
62
|
+
data=layout,
|
|
63
|
+
)
|
|
64
|
+
ndim = vertex_df.shape[1]
|
|
65
|
+
vertex_df.columns = _make_layout_columns(ndim)
|
|
66
|
+
|
|
67
|
+
# Vertex labels
|
|
68
|
+
if vertex_labels is not None:
|
|
69
|
+
if np.isscalar(vertex_labels):
|
|
70
|
+
vertex_df["label"] = vertex_df.index.astype(str)
|
|
71
|
+
elif len(vertex_labels) != len(vertex_df):
|
|
72
|
+
raise ValueError(
|
|
73
|
+
"Vertex labels must be the same length as the number of vertices."
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
vertex_df["label"] = vertex_labels
|
|
77
|
+
|
|
78
|
+
# Edges are a list of tuples, because of multiedges
|
|
79
|
+
tmp = []
|
|
80
|
+
for edge in network.get("edges", []):
|
|
81
|
+
row = {"_ipx_source": edge[0], "_ipx_target": edge[1]}
|
|
82
|
+
tmp.append(row)
|
|
83
|
+
if len(tmp):
|
|
84
|
+
edge_df = pd.DataFrame(tmp)
|
|
85
|
+
else:
|
|
86
|
+
edge_df = pd.DataFrame(columns=["_ipx_source", "_ipx_target"])
|
|
87
|
+
del tmp
|
|
88
|
+
|
|
89
|
+
network_data = {
|
|
90
|
+
"vertex_df": vertex_df,
|
|
91
|
+
"edge_df": edge_df,
|
|
92
|
+
"directed": directed,
|
|
93
|
+
"ndim": ndim,
|
|
94
|
+
}
|
|
95
|
+
return network_data
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def check_dependencies() -> bool:
|
|
99
|
+
"""Check dependencies. Returns True since this provider has no dependencies."""
|
|
100
|
+
return True
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def graph_type():
|
|
104
|
+
return dict
|
|
105
|
+
|
|
106
|
+
def is_directed(self):
|
|
107
|
+
"""Whether the network is directed."""
|
|
108
|
+
return self.network.get("directed", False)
|
|
109
|
+
|
|
110
|
+
def number_of_vertices(self):
|
|
111
|
+
"""The number of vertices/nodes in the network."""
|
|
112
|
+
for key in ("nodes", "vertices"):
|
|
113
|
+
if key in self.network:
|
|
114
|
+
return len(self.network[key])
|
|
115
|
+
|
|
116
|
+
# Default to unique edge adjacent nodes (this will ignore singletons)
|
|
117
|
+
nodes = set()
|
|
118
|
+
for edge in self.network.get("edges", []):
|
|
119
|
+
nodes.add(edge[0])
|
|
120
|
+
nodes.add(edge[1])
|
|
121
|
+
return len(nodes)
|
|
@@ -45,3 +45,16 @@ class BiopythonDataProvider(TreeDataProvider):
|
|
|
45
45
|
from Bio import Phylo
|
|
46
46
|
|
|
47
47
|
return Phylo.BaseTree.Tree
|
|
48
|
+
|
|
49
|
+
def get_support(self):
|
|
50
|
+
"""Get support/confidence values for all nodes."""
|
|
51
|
+
support_dict = {}
|
|
52
|
+
for node in self.preorder():
|
|
53
|
+
if hasattr(node, "confidences"):
|
|
54
|
+
support = node.confidences
|
|
55
|
+
elif hasattr(node, "confidence"):
|
|
56
|
+
support = node.confidence
|
|
57
|
+
else:
|
|
58
|
+
support = None
|
|
59
|
+
support_dict[node] = support
|
|
60
|
+
return support_dict
|
|
@@ -39,3 +39,10 @@ class Cogent3DataProvider(TreeDataProvider):
|
|
|
39
39
|
from cogent3.core.tree import PhyloNode
|
|
40
40
|
|
|
41
41
|
return PhyloNode
|
|
42
|
+
|
|
43
|
+
def get_support(self):
|
|
44
|
+
"""Get support values for all nodes."""
|
|
45
|
+
support_dict = {}
|
|
46
|
+
for node in self.preorder():
|
|
47
|
+
support_dict[node] = node.params.get("support", None)
|
|
48
|
+
return support_dict
|
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,70 @@ 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
258
|
vertex_labels: Optional[
|
|
200
259
|
Sequence[str] | dict[Hashable, str] | pd.Series | bool
|
|
201
260
|
] = None,
|
|
202
261
|
edge_labels: Optional[Sequence[str] | dict] = None,
|
|
203
|
-
leaf_labels: Optional[
|
|
262
|
+
leaf_labels: Optional[
|
|
263
|
+
Sequence[str] | dict[Hashable, str] | pd.Series | bool
|
|
264
|
+
] = None,
|
|
204
265
|
) -> TreeData:
|
|
205
|
-
"""Create tree data object for iplotx from ete4.core.tre.Tree classes.
|
|
266
|
+
"""Create tree data object for iplotx from ete4.core.tre.Tree classes.
|
|
267
|
+
|
|
268
|
+
NOTE: This function needs NOT be implemented by individual providers.
|
|
269
|
+
"""
|
|
206
270
|
|
|
207
271
|
if layout_style is None:
|
|
208
272
|
layout_style = {}
|
|
209
273
|
|
|
274
|
+
orientation = layout_style.pop("orientation", None)
|
|
210
275
|
if orientation is None:
|
|
211
276
|
if layout == "horizontal":
|
|
212
277
|
orientation = "right"
|
|
@@ -240,6 +305,15 @@ class TreeDataProvider(Protocol):
|
|
|
240
305
|
else:
|
|
241
306
|
tree_data["layout_coordinate_system"] = "cartesian"
|
|
242
307
|
|
|
308
|
+
# Add leaf_df
|
|
309
|
+
# NOTE: Sometimes (e.g. cogent3) the leaves convert into a pd.Index
|
|
310
|
+
# in a strange way, whereby their name disappears upon printing the
|
|
311
|
+
# index but is actually visible (and kept) when inspecting the
|
|
312
|
+
# individual elements (leaves). Seems ok functionally, though a little
|
|
313
|
+
# awkward visually during debugging.
|
|
314
|
+
tree_data["leaf_df"] = pd.DataFrame(index=self.get_leaves())
|
|
315
|
+
leaf_name_attrs = ("name",)
|
|
316
|
+
|
|
243
317
|
# Add edge_df
|
|
244
318
|
edge_data = {"_ipx_source": [], "_ipx_target": []}
|
|
245
319
|
for node in self.preorder():
|
|
@@ -253,8 +327,26 @@ class TreeDataProvider(Protocol):
|
|
|
253
327
|
edge_df = pd.DataFrame(edge_data)
|
|
254
328
|
tree_data["edge_df"] = edge_df
|
|
255
329
|
|
|
256
|
-
# Add
|
|
257
|
-
|
|
330
|
+
# Add branch support
|
|
331
|
+
if hasattr(self, "get_support"):
|
|
332
|
+
support = self.get_support()
|
|
333
|
+
|
|
334
|
+
for key, value in support.items():
|
|
335
|
+
# Leaves never show support, it's not a branching point
|
|
336
|
+
if key in tree_data["leaf_df"].index:
|
|
337
|
+
support[key] = ""
|
|
338
|
+
elif value is None:
|
|
339
|
+
support[key] = ""
|
|
340
|
+
elif np.isscalar(value):
|
|
341
|
+
# Assume support is in percentage and round it to nearest integer.
|
|
342
|
+
support[key] = str(int(np.round(value, 0)))
|
|
343
|
+
else:
|
|
344
|
+
# Apparently multiple supports are accepted in some XML format
|
|
345
|
+
support[key] = "/".join(str(int(np.round(v, 0))) for v in value)
|
|
346
|
+
|
|
347
|
+
tree_data["vertex_df"]["support"] = pd.Series(support).loc[
|
|
348
|
+
tree_data["vertex_df"].index
|
|
349
|
+
]
|
|
258
350
|
|
|
259
351
|
# Add vertex labels
|
|
260
352
|
if vertex_labels is None:
|
|
@@ -278,11 +370,18 @@ class TreeDataProvider(Protocol):
|
|
|
278
370
|
if leaf_labels is None:
|
|
279
371
|
leaf_labels = False
|
|
280
372
|
if np.isscalar(leaf_labels) and leaf_labels:
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
373
|
+
leaf_labels = []
|
|
374
|
+
for leaf in tree_data["leaf_df"].index:
|
|
375
|
+
for name_attr in leaf_name_attrs:
|
|
376
|
+
if hasattr(leaf, name_attr):
|
|
377
|
+
label = getattr(leaf, name_attr)
|
|
378
|
+
break
|
|
379
|
+
else:
|
|
380
|
+
raise ValueError(
|
|
381
|
+
"Could not find leaf name attribute.",
|
|
382
|
+
)
|
|
383
|
+
leaf_labels.append(label)
|
|
384
|
+
tree_data["leaf_df"]["label"] = leaf_labels
|
|
286
385
|
elif not np.isscalar(leaf_labels):
|
|
287
386
|
# Leaves are already in the dataframe in a certain order, so sequences are allowed
|
|
288
387
|
if isinstance(leaf_labels, (list, tuple, np.ndarray)):
|
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,65 @@ 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
|
+
self._ipx_internal_data = other._ipx_internal_data
|
|
108
|
+
return self
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_edgecollection(
|
|
112
|
+
cls: "NetworkArtist", # NOTE: This is fixed in Python 3.14
|
|
113
|
+
edge_collection: EdgeCollection,
|
|
114
|
+
) -> Self:
|
|
115
|
+
"""Create a NetworkArtist from iplotx artists.
|
|
116
|
+
|
|
117
|
+
Parameters:
|
|
118
|
+
edge_collection: The edge collection to use to initialise the artist. Vertices will
|
|
119
|
+
be obtained automatically.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
The initialised NetworkArtist.
|
|
123
|
+
"""
|
|
124
|
+
vertex_collection = edge_collection._vertex_collection
|
|
125
|
+
layout = vertex_collection._layout
|
|
126
|
+
transform = vertex_collection.get_transform()
|
|
127
|
+
offset_transform = edge_collection.get_transform()
|
|
128
|
+
|
|
129
|
+
# Follow the steps in the normal constructor
|
|
130
|
+
self = cls(
|
|
131
|
+
network=None,
|
|
132
|
+
layout=layout,
|
|
133
|
+
transform=transform,
|
|
134
|
+
offset_transform=offset_transform,
|
|
135
|
+
)
|
|
136
|
+
self._vertices = vertex_collection
|
|
137
|
+
self._edges = edge_collection
|
|
138
|
+
|
|
139
|
+
return self
|
|
85
140
|
|
|
86
141
|
def get_children(self):
|
|
87
142
|
return (self._vertices, self._edges)
|
|
@@ -189,6 +244,7 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
189
244
|
cmap_fun = _build_cmap_fun(
|
|
190
245
|
edge_style["color"],
|
|
191
246
|
edge_style["cmap"],
|
|
247
|
+
edge_style.get("norm", None),
|
|
192
248
|
)
|
|
193
249
|
else:
|
|
194
250
|
cmap_fun = None
|
|
@@ -231,7 +287,7 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
231
287
|
edgepatches.append(patch)
|
|
232
288
|
adjacent_vertex_ids.append((vid1, vid2))
|
|
233
289
|
|
|
234
|
-
if "cmap" in edge_style:
|
|
290
|
+
if ("cmap" in edge_style) and ("norm" not in edge_style):
|
|
235
291
|
vmin = np.min(colorarray)
|
|
236
292
|
vmax = np.max(colorarray)
|
|
237
293
|
norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
|
|
@@ -259,8 +315,6 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
259
315
|
if not self.get_visible():
|
|
260
316
|
return
|
|
261
317
|
|
|
262
|
-
# FIXME: Callbacks on stale vertices/edges??
|
|
263
|
-
|
|
264
318
|
# NOTE: looks like we have to manage the zorder ourselves
|
|
265
319
|
# this is kind of funny actually
|
|
266
320
|
children = list(self.get_children())
|
iplotx/plotting.py
CHANGED
|
@@ -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
|
|