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.
@@ -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
- try:
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
- try:
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 __call__(
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 leaf_df
257
- tree_data["leaf_df"] = pd.DataFrame(index=self.get_leaves())
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
- tree_data["leaf_labels"]["label"] = [
282
- # FIXME: this is likely broken
283
- x.name
284
- for x in tree_data["leaf_df"].index
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.zeros(len(self._labels))
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
- rot_deg = ((rot_deg + 90) % 180) - 90
178
- art.set_rotation(rot_deg)
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 of the children of this artist."""
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
- trans_inv = transData.inverted().transform_bbox
194
+ trans = transData.transform
195
+ trans_inv = transData.inverted().transform
191
196
  bboxes = []
192
- for art in self._labelartists:
193
- bbox_fig = art.get_bbox_patch().get_extents()
194
- bbox_data = trans_inv(bbox_fig)
195
- bboxes.append(bbox_data)
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", "vertial", or "radial".
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 Optional, Sequence
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
- self._add_vertices()
84
- self._add_edges()
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
- **kwargs: Additional arguments are treated as an alternate way to specify style. If
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