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/cascades.py +223 -0
- iplotx/edge/__init__.py +180 -420
- iplotx/edge/arrow.py +20 -20
- iplotx/edge/geometry.py +448 -0
- iplotx/edge/ports.py +7 -2
- iplotx/groups.py +24 -14
- 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 +103 -21
- iplotx/layout.py +57 -36
- iplotx/network.py +9 -8
- iplotx/plotting.py +6 -3
- iplotx/style.py +36 -10
- iplotx/tree.py +237 -29
- iplotx/typing.py +19 -0
- iplotx/version.py +1 -1
- iplotx/vertex.py +122 -35
- {iplotx-0.2.0.dist-info → iplotx-0.3.0.dist-info}/METADATA +16 -3
- iplotx-0.3.0.dist-info/RECORD +32 -0
- iplotx-0.2.0.dist-info/RECORD +0 -30
- {iplotx-0.2.0.dist-info → iplotx-0.3.0.dist-info}/WHEEL +0 -0
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 =
|
|
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
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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"]["
|
|
119
|
-
tree["vertex"]["label"]["hmargin"] =
|
|
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() ->
|
|
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
|
-
) ->
|
|
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] =
|
|
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.
|
|
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=
|
|
60
|
+
layout: Optional[str] = "horizontal",
|
|
61
|
+
orientation: Optional[str] = None,
|
|
52
62
|
directed: bool | str = False,
|
|
53
|
-
vertex_labels: Optional[
|
|
54
|
-
|
|
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
|
-
|
|
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
|
|
84
|
-
|
|
138
|
+
def set_figure(self, fig) -> None:
|
|
139
|
+
"""Set the figure for this artist and its children.
|
|
85
140
|
|
|
86
|
-
|
|
87
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 = "
|
|
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
|
-
#
|
|
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