iplotx 0.4.0__py3-none-any.whl → 0.5.1__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.
@@ -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
@@ -255,13 +255,9 @@ class TreeDataProvider(Protocol):
255
255
  layout: str | LayoutType,
256
256
  layout_style: Optional[dict[str, int | float | str]] = None,
257
257
  directed: bool | str = False,
258
- vertex_labels: Optional[
259
- Sequence[str] | dict[Hashable, str] | pd.Series | bool
260
- ] = None,
258
+ vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series | bool] = None,
261
259
  edge_labels: Optional[Sequence[str] | dict] = None,
262
- leaf_labels: Optional[
263
- Sequence[str] | dict[Hashable, str] | pd.Series | bool
264
- ] = None,
260
+ leaf_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series | bool] = None,
265
261
  ) -> TreeData:
266
262
  """Create tree data object for iplotx from ete4.core.tre.Tree classes.
267
263
 
@@ -344,17 +340,13 @@ class TreeDataProvider(Protocol):
344
340
  # Apparently multiple supports are accepted in some XML format
345
341
  support[key] = "/".join(str(int(np.round(v, 0))) for v in value)
346
342
 
347
- tree_data["vertex_df"]["support"] = pd.Series(support).loc[
348
- tree_data["vertex_df"].index
349
- ]
343
+ tree_data["vertex_df"]["support"] = pd.Series(support).loc[tree_data["vertex_df"].index]
350
344
 
351
345
  # Add vertex labels
352
346
  if vertex_labels is None:
353
347
  vertex_labels = False
354
348
  if np.isscalar(vertex_labels) and vertex_labels:
355
- tree_data["vertex_df"]["label"] = [
356
- x.name for x in tree_data["vertex_df"].index
357
- ]
349
+ tree_data["vertex_df"]["label"] = [x.name for x in tree_data["vertex_df"].index]
358
350
  elif not np.isscalar(vertex_labels):
359
351
  # If a dict-like object is passed, it can be incomplete (e.g. only the leaves):
360
352
  # we fill the rest with empty strings which are not going to show up in the plot.
@@ -386,8 +378,7 @@ class TreeDataProvider(Protocol):
386
378
  # Leaves are already in the dataframe in a certain order, so sequences are allowed
387
379
  if isinstance(leaf_labels, (list, tuple, np.ndarray)):
388
380
  leaf_labels = {
389
- leaf: label
390
- 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)
391
382
  }
392
383
  # If a dict-like object is passed, it can be incomplete (e.g. only the leaves):
393
384
  # we fill the rest with empty strings which are not going to show up in the plot.
iplotx/network.py CHANGED
@@ -104,7 +104,8 @@ class NetworkArtist(mpl.artist.Artist):
104
104
  """
105
105
  self = cls.from_edgecollection(other._edges)
106
106
  self.network = other.network
107
- self._ipx_internal_data = other._ipx_internal_data
107
+ if hasattr(other, "_ipx_internal_data"):
108
+ self._ipx_internal_data = other._ipx_internal_data
108
109
  return self
109
110
 
110
111
  @classmethod
@@ -200,9 +201,7 @@ class NetworkArtist(mpl.artist.Artist):
200
201
  self.axes.autoscale_view(tight=tight)
201
202
 
202
203
  def get_layout(self):
203
- layout_columns = [
204
- f"_ipx_layout_{i}" for i in range(self._ipx_internal_data["ndim"])
205
- ]
204
+ layout_columns = [f"_ipx_layout_{i}" for i in range(self._ipx_internal_data["ndim"])]
206
205
  vertex_layout_df = self._ipx_internal_data["vertex_df"][layout_columns]
207
206
  return vertex_layout_df
208
207
 
@@ -249,9 +248,7 @@ class NetworkArtist(mpl.artist.Artist):
249
248
  else:
250
249
  cmap_fun = None
251
250
 
252
- edge_df = self._ipx_internal_data["edge_df"].set_index(
253
- ["_ipx_source", "_ipx_target"]
254
- )
251
+ edge_df = self._ipx_internal_data["edge_df"].set_index(["_ipx_source", "_ipx_target"])
255
252
 
256
253
  if "cmap" in edge_style:
