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/cascades.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Any,
|
|
3
|
+
Optional,
|
|
4
|
+
)
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from .typing import (
|
|
9
|
+
TreeType,
|
|
10
|
+
)
|
|
11
|
+
from .ingest.typing import (
|
|
12
|
+
TreeDataProvider,
|
|
13
|
+
)
|
|
14
|
+
import matplotlib as mpl
|
|
15
|
+
|
|
16
|
+
from .style import (
|
|
17
|
+
copy_with_deep_values,
|
|
18
|
+
rotate_style,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CascadeCollection(mpl.collections.PatchCollection):
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
tree: TreeType,
|
|
26
|
+
layout: pd.DataFrame,
|
|
27
|
+
layout_name: str,
|
|
28
|
+
orientation: str,
|
|
29
|
+
style: dict[str, Any],
|
|
30
|
+
provider: TreeDataProvider,
|
|
31
|
+
transform: mpl.transforms.Transform,
|
|
32
|
+
maxdepth: Optional[float] = None,
|
|
33
|
+
):
|
|
34
|
+
self._layout_name = layout_name
|
|
35
|
+
self._orientation = orientation
|
|
36
|
+
style = copy_with_deep_values(style)
|
|
37
|
+
zorder = style.get("zorder", 0)
|
|
38
|
+
|
|
39
|
+
# NOTE: there is a weird bug in pandas when using generic Hashable-s
|
|
40
|
+
# with .loc. Seems like doing .T[...] works for individual index
|
|
41
|
+
# elements only though
|
|
42
|
+
def get_node_coords(node):
|
|
43
|
+
return layout.T[node].values
|
|
44
|
+
|
|
45
|
+
def get_leaves_coords(leaves):
|
|
46
|
+
return np.array(
|
|
47
|
+
[get_node_coords(leaf) for leaf in leaves],
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if "color" in style:
|
|
51
|
+
style["facecolor"] = style["edgecolor"] = style.pop("color")
|
|
52
|
+
extend = style.get("extend", False)
|
|
53
|
+
|
|
54
|
+
# These patches need at least a facecolor (usually) or an edgecolor
|
|
55
|
+
# so it's safe to make a list from these
|
|
56
|
+
nodes_unordered = set()
|
|
57
|
+
for prop in ("facecolor", "edgecolor"):
|
|
58
|
+
if prop in style:
|
|
59
|
+
nodes_unordered |= set(style[prop].keys())
|
|
60
|
+
|
|
61
|
+
# Draw the patches from the closest to the root (earlier drawing)
|
|
62
|
+
# to the closer to the leaves (later drawing).
|
|
63
|
+
drawing_order = []
|
|
64
|
+
for node in provider(tree).preorder():
|
|
65
|
+
if node in nodes_unordered:
|
|
66
|
+
drawing_order.append(node)
|
|
67
|
+
|
|
68
|
+
if layout_name not in ("horizontal", "vertical", "radial"):
|
|
69
|
+
raise NotImplementedError(
|
|
70
|
+
f"Cascading patches not implemented for layout: {layout_name}.",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
nleaves = sum(1 for leaf in provider(tree).get_leaves())
|
|
74
|
+
extend_mode = style.get("extend", False)
|
|
75
|
+
if extend_mode and (extend_mode != "leaf_labels"):
|
|
76
|
+
if layout_name == "horizontal":
|
|
77
|
+
if orientation == "right":
|
|
78
|
+
maxdepth = layout.values[:, 0].max()
|
|
79
|
+
else:
|
|
80
|
+
maxdepth = layout.values[:, 0].min()
|
|
81
|
+
elif layout_name == "vertical":
|
|
82
|
+
if orientation == "descending":
|
|
83
|
+
maxdepth = layout.values[:, 1].min()
|
|
84
|
+
else:
|
|
85
|
+
maxdepth = layout.values[:, 1].max()
|
|
86
|
+
elif layout_name == "radial":
|
|
87
|
+
# layout values are: r, theta
|
|
88
|
+
maxdepth = layout.values[:, 0].max()
|
|
89
|
+
self._maxdepth = maxdepth
|
|
90
|
+
|
|
91
|
+
cascading_patches = []
|
|
92
|
+
for node in drawing_order:
|
|
93
|
+
stylei = rotate_style(style, key=node)
|
|
94
|
+
stylei.pop("extend", None)
|
|
95
|
+
# Default alpha is 0.5 for simple colors
|
|
96
|
+
if isinstance(stylei.get("facecolor", None), str) and (
|
|
97
|
+
"alpha" not in stylei
|
|
98
|
+
):
|
|
99
|
+
stylei["alpha"] = 0.5
|
|
100
|
+
|
|
101
|
+
provider_node = provider(node)
|
|
102
|
+
bl = provider_node.get_branch_length_default_to_one(node)
|
|
103
|
+
node_coords = get_node_coords(node).copy()
|
|
104
|
+
leaves_coords = get_leaves_coords(provider_node.get_leaves())
|
|
105
|
+
if len(leaves_coords) == 0:
|
|
106
|
+
leaves_coords = np.array([node_coords])
|
|
107
|
+
|
|
108
|
+
if layout_name in ("horizontal", "vertical"):
|
|
109
|
+
if layout_name == "horizontal":
|
|
110
|
+
ybot = leaves_coords[:, 1].min() - 0.5
|
|
111
|
+
ytop = leaves_coords[:, 1].max() + 0.5
|
|
112
|
+
if orientation == "right":
|
|
113
|
+
xleft = node_coords[0] - bl
|
|
114
|
+
xright = maxdepth if extend else leaves_coords[:, 0].max()
|
|
115
|
+
else:
|
|
116
|
+
xleft = maxdepth if extend else leaves_coords[:, 0].min()
|
|
117
|
+
xright = node_coords[0] + bl
|
|
118
|
+
elif layout_name == "vertical":
|
|
119
|
+
xleft = leaves_coords[:, 0].min() - 0.5
|
|
120
|
+
xright = leaves_coords[:, 0].max() + 0.5
|
|
121
|
+
if orientation == "descending":
|
|
122
|
+
ytop = node_coords[1] + bl
|
|
123
|
+
ybot = maxdepth if extend else leaves_coords[:, 1].min()
|
|
124
|
+
else:
|
|
125
|
+
ytop = maxdepth if extend else leaves_coords[:, 1].max()
|
|
126
|
+
ybot = node_coords[1] - bl
|
|
127
|
+
|
|
128
|
+
patch = mpl.patches.Rectangle(
|
|
129
|
+
(xleft, ybot),
|
|
130
|
+
xright - xleft,
|
|
131
|
+
ytop - ybot,
|
|
132
|
+
**stylei,
|
|
133
|
+
)
|
|
134
|
+
elif layout_name == "radial":
|
|
135
|
+
dtheta = 2 * np.pi / nleaves
|
|
136
|
+
rmin = node_coords[0] - bl
|
|
137
|
+
rmax = maxdepth if extend else leaves_coords[:, 0].max()
|
|
138
|
+
thetamin = leaves_coords[:, 1].min() - 0.5 * dtheta
|
|
139
|
+
thetamax = leaves_coords[:, 1].max() + 0.5 * dtheta
|
|
140
|
+
thetas = np.linspace(
|
|
141
|
+
thetamin, thetamax, max(30, (thetamax - thetamin) // 3)
|
|
142
|
+
)
|
|
143
|
+
xs = list(rmin * np.cos(thetas)) + list(rmax * np.cos(thetas[::-1]))
|
|
144
|
+
ys = list(rmin * np.sin(thetas)) + list(rmax * np.sin(thetas[::-1]))
|
|
145
|
+
points = list(zip(xs, ys))
|
|
146
|
+
points.append(points[0])
|
|
147
|
+
codes = ["MOVETO"] + ["LINETO"] * (len(points) - 2) + ["CLOSEPOLY"]
|
|
148
|
+
|
|
149
|
+
if "edgecolor" not in stylei:
|
|
150
|
+
stylei["edgecolor"] = "none"
|
|
151
|
+
|
|
152
|
+
path = mpl.path.Path(
|
|
153
|
+
points,
|
|
154
|
+
codes=[getattr(mpl.path.Path, code) for code in codes],
|
|
155
|
+
)
|
|
156
|
+
patch = mpl.patches.PathPatch(
|
|
157
|
+
path,
|
|
158
|
+
**stylei,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
cascading_patches.append(patch)
|
|
162
|
+
|
|
163
|
+
super().__init__(
|
|
164
|
+
cascading_patches,
|
|
165
|
+
transform=transform,
|
|
166
|
+
match_original=True,
|
|
167
|
+
zorder=zorder,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def get_maxdepth(self) -> float:
|
|
171
|
+
"""Get the maxdepth of the cascades.
|
|
172
|
+
|
|
173
|
+
Returns: The maximum depth of the cascading patches.
|
|
174
|
+
"""
|
|
175
|
+
return self._maxdepth
|
|
176
|
+
|
|
177
|
+
def set_maxdepth(self, maxdepth: float):
|
|
178
|
+
"""Set the maximum depth of the cascading patches.
|
|
179
|
+
|
|
180
|
+
Parameters:
|
|
181
|
+
maxdepth: The new maximum depth for the cascades.
|
|
182
|
+
|
|
183
|
+
NOTE: Calling this function updates the cascade patches
|
|
184
|
+
without chechking whether the extent style requires it.
|
|
185
|
+
"""
|
|
186
|
+
self._maxdepth = maxdepth
|
|
187
|
+
self._update_maxdepth()
|
|
188
|
+
|
|
189
|
+
def _update_maxdepth(self):
|
|
190
|
+
"""Update the cascades with a new max depth.
|
|
191
|
+
|
|
192
|
+
Note: This function changes the paths without checking whether
|
|
193
|
+
the extent is set or not.
|
|
194
|
+
"""
|
|
195
|
+
layout_name = self._layout_name
|
|
196
|
+
orientation = self._orientation
|
|
197
|
+
|
|
198
|
+
# This being a PatchCollection, we have to touch the paths
|
|
199
|
+
if layout_name == "radial":
|
|
200
|
+
for path in self.get_paths():
|
|
201
|
+
# Old radii
|
|
202
|
+
r2old = np.linalg.norm(path.vertices[-2])
|
|
203
|
+
path.vertices[(len(path.vertices) - 1) // 2 :] *= (
|
|
204
|
+
self.get_maxdepth() / r2old
|
|
205
|
+
)
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
if (layout_name, orientation) == ("horizontal", "right"):
|
|
209
|
+
for path in self.get_paths():
|
|
210
|
+
path.vertices[[1, 2], 0] = self.get_maxdepth()
|
|
211
|
+
elif (layout_name, orientation) == ("horizontal", "right"):
|
|
212
|
+
for path in self.get_paths():
|
|
213
|
+
path.vertices[[0, 3], 0] = self.get_maxdepth()
|
|
214
|
+
elif (layout_name, orientation) == ("vertical", "descending"):
|
|
215
|
+
for path in self.get_paths():
|
|
216
|
+
path.vertices[[1, 2], 1] = self.get_maxdepth()
|
|
217
|
+
elif (layout_name, orientation) == ("vertical", "ascending"):
|
|
218
|
+
for path in self.get_paths():
|
|
219
|
+
path.vertices[[0, 3], 1] = self.get_maxdepth()
|
|
220
|
+
else:
|
|
221
|
+
raise ValueError(
|
|
222
|
+
f"Layout name and orientation not supported: {layout_name}, {orientation}."
|
|
223
|
+
)
|
iplotx/edge/__init__.py
CHANGED
|
@@ -297,6 +297,8 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
297
297
|
ports = None
|
|
298
298
|
|
|
299
299
|
waypoints = edge_stylei.get("waypoints", "none")
|
|
300
|
+
if waypoints != "none":
|
|
301
|
+
ports = edge_stylei.get("ports", (None, None))
|
|
300
302
|
|
|
301
303
|
# Compute actual edge path
|
|
302
304
|
path, angles = _compute_edge_path(
|
|
@@ -311,6 +313,22 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
311
313
|
layout_coordinate_system=self._vertex_collection.get_layout_coordinate_system(),
|
|
312
314
|
)
|
|
313
315
|
|
|
316
|
+
offset = edge_stylei.get("offset", 0)
|
|
317
|
+
if np.isscalar(offset):
|
|
318
|
+
if offset == 0:
|
|
319
|
+
offset = (0, 0)
|
|
320
|
+
else:
|
|
321
|
+
vd_fig = trans(vcoord_data[1]) - trans(vcoord_data[0])
|
|
322
|
+
vd_fig /= np.linalg.norm(vd_fig)
|
|
323
|
+
vrot = vd_fig @ np.array([[0, -1], [1, 0]])
|
|
324
|
+
offset = offset * vrot
|
|
325
|
+
offset = np.asarray(offset, dtype=float)
|
|
326
|
+
# Scale by dpi
|
|
327
|
+
dpi = self.figure.dpi if hasattr(self, "figure") else 72.0
|
|
328
|
+
offset *= dpi / 72.0
|
|
329
|
+
if (offset != 0).any():
|
|
330
|
+
path.vertices[:] = trans_inv(trans(path.vertices) + offset)
|
|
331
|
+
|
|
314
332
|
# Collect angles for this vertex, to be used for loops plotting below
|
|
315
333
|
if v1 in loop_vertex_dict:
|
|
316
334
|
loop_vertex_dict[v1]["edge_angles"].append(angles[0])
|
|
@@ -338,7 +356,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
338
356
|
indices_inv,
|
|
339
357
|
trans,
|
|
340
358
|
trans_inv,
|
|
341
|
-
|
|
359
|
+
paralleloffset=self._style.get("paralleloffset", 3),
|
|
342
360
|
)
|
|
343
361
|
|
|
344
362
|
# 3. Deal with loops at the end
|
|
@@ -609,6 +627,7 @@ def make_stub_patch(**kwargs):
|
|
|
609
627
|
"looptension",
|
|
610
628
|
"loopmaxangle",
|
|
611
629
|
"offset",
|
|
630
|
+
"paralleloffset",
|
|
612
631
|
"cmap",
|
|
613
632
|
]
|
|
614
633
|
for prop in forbidden_props:
|
iplotx/edge/geometry.py
CHANGED
|
@@ -108,7 +108,7 @@ def _fix_parallel_edges_straight(
|
|
|
108
108
|
indices_inv,
|
|
109
109
|
trans,
|
|
110
110
|
trans_inv,
|
|
111
|
-
|
|
111
|
+
paralleloffset=3,
|
|
112
112
|
):
|
|
113
113
|
"""Offset parallel edges along the same path."""
|
|
114
114
|
ntot = len(indices) + len(indices_inv)
|
|
@@ -124,7 +124,7 @@ def _fix_parallel_edges_straight(
|
|
|
124
124
|
for i, idx in enumerate(indices + indices_inv):
|
|
125
125
|
# Offset the path
|
|
126
126
|
paths[idx].vertices = trans_inv(
|
|
127
|
-
trans(paths[idx].vertices) + fracs *
|
|
127
|
+
trans(paths[idx].vertices) + fracs * paralleloffset * (i - ntot / 2)
|
|
128
128
|
)
|
|
129
129
|
|
|
130
130
|
|
|
@@ -210,6 +210,7 @@ def _compute_edge_path_waypoints(
|
|
|
210
210
|
trans_inv,
|
|
211
211
|
layout_coordinate_system: str = "cartesian",
|
|
212
212
|
points_per_curve: int = 30,
|
|
213
|
+
ports: Pair[Optional[str]] = (None, None),
|
|
213
214
|
**kwargs,
|
|
214
215
|
):
|
|
215
216
|
|
|
@@ -225,22 +226,76 @@ def _compute_edge_path_waypoints(
|
|
|
225
226
|
waypoint = np.array([vcoord_fig[1][0], vcoord_fig[0][1]])
|
|
226
227
|
|
|
227
228
|
# Angles of the straight lines
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
229
|
+
thetas = [None, None]
|
|
230
|
+
vshorts = [None, None]
|
|
231
|
+
for i in range(2):
|
|
232
|
+
if ports[i] is None:
|
|
233
|
+
thetas[i] = atan2(*((waypoint - vcoord_fig[i])[::-1]))
|
|
234
|
+
else:
|
|
235
|
+
thetas[i] = atan2(*(_get_port_unit_vector(ports[i], trans_inv)[::-1]))
|
|
236
|
+
|
|
237
|
+
# Shorten at vertex border
|
|
238
|
+
vshorts[i] = (
|
|
239
|
+
_get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], thetas[i])
|
|
240
|
+
+ vcoord_fig[i]
|
|
241
|
+
)
|
|
235
242
|
|
|
236
|
-
# Shorten
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
243
|
+
# Shorten waypoints to keep the angles right
|
|
244
|
+
if waypoints == "x0y1":
|
|
245
|
+
waypoint[0] = vshorts[0][0]
|
|
246
|
+
waypoint[1] = vshorts[1][1]
|
|
247
|
+
else:
|
|
248
|
+
waypoint[1] = vshorts[0][1]
|
|
249
|
+
waypoint[0] = vshorts[1][0]
|
|
240
250
|
|
|
241
|
-
points = [
|
|
251
|
+
points = [vshorts[0], waypoint, vshorts[1]]
|
|
242
252
|
codes = ["MOVETO", "LINETO", "LINETO"]
|
|
243
|
-
angles = (
|
|
253
|
+
angles = tuple(thetas)
|
|
254
|
+
elif waypoints in ("xmidy0,xmidy1", "x0ymid,x1ymid"):
|
|
255
|
+
# S-shaped orthogonal line
|
|
256
|
+
assert layout_coordinate_system == "cartesian"
|
|
257
|
+
|
|
258
|
+
# Coordinates in figure (default) coords
|
|
259
|
+
vcoord_fig = trans(vcoord_data)
|
|
260
|
+
|
|
261
|
+
if waypoints == "xmidy0,xmidy1":
|
|
262
|
+
xmid = 0.5 * (vcoord_fig[0][0] + vcoord_fig[1][0])
|
|
263
|
+
waypoint_array = np.array(
|
|
264
|
+
[
|
|
265
|
+
[xmid, vcoord_fig[0][1]],
|
|
266
|
+
[xmid, vcoord_fig[1][1]],
|
|
267
|
+
]
|
|
268
|
+
)
|
|
269
|
+
else:
|
|
270
|
+
ymid = 0.5 * (vcoord_fig[0][1] + vcoord_fig[1][1])
|
|
271
|
+
waypoint_array = np.array(
|
|
272
|
+
[
|
|
273
|
+
[vcoord_fig[0][0], ymid],
|
|
274
|
+
[vcoord_fig[1][0], ymid],
|
|
275
|
+
]
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Angles of the straight lines
|
|
279
|
+
thetas = []
|
|
280
|
+
vshorts = []
|
|
281
|
+
for i in range(2):
|
|
282
|
+
if ports[i] is None:
|
|
283
|
+
theta = atan2(*((waypoint_array[i] - vcoord_fig[i])[::-1]))
|
|
284
|
+
else:
|
|
285
|
+
theta = atan2(*(_get_port_unit_vector(ports[i], trans_inv)[::-1]))
|
|
286
|
+
|
|
287
|
+
# Shorten at vertex border
|
|
288
|
+
vshort = (
|
|
289
|
+
_get_shorter_edge_coords(vpath_fig[i], vsize_fig[i], theta)
|
|
290
|
+
+ vcoord_fig[i]
|
|
291
|
+
)
|
|
292
|
+
thetas.append(theta)
|
|
293
|
+
vshorts.append(vshort)
|
|
294
|
+
|
|
295
|
+
points = [vshorts[0], waypoint_array[0], waypoint_array[1], vshorts[1]]
|
|
296
|
+
codes = ["MOVETO", "LINETO", "LINETO", "LINETO"]
|
|
297
|
+
angles = tuple(thetas)
|
|
298
|
+
|
|
244
299
|
elif waypoints == "r0a1":
|
|
245
300
|
assert layout_coordinate_system == "polar"
|
|
246
301
|
|
|
@@ -283,7 +338,7 @@ def _compute_edge_path_curved(
|
|
|
283
338
|
vsize_fig,
|
|
284
339
|
trans,
|
|
285
340
|
trans_inv,
|
|
286
|
-
ports=(None, None),
|
|
341
|
+
ports: Pair[Optional[str]] = (None, None),
|
|
287
342
|
):
|
|
288
343
|
"""Shorten the edge path along a cubic Bezier between the vertex centres.
|
|
289
344
|
|
|
@@ -378,6 +433,7 @@ def _compute_edge_path(
|
|
|
378
433
|
waypoints,
|
|
379
434
|
*args,
|
|
380
435
|
layout_coordinate_system=layout_coordinate_system,
|
|
436
|
+
ports=ports,
|
|
381
437
|
**kwargs,
|
|
382
438
|
)
|
|
383
439
|
|
iplotx/ingest/__init__.py
CHANGED
|
@@ -45,7 +45,7 @@ for kind in data_providers:
|
|
|
45
45
|
if key == provider_protocols[kind].__name__:
|
|
46
46
|
continue
|
|
47
47
|
if key.endswith("DataProvider"):
|
|
48
|
-
data_providers[kind][module_name] = val
|
|
48
|
+
data_providers[kind][module_name] = val
|
|
49
49
|
break
|
|
50
50
|
del providers_path
|
|
51
51
|
|
|
@@ -95,7 +95,7 @@ def ingest_network_data(
|
|
|
95
95
|
f"Currently installed supported libraries: {sup}."
|
|
96
96
|
)
|
|
97
97
|
|
|
98
|
-
result = provider(
|
|
98
|
+
result = provider()(
|
|
99
99
|
network=network,
|
|
100
100
|
layout=layout,
|
|
101
101
|
vertex_labels=vertex_labels,
|
|
@@ -108,10 +108,12 @@ def ingest_network_data(
|
|
|
108
108
|
def ingest_tree_data(
|
|
109
109
|
tree: TreeType,
|
|
110
110
|
layout: Optional[str] = "horizontal",
|
|
111
|
-
orientation: Optional[str] =
|
|
111
|
+
orientation: Optional[str] = None,
|
|
112
112
|
directed: bool | str = False,
|
|
113
|
+
layout_style: Optional[dict[str, str | int | float]] = None,
|
|
113
114
|
vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
|
|
114
|
-
edge_labels: Optional[Sequence[str] | dict[str
|
|
115
|
+
edge_labels: Optional[Sequence[str] | dict[Hashable, str]] = None,
|
|
116
|
+
leaf_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
|
|
115
117
|
) -> TreeData:
|
|
116
118
|
"""Create internal data for the tree."""
|
|
117
119
|
_update_data_providers("tree")
|
|
@@ -129,13 +131,19 @@ def ingest_tree_data(
|
|
|
129
131
|
|
|
130
132
|
result = provider(
|
|
131
133
|
tree=tree,
|
|
134
|
+
)(
|
|
132
135
|
layout=layout,
|
|
133
136
|
orientation=orientation,
|
|
134
137
|
directed=directed,
|
|
138
|
+
layout_style=layout_style,
|
|
135
139
|
vertex_labels=vertex_labels,
|
|
136
140
|
edge_labels=edge_labels,
|
|
141
|
+
leaf_labels=leaf_labels,
|
|
137
142
|
)
|
|
138
143
|
result["tree_library"] = tl
|
|
144
|
+
|
|
145
|
+
# TODO: cascading thing here
|
|
146
|
+
|
|
139
147
|
return result
|
|
140
148
|
|
|
141
149
|
|
iplotx/ingest/heuristics.py
CHANGED
|
@@ -15,7 +15,6 @@ from ..layout import compute_tree_layout
|
|
|
15
15
|
from ..typing import (
|
|
16
16
|
GraphType,
|
|
17
17
|
GroupingType,
|
|
18
|
-
TreeType,
|
|
19
18
|
LayoutType,
|
|
20
19
|
)
|
|
21
20
|
|
|
@@ -91,7 +90,6 @@ def normalise_layout(layout, network=None):
|
|
|
91
90
|
|
|
92
91
|
def normalise_tree_layout(
|
|
93
92
|
layout: str | Any,
|
|
94
|
-
tree: Optional[TreeType] = None,
|
|
95
93
|
**kwargs,
|
|
96
94
|
) -> pd.DataFrame:
|
|
97
95
|
"""Normalise tree layout from a variety of inputs.
|
|
@@ -108,7 +106,7 @@ def normalise_tree_layout(
|
|
|
108
106
|
the layout internally. This might change in the future.
|
|
109
107
|
"""
|
|
110
108
|
if isinstance(layout, str):
|
|
111
|
-
layout = compute_tree_layout(
|
|
109
|
+
layout = compute_tree_layout(layout, **kwargs)
|
|
112
110
|
else:
|
|
113
111
|
raise NotImplementedError(
|
|
114
112
|
"Only internally computed tree layout currently accepted."
|
|
@@ -83,14 +83,16 @@ class IGraphDataProvider(NetworkDataProvider):
|
|
|
83
83
|
}
|
|
84
84
|
return network_data
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
@staticmethod
|
|
87
|
+
def check_dependencies() -> bool:
|
|
87
88
|
try:
|
|
88
89
|
import igraph
|
|
89
90
|
except ImportError:
|
|
90
91
|
return False
|
|
91
92
|
return True
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
@staticmethod
|
|
95
|
+
def graph_type():
|
|
94
96
|
import igraph as ig
|
|
95
97
|
|
|
96
98
|
return ig.Graph
|
|
@@ -120,14 +120,16 @@ class NetworkXDataProvider(NetworkDataProvider):
|
|
|
120
120
|
}
|
|
121
121
|
return network_data
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
@staticmethod
|
|
124
|
+
def check_dependencies() -> bool:
|
|
124
125
|
try:
|
|
125
126
|
import networkx
|
|
126
127
|
except ImportError:
|
|
127
128
|
return False
|
|
128
129
|
return True
|
|
129
130
|
|
|
130
|
-
|
|
131
|
+
@staticmethod
|
|
132
|
+
def graph_type():
|
|
131
133
|
from networkx import Graph
|
|
132
134
|
|
|
133
135
|
return Graph
|
|
@@ -1,105 +1,47 @@
|
|
|
1
1
|
from typing import (
|
|
2
|
+
Any,
|
|
2
3
|
Optional,
|
|
3
4
|
Sequence,
|
|
4
5
|
)
|
|
5
|
-
from
|
|
6
|
-
from operator import attrgetter
|
|
7
|
-
import numpy as np
|
|
8
|
-
import pandas as pd
|
|
6
|
+
from functools import partialmethod
|
|
9
7
|
|
|
10
|
-
from ....typing import (
|
|
11
|
-
TreeType,
|
|
12
|
-
LayoutType,
|
|
13
|
-
)
|
|
14
8
|
from ...typing import (
|
|
15
9
|
TreeDataProvider,
|
|
16
|
-
TreeData,
|
|
17
|
-
)
|
|
18
|
-
from ...heuristics import (
|
|
19
|
-
normalise_tree_layout,
|
|
20
10
|
)
|
|
21
11
|
|
|
22
12
|
|
|
23
13
|
class BiopythonDataProvider(TreeDataProvider):
|
|
24
|
-
def
|
|
25
|
-
self
|
|
26
|
-
tree: TreeType,
|
|
27
|
-
layout: str | LayoutType,
|
|
28
|
-
orientation: str = "horizontal",
|
|
29
|
-
directed: bool | str = False,
|
|
30
|
-
vertex_labels: Optional[
|
|
31
|
-
Sequence[str] | dict[Hashable, str] | pd.Series | bool
|
|
32
|
-
] = None,
|
|
33
|
-
edge_labels: Optional[Sequence[str] | dict] = None,
|
|
34
|
-
) -> TreeData:
|
|
35
|
-
"""Create tree data object for iplotx from BioPython.Phylo.Tree classes."""
|
|
14
|
+
def is_rooted(self) -> bool:
|
|
15
|
+
return self.tree.rooted
|
|
36
16
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"rooted": tree.rooted,
|
|
41
|
-
"directed": directed,
|
|
42
|
-
"ndim": 2,
|
|
43
|
-
"layout_name": layout,
|
|
44
|
-
}
|
|
17
|
+
def _traverse(self, order: str) -> Any:
|
|
18
|
+
"""Traverse the tree."""
|
|
19
|
+
return self.tree.find_clades(order=order)
|
|
45
20
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
layout,
|
|
49
|
-
tree=tree,
|
|
50
|
-
orientation=orientation,
|
|
51
|
-
root_fun=attrgetter("root"),
|
|
52
|
-
preorder_fun=lambda tree: tree.find_clades(order="preorder"),
|
|
53
|
-
postorder_fun=lambda tree: tree.find_clades(order="postorder"),
|
|
54
|
-
children_fun=attrgetter("clades"),
|
|
55
|
-
branch_length_fun=attrgetter("branch_length"),
|
|
56
|
-
)
|
|
57
|
-
if layout in ("radial",):
|
|
58
|
-
tree_data["layout_coordinate_system"] = "polar"
|
|
59
|
-
else:
|
|
60
|
-
tree_data["layout_coordinate_system"] = "cartesian"
|
|
21
|
+
preorder = partialmethod(_traverse, order="preorder")
|
|
22
|
+
postorder = partialmethod(_traverse, order="postorder")
|
|
61
23
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
for node in tree.find_clades(order="preorder"):
|
|
65
|
-
for child in node.clades:
|
|
66
|
-
if directed == "parent":
|
|
67
|
-
edge_data["_ipx_source"].append(child)
|
|
68
|
-
edge_data["_ipx_target"].append(node)
|
|
69
|
-
else:
|
|
70
|
-
edge_data["_ipx_source"].append(node)
|
|
71
|
-
edge_data["_ipx_target"].append(child)
|
|
72
|
-
edge_df = pd.DataFrame(edge_data)
|
|
73
|
-
tree_data["edge_df"] = edge_df
|
|
24
|
+
def get_leaves(self) -> Sequence[Any]:
|
|
25
|
+
return self.tree.get_terminals()
|
|
74
26
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if np.isscalar(vertex_labels) and vertex_labels:
|
|
79
|
-
tree_data["vertex_df"]["label"] = [
|
|
80
|
-
x.name for x in tree_data["vertices"].index
|
|
81
|
-
]
|
|
82
|
-
elif not np.isscalar(vertex_labels):
|
|
83
|
-
# If a dict-like object is passed, it can be incomplete (e.g. only the leaves):
|
|
84
|
-
# we fill the rest with empty strings which are not going to show up in the plot.
|
|
85
|
-
if isinstance(vertex_labels, pd.Series):
|
|
86
|
-
vertex_labels = dict(vertex_labels)
|
|
87
|
-
if isinstance(vertex_labels, dict):
|
|
88
|
-
for vertex in tree_data["vertex_df"].index:
|
|
89
|
-
if vertex not in vertex_labels:
|
|
90
|
-
vertex_labels[vertex] = ""
|
|
91
|
-
tree_data["vertex_df"]["label"] = pd.Series(vertex_labels)
|
|
27
|
+
@staticmethod
|
|
28
|
+
def get_children(node: Any) -> Sequence[Any]:
|
|
29
|
+
return node.clades
|
|
92
30
|
|
|
93
|
-
|
|
31
|
+
@staticmethod
|
|
32
|
+
def get_branch_length(node: Any) -> Optional[float]:
|
|
33
|
+
return node.branch_length
|
|
94
34
|
|
|
95
|
-
|
|
35
|
+
@staticmethod
|
|
36
|
+
def check_dependencies() -> bool:
|
|
96
37
|
try:
|
|
97
38
|
from Bio import Phylo
|
|
98
39
|
except ImportError:
|
|
99
40
|
return False
|
|
100
41
|
return True
|
|
101
42
|
|
|
102
|
-
|
|
43
|
+
@staticmethod
|
|
44
|
+
def tree_type():
|
|
103
45
|
from Bio import Phylo
|
|
104
46
|
|
|
105
47
|
return Phylo.BaseTree.Tree
|