iplotx 0.2.0__py3-none-any.whl → 0.2.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.
- iplotx/edge/__init__.py +160 -419
- iplotx/edge/arrow.py +20 -20
- iplotx/edge/geometry.py +392 -0
- iplotx/edge/ports.py +7 -2
- iplotx/groups.py +24 -14
- iplotx/label.py +49 -14
- iplotx/layout.py +3 -3
- iplotx/network.py +9 -8
- iplotx/style.py +18 -6
- iplotx/tree.py +48 -21
- iplotx/typing.py +19 -0
- iplotx/version.py +1 -1
- iplotx/vertex.py +84 -29
- {iplotx-0.2.0.dist-info → iplotx-0.2.1.dist-info}/METADATA +15 -3
- {iplotx-0.2.0.dist-info → iplotx-0.2.1.dist-info}/RECORD +16 -15
- {iplotx-0.2.0.dist-info → iplotx-0.2.1.dist-info}/WHEEL +0 -0
iplotx/label.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for label collection in iplotx.
|
|
3
|
+
"""
|
|
4
|
+
|
|
1
5
|
from typing import (
|
|
2
6
|
Optional,
|
|
3
7
|
Sequence,
|
|
@@ -26,13 +30,28 @@ from .utils.matplotlib import (
|
|
|
26
30
|
)
|
|
27
31
|
)
|
|
28
32
|
class LabelCollection(mpl.artist.Artist):
|
|
33
|
+
"""Collection of labels for iplotx with styles.
|
|
34
|
+
|
|
35
|
+
NOTE: This class is not a subclass of `mpl.collections.Collection`, although in some ways items
|
|
36
|
+
behaves like one. It is named LabelCollection quite literally to indicate it contains a list of
|
|
37
|
+
labels for vertices, edges, etc.
|
|
38
|
+
"""
|
|
39
|
+
|
|
29
40
|
def __init__(
|
|
30
41
|
self,
|
|
31
42
|
labels: Sequence[str],
|
|
32
43
|
style: Optional[dict[str, dict]] = None,
|
|
33
44
|
offsets: Optional[np.ndarray] = None,
|
|
34
45
|
transform: mpl.transforms.Transform = mpl.transforms.IdentityTransform(),
|
|
35
|
-
):
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Initialize a collection of labels.
|
|
48
|
+
|
|
49
|
+
Parameters:
|
|
50
|
+
labels: A sequence of labels to be displayed.
|
|
51
|
+
style: A dictionary of styles to apply to the labels. The keys are style properties.
|
|
52
|
+
offsets: A sequence of offsets for each label, specifying the position of the label.
|
|
53
|
+
transform: A transform to apply to the labels. This is usually ax.transData.
|
|
54
|
+
"""
|
|
36
55
|
self._labels = labels
|
|
37
56
|
self._offsets = offsets if offsets is not None else np.zeros((len(labels), 2))
|
|
38
57
|
self._style = style
|
|
@@ -41,19 +60,25 @@ class LabelCollection(mpl.artist.Artist):
|
|
|
41
60
|
self.set_transform(transform)
|
|
42
61
|
self._create_artists()
|
|
43
62
|
|
|
44
|
-
def get_children(self):
|
|
63
|
+
def get_children(self) -> tuple[mpl.artist.Artist]:
|
|
64
|
+
"""Get the children of this artist, which are the label artists."""
|
|
45
65
|
return tuple(self._labelartists)
|
|
46
66
|
|
|
47
|
-
def set_figure(self,
|
|
48
|
-
|
|
67
|
+
def set_figure(self, fig) -> None:
|
|
68
|
+
"""Set the figure of this artist.
|
|
69
|
+
|
|
70
|
+
Parameters:
|
|
71
|
+
fig: The figure to set.
|
|
72
|
+
"""
|
|
73
|
+
super().set_figure(fig)
|
|
49
74
|
for child in self.get_children():
|
|
50
|
-
child.set_figure(
|
|
51
|
-
self._update_offsets(dpi=
|
|
75
|
+
child.set_figure(fig)
|
|
76
|
+
self._update_offsets(dpi=fig.dpi)
|
|
52
77
|
|
|
53
|
-
def _get_margins_with_dpi(self, dpi=72.0):
|
|
78
|
+
def _get_margins_with_dpi(self, dpi: float = 72.0) -> np.ndarray:
|
|
54
79
|
return self._margins * dpi / 72.0
|
|
55
80
|
|
|
56
|
-
def _create_artists(self):
|
|
81
|
+
def _create_artists(self) -> None:
|
|
57
82
|
style = copy_with_deep_values(self._style) if self._style is not None else {}
|
|
58
83
|
transform = self.get_transform()
|
|
59
84
|
|
|
@@ -83,12 +108,13 @@ class LabelCollection(mpl.artist.Artist):
|
|
|
83
108
|
self._labelartists = arts
|
|
84
109
|
self._margins = np.array(margins)
|
|
85
110
|
|
|
86
|
-
def _update_offsets(self, dpi=72.0):
|
|
111
|
+
def _update_offsets(self, dpi: float = 72.0) -> None:
|
|
87
112
|
"""Update offsets including margins."""
|
|
88
113
|
offsets = self._adjust_offsets_for_margins(self._offsets, dpi=dpi)
|
|
89
114
|
self.set_offsets(offsets)
|
|
90
115
|
|
|
91
|
-
def get_offsets(self):
|
|
116
|
+
def get_offsets(self) -> np.ndarray:
|
|
117
|
+
"""Get the positions (offsets) of the labels."""
|
|
92
118
|
return self._offsets
|
|
93
119
|
|
|
94
120
|
def _adjust_offsets_for_margins(self, offsets, dpi=72.0):
|
|
@@ -100,13 +126,22 @@ class LabelCollection(mpl.artist.Artist):
|
|
|
100
126
|
offsets = trans_inv(trans(offsets) + margins)
|
|
101
127
|
return offsets
|
|
102
128
|
|
|
103
|
-
def set_offsets(self, offsets):
|
|
104
|
-
"""Set positions (offsets) of the labels.
|
|
129
|
+
def set_offsets(self, offsets) -> None:
|
|
130
|
+
"""Set positions (offsets) of the labels.
|
|
131
|
+
|
|
132
|
+
Parameters:
|
|
133
|
+
offsets: A sequence of offsets for each label, specifying the position of the label.
|
|
134
|
+
"""
|
|
105
135
|
self._offsets = np.asarray(offsets)
|
|
106
136
|
for art, offset in zip(self._labelartists, self._offsets):
|
|
107
137
|
art.set_position((offset[0], offset[1]))
|
|
108
138
|
|
|
109
|
-
def set_rotations(self, rotations):
|
|
139
|
+
def set_rotations(self, rotations: Sequence[float]) -> None:
|
|
140
|
+
"""Set the rotations of the labels.
|
|
141
|
+
|
|
142
|
+
Parameters:
|
|
143
|
+
rotations: A sequence of rotations in radians for each label.
|
|
144
|
+
"""
|
|
110
145
|
for art, rotation in zip(self._labelartists, rotations):
|
|
111
146
|
rot_deg = 180.0 / np.pi * rotation
|
|
112
147
|
# Force the font size to be upwards
|
|
@@ -114,7 +149,7 @@ class LabelCollection(mpl.artist.Artist):
|
|
|
114
149
|
art.set_rotation(rot_deg)
|
|
115
150
|
|
|
116
151
|
@_stale_wrapper
|
|
117
|
-
def draw(self, renderer):
|
|
152
|
+
def draw(self, renderer) -> None:
|
|
118
153
|
"""Draw each of the children, with some buffering mechanism."""
|
|
119
154
|
if not self.get_visible():
|
|
120
155
|
return
|
iplotx/layout.py
CHANGED
|
@@ -18,7 +18,8 @@ def compute_tree_layout(
|
|
|
18
18
|
Parameters:
|
|
19
19
|
tree: The tree to compute the layout for.
|
|
20
20
|
layout: The name of the layout, e.g. "horizontal" or "radial".
|
|
21
|
-
orientation: The orientation of the layout, e.g. "right", "left", "descending", or
|
|
21
|
+
orientation: The orientation of the layout, e.g. "right", "left", "descending", or
|
|
22
|
+
"ascending".
|
|
22
23
|
|
|
23
24
|
Returns:
|
|
24
25
|
A layout dictionary with node positions.
|
|
@@ -71,7 +72,6 @@ def _horizontal_tree_layout_right(
|
|
|
71
72
|
# Set the x values for vertices
|
|
72
73
|
layout[root_fun(tree)][0] = 0
|
|
73
74
|
for node in preorder_fun(tree):
|
|
74
|
-
x0, y0 = layout[node]
|
|
75
75
|
for child in children_fun(node):
|
|
76
76
|
bl = branch_length_fun(child)
|
|
77
77
|
if bl is None:
|
|
@@ -93,7 +93,7 @@ def _horizontal_tree_layout(
|
|
|
93
93
|
layout = _horizontal_tree_layout_right(tree, **kwargs)
|
|
94
94
|
|
|
95
95
|
if orientation == "left":
|
|
96
|
-
for key
|
|
96
|
+
for key in layout:
|
|
97
97
|
layout[key][0] *= -1
|
|
98
98
|
return layout
|
|
99
99
|
|
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/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,7 @@ style_leaves = (
|
|
|
23
22
|
"zorder",
|
|
24
23
|
"tension",
|
|
25
24
|
"looptension",
|
|
26
|
-
"
|
|
25
|
+
"loopmaxangle",
|
|
27
26
|
"rotate",
|
|
28
27
|
"marker",
|
|
29
28
|
"waypoints",
|
|
@@ -37,6 +36,15 @@ style_leaves = (
|
|
|
37
36
|
"ports",
|
|
38
37
|
)
|
|
39
38
|
|
|
39
|
+
# These properties are not allowed to be rotated (global throughout the graph).
|
|
40
|
+
# This might change in the future as the API improves.
|
|
41
|
+
nonrotating_leaves = (
|
|
42
|
+
"offset",
|
|
43
|
+
"looptension",
|
|
44
|
+
"loopmaxangle",
|
|
45
|
+
"vertexpadding",
|
|
46
|
+
)
|
|
47
|
+
|
|
40
48
|
|
|
41
49
|
default = {
|
|
42
50
|
"vertex": {
|
|
@@ -247,7 +255,7 @@ def use(style: Optional[str | dict | Sequence] = None, **kwargs):
|
|
|
247
255
|
raise
|
|
248
256
|
|
|
249
257
|
|
|
250
|
-
def reset() ->
|
|
258
|
+
def reset() -> None:
|
|
251
259
|
"""Reset to default style."""
|
|
252
260
|
global current
|
|
253
261
|
current = copy_with_deep_values(styles["default"])
|
|
@@ -276,7 +284,7 @@ def context(style: Optional[str | dict | Sequence] = None, **kwargs):
|
|
|
276
284
|
|
|
277
285
|
def unflatten_style(
|
|
278
286
|
style_flat: dict[str, str | dict | int | float],
|
|
279
|
-
) ->
|
|
287
|
+
) -> None:
|
|
280
288
|
"""Convert a flat or semi-flat style into a fully structured dict.
|
|
281
289
|
|
|
282
290
|
Parameters:
|
|
@@ -326,7 +334,7 @@ def rotate_style(
|
|
|
326
334
|
style: dict[str, Any],
|
|
327
335
|
index: Optional[int] = None,
|
|
328
336
|
key: Optional[Hashable] = None,
|
|
329
|
-
props: Sequence[str] =
|
|
337
|
+
props: Optional[Sequence[str]] = None,
|
|
330
338
|
) -> dict[str, Any]:
|
|
331
339
|
"""Rotate leaves of a style for a certain index or key.
|
|
332
340
|
|
|
@@ -338,7 +346,8 @@ def rotate_style(
|
|
|
338
346
|
props: The properties to rotate, usually all leaf properties.
|
|
339
347
|
|
|
340
348
|
Returns:
|
|
341
|
-
A style with rotated leaves, which describes the properties of a single element (e.g.
|
|
349
|
+
A style with rotated leaves, which describes the properties of a single element (e.g.
|
|
350
|
+
vertex).
|
|
342
351
|
|
|
343
352
|
Example:
|
|
344
353
|
>>> style = {'vertex': {'size': [10, 20]}}
|
|
@@ -350,6 +359,9 @@ def rotate_style(
|
|
|
350
359
|
"At least one of 'index' or 'key' must be provided to rotate_style."
|
|
351
360
|
)
|
|
352
361
|
|
|
362
|
+
if props is None:
|
|
363
|
+
props = tuple(prop for prop in style_leaves if prop not in nonrotating_leaves)
|
|
364
|
+
|
|
353
365
|
style = copy_with_deep_values(style)
|
|
354
366
|
|
|
355
367
|
for prop in props:
|
iplotx/tree.py
CHANGED
|
@@ -2,6 +2,7 @@ from typing import (
|
|
|
2
2
|
Optional,
|
|
3
3
|
Sequence,
|
|
4
4
|
)
|
|
5
|
+
from collections.abc import Hashable
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
7
8
|
import pandas as pd
|
|
@@ -26,6 +27,9 @@ from .edge import (
|
|
|
26
27
|
EdgeCollection,
|
|
27
28
|
make_stub_patch as make_undirected_edge_patch,
|
|
28
29
|
)
|
|
30
|
+
from .label import (
|
|
31
|
+
LabelCollection,
|
|
32
|
+
)
|
|
29
33
|
from .network import (
|
|
30
34
|
_update_from_internal,
|
|
31
35
|
)
|
|
@@ -50,13 +54,31 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
50
54
|
layout="horizontal",
|
|
51
55
|
orientation="right",
|
|
52
56
|
directed: bool | str = False,
|
|
53
|
-
vertex_labels: Optional[
|
|
57
|
+
vertex_labels: Optional[
|
|
58
|
+
bool | list[str] | dict[Hashable, str] | pd.Series
|
|
59
|
+
] = None,
|
|
54
60
|
edge_labels: Optional[Sequence] = None,
|
|
55
61
|
transform: mpl.transforms.Transform = mpl.transforms.IdentityTransform(),
|
|
56
62
|
offset_transform: Optional[mpl.transforms.Transform] = None,
|
|
57
63
|
):
|
|
58
|
-
|
|
64
|
+
"""Initialize the TreeArtist.
|
|
65
|
+
|
|
66
|
+
Parameters:
|
|
67
|
+
tree: The tree to plot.
|
|
68
|
+
layout: The layout to use for the tree. Can be "horizontal", "vertical", or "radial".
|
|
69
|
+
orientation: The orientation of the tree layout. Can be "right" or "left" (for
|
|
70
|
+
horizontal and radial layouts) and "descending" or "ascending" (for vertical
|
|
71
|
+
layouts).
|
|
72
|
+
directed: Whether the tree is directed. Can be a boolean or a string with the
|
|
73
|
+
following choices: "parent" or "child".
|
|
74
|
+
vertex_labels: Labels for the vertices. Can be a list, dictionary, or pandas Series.
|
|
75
|
+
edge_labels: Labels for the edges. Can be a sequence of strings.
|
|
76
|
+
transform: The transform to apply to the tree artist. This is usually the identity.
|
|
77
|
+
offset_transform: The offset transform to apply to the tree artist. This is
|
|
78
|
+
usually `ax.transData`.
|
|
79
|
+
"""
|
|
59
80
|
|
|
81
|
+
self.tree = tree
|
|
60
82
|
self._ipx_internal_data = ingest_tree_data(
|
|
61
83
|
tree,
|
|
62
84
|
layout,
|
|
@@ -80,13 +102,23 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
80
102
|
self._add_vertices()
|
|
81
103
|
self._add_edges()
|
|
82
104
|
|
|
83
|
-
def get_children(self):
|
|
105
|
+
def get_children(self) -> tuple[mpl.artist.Artist]:
|
|
106
|
+
"""Get the children of this artist.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The artists for vertices and edges.
|
|
110
|
+
"""
|
|
84
111
|
return (self._vertices, self._edges)
|
|
85
112
|
|
|
86
|
-
def set_figure(self,
|
|
87
|
-
|
|
113
|
+
def set_figure(self, fig) -> None:
|
|
114
|
+
"""Set the figure for this artist and its children.
|
|
115
|
+
|
|
116
|
+
Parameters:
|
|
117
|
+
fig: the figure to set for this artist and its children.
|
|
118
|
+
"""
|
|
119
|
+
super().set_figure(fig)
|
|
88
120
|
for child in self.get_children():
|
|
89
|
-
child.set_figure(
|
|
121
|
+
child.set_figure(fig)
|
|
90
122
|
|
|
91
123
|
def get_offset_transform(self):
|
|
92
124
|
"""Get the offset transform (for vertices/edges)."""
|
|
@@ -132,43 +164,43 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
132
164
|
bbox = bbox.expanded(sw=(1.0 + pad), sh=(1.0 + pad))
|
|
133
165
|
return bbox
|
|
134
166
|
|
|
135
|
-
def _get_label_series(self, kind):
|
|
167
|
+
def _get_label_series(self, kind: str) -> Optional[pd.Series]:
|
|
136
168
|
if "label" in self._ipx_internal_data[f"{kind}_df"].columns:
|
|
137
169
|
return self._ipx_internal_data[f"{kind}_df"]["label"]
|
|
138
170
|
else:
|
|
139
171
|
return None
|
|
140
172
|
|
|
141
|
-
def get_vertices(self):
|
|
173
|
+
def get_vertices(self) -> VertexCollection:
|
|
142
174
|
"""Get VertexCollection artist."""
|
|
143
175
|
return self._vertices
|
|
144
176
|
|
|
145
|
-
def get_edges(self):
|
|
177
|
+
def get_edges(self) -> EdgeCollection:
|
|
146
178
|
"""Get EdgeCollection artist."""
|
|
147
179
|
return self._edges
|
|
148
180
|
|
|
149
|
-
def get_vertex_labels(self):
|
|
181
|
+
def get_vertex_labels(self) -> LabelCollection:
|
|
150
182
|
"""Get list of vertex label artists."""
|
|
151
183
|
return self._vertices.get_labels()
|
|
152
184
|
|
|
153
|
-
def get_edge_labels(self):
|
|
185
|
+
def get_edge_labels(self) -> LabelCollection:
|
|
154
186
|
"""Get list of edge label artists."""
|
|
155
187
|
return self._edges.get_labels()
|
|
156
188
|
|
|
157
|
-
def _add_vertices(self):
|
|
189
|
+
def _add_vertices(self) -> None:
|
|
158
190
|
"""Add vertices to the tree."""
|
|
159
191
|
self._vertices = VertexCollection(
|
|
160
192
|
layout=self.get_layout(),
|
|
161
|
-
style=get_style(".vertex"),
|
|
162
|
-
labels=self._get_label_series("vertex"),
|
|
163
193
|
layout_coordinate_system=self._ipx_internal_data.get(
|
|
164
194
|
"layout_coordinate_system",
|
|
165
195
|
"catesian",
|
|
166
196
|
),
|
|
197
|
+
style=get_style(".vertex"),
|
|
198
|
+
labels=self._get_label_series("vertex"),
|
|
167
199
|
transform=self.get_transform(),
|
|
168
200
|
offset_transform=self.get_offset_transform(),
|
|
169
201
|
)
|
|
170
202
|
|
|
171
|
-
def _add_edges(self):
|
|
203
|
+
def _add_edges(self) -> None:
|
|
172
204
|
"""Add edges to the network artist.
|
|
173
205
|
|
|
174
206
|
NOTE: UndirectedEdgeCollection and ArrowCollection are both subclasses of
|
|
@@ -256,11 +288,6 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
256
288
|
edgepatches,
|
|
257
289
|
vertex_ids=adjacent_vertex_ids,
|
|
258
290
|
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
291
|
labels=labels,
|
|
265
292
|
transform=self.get_offset_transform(),
|
|
266
293
|
style=edge_style,
|
|
@@ -270,7 +297,7 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
270
297
|
self._edges.set_array(colorarray)
|
|
271
298
|
|
|
272
299
|
@_stale_wrapper
|
|
273
|
-
def draw(self, renderer):
|
|
300
|
+
def draw(self, renderer) -> None:
|
|
274
301
|
"""Draw each of the children, with some buffering mechanism."""
|
|
275
302
|
if not self.get_visible():
|
|
276
303
|
return
|
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
iplotx/vertex.py
CHANGED
|
@@ -6,7 +6,6 @@ from typing import (
|
|
|
6
6
|
Optional,
|
|
7
7
|
Sequence,
|
|
8
8
|
Any,
|
|
9
|
-
Never,
|
|
10
9
|
)
|
|
11
10
|
import warnings
|
|
12
11
|
import numpy as np
|
|
@@ -70,20 +69,21 @@ class VertexCollection(PatchCollection):
|
|
|
70
69
|
self._index = layout.index
|
|
71
70
|
self._style = style
|
|
72
71
|
self._labels = labels
|
|
72
|
+
self._layout = layout
|
|
73
|
+
self._layout_coordinate_system = layout_coordinate_system
|
|
73
74
|
|
|
74
75
|
# Create patches from structured data
|
|
75
|
-
patches,
|
|
76
|
-
layout,
|
|
77
|
-
layout_coordinate_system=layout_coordinate_system,
|
|
78
|
-
)
|
|
76
|
+
patches, sizes, kwargs2 = self._init_vertex_patches()
|
|
79
77
|
|
|
80
78
|
kwargs.update(kwargs2)
|
|
81
|
-
kwargs["offsets"] = offsets
|
|
82
79
|
kwargs["match_original"] = True
|
|
83
80
|
|
|
84
81
|
# Pass to PatchCollection constructor
|
|
85
82
|
super().__init__(patches, *args, **kwargs)
|
|
86
83
|
|
|
84
|
+
# Set offsets in coordinate system
|
|
85
|
+
self._update_offsets_from_layout()
|
|
86
|
+
|
|
87
87
|
# Compute _transforms like in _CollectionWithScales for dpi issues
|
|
88
88
|
self.set_sizes(sizes)
|
|
89
89
|
|
|
@@ -100,7 +100,7 @@ class VertexCollection(PatchCollection):
|
|
|
100
100
|
children.append(self._label_collection)
|
|
101
101
|
return tuple(children)
|
|
102
102
|
|
|
103
|
-
def set_figure(self, fig) ->
|
|
103
|
+
def set_figure(self, fig) -> None:
|
|
104
104
|
"""Set the figure for this artist and all children."""
|
|
105
105
|
super().set_figure(fig)
|
|
106
106
|
self.set_sizes(self._sizes, self.get_figure(root=True).dpi)
|
|
@@ -123,7 +123,7 @@ class VertexCollection(PatchCollection):
|
|
|
123
123
|
"""Get vertex sizes (max of width and height), scaled by dpi."""
|
|
124
124
|
return self._transforms[:, 0, 0]
|
|
125
125
|
|
|
126
|
-
def set_sizes(self, sizes, dpi=72.0):
|
|
126
|
+
def set_sizes(self, sizes, dpi: float = 72.0) -> None:
|
|
127
127
|
"""Set vertex sizes.
|
|
128
128
|
|
|
129
129
|
This rescales the current vertex symbol/path linearly, using this
|
|
@@ -146,9 +146,70 @@ class VertexCollection(PatchCollection):
|
|
|
146
146
|
get_size = get_sizes
|
|
147
147
|
set_size = set_sizes
|
|
148
148
|
|
|
149
|
-
def
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
def get_layout(self) -> pd.DataFrame:
|
|
150
|
+
"""Get the vertex layout.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
The vertex layout as a DataFrame.
|
|
154
|
+
"""
|
|
155
|
+
return self._layout
|
|
156
|
+
|
|
157
|
+
def get_layout_coordinate_system(self) -> str:
|
|
158
|
+
"""Get the layout coordinate system.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Name of the layout coordinate system, e.g. "cartesian" or "polar".
|
|
162
|
+
"""
|
|
163
|
+
return self._layout_coordinate_system
|
|
164
|
+
|
|
165
|
+
def get_offsets(self, ignore_layout: bool = True) -> np.ndarray:
|
|
166
|
+
"""Get the vertex offsets.
|
|
167
|
+
|
|
168
|
+
Parameters:
|
|
169
|
+
ignore_layout: If True, return the matplotlib Artist._offsets directly, ignoring the
|
|
170
|
+
layout coordinate system. If False, it's equivalent to get_layout().values.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
The vertex offsets as a 2D numpy array.
|
|
174
|
+
|
|
175
|
+
Note: It is best for users to *not* ignore the layout coordinate system, as it may lead
|
|
176
|
+
to inconsistencies. However, some internal matplotlib functions require the default
|
|
177
|
+
signature of this function to look at the vanilla offsets, hence the default parameters.
|
|
178
|
+
"""
|
|
179
|
+
if not ignore_layout:
|
|
180
|
+
return self.get_layout().values
|
|
181
|
+
else:
|
|
182
|
+
return self._offsets
|
|
183
|
+
|
|
184
|
+
def _update_offsets_from_layout(self) -> None:
|
|
185
|
+
"""Update offsets in matplotlib coordinates from the layout DataFrame."""
|
|
186
|
+
if self._layout_coordinate_system == "cartesian":
|
|
187
|
+
self._offsets = self._layout.values
|
|
188
|
+
elif self._layout_coordinate_system == "polar":
|
|
189
|
+
# Convert polar coordinates (r, theta) to cartesian (x, y)
|
|
190
|
+
r = self._layout.iloc[:, 0].values
|
|
191
|
+
theta = self._layout.iloc[:, 1].values
|
|
192
|
+
if self._offsets is None:
|
|
193
|
+
self._offsets = np.zeros((len(r), 2))
|
|
194
|
+
self._offsets[:, 0] = r * np.cos(theta)
|
|
195
|
+
self._offsets[:, 1] = r * np.sin(theta)
|
|
196
|
+
else:
|
|
197
|
+
raise ValueError(
|
|
198
|
+
f"Layout coordinate system not supported: {self._layout_coordinate_system}."
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def set_offsets(self, offsets: np.ndarray) -> None:
|
|
202
|
+
"""Set the vertex positions/offsets in layout coordinates.
|
|
203
|
+
|
|
204
|
+
Parameters:
|
|
205
|
+
offsets: Array of coordinates in the layout coordinate system. For polar layouts,
|
|
206
|
+
these should be in the form of (r, theta) pairs.
|
|
207
|
+
"""
|
|
208
|
+
self._layout.values[:] = offsets
|
|
209
|
+
self._update_offsets_from_layout()
|
|
210
|
+
self.stale = True
|
|
211
|
+
|
|
212
|
+
def _init_vertex_patches(self):
|
|
152
213
|
style = self._style or {}
|
|
153
214
|
if "cmap" in style:
|
|
154
215
|
cmap_fun = _build_cmap_fun(
|
|
@@ -164,29 +225,16 @@ class VertexCollection(PatchCollection):
|
|
|
164
225
|
"No labels found, cannot resize vertices based on labels."
|
|
165
226
|
)
|
|
166
227
|
style["size"] = get_style("default.vertex")["size"]
|
|
167
|
-
else:
|
|
168
|
-
vertex_labels = self._labels
|
|
169
228
|
|
|
170
229
|
if "cmap" in style:
|
|
171
230
|
colorarray = []
|
|
172
231
|
patches = []
|
|
173
|
-
offsets = []
|
|
174
232
|
sizes = []
|
|
175
|
-
for i, (vid, row) in enumerate(
|
|
176
|
-
|
|
177
|
-
offset = list(row.values)
|
|
178
|
-
|
|
179
|
-
# Transform to cartesian coordinates if needed
|
|
180
|
-
if layout_coordinate_system == "polar":
|
|
181
|
-
r, theta = offset
|
|
182
|
-
offset = [r * np.cos(theta), r * np.sin(theta)]
|
|
183
|
-
|
|
184
|
-
offsets.append(offset)
|
|
185
|
-
|
|
186
|
-
if style.get("size") == "label":
|
|
233
|
+
for i, (vid, row) in enumerate(self._layout.iterrows()):
|
|
234
|
+
if style.get("size", 20) == "label":
|
|
187
235
|
# NOTE: it's ok to overwrite the dict here
|
|
188
236
|
style["size"] = _get_label_width_height(
|
|
189
|
-
str(
|
|
237
|
+
str(self._labels[vid]), **style.get("label", {})
|
|
190
238
|
)
|
|
191
239
|
|
|
192
240
|
stylei = rotate_style(style, index=i, key=vid)
|
|
@@ -207,7 +255,7 @@ class VertexCollection(PatchCollection):
|
|
|
207
255
|
kwargs["cmap"] = style["cmap"]
|
|
208
256
|
kwargs["norm"] = norm
|
|
209
257
|
|
|
210
|
-
return patches,
|
|
258
|
+
return patches, sizes, kwargs
|
|
211
259
|
|
|
212
260
|
def _compute_label_collection(self):
|
|
213
261
|
transform = self.get_offset_transform()
|
|
@@ -230,6 +278,12 @@ class VertexCollection(PatchCollection):
|
|
|
230
278
|
)
|
|
231
279
|
|
|
232
280
|
def get_labels(self):
|
|
281
|
+
"""Get the vertex labels.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
The artist with the LabelCollection.
|
|
285
|
+
"""
|
|
286
|
+
|
|
233
287
|
if hasattr(self, "_label_collection"):
|
|
234
288
|
return self._label_collection
|
|
235
289
|
else:
|
|
@@ -280,7 +334,8 @@ def make_patch(
|
|
|
280
334
|
|
|
281
335
|
# Size of vertices is determined in self._transforms, which scales with dpi, rather than here,
|
|
282
336
|
# so normalise by the average dimension (btw x and y) to keep the ratio of the marker.
|
|
283
|
-
# If you check in get_sizes, you will see that rescaling also happens with the max of width
|
|
337
|
+
# If you check in get_sizes, you will see that rescaling also happens with the max of width
|
|
338
|
+
# and height.
|
|
284
339
|
size = np.asarray(size, dtype=float)
|
|
285
340
|
size_max = size.max()
|
|
286
341
|
if size_max > 0:
|