257
254
  colorarray = []
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] = (),
iplotx/style/__init__.py CHANGED
@@ -1,8 +1,10 @@
1
1
  from typing import (
2
2
  Any,
3
+ Iterable,
3
4
  Optional,
4
5
  Sequence,
5
6
  )
7
+ from collections import defaultdict
6
8
  from collections.abc import Hashable
7
9
  from contextlib import contextmanager
8
10
  import numpy as np
@@ -16,53 +18,8 @@ from .leaf_info import (
16
18
  )
17
19
 
18
20
 
19
- default = {
20
- "vertex": {
21
- "size": 20,
22
- "facecolor": "black",
23
- "marker": "o",
24
- "label": {
25
- "color": "white",
26
- "horizontalalignment": "center",
27
- "verticalalignment": "center",
28
- "hpadding": 18,
29
- "vpadding": 12,
30
- },
31
- },
32
- "edge": {
33
- "linewidth": 1.5,
34
- "linestyle": "-",
35
- "color": "black",
36
- "curved": False,
37
- "tension": 1,
38
- "looptension": 4,
39
- "loopmaxangle": 60,
40
- "paralleloffset": 3,
41
- "label": {
42
- "horizontalalignment": "center",
43
- "verticalalignment": "center",
44
- "rotate": False,
45
- "bbox": {
46
- "boxstyle": "round",
47
- "facecolor": "white",
48
- "edgecolor": "none",
49
- },
50
- },
51
- "arrow": {
52
- "marker": "|>",
53
- "width": 8,
54
- },
55
- },
56
- "grouping": {
57
- "facecolor": ["grey", "steelblue", "tomato"],
58
- "edgecolor": "black",
59
- "linewidth": 1.5,
60
- "alpha": 0.5,
61
- "vertexpadding": 18,
62
- },
63
- }
64
-
65
-
21
+ # Prepopulate default style, it's used later as a backbone for everything else
22
+ default = style_library["default"]
66
23
  styles = {
67
24
  "default": default,
68
25
  }
@@ -115,36 +72,33 @@ def get_style(name: str = "", *args) -> dict[str, Any]:
115
72
  return style
116
73
 
117
74
 
118
- # The following is inspired by matplotlib's style library
119
- # https://github.com/matplotlib/matplotlib/blob/v3.10.3/lib/matplotlib/style/core.py#L45
120
- def use(style: Optional[str | dict | Sequence] = None, **kwargs):
121
- """Use iplotx style setting for a style specification.
122
-
123
- The style name of 'default' is reserved for reverting back to
124
- the default style settings.
75
+ def merge_styles(
76
+ styles: Sequence[str | dict[str, Any]] | Iterable[str | dict[str, Any]],
77
+ ) -> dict[str, Any]:
78
+ """Merge a sequence of styles into a single one.
125
79
 
126
80
  Parameters:
