iplotx 0.2.0__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/network.py CHANGED
@@ -128,12 +128,12 @@ class NetworkArtist(mpl.artist.Artist):
128
128
  if len(layout) == 0:
129
129
  return mpl.transforms.Bbox([[0, 0], [1, 1]])
130
130
 
131
- if self._vertices is not None:
132
- bbox = self._vertices.get_datalim(transData)
133
-
134
- if self._edges is not None:
135
- edge_bbox = self._edges.get_datalim(transData)
136
- bbox = mpl.transforms.Bbox.union([bbox, edge_bbox])
131
+ bbox = mpl.transforms.Bbox.union(
132
+ [
133
+ self._vertices.get_datalim(transData),
134
+ self._edges.get_datalim(transData),
135
+ ]
136
+ )
137
137
 
138
138
  bbox = bbox.expanded(sw=(1.0 + pad), sh=(1.0 + pad))
139
139
  return bbox
@@ -162,6 +162,9 @@ class NetworkArtist(mpl.artist.Artist):
162
162
 
163
163
  self._vertices = VertexCollection(
164
164
  layout=self.get_layout(),
165
+ layout_coordinate_system=self._ipx_internal_data.get(
166
+ "layout_coordinate_system", "cartesian"
167
+ ),
165
168
  style=get_style(".vertex"),
166
169
  labels=self._get_label_series("vertex"),
167
170
  transform=self.get_transform(),
@@ -238,8 +241,6 @@ class NetworkArtist(mpl.artist.Artist):
238
241
  edgepatches,
239
242
  vertex_ids=adjacent_vertex_ids,
240
243
  vertex_collection=self._vertices,
241
- layout=self.get_layout(),
242
- layout_coordinate_system="cartesian",
243
244
  labels=labels,
244
245
  transform=self.get_offset_transform(),
245
246
  style=edge_style,
iplotx/plotting.py CHANGED
@@ -123,9 +123,10 @@ def network(
123
123
  def tree(
124
124
  tree: Optional[TreeType] = None,
125
125
  layout: str | LayoutType = "horizontal",
126
- orientation: str = "right",
126
+ orientation: Optional[str] = None,
127
127
  directed: bool | str = False,
128
128
  vertex_labels: Optional[list | dict | pd.Series] = None,
129
+ leaf_labels: Optional[list | dict | pd.Series] = None,
129
130
  ax: Optional[mpl.axes.Axes] = None,
130
131
  style: str | dict | Sequence[str | dict] = "tree",
131
132
  title: Optional[str] = None,
@@ -138,8 +139,9 @@ def tree(
138
139
  Parameters:
139
140
  tree: The tree to plot. Can be a BioPython.Phylo.Tree object.
140
141
  layout: The layout to use for plotting.
141
- orientation: The orientation of the horizontal layout. Can be "right" or "left". Defaults to
142
- "right".
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.
143
145
  directed: If False, donot draw arrows. If True or "child", draw arrows from parent to child
144
146
  node. If "parent", draw arrows the other way around.
145
147
 
@@ -160,6 +162,7 @@ def tree(
160
162
  transform=mpl.transforms.IdentityTransform(),
161
163
  offset_transform=ax.transData,
162
164
  vertex_labels=vertex_labels,
165
+ leaf_labels=leaf_labels,
163
166
  )
164
167
  ax.add_artist(artist)
165
168
 
iplotx/style.py CHANGED
@@ -1,6 +1,5 @@
1
1
  from typing import (
2
2
  Any,
3
- Never,
4
3
  Optional,
5
4
  Sequence,
6
5
  )
@@ -23,7 +22,9 @@ style_leaves = (
23
22
  "zorder",
24
23
  "tension",
25
24
  "looptension",
26
- "lloopmaxangle",
25
+ "loopmaxangle",
26
+ "paralleloffset",
27
+ "offset",
27
28
  "rotate",
28
29
  "marker",
29
30
  "waypoints",
@@ -35,6 +36,17 @@ style_leaves = (
35
36
  "hmargin",
36
37
  "vmargin",
37
38
  "ports",
39
+ "extend",
40
+ )
41
+
42
+ # These properties are not allowed to be rotated (global throughout the graph).
43
+ # This might change in the future as the API improves.
44
+ nonrotating_leaves = (
45
+ "paralleloffset",
46
+ "looptension",
47
+ "loopmaxangle",
48
+ "vertexpadding",
49
+ "extend",
38
50
  )
39
51
 
40
52
 
@@ -56,7 +68,7 @@ default = {
56
68
  "linestyle": "-",
57
69
  "color": "black",
58
70
  "curved": False,
59
- "offset": 3,
71
+ "paralleloffset": 3,
60
72
  "tension": 1,
61
73
  "looptension": 4,
62
74
  "loopmaxangle": 60,
@@ -103,6 +115,7 @@ hollow["vertex"]["edgecolor"] = "black"
103
115
  hollow["vertex"]["linewidth"] = 1.5
104
116
  hollow["vertex"]["marker"] = "r"
105
117
  hollow["vertex"]["size"] = "label"
118
+ hollow["vertex"]["label"]["color"] = "black"
106
119
 
107
120
  tree = copy_with_deep_values(default)
108
121
  tree["vertex"]["size"] = 0
@@ -115,8 +128,8 @@ tree["vertex"]["label"]["bbox"] = {
115
128
  }
116
129
  tree["vertex"]["label"]["color"] = "black"
117
130
  tree["vertex"]["label"]["size"] = 12
118
- tree["vertex"]["label"]["horizontalalignment"] = "left"
119
- tree["vertex"]["label"]["hmargin"] = 5
131
+ tree["vertex"]["label"]["verticalalignment"] = "center"
132
+ tree["vertex"]["label"]["hmargin"] = 10
120
133
 
121
134
 
122
135
  styles = {
@@ -137,13 +150,17 @@ def get_stylename():
137
150
  return str(stylename)
138
151
 
139
152
 
140
- def get_style(name: str = "") -> dict[str, Any]:
153
+ def get_style(name: str = "", *args) -> dict[str, Any]:
141
154
  """Get a *deep copy* of the chosen style.
142
155
 
143
156
  Parameters:
144
157
  name: The name of the style to get. If empty, the current style is returned.
145
158
  Substyles can be obtained by using a dot notation, e.g. "default.vertex".
146
159
  If "name" starts with a dot, it means a substyle of the current style.
160
+ *args: A single argument is accepted. If present, this value (usually a
161
+ dictionary) is returned if the queried style is not found. For example,
162
+ get_style(".nonexistent") raises an Exception but
163
+ get_style("nonexistent", {}) does not, returning an empty dict instead.
147
164
  Returns:
148
165
  The requected style or substyle.
149
166
 
@@ -152,6 +169,9 @@ def get_style(name: str = "") -> dict[str, Any]:
152
169
  useful for hashables that change hash upon copying, such as Biopython's
153
170
  tree nodes.
154
171
  """
172
+ if len(args) > 1:
173
+ raise ValueError("get_style() accepts at most one additional argument.")
174
+
155
175
  namelist = name.split(".")
156
176
  style = styles
157
177
  for i, namei in enumerate(namelist):
@@ -165,6 +185,8 @@ def get_style(name: str = "") -> dict[str, Any]:
165
185
  # which will not fail unless the uder tries to enter it
166
186
  elif namei not in style_leaves:
167
187
  style = {}
188
+ elif len(args) > 0:
189
+ return args[0]
168
190
  else:
169
191
  raise KeyError(f"Style not found: {name}")
170
192
 
@@ -247,7 +269,7 @@ def use(style: Optional[str | dict | Sequence] = None, **kwargs):
247
269
  raise
248
270
 
249
271
 
250
- def reset() -> Never:
272
+ def reset() -> None:
251
273
  """Reset to default style."""
252
274
  global current
253
275
  current = copy_with_deep_values(styles["default"])
@@ -276,7 +298,7 @@ def context(style: Optional[str | dict | Sequence] = None, **kwargs):
276
298
 
277
299
  def unflatten_style(
278
300
  style_flat: dict[str, str | dict | int | float],
279
- ) -> Never:
301
+ ) -> None:
280
302
  """Convert a flat or semi-flat style into a fully structured dict.
281
303
 
282
304
  Parameters:
@@ -326,7 +348,7 @@ def rotate_style(
326
348
  style: dict[str, Any],
327
349
  index: Optional[int] = None,
328
350
  key: Optional[Hashable] = None,
329
- props: Sequence[str] = style_leaves,
351
+ props: Optional[Sequence[str]] = None,
330
352
  ) -> dict[str, Any]:
331
353
  """Rotate leaves of a style for a certain index or key.
332
354
 
@@ -338,7 +360,8 @@ def rotate_style(
338
360
  props: The properties to rotate, usually all leaf properties.
339
361
 
340
362
  Returns:
341
- A style with rotated leaves, which describes the properties of a single element (e.g. vertex).
363
+ A style with rotated leaves, which describes the properties of a single element (e.g.
364
+ vertex).
342
365
 
343
366
  Example:
344
367
  >>> style = {'vertex': {'size': [10, 20]}}
@@ -350,6 +373,9 @@ def rotate_style(
350
373
  "At least one of 'index' or 'key' must be provided to rotate_style."
351
374
  )
352
375
 
376
+ if props is None:
377
+ props = tuple(prop for prop in style_leaves if prop not in nonrotating_leaves)
378
+
353
379
  style = copy_with_deep_values(style)
354
380
 
355
381
  for prop in props:
iplotx/tree.py CHANGED
@@ -2,12 +2,15 @@ from typing import (
2
2
  Optional,
3
3
  Sequence,
4
4
  )
5
+ from collections.abc import Hashable
6
+ from collections import defaultdict
5
7
 
6
8
  import numpy as np
7
9
  import pandas as pd
8
10
  import matplotlib as mpl
9
11
 
10
12
  from .style import (
13
+ context,
11
14
  get_style,
12
15
  rotate_style,
13
16
  )
@@ -18,6 +21,7 @@ from .utils.matplotlib import (
18
21
  )
19
22
  from .ingest import (
20
23
  ingest_tree_data,
24
+ data_providers,
21
25
  )
22
26
  from .vertex import (
23
27
  VertexCollection,
@@ -26,6 +30,12 @@ from .edge import (
26
30
  EdgeCollection,
27
31
  make_stub_patch as make_undirected_edge_patch,
28
32
  )
33
+ from .label import (
34
+ LabelCollection,
35
+ )
36
+ from .cascades import (
37
+ CascadeCollection,
38
+ )
29
39
  from .network import (
30
40
  _update_from_internal,
31
41
  )
@@ -47,23 +57,49 @@ class TreeArtist(mpl.artist.Artist):
47
57
  def __init__(
48
58
  self,
49
59
  tree,
50
- layout="horizontal",
51
- orientation="right",
60
+ layout: Optional[str] = "horizontal",
61
+ orientation: Optional[str] = None,
52
62
  directed: bool | str = False,
53
- vertex_labels: Optional[list | dict | pd.Series] = None,
54
- edge_labels: Optional[Sequence] = None,
63
+ vertex_labels: Optional[
64
+ bool | list[str] | dict[Hashable, str] | pd.Series
65
+ ] = None,
66
+ edge_labels: Optional[Sequence | dict[Hashable, str] | pd.Series] = None,
67
+ leaf_labels: Optional[Sequence | dict[Hashable, str]] | pd.Series = None,
55
68
  transform: mpl.transforms.Transform = mpl.transforms.IdentityTransform(),
56
69
  offset_transform: Optional[mpl.transforms.Transform] = None,
57
70
  ):
58
- self.tree = tree
71
+ """Initialize the TreeArtist.
72
+
73
+ Parameters:
74
+ tree: The tree to plot.
75
+ layout: The layout to use for the tree. Can be "horizontal", "vertical", or "radial".
76
+ orientation: The orientation of the tree layout. Can be "right" or "left" (for
77
+ horizontal and radial layouts) and "descending" or "ascending" (for vertical
78
+ layouts).
79
+ directed: Whether the tree is directed. Can be a boolean or a string with the
80
+ following choices: "parent" or "child".
81
+ vertex_labels: Labels for the vertices. Can be a list, dictionary, or pandas Series.
82
+ edge_labels: Labels for the edges. Can be a sequence of strings.
83
+ leaf_labels: Labels for the leaves. Can be a sequence of strings or a pandas Series.
84
+ These labels are positioned at the depth of the deepest leaf. If you want to
85
+ label leaves next to each leaf independently of how deep they are, use
86
+ the "vertex_labels" parameter instead - usually as a dict with the leaves
87
+ as keys and the labels as values.
88
+ transform: The transform to apply to the tree artist. This is usually the identity.
89
+ offset_transform: The offset transform to apply to the tree artist. This is
90
+ usually `ax.transData`.
91
+ """
59
92
 
93
+ self.tree = tree
60
94
  self._ipx_internal_data = ingest_tree_data(
61
95
  tree,
62
96
  layout,
63
97
  orientation=orientation,
64
98
  directed=directed,
99
+ layout_style=get_style(".layout", {}),
65
100
  vertex_labels=vertex_labels,
66
101
  edge_labels=edge_labels,
102
+ leaf_labels=leaf_labels,
67
103
  )
68
104
 
69
105
  super().__init__()
@@ -79,14 +115,52 @@ class TreeArtist(mpl.artist.Artist):
79
115
 
80
116
  self._add_vertices()
81
117
  self._add_edges()
118
+ self._add_leaf_vertices()
119
+
120
+ # NOTE: cascades need to be created after leaf vertices in case
121
+ # they are requested to wrap around them.
122
+ if "cascade" in self.get_vertices().get_style():
123
+ self._add_cascades()
124
+
125
+ def get_children(self) -> tuple[mpl.artist.Artist]:
126
+ """Get the children of this artist.
127
+
128
+ Returns:
129
+ The artists for vertices and edges.
130
+ """
131
+ children = [self._vertices, self._edges]
132
+ if hasattr(self, "_leaf_vertices"):
133
+ children.append(self._leaf_vertices)
134
+ if hasattr(self, "_cascades"):
135
+ children.append(self._cascades)
136
+ return tuple(children)
82
137
 
83
- def get_children(self):
84
- return (self._vertices, self._edges)
138
+ def set_figure(self, fig) -> None:
139
+ """Set the figure for this artist and its children.
85
140
 
86
- def set_figure(self, figure):
87
- super().set_figure(figure)
141
+ Parameters:
142
+ fig: the figure to set for this artist and its children.
143
+ """
144
+ super().set_figure(fig)
88
145
  for child in self.get_children():
89
- child.set_figure(figure)
146
+ child.set_figure(fig)
147
+
148
+ # At the end, if there are cadcades with extent depending on
149
+ # leaf edges, we should update them
150
+ self._update_cascades_extent()
151
+
152
+ def _update_cascades_extent(self) -> None:
153
+ """Update cascades if extent depends on leaf labels."""
154
+ if not hasattr(self, "_cascades"):
155
+ return
156
+
157
+ style_cascade = self.get_vertices().get_style()["cascade"]
158
+ extend_to_labels = style_cascade.get("extend", False) == "leaf_labels"
159
+ if not extend_to_labels:
160
+ return
161
+
162
+ maxdepth = self._get_maxdepth_leaf_labels()
163
+ self._cascades.set_maxdepth(maxdepth)
90
164
 
91
165
  def get_offset_transform(self):
92
166
  """Get the offset transform (for vertices/edges)."""
@@ -105,6 +179,16 @@ class TreeArtist(mpl.artist.Artist):
105
179
  if kind == "vertex":
106
180
  layout = self._ipx_internal_data["vertex_df"][layout_columns]
107
181
  return layout
182
+ elif kind == "leaf":
183
+ leaves = self._ipx_internal_data["leaf_df"].index
184
+ layout = self._ipx_internal_data["vertex_df"][layout_columns]
185
+ # NOTE: workaround for a pandas bug
186
+ idxs = []
187
+ for i, vid in enumerate(layout.index):
188
+ if vid in leaves:
189
+ idxs.append(i)
190
+ layout = layout.iloc[idxs]
191
+ return layout
108
192
 
109
193
  elif kind == "edge":
110
194
  return self._ipx_internal_data["edge_df"][layout_columns]
@@ -129,46 +213,160 @@ class TreeArtist(mpl.artist.Artist):
129
213
  edge_bbox = self._edges.get_datalim(transData)
130
214
  bbox = mpl.transforms.Bbox.union([bbox, edge_bbox])
131
215
 
216
+ if hasattr(self, "_cascades"):
217
+ cascades_bbox = self._cascades.get_datalim(transData)
218
+ bbox = mpl.transforms.Bbox.union([bbox, cascades_bbox])
219
+
220
+ if hasattr(self, "_leaf_vertices"):
221
+ leaf_labels_bbox = self._leaf_vertices.get_datalim(transData)
222
+ bbox = mpl.transforms.Bbox.union([bbox, leaf_labels_bbox])
223
+
132
224
  bbox = bbox.expanded(sw=(1.0 + pad), sh=(1.0 + pad))
133
225
  return bbox
134
226
 
135
- def _get_label_series(self, kind):
227
+ def _get_label_series(self, kind: str) -> Optional[pd.Series]:
136
228
  if "label" in self._ipx_internal_data[f"{kind}_df"].columns:
137
229
  return self._ipx_internal_data[f"{kind}_df"]["label"]
138
230
  else:
139
231
  return None
140
232
 
141
- def get_vertices(self):
233
+ def get_vertices(self) -> VertexCollection:
142
234
  """Get VertexCollection artist."""
143
235
  return self._vertices
144
236
 
145
- def get_edges(self):
237
+ def get_edges(self) -> EdgeCollection:
146
238
  """Get EdgeCollection artist."""
147
239
  return self._edges
148
240
 
149
- def get_vertex_labels(self):
241
+ def get_leaf_vertices(self) -> Optional[VertexCollection]:
242
+ """Get leaf VertexCollection artist."""
243
+ if hasattr(self, "_leaf_vertices"):
244
+ return self._leaf_vertices
245
+ return None
246
+
247
+ def get_vertex_labels(self) -> LabelCollection:
150
248
  """Get list of vertex label artists."""
151
249
  return self._vertices.get_labels()
152
250
 
153
- def get_edge_labels(self):
251
+ def get_edge_labels(self) -> LabelCollection:
154
252
  """Get list of edge label artists."""
155
253
  return self._edges.get_labels()
156
254
 
157
- def _add_vertices(self):
255
+ def get_leaf_labels(self) -> Optional[LabelCollection]:
256
+ if hasattr(self, "_leaf_vertices"):
257
+ return self._leaf_vertices.get_labels()
258
+ return None
259
+
260
+ def _add_vertices(self) -> None:
158
261
  """Add vertices to the tree."""
159
262
  self._vertices = VertexCollection(
160
263
  layout=self.get_layout(),
161
- style=get_style(".vertex"),
162
- labels=self._get_label_series("vertex"),
163
264
  layout_coordinate_system=self._ipx_internal_data.get(
164
265
  "layout_coordinate_system",
165
266
  "catesian",
166
267
  ),
268
+ style=get_style(".vertex"),
269
+ labels=self._get_label_series("vertex"),
167
270
  transform=self.get_transform(),
168
271
  offset_transform=self.get_offset_transform(),
169
272
  )
170
273
 
171
- def _add_edges(self):
274
+ def _add_leaf_vertices(self) -> None:
275
+ """Add invisible deep vertices as leaf label anchors."""
276
+ leaf_layout = self.get_layout("leaf").copy()
277
+ # Set all to max depth
278
+ depth_idx = int(self._ipx_internal_data["layout_name"] == "vertical")
279
+ leaf_layout.iloc[:, depth_idx] = leaf_layout.iloc[:, depth_idx].max()
280
+
281
+ # Set invisible vertices with visible labels
282
+ layout_name = self._ipx_internal_data["layout_name"]
283
+ orientation = self._ipx_internal_data["orientation"]
284
+ if layout_name == "radial":
285
+ ha = "auto"
286
+ elif orientation in ("left", "ascending"):
287
+ ha = "right"
288
+ else:
289
+ ha = "left"
290
+
291
+ leaf_vertex_style = {
292
+ "size": 0,
293
+ "label": {
294
+ "verticalalignment": "center",
295
+ "horizontalalignment": ha,
296
+ "hmargin": 5,
297
+ "bbox": {
298
+ "facecolor": (1, 1, 1, 0),
299
+ },
300
+ },
301
+ }
302
+ with context({"vertex": leaf_vertex_style}):
303
+ leaf_vertex_style = get_style(".vertex")
304
+ self._leaf_vertices = VertexCollection(
305
+ layout=leaf_layout,
306
+ layout_coordinate_system=self._ipx_internal_data.get(
307
+ "layout_coordinate_system",
308
+ "catesian",
309
+ ),
310
+ style=leaf_vertex_style,
311
+ labels=self._get_label_series("leaf"),
312
+ transform=self.get_transform(),
313
+ offset_transform=self.get_offset_transform(),
314
+ )
315
+
316
+ def _add_cascades(self) -> None:
317
+ """Add cascade patches."""
318
+ # NOTE: If leaf labels are present and the cascades are requested to wrap around them,
319
+ # we have to compute the max extend of the cascades from the leaf labels.
320
+ maxdepth = None
321
+ style_cascade = self.get_vertices().get_style()["cascade"]
322
+ extend_to_labels = style_cascade.get("extend", False) == "leaf_labels"
323
+ has_leaf_labels = self.get_leaf_labels() is not None
324
+ if extend_to_labels and not has_leaf_labels:
325
+ raise ValueError("Cannot extend cascades: no leaf labels.")
326
+
327
+ if extend_to_labels and has_leaf_labels:
328
+ maxdepth = self._get_maxdepth_leaf_labels()
329
+
330
+ self._cascades = CascadeCollection(
331
+ tree=self.tree,
332
+ layout=self.get_layout(),
333
+ layout_name=self._ipx_internal_data["layout_name"],
334
+ orientation=self._ipx_internal_data["orientation"],
335
+ style=style_cascade,
336
+ provider=data_providers["tree"][self._ipx_internal_data["tree_library"]],
337
+ transform=self.get_offset_transform(),
338
+ maxdepth=maxdepth,
339
+ )
340
+
341
+ def _get_maxdepth_leaf_labels(self):
342
+ layout_name = self.get_layout_name()
343
+ if layout_name == "radial":
344
+ maxdepth = 0
345
+ # These are the text boxes, they must all be included
346
+ bboxes = self.get_leaf_labels().get_datalims_children(
347
+ self.get_offset_transform()
348
+ )
349
+ for bbox in bboxes:
350
+ r1 = np.linalg.norm([bbox.xmax, bbox.ymax])
351
+ r2 = np.linalg.norm([bbox.xmax, bbox.ymin])
352
+ r3 = np.linalg.norm([bbox.xmin, bbox.ymax])
353
+ r4 = np.linalg.norm([bbox.xmin, bbox.ymin])
354
+ maxdepth = max(maxdepth, r1, r2, r3, r4)
355
+ else:
356
+ orientation = self.get_orientation()
357
+ bbox = self.get_leaf_labels().get_datalim(self.get_offset_transform())
358
+ if (layout_name, orientation) == ("horizontal", "right"):
359
+ maxdepth = bbox.xmax
360
+ elif layout_name == "horizontal":
361
+ maxdepth = bbox.xmin
362
+ elif (layout_name, orientation) == ("vertical", "descending"):
363
+ maxdepth = bbox.ymin
364
+ elif layout_name == "vertical":
365
+ maxdepth = bbox.ymax
366
+
367
+ return maxdepth
368
+
369
+ def _add_edges(self) -> None:
172
370
  """Add edges to the network artist.
173
371
 
174
372
  NOTE: UndirectedEdgeCollection and ArrowCollection are both subclasses of
@@ -227,7 +425,7 @@ class TreeArtist(mpl.artist.Artist):
227
425
  if layout_name == "horizontal":
228
426
  waypointsi = "x0y1"
229
427
  elif layout_name == "vertical":
230
- waypointsi = "y0y0"
428
+ waypointsi = "y0x1"
231
429
  elif layout_name == "radial":
232
430
  waypointsi = "r0a1"
233
431
  else:
@@ -256,11 +454,6 @@ class TreeArtist(mpl.artist.Artist):
256
454
  edgepatches,
257
455
  vertex_ids=adjacent_vertex_ids,
258
456
  vertex_collection=self._vertices,
259
- layout=self.get_layout(kind="vertex"),
260
- layout_coordinate_system=self._ipx_internal_data.get(
261
- "layout_coordinate_system",
262
- "cartesian",
263
- ),
264
457
  labels=labels,
265
458
  transform=self.get_offset_transform(),
266
459
  style=edge_style,
@@ -269,17 +462,32 @@ class TreeArtist(mpl.artist.Artist):
269
462
  if "cmap" in edge_style:
270
463
  self._edges.set_array(colorarray)
271
464
 
465
+ def get_layout_name(self) -> str:
466
+ """Get the layout name."""
467
+ return self._ipx_internal_data["layout_name"]
468
+
469
+ def get_orientation(self) -> Optional[str]:
470
+ """Get the orientation of the tree layout."""
471
+ return self._ipx_internal_data.get("orientation", None)
472
+
272
473
  @_stale_wrapper
273
- def draw(self, renderer):
474
+ def draw(self, renderer) -> None:
274
475
  """Draw each of the children, with some buffering mechanism."""
275
476
  if not self.get_visible():
276
477
  return
277
478
 
278
- # FIXME: Callbacks on stale vertices/edges??
479
+ # At the end, if there are cadcades with extent depending on
480
+ # leaf edges, we should update them
481
+ self._update_cascades_extent()
279
482
 
280
483
  # NOTE: looks like we have to manage the zorder ourselves
281
- # this is kind of funny actually
484
+ # this is kind of funny actually. Btw we need to ensure
485
+ # that cascades are drawn behind (earlier than) vertices
486
+ # and edges at equal zorder because it looks better that way.
487
+ z_suborder = defaultdict(int)
488
+ if hasattr(self, "_cascades"):
489
+ z_suborder[self._cascades] = -1
282
490
  children = list(self.get_children())
283
- children.sort(key=lambda x: x.zorder)
491
+ children.sort(key=lambda x: (x.zorder, z_suborder[x]))
284
492
  for art in children:
285
493
  art.draw(renderer)
iplotx/typing.py CHANGED
@@ -1,8 +1,17 @@
1
+ """
2
+ Typing hints for iplotx.
3
+
4
+ Some of these types are legit, others are just Any but renamed to improve readability throughout
5
+ the codebase.
6
+ """
7
+
1
8
  from typing import (
2
9
  Union,
3
10
  Sequence,
4
11
  Any,
12
+ TypeVar,
5
13
  )
14
+ from collections.abc import Hashable
6
15
  import numpy as np
7
16
  import pandas as pd
8
17
 
@@ -34,3 +43,13 @@ GroupingType = Union[
34
43
  # igraph.clustering.Cover,
35
44
  # igraph.clustering.VertexCover,
36
45
  ]
46
+
47
+
48
+ T = TypeVar("T")
49
+ LeafProperty = Union[
50
+ T,
51
+ Sequence[T],
52
+ dict[Hashable, T],
53
+ ]
54
+
55
+ Pair = tuple[T, T]
iplotx/version.py CHANGED
@@ -2,4 +2,4 @@
2
2
  iplotx version information module.
3
3
  """
4
4
 
5
- __version__ = "0.2.0"
5
+ __version__ = "0.3.0"