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 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
- offset=self._style.get("offset", 3),
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
- offset=3,
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 * offset * (i - ntot / 2)
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
- theta0 = atan2(*((waypoint - vcoord_fig[0])[::-1]))
229
- theta1 = atan2(*((waypoint - vcoord_fig[1])[::-1]))
230
-
231
- # Shorten at starting vertex
232
- vs = (
233
- _get_shorter_edge_coords(vpath_fig[0], vsize_fig[0], theta0) + vcoord_fig[0]
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 at end vertex
237
- ve = (
238
- _get_shorter_edge_coords(vpath_fig[1], vsize_fig[1], theta1) + vcoord_fig[1]
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 = [vs, waypoint, ve]
251
+ points = [vshorts[0], waypoint, vshorts[1]]
242
252
  codes = ["MOVETO", "LINETO", "LINETO"]
243
- angles = (theta0, theta1)
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] = "right",
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,]] = None,
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
 
@@ -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(tree, layout, **kwargs)
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
- def check_dependencies(self) -> bool:
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
- def graph_type(self):
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
- def check_dependencies(self) -> bool:
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
- def graph_type(self):
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 collections.abc import Hashable
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 __call__(
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
- tree_data = {
38
- "root": tree.root,
39
- "leaves": tree.get_terminals(),
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
- # Add vertex_df including layout
47
- tree_data["vertex_df"] = normalise_tree_layout(
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
- # Add edge_df
63
- edge_data = {"_ipx_source": [], "_ipx_target": []}
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
- # Add vertex labels
76
- if vertex_labels is None:
77
- vertex_labels = False
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
- return tree_data
31
+ @staticmethod
32
+ def get_branch_length(node: Any) -> Optional[float]:
33
+ return node.branch_length
94
34
 
95
- def check_dependencies(self) -> bool:
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
- def tree_type(self):
43
+ @staticmethod
44
+ def tree_type():
103
45
  from Bio import Phylo
104
46
 
105
47
  return Phylo.BaseTree.Tree