127
- style: A style specification, currently either a name of an existing style
128
- or a dict with specific parts of the style to override. The string
129
- "default" resets the style to the default one. If this is a sequence,
130
- each style is applied in order.
131
- **kwargs: Additional style changes to be applied at the end of any style.
81
+ styles: Sequence (list, tuple, etc.) of styles, each either the name of an internal
82
+ style or a dict-like with custom properties.
83
+ Returns:
84
+ The composite style as a dict.
132
85
  """
133
86
  try:
134
87
  import networkx as nx
135
88
  except ImportError:
136
89
  nx = None
137
90
 
138
- global current
139
-
140
91
  def _sanitize_leaves(style: dict):
141
92
  for key, value in style.items():
142
93
  if key in style_leaves:
94
+ # Networkx has a few lazy data structures
95
+ # TODO: move this code to provider
143
96
  if nx is not None:
144
97
  if isinstance(value, nx.classes.reportviews.NodeView):
145
98
  style[key] = dict(value)
146
99
  elif isinstance(value, nx.classes.reportviews.EdgeViewABC):
147
100
  style[key] = [v for *e, v in value]
101
+
148
102
  elif isinstance(value, dict):
149
103
  _sanitize_leaves(value)
150
104
 
@@ -154,37 +108,97 @@ def use(style: Optional[str | dict | Sequence] = None, **kwargs):
154
108
  current[key] = value
155
109
  continue
156
110
 
157
- # Style leaves are by definition not to be recurred into
158
- if isinstance(value, dict) and (key not in style_leaves):
159
- _update(value, current[key])
160
- elif value is None:
161
- del current[key]
111
+ # Style non-leaves are either recurred into or deleted
112
+ if key not in style_leaves:
113
+ if isinstance(value, dict):
114
+ _update(value, current[key])
115
+ elif value is None:
116
+ del current[key]
117
+ else:
118
+ raise ValueError(
119
+ f"Setting non-leaf style value to a non-dict: {key}, {value}",
120
+ )
162
121
  else:
163
- current[key] = value
122
+ # Style leaves could be incomplete, ensure a sensible default
123
+ if value is None:
124
+ del current[key]
125
+ continue
164
126
 
165
- old_style = copy_with_deep_values(current)
127
+ if not isinstance(value, dict):
128
+ current[key] = value
129
+ continue
166
130
 
167
- try:
168
- if style is None:
169
- styles = []
170
- elif isinstance(style, (dict, str)):
171
- styles = [style]
131
+ if hasattr(value, "default_factory"):
132
+ current[key] = value
133
+ continue
134
+
135
+ if hasattr(current[key], "default_factory"):
136
+ default_value = current[key].default_factory()
137
+ else:
138
+ default_value = current[key]
139
+ current[key] = defaultdict(
140
+ lambda: default_value,
141
+ value,
142
+ )
143
+
144
+ merged = {}
145
+ for style in styles:
146
+ if isinstance(style, str):
147
+ style = get_style(style)
172
148
  else:
173
- styles = list(style)
149
+ _sanitize_leaves(style)
150
+ unflatten_style(style)
151
+ _update(style, merged)
174
152
 
175
- if kwargs:
176
- styles.append(kwargs)
153
+ return merged
177
154
 
178
- for style in styles:
179
- if style == "default":
180
- reset()
181
- else:
182
- if isinstance(style, str):
183
- current = get_style(style)
184
- else:
185
- _sanitize_leaves(style)
186
- unflatten_style(style)
187
- _update(style, current)
155
+
156
+ # The following is inspired by matplotlib's style library
157
+ # https://github.com/matplotlib/matplotlib/blob/v3.10.3/lib/matplotlib/style/core.py#L45
158
+ def use(
159
+ style: Optional[
160
+ str | dict[str, Any] | Sequence[str | dict[str, Any]] | Iterable[str | dict[str, Any]]
161
+ ] = None,
162
+ **kwargs,
163
+ ):
164
+ """Use iplotx style setting for a style specification.
165
+
166
+ The style name of 'default' is reserved for reverting back to
167
+ the default style settings.
168
+
169
+ Parameters:
170
+ style: A style specification, currently either a name of an existing style
171
+ or a dict with specific parts of the style to override. The string
172
+ "default" resets the style to the default one. If this is a sequence,
173
+ each style is applied in order.
174
+ **kwargs: Additional style changes to be applied at the end of any style.
175
+ """
176
+ global current
177
+
178
+ styles = []
179
+ if isinstance(style, (dict, str)):
180
+ styles.append(style)
181
+ elif style is not None:
182
+ styles.extend(list(style))
183
+ if kwargs:
184
+ styles.append(kwargs)
185
+
186
+ # Discard empty styles for speed
187
+ styles = [style for style in styles if style]
188
+
189
+ if len(styles) == 0:
190
+ return
191
+
192
+ # If the first style is a string (internal style), apply it cold. If it's a
193
+ # dict, apply it hot (on top of the current style(. Any style after the first
194
+ # is always applied hot - otherwise it would invalidate previous styles.
195
+ if not isinstance(styles[0], str):
196
+ # hot insertion on top of current
197
+ styles.insert(0, current)
198
+
199
+ old_style = copy_with_deep_values(current)
200
+ try:
201
+ current = merge_styles(styles)
188
202
  except:
189
203
  current = old_style
190
204
  raise
@@ -197,7 +211,12 @@ def reset() -> None:
197
211
 
198
212
 
199
213
  @contextmanager
200
- def context(style: Optional[str | dict | Sequence] = None, **kwargs):
214
+ def context(
215
+ style: Optional[
216
+ str | dict[str, Any] | Sequence[str | dict[str, Any]] | Iterable[str | dict[str, Any]]
217
+ ] = None,
218
+ **kwargs,
219
+ ):
201
220
  """Create a style context for iplotx.
