iplotx 0.1.0__py3-none-any.whl → 0.2.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/__init__.py +22 -1
- iplotx/edge/__init__.py +882 -0
- iplotx/edge/arrow.py +220 -10
- iplotx/edge/ports.py +42 -0
- iplotx/groups.py +79 -41
- iplotx/ingest/__init__.py +155 -0
- iplotx/ingest/heuristics.py +209 -0
- iplotx/ingest/providers/network/igraph.py +96 -0
- iplotx/ingest/providers/network/networkx.py +133 -0
- iplotx/ingest/providers/tree/biopython.py +105 -0
- iplotx/ingest/providers/tree/cogent3.py +112 -0
- iplotx/ingest/providers/tree/ete4.py +112 -0
- iplotx/ingest/providers/tree/skbio.py +112 -0
- iplotx/ingest/typing.py +100 -0
- iplotx/label.py +127 -0
- iplotx/layout.py +139 -0
- iplotx/network.py +156 -375
- iplotx/plotting.py +157 -56
- iplotx/style.py +379 -0
- iplotx/tree.py +285 -0
- iplotx/typing.py +33 -38
- iplotx/utils/geometry.py +128 -81
- iplotx/utils/internal.py +3 -0
- iplotx/utils/matplotlib.py +58 -38
- iplotx/utils/style.py +1 -0
- iplotx/version.py +5 -1
- iplotx/vertex.py +250 -55
- {iplotx-0.1.0.dist-info → iplotx-0.2.0.dist-info}/METADATA +37 -8
- iplotx-0.2.0.dist-info/RECORD +30 -0
- iplotx/edge/common.py +0 -47
- iplotx/edge/directed.py +0 -149
- iplotx/edge/label.py +0 -50
- iplotx/edge/undirected.py +0 -447
- iplotx/heuristics.py +0 -114
- iplotx/importing.py +0 -13
- iplotx/styles.py +0 -186
- iplotx-0.1.0.dist-info/RECORD +0 -20
- {iplotx-0.1.0.dist-info → iplotx-0.2.0.dist-info}/WHEEL +0 -0
iplotx/edge/arrow.py
CHANGED
|
@@ -1,16 +1,171 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Never,
|
|
3
|
+
)
|
|
1
4
|
import numpy as np
|
|
2
5
|
import matplotlib as mpl
|
|
3
6
|
from matplotlib.patches import PathPatch
|
|
4
7
|
|
|
8
|
+
from ..style import (
|
|
9
|
+
get_style,
|
|
10
|
+
rotate_style,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EdgeArrowCollection(mpl.collections.PatchCollection):
|
|
15
|
+
"""Collection of arrow patches for plotting directed edgs."""
|
|
16
|
+
|
|
17
|
+
_factor = 1.0
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
edge_collection,
|
|
22
|
+
transform: mpl.transforms.Transform = mpl.transforms.IdentityTransform(),
|
|
23
|
+
*args,
|
|
24
|
+
**kwargs,
|
|
25
|
+
):
|
|
26
|
+
|
|
27
|
+
self._edge_collection = edge_collection
|
|
28
|
+
self._style = get_style(".edge.arrow")
|
|
29
|
+
|
|
30
|
+
patches, sizes = self._create_artists()
|
|
31
|
+
|
|
32
|
+
if "cmap" in self._edge_collection._style:
|
|
33
|
+
kwargs["cmap"] = self._edge_collection._style["cmap"]
|
|
34
|
+
kwargs["norm"] = self._edge_collection._style["norm"]
|
|
35
|
+
|
|
36
|
+
super().__init__(
|
|
37
|
+
patches,
|
|
38
|
+
offsets=np.zeros((len(patches), 2)),
|
|
39
|
+
offset_transform=self.get_offset_transform(),
|
|
40
|
+
transform=transform,
|
|
41
|
+
match_original=True,
|
|
42
|
+
*args,
|
|
43
|
+
**kwargs,
|
|
44
|
+
)
|
|
45
|
+
self._angles = np.zeros(len(self._paths))
|
|
46
|
+
|
|
47
|
+
# Compute _transforms like in _CollectionWithScales for dpi issues
|
|
48
|
+
self.set_sizes(sizes)
|
|
49
|
+
|
|
50
|
+
def get_sizes(self):
|
|
51
|
+
"""Get vertex sizes (max of width and height), not scaled by dpi."""
|
|
52
|
+
return self._sizes
|
|
53
|
+
|
|
54
|
+
def get_sizes_dpi(self):
|
|
55
|
+
return self._transforms[:, 0, 0]
|
|
56
|
+
|
|
57
|
+
def set_sizes(self, sizes, dpi=72.0):
|
|
58
|
+
"""Set vertex sizes.
|
|
59
|
+
|
|
60
|
+
This rescales the current vertex symbol/path linearly, using this
|
|
61
|
+
value as the largest of width and height.
|
|
62
|
+
|
|
63
|
+
@param sizes: A sequence of vertex sizes or a single size.
|
|
64
|
+
"""
|
|
65
|
+
if sizes is None:
|
|
66
|
+
self._sizes = np.array([])
|
|
67
|
+
self._transforms = np.empty((0, 3, 3))
|
|
68
|
+
else:
|
|
69
|
+
self._sizes = np.asarray(sizes)
|
|
70
|
+
self._transforms = np.zeros((len(self._sizes), 3, 3))
|
|
71
|
+
scale = self._sizes * dpi / 72.0 * self._factor
|
|
72
|
+
self._transforms[:, 0, 0] = scale
|
|
73
|
+
self._transforms[:, 1, 1] = scale
|
|
74
|
+
self._transforms[:, 2, 2] = 1.0
|
|
75
|
+
self.stale = True
|
|
76
|
+
|
|
77
|
+
get_size = get_sizes
|
|
78
|
+
set_size = set_sizes
|
|
79
|
+
|
|
80
|
+
def set_figure(self, fig) -> Never:
|
|
81
|
+
"""Set the figure for this artist and all children."""
|
|
82
|
+
super().set_figure(fig)
|
|
83
|
+
self.set_sizes(self._sizes, self.get_figure(root=True).dpi)
|
|
84
|
+
for child in self.get_children():
|
|
85
|
+
child.set_figure(fig)
|
|
86
|
+
|
|
87
|
+
def get_offset_transform(self):
|
|
88
|
+
return self._edge_collection.get_transform()
|
|
89
|
+
|
|
90
|
+
get_size = get_sizes
|
|
91
|
+
set_size = set_sizes
|
|
92
|
+
|
|
93
|
+
def _create_artists(self):
|
|
94
|
+
style = self._style if self._style is not None else {}
|
|
95
|
+
|
|
96
|
+
patches = []
|
|
97
|
+
sizes = []
|
|
98
|
+
for i, (vid1, vid2) in enumerate(self._edge_collection._vertex_ids):
|
|
99
|
+
stylei = rotate_style(style, index=i)
|
|
100
|
+
if ("facecolor" not in stylei) and ("color" not in stylei):
|
|
101
|
+
stylei["facecolor"] = self._edge_collection.get_edgecolors()[i][:3]
|
|
102
|
+
if ("edgecolor" not in stylei) and ("color" not in stylei):
|
|
103
|
+
stylei["edgecolor"] = self._edge_collection.get_edgecolors()[i][:3]
|
|
104
|
+
if "alpha" not in stylei:
|
|
105
|
+
stylei["alpha"] = self._edge_collection.get_edgecolors()[i][3]
|
|
106
|
+
if "linewidth" not in stylei:
|
|
107
|
+
stylei["linewidth"] = self._edge_collection.get_linewidths()[i]
|
|
108
|
+
|
|
109
|
+
patch, size = make_arrow_patch(
|
|
110
|
+
**stylei,
|
|
111
|
+
)
|
|
112
|
+
patches.append(patch)
|
|
113
|
+
sizes.append(size)
|
|
114
|
+
|
|
115
|
+
return patches, sizes
|
|
116
|
+
|
|
117
|
+
def set_array(self, array):
|
|
118
|
+
"""Set the array for cmap/norm coloring, but keep the facecolors as set (usually 'none')."""
|
|
119
|
+
raise ValueError("Setting an array for arrows directly is not supported.")
|
|
120
|
+
|
|
121
|
+
def set_colors(self, colors):
|
|
122
|
+
"""Set arrow colors (edge and/or face) based on a colormap."""
|
|
123
|
+
# NOTE: facecolors is always an array because we come from patches
|
|
124
|
+
# It can have zero alpha (i.e. if we choose "none", or a hollow marker)
|
|
125
|
+
self.set_edgecolors(colors)
|
|
126
|
+
has_facecolor = self._facecolors[:, 3] > 0
|
|
127
|
+
self._facecolors[has_facecolor] = colors[has_facecolor]
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def stale(self):
|
|
131
|
+
return super().stale
|
|
132
|
+
|
|
133
|
+
@stale.setter
|
|
134
|
+
def stale(self, val):
|
|
135
|
+
mpl.collections.PatchCollection.stale.fset(self, val)
|
|
136
|
+
if val and hasattr(self, "stale_callback_post"):
|
|
137
|
+
self.stale_callback_post(self)
|
|
138
|
+
|
|
139
|
+
@mpl.artist.allow_rasterization
|
|
140
|
+
def draw(self, renderer):
|
|
141
|
+
self.set_sizes(self._sizes, self.get_figure(root=True).dpi)
|
|
142
|
+
super().draw(renderer)
|
|
143
|
+
|
|
5
144
|
|
|
6
145
|
def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
7
146
|
"""Make a patch of the given marker shape and size."""
|
|
8
147
|
height = kwargs.pop("height", width * 1.3)
|
|
9
148
|
|
|
149
|
+
# Normalise by the max size, this is taken care of in _transforms
|
|
150
|
+
# subsequently in a way that is nice to dpi scaling
|
|
151
|
+
size_max = max(width, height)
|
|
152
|
+
if size_max > 0:
|
|
153
|
+
height /= size_max
|
|
154
|
+
width /= size_max
|
|
155
|
+
|
|
10
156
|
if marker == "|>":
|
|
11
|
-
codes = ["MOVETO", "LINETO", "LINETO"]
|
|
157
|
+
codes = ["MOVETO", "LINETO", "LINETO", "CLOSEPOLY"]
|
|
158
|
+
if "color" in kwargs:
|
|
159
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
12
160
|
path = mpl.path.Path(
|
|
13
|
-
np.array(
|
|
161
|
+
np.array(
|
|
162
|
+
[
|
|
163
|
+
[-height, width * 0.5],
|
|
164
|
+
[-height, -width * 0.5],
|
|
165
|
+
[0, 0],
|
|
166
|
+
[-height, width * 0.5],
|
|
167
|
+
]
|
|
168
|
+
),
|
|
14
169
|
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
15
170
|
closed=True,
|
|
16
171
|
)
|
|
@@ -25,8 +180,10 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
|
25
180
|
closed=False,
|
|
26
181
|
)
|
|
27
182
|
elif marker == ">>":
|
|
183
|
+
if "color" in kwargs:
|
|
184
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
28
185
|
overhang = kwargs.pop("overhang", 0.25)
|
|
29
|
-
codes = ["MOVETO", "LINETO", "LINETO", "LINETO"]
|
|
186
|
+
codes = ["MOVETO", "LINETO", "LINETO", "LINETO", "CLOSEPOLY"]
|
|
30
187
|
path = mpl.path.Path(
|
|
31
188
|
np.array(
|
|
32
189
|
[
|
|
@@ -34,14 +191,17 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
|
34
191
|
[-height, -width * 0.5],
|
|
35
192
|
[-height * (1.0 - overhang), 0],
|
|
36
193
|
[-height, width * 0.5],
|
|
194
|
+
[0, 0],
|
|
37
195
|
]
|
|
38
196
|
),
|
|
39
197
|
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
40
198
|
closed=True,
|
|
41
199
|
)
|
|
42
200
|
elif marker == ")>":
|
|
201
|
+
if "color" in kwargs:
|
|
202
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
43
203
|
overhang = kwargs.pop("overhang", 0.25)
|
|
44
|
-
codes = ["MOVETO", "LINETO", "CURVE3", "CURVE3"]
|
|
204
|
+
codes = ["MOVETO", "LINETO", "CURVE3", "CURVE3", "CLOSEPOLY"]
|
|
45
205
|
path = mpl.path.Path(
|
|
46
206
|
np.array(
|
|
47
207
|
[
|
|
@@ -49,6 +209,7 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
|
49
209
|
[-height, -width * 0.5],
|
|
50
210
|
[-height * (1.0 - overhang), 0],
|
|
51
211
|
[-height, width * 0.5],
|
|
212
|
+
[0, 0],
|
|
52
213
|
]
|
|
53
214
|
),
|
|
54
215
|
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
@@ -70,8 +231,37 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
|
70
231
|
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
71
232
|
closed=False,
|
|
72
233
|
)
|
|
234
|
+
elif marker == "|":
|
|
235
|
+
kwargs["facecolor"] = "none"
|
|
236
|
+
if "color" in kwargs:
|
|
237
|
+
kwargs["edgecolor"] = kwargs.pop("color")
|
|
238
|
+
codes = ["MOVETO", "LINETO"]
|
|
239
|
+
path = mpl.path.Path(
|
|
240
|
+
np.array([[-height, width * 0.5], [-height, -width * 0.5]]),
|
|
241
|
+
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
242
|
+
closed=False,
|
|
243
|
+
)
|
|
244
|
+
elif marker == "s":
|
|
245
|
+
if "color" in kwargs:
|
|
246
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
247
|
+
codes = ["MOVETO", "LINETO", "LINETO", "LINETO", "CLOSEPOLY"]
|
|
248
|
+
path = mpl.path.Path(
|
|
249
|
+
np.array(
|
|
250
|
+
[
|
|
251
|
+
[-height, width * 0.5],
|
|
252
|
+
[-height, -width * 0.5],
|
|
253
|
+
[0, -width * 0.5],
|
|
254
|
+
[0, width * 0.5],
|
|
255
|
+
[-height, width * 0.5],
|
|
256
|
+
]
|
|
257
|
+
),
|
|
258
|
+
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
259
|
+
closed=True,
|
|
260
|
+
)
|
|
73
261
|
elif marker == "d":
|
|
74
|
-
|
|
262
|
+
if "color" in kwargs:
|
|
263
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
264
|
+
codes = ["MOVETO", "LINETO", "LINETO", "LINETO", "CLOSEPOLY"]
|
|
75
265
|
path = mpl.path.Path(
|
|
76
266
|
np.array(
|
|
77
267
|
[
|
|
@@ -79,13 +269,16 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
|
79
269
|
[-height, 0],
|
|
80
270
|
[-height * 0.5, -width * 0.5],
|
|
81
271
|
[0, 0],
|
|
272
|
+
[-height * 0.5, width * 0.5],
|
|
82
273
|
]
|
|
83
274
|
),
|
|
84
275
|
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
85
276
|
closed=True,
|
|
86
277
|
)
|
|
87
278
|
elif marker == "p":
|
|
88
|
-
|
|
279
|
+
if "color" in kwargs:
|
|
280
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
281
|
+
codes = ["MOVETO", "LINETO", "LINETO", "LINETO", "CLOSEPOLY"]
|
|
89
282
|
path = mpl.path.Path(
|
|
90
283
|
np.array(
|
|
91
284
|
[
|
|
@@ -93,13 +286,16 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
|
93
286
|
[0, 0],
|
|
94
287
|
[0, -width],
|
|
95
288
|
[-height, -width],
|
|
289
|
+
[-height, 0],
|
|
96
290
|
]
|
|
97
291
|
),
|
|
98
292
|
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
99
293
|
closed=True,
|
|
100
294
|
)
|
|
101
295
|
elif marker == "q":
|
|
102
|
-
|
|
296
|
+
if "color" in kwargs:
|
|
297
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
298
|
+
codes = ["MOVETO", "LINETO", "LINETO", "LINETO", "CLOSEPOLY"]
|
|
103
299
|
path = mpl.path.Path(
|
|
104
300
|
np.array(
|
|
105
301
|
[
|
|
@@ -107,16 +303,30 @@ def make_arrow_patch(marker: str = "|>", width: float = 8, **kwargs):
|
|
|
107
303
|
[0, 0],
|
|
108
304
|
[0, width],
|
|
109
305
|
[-height, width],
|
|
306
|
+
[-height, 0],
|
|
307
|
+
]
|
|
308
|
+
),
|
|
309
|
+
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
310
|
+
closed=True,
|
|
311
|
+
)
|
|
312
|
+
elif marker == "none":
|
|
313
|
+
if "color" in kwargs:
|
|
314
|
+
kwargs["facecolor"] = kwargs["edgecolor"] = kwargs.pop("color")
|
|
315
|
+
codes = ["MOVETO"]
|
|
316
|
+
path = mpl.path.Path(
|
|
317
|
+
np.array(
|
|
318
|
+
[
|
|
319
|
+
[0, 0],
|
|
110
320
|
]
|
|
111
321
|
),
|
|
112
322
|
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
113
323
|
closed=True,
|
|
114
324
|
)
|
|
325
|
+
else:
|
|
326
|
+
raise ValueError(f"Arrow marker not found: {marker}.")
|
|
115
327
|
|
|
116
328
|
patch = PathPatch(
|
|
117
329
|
path,
|
|
118
330
|
**kwargs,
|
|
119
331
|
)
|
|
120
|
-
return patch
|
|
121
|
-
|
|
122
|
-
raise KeyError(f"Unknown marker: {marker}")
|
|
332
|
+
return patch, size_max
|
iplotx/edge/ports.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
sq2 = np.sqrt(2) / 2
|
|
4
|
+
|
|
5
|
+
port_dict = {
|
|
6
|
+
"s": (0, -1),
|
|
7
|
+
"w": (-1, 0),
|
|
8
|
+
"n": (0, 1),
|
|
9
|
+
"e": (1, 0),
|
|
10
|
+
"sw": (-sq2, -sq2),
|
|
11
|
+
"nw": (-sq2, sq2),
|
|
12
|
+
"ne": (sq2, sq2),
|
|
13
|
+
"se": (sq2, -sq2),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _get_port_unit_vector(
|
|
18
|
+
portstring,
|
|
19
|
+
trans_inv,
|
|
20
|
+
):
|
|
21
|
+
"""Get the tangent unit vector from a port string."""
|
|
22
|
+
# The only tricky bit is if the port says e.g. north but the y axis is inverted, in which case the port should go south.
|
|
23
|
+
# We can figure it out by checking the sign of the monotonic trans_inv from figure to data coordinates.
|
|
24
|
+
v12 = trans_inv(
|
|
25
|
+
np.array(
|
|
26
|
+
[
|
|
27
|
+
[0, 0],
|
|
28
|
+
[1, 1],
|
|
29
|
+
]
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
invertx = v12[1, 0] - v12[0, 0] < 0
|
|
33
|
+
inverty = v12[1, 1] - v12[0, 1] < 0
|
|
34
|
+
|
|
35
|
+
if invertx:
|
|
36
|
+
portstring = portstring.replace("w", "x").replace("e", "w").replace("x", "e")
|
|
37
|
+
if inverty:
|
|
38
|
+
portstring = portstring.replace("n", "x").replace("s", "n").replace("x", "s")
|
|
39
|
+
|
|
40
|
+
if portstring not in port_dict:
|
|
41
|
+
raise KeyError(f"Port not found: {portstring}")
|
|
42
|
+
return np.array(port_dict[portstring])
|
iplotx/groups.py
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
"""
|
|
2
|
+
Module for vertex groupings code, especially the GroupingArtist class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Union
|
|
4
6
|
import numpy as np
|
|
5
|
-
import pandas as pd
|
|
6
7
|
import matplotlib as mpl
|
|
7
8
|
from matplotlib.collections import PatchCollection
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
from .importing import igraph
|
|
11
11
|
from .typing import (
|
|
12
12
|
GroupingType,
|
|
13
13
|
LayoutType,
|
|
14
14
|
)
|
|
15
|
-
from .heuristics import
|
|
16
|
-
|
|
15
|
+
from .ingest.heuristics import (
|
|
16
|
+
normalise_layout,
|
|
17
|
+
normalise_grouping,
|
|
18
|
+
)
|
|
19
|
+
from .style import get_style, rotate_style
|
|
17
20
|
from .utils.geometry import (
|
|
18
21
|
convex_hull,
|
|
19
22
|
_compute_group_path_with_vertex_padding,
|
|
@@ -21,11 +24,20 @@ from .utils.geometry import (
|
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
class GroupingArtist(PatchCollection):
|
|
27
|
+
"""Matplotlib artist for a vertex grouping (clustering/cover).
|
|
28
|
+
|
|
29
|
+
This class is used to plot patches surrounding groups of vertices in a network.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
_factor = 1.0
|
|
33
|
+
|
|
24
34
|
def __init__(
|
|
25
35
|
self,
|
|
26
36
|
grouping: GroupingType,
|
|
27
37
|
layout: LayoutType,
|
|
28
38
|
vertexpadding: Union[None, int] = None,
|
|
39
|
+
points_per_curve: int = 30,
|
|
40
|
+
transform: mpl.transforms.Transform = mpl.transforms.IdentityTransform(),
|
|
29
41
|
*args,
|
|
30
42
|
**kwargs,
|
|
31
43
|
):
|
|
@@ -38,21 +50,52 @@ class GroupingArtist(PatchCollection):
|
|
|
38
50
|
layout: The layout of the vertices. If this object has no keys/index, the
|
|
39
51
|
vertices are assumed to have IDs corresponding to integers starting from
|
|
40
52
|
zero.
|
|
53
|
+
vertexpadding: How may points of padding to leave around each vertex centre.
|
|
54
|
+
points_per_curve: How many points to use to approximate a round envelope around
|
|
55
|
+
each convex hull vertex.
|
|
56
|
+
transform: The matplotlib transform to use for the patches (typically transData).
|
|
41
57
|
"""
|
|
42
58
|
if vertexpadding is not None:
|
|
43
59
|
self._vertexpadding = vertexpadding
|
|
44
60
|
else:
|
|
45
61
|
style = get_style(".grouping")
|
|
46
62
|
self._vertexpadding = style.get("vertexpadding", 10)
|
|
47
|
-
|
|
63
|
+
|
|
64
|
+
self._points_per_curve = points_per_curve
|
|
65
|
+
|
|
66
|
+
network = kwargs.pop("network", None)
|
|
67
|
+
patches, grouping, coords_hulls = self._create_patches(
|
|
68
|
+
grouping, layout, network, **kwargs
|
|
69
|
+
)
|
|
70
|
+
if "network" in kwargs:
|
|
71
|
+
del kwargs["network"]
|
|
48
72
|
self._grouping = grouping
|
|
49
|
-
self.
|
|
73
|
+
self._coords_hulls = coords_hulls
|
|
50
74
|
kwargs["match_original"] = True
|
|
51
75
|
|
|
52
76
|
super().__init__(patches, *args, **kwargs)
|
|
53
77
|
|
|
54
|
-
|
|
55
|
-
|
|
78
|
+
zorder = get_style(".grouping").get("zorder", 1)
|
|
79
|
+
self.set_zorder(zorder)
|
|
80
|
+
|
|
81
|
+
self.set_transform(transform)
|
|
82
|
+
|
|
83
|
+
def set_figure(self, figure):
|
|
84
|
+
"""Set the figure for the grouping, recomputing the paths depending on the figure's dpi."""
|
|
85
|
+
ret = super().set_figure(figure)
|
|
86
|
+
self._compute_paths(self.get_figure(root=True).dpi)
|
|
87
|
+
return ret
|
|
88
|
+
|
|
89
|
+
def get_vertexpadding(self):
|
|
90
|
+
"""Get the vertex padding of each group."""
|
|
91
|
+
return self._vertexpadding
|
|
92
|
+
|
|
93
|
+
def get_vertexpadding_dpi(self, dpi=72.0):
|
|
94
|
+
"""Get vertex padding of each group, scaled by dpi of the figure."""
|
|
95
|
+
return self.get_vertexpadding() * dpi / 72.0 * self._factor
|
|
96
|
+
|
|
97
|
+
def _create_patches(self, grouping, layout, network, **kwargs):
|
|
98
|
+
layout = normalise_layout(layout, network=network)
|
|
56
99
|
grouping = normalise_grouping(grouping, layout)
|
|
57
100
|
style = get_style(".grouping")
|
|
58
101
|
style.pop("vertexpadding", None)
|
|
@@ -60,6 +103,7 @@ class GroupingArtist(PatchCollection):
|
|
|
60
103
|
style.update(kwargs)
|
|
61
104
|
|
|
62
105
|
patches = []
|
|
106
|
+
coords_hulls = []
|
|
63
107
|
for i, (name, vids) in enumerate(grouping.items()):
|
|
64
108
|
if len(vids) == 0:
|
|
65
109
|
continue
|
|
@@ -67,6 +111,7 @@ class GroupingArtist(PatchCollection):
|
|
|
67
111
|
coords = layout.loc[vids].values
|
|
68
112
|
idx_hull = convex_hull(coords)
|
|
69
113
|
coords_hull = coords[idx_hull]
|
|
114
|
+
coords_hulls.append(coords_hull)
|
|
70
115
|
|
|
71
116
|
stylei = rotate_style(style, i)
|
|
72
117
|
|
|
@@ -79,23 +124,30 @@ class GroupingArtist(PatchCollection):
|
|
|
79
124
|
)
|
|
80
125
|
|
|
81
126
|
patches.append(patch)
|
|
82
|
-
return patches, grouping,
|
|
83
|
-
|
|
84
|
-
def _compute_paths(self):
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
)
|
|
127
|
+
return patches, grouping, coords_hulls
|
|
128
|
+
|
|
129
|
+
def _compute_paths(self, dpi=72.0):
|
|
130
|
+
ppc = self._points_per_curve
|
|
131
|
+
for i, hull in enumerate(self._coords_hulls):
|
|
132
|
+
self._paths[i].vertices = _compute_group_path_with_vertex_padding(
|
|
133
|
+
hull,
|
|
134
|
+
self._paths[i].vertices,
|
|
135
|
+
self.get_transform(),
|
|
136
|
+
vertexpadding=self.get_vertexpadding_dpi(dpi),
|
|
137
|
+
points_per_curve=ppc,
|
|
138
|
+
)
|
|
92
139
|
|
|
93
140
|
def _process(self):
|
|
94
|
-
self.set_transform(self.axes.transData)
|
|
95
141
|
self._compute_paths()
|
|
96
142
|
|
|
97
143
|
def draw(self, renderer):
|
|
98
|
-
|
|
144
|
+
# FIXME: this kind of breaks everything since the vertices' magical "_transforms" does not really
|
|
145
|
+
# scale from 72 pixels but rather from the screen's or something. Conclusion: using this keeps
|
|
146
|
+
# consistency across dpis but breaks proportionality of vertexpadding and vertex_size (for now).
|
|
147
|
+
# NOTE: this might be less bad than initially thought in the sense that even perfect scaling
|
|
148
|
+
# does not seem to align the center of the perimeter of the group with the center of the perimeter
|
|
149
|
+
# of the vertex when of the same exact size. So we are probably ok winging it as users will adapt.
|
|
150
|
+
self._compute_paths(self.get_figure(root=True).dpi)
|
|
99
151
|
super().draw(renderer)
|
|
100
152
|
|
|
101
153
|
|
|
@@ -111,24 +163,10 @@ def _compute_group_patch_stub(
|
|
|
111
163
|
)
|
|
112
164
|
|
|
113
165
|
# NOTE: Closing point: mpl is a bit quirky here
|
|
114
|
-
vertices =
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
elif len(points) == 1:
|
|
119
|
-
vertices = [points[0]] * 9
|
|
120
|
-
codes = ["MOVETO"] + ["CURVE3"] * 8
|
|
121
|
-
elif len(points) == 2:
|
|
122
|
-
vertices = [points[0]] * 5 + [points[1]] * 5 + [points[0]]
|
|
123
|
-
codes = ["MOVETO"] + ["CURVE3"] * 4 + ["LINETO"] + ["CURVE3"] * 4 + ["LINETO"]
|
|
124
|
-
else:
|
|
125
|
-
for point in points:
|
|
126
|
-
vertices.extend([point] * 3)
|
|
127
|
-
codes.extend(["LINETO", "CURVE3", "CURVE3"])
|
|
128
|
-
vertices.append(vertices[0])
|
|
129
|
-
codes.append("LINETO")
|
|
130
|
-
codes[0] = "MOVETO"
|
|
131
|
-
|
|
166
|
+
vertices = np.zeros(
|
|
167
|
+
(1 + 30 * len(points), 2),
|
|
168
|
+
)
|
|
169
|
+
codes = ["MOVETO"] + ["LINETO"] * (len(vertices) - 2) + ["CLOSEPOLY"]
|
|
132
170
|
codes = [getattr(mpl.path.Path, x) for x in codes]
|
|
133
171
|
patch = mpl.patches.PathPatch(
|
|
134
172
|
mpl.path.Path(
|