202
221
 
203
222
  Parameters:
@@ -257,9 +276,7 @@ def unflatten_style(
257
276
 
258
277
  # top-level adjustments
259
278
  if "zorder" in style_flat:
260
- style_flat["network_zorder"] = style_flat["grouping_zorder"] = style_flat.pop(
261
- "zorder"
262
- )
279
+ style_flat["network_zorder"] = style_flat["grouping_zorder"] = style_flat.pop("zorder")
263
280
 
264
281
  # Begin recursion
265
282
  _inner(style_flat)
@@ -269,6 +286,7 @@ def rotate_style(
269
286
  style: dict[str, Any],
270
287
  index: Optional[int] = None,
271
288
  key: Optional[Hashable] = None,
289
+ key2: Optional[Hashable] = None,
272
290
  props: Optional[Sequence[str]] = None,
273
291
  ) -> dict[str, Any]:
274
292
  """Rotate leaves of a style for a certain index or key.
@@ -278,6 +296,9 @@ def rotate_style(
278
296
  index: The integer to rotate the style leaves into.
279
297
  key: For dict-like leaves (e.g. vertex properties specified as a dict-like object over the
280
298
  vertices themselves), the key to use for rotation (e.g. the vertex itself).
299
+ key2: For dict-like leaves, a backup key in case the first key fails. If this is None
300
+ or also a failure (i.e. KeyError), default to the empty type constructor for the
301
+ first value of the dict-like style leaf.
281
302
  props: The properties to rotate, usually all leaf properties.
282
303
 
283
304
  Returns:
@@ -290,9 +311,7 @@ def rotate_style(
290
311
  {'vertex': {'size': 10}}
291
312
  """
292
313
  if (index is None) and (key is None):
293
- raise ValueError(
294
- "At least one of 'index' or 'key' must be provided to rotate_style."
295
- )
314
+ raise ValueError("At least one of 'index' or 'key' must be provided to rotate_style.")
296
315
 
297
316
  if props is None:
298
317
  props = tuple(prop for prop in style_leaves if prop not in nonrotating_leaves)
@@ -304,24 +323,29 @@ def rotate_style(
304
323
  if val is None:
305
324
  continue
306
325
  # Try integer indexing for ordered types
307
- if (index is not None) and isinstance(
308
- val, (tuple, list, np.ndarray, pd.Index, pd.Series)
309
- ):
310
- style[prop] = np.asarray(val)[index % len(val)]
326
+ if (index is not None) and isinstance(val, (tuple, list, np.ndarray, pd.Index, pd.Series)):
327
+ # NOTE: cannot cast to ndarray because rotation might involve
328
+ # cross-type lists (e.g. ["none", False])
329
+ style[prop] = list(val)[index % len(val)]
311
330
  # Try key indexing for unordered, dict-like types
312
331
  if (
313
332
  (key is not None)
314
333
  and (not isinstance(val, (str, tuple, list, np.ndarray)))
315
334
  and hasattr(val, "__getitem__")
316
335
  ):
317
- # If only a subset of keys is provided, default the other ones
318
- # to the empty type constructor (e.g. 0 for ints, 0.0 for floats,
319
- # empty strings).
336
+ # If only a subset of keys is provided, try the default value a la
337
+ # defaultdict. If that fails, use an empty constructor
320
338
  if key in val:
321
- style[prop] = val[key]
339
+ newval = val[key]
340
+ elif key2 is not None and (key2 in val):
341
+ newval = val[key2]
322
342
  else:
323
- valtype = type(next(iter(val.values())))
324
- style[prop] = valtype()
343
+ try:
344
+ newval = val[key]
345
+ except KeyError:
346
+ valtype = type(next(iter(val.values())))
347
+ newval = valtype()
348
+ style[prop] = newval
325
349
 
326
350
  return style
327
351
 
@@ -337,7 +361,8 @@ def add_style(name: str, style: dict[str, Any]) -> None:
337
361
  styles[name] = get_style()
338
362
 
339
363
 
340
- # Populate style library
364
+ # Populate style library (default is already there)
341
365
  for name, style in style_library.items():
342
- add_style(name, style)
343
- del name, style
366
+ if name != "default":
367
+ add_style(name, style)
368
+ del name, style
iplotx/style/leaf_info.py CHANGED
@@ -23,6 +23,8 @@ rotating_leaves = (
23
23
  "hmargin",
24
24
  "vmargin",
25
25
  "ports",
26
+ "width",
27
+ "height",
26
28
  )
27
29
 
28
30
  # These properties are also terminal style properties, but they cannot be rotated.
@@ -35,6 +37,7 @@ nonrotating_leaves = (
35
37
  "extend",
36
38
  "deep",
37
39
  "angular",
40
+ "curved",
38
41
  )
39
42
 
40
43
  # Union of all style leaves (rotating and nonrotating)
iplotx/style/library.py CHANGED
@@ -1,4 +1,49 @@
1
1
  style_library = {
2
+ "default": {
3
+ "vertex": {
4
+ "size": 20,
5
+ "facecolor": "black",
6
+ "marker": "o",
7
+ "label": {
8
+ "color": "white",
9
+ "horizontalalignment": "center",
10
+ "verticalalignment": "center",
11
+ "hpadding": 18,
12
+ "vpadding": 12,
13
+ },
14
+ },
15
+ "edge": {
16
+ "linewidth": 1.5,
17
+ "linestyle": "-",
18
+ "color": "black",
19
+ "curved": False,
20
+ "tension": 1,
21
+ "looptension": 4,
22
+ "loopmaxangle": 60,
23
+ "paralleloffset": 3,
24
+ "label": {
25
+ "horizontalalignment": "center",
26
+ "verticalalignment": "center",
27
+ "rotate": False,
28
+ "bbox": {
29
+ "boxstyle": "round",
30
+ "facecolor": "white",
31
+ "edgecolor": "none",
32
+ },
33
+ },
34
+ "arrow": {
35
+ "marker": "|>",
36
+ "width": 8,
37
+ },
38
+ },
39
+ "grouping": {
40
+ "facecolor": ["grey", "steelblue", "tomato"],
41
+ "edgecolor": "black",
42
+ "linewidth": 1.5,
43
+ "alpha": 0.5,
44
+ "vertexpadding": 18,
45
+ },
46
+ },
2
47
  # Hollow style for organization charts et similar
3
48
  "hollow": {
4
49
  "vertex": {
@@ -54,6 +99,55 @@ style_library = {
54
99
  "deep": False,
55
100
  },
56
101
  },
102
+ # Dashed depth tree branches
103
+ "dashdepth": {
104
+ "vertex": {
105
+ "size": 2,
106
+ "label": {
107
+ "color": "black",
108
+ "size": 10,
109
+ "verticalalignment": "center",
110
+ "bbox": {
111
+ "facecolor": "none",
112
+ "edgecolor": "none",
113
+ },
114
+ },
115
+ },
116
+ "edge": {
117
+ "linewidth": 1.5,
118
+ "split": {
119
+ "linestyle": ":",
120
+ },
121
+ },
122
+ "leaf": {
123
+ "deep": False,
124
+ },
125
+ },
126
+ # Dashed depth tree branches
127
+ "dashwidth": {
128
+ "vertex": {
129
+ "size": 2,
130
+ "label": {
131
+ "color": "black",
132
+ "size": 10,
133
+ "verticalalignment": "center",
134
+ "bbox": {
135
+ "facecolor": "none",
136
+ "edgecolor": "none",
137
+ },
138
+ },
139
+ },
140
+ "edge": {
141
+ "linewidth": 1.5,
142
+ "linestyle": ":",
143
+ "split": {
144
+ "linestyle": "-",
145
+ },
146
+ },
147
+ "leaf": {
148
+ "deep": False,
149
+ },
150
+ },
57
151
  # Greyscale style
58
152
  "greyscale": {
59
153
  "vertex": {