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/utils/matplotlib.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from functools import wraps, partial
|
|
2
2
|
from math import atan2
|
|
3
|
+
import numpy as np
|
|
3
4
|
import matplotlib as mpl
|
|
4
5
|
|
|
5
6
|
from .geometry import (
|
|
@@ -29,6 +30,7 @@ def _forwarder(forwards, cls=None):
|
|
|
29
30
|
|
|
30
31
|
def make_forward(name):
|
|
31
32
|
def method(self, *args, **kwargs):
|
|
33
|
+
"""Each decorated method is called on the decorated class and then, nonrecursively, on all children."""
|
|
32
34
|
ret = getattr(cls.mro()[1], name)(self, *args, **kwargs)
|
|
33
35
|
for c in self.get_children():
|
|
34
36
|
getattr(c, name)(*args, **kwargs)
|
|
@@ -46,7 +48,13 @@ def _forwarder(forwards, cls=None):
|
|
|
46
48
|
|
|
47
49
|
|
|
48
50
|
def _additional_set_methods(attributes, cls=None):
|
|
49
|
-
"""Decorator to add specific set methods for children properties.
|
|
51
|
+
"""Decorator to add specific set methods for children properties.
|
|
52
|
+
|
|
53
|
+
This is useful to autogenerate methods a la set_<key>(value), for
|
|
54
|
+
instance set_alpha(value). It works by delegating to set(alpha=value).
|
|
55
|
+
|
|
56
|
+
Overall, this is a minor tweak compared to the previous decorator.
|
|
57
|
+
"""
|
|
50
58
|
if cls is None:
|
|
51
59
|
return partial(_additional_set_methods, attributes)
|
|
52
60
|
|
|
@@ -73,64 +81,76 @@ def _additional_set_methods(attributes, cls=None):
|
|
|
73
81
|
# many use cases.
|
|
74
82
|
def _get_label_width_height(text, hpadding=18, vpadding=12, **kwargs):
|
|
75
83
|
"""Get the bounding box size for a text with certain properties."""
|
|
76
|
-
forbidden_props = [
|
|
84
|
+
forbidden_props = [
|
|
85
|
+
"horizontalalignment",
|
|
86
|
+
"verticalalignment",
|
|
87
|
+
"ha",
|
|
88
|
+
"va",
|
|
89
|
+
"color",
|
|
90
|
+
"edgecolor",
|
|
91
|
+
"facecolor",
|
|
92
|
+
]
|
|
77
93
|
for prop in forbidden_props:
|
|
78
94
|
if prop in kwargs:
|
|
79
95
|
del kwargs[prop]
|
|
80
96
|
|
|
81
97
|
path = mpl.textpath.TextPath((0, 0), text, **kwargs)
|
|
82
98
|
boundingbox = path.get_extents()
|
|
83
|
-
width = boundingbox.width
|
|
84
|
-
height = boundingbox.height
|
|
99
|
+
width = boundingbox.width
|
|
100
|
+
height = boundingbox.height
|
|
101
|
+
|
|
102
|
+
# Scaling with font size appears broken... try to patch it up linearly here, even though we know it don't work well
|
|
103
|
+
width *= kwargs.get("size", 12) / 12.0
|
|
104
|
+
height *= kwargs.get("size", 12) / 12.0
|
|
105
|
+
|
|
106
|
+
width += hpadding
|
|
107
|
+
height += vpadding
|
|
85
108
|
return (width, height)
|
|
86
109
|
|
|
87
110
|
|
|
88
|
-
def
|
|
111
|
+
def _compute_mid_coord_and_rot(path, trans):
|
|
89
112
|
"""Compute mid point of an edge, straight or curved."""
|
|
90
113
|
# Distinguish between straight and curved paths
|
|
91
114
|
if path.codes[-1] == mpl.path.Path.LINETO:
|
|
92
|
-
|
|
115
|
+
coord = path.vertices.mean(axis=0)
|
|
116
|
+
vtr = trans(path.vertices)
|
|
117
|
+
rot = atan2(
|
|
118
|
+
vtr[-1, 1] - vtr[0, 1],
|
|
119
|
+
vtr[-1, 0] - vtr[0, 0],
|
|
120
|
+
)
|
|
93
121
|
|
|
94
122
|
# Cubic Bezier
|
|
95
|
-
|
|
96
|
-
|
|
123
|
+
elif path.codes[-1] == mpl.path.Path.CURVE4:
|
|
124
|
+
coord = _evaluate_cubic_bezier(path.vertices, 0.5)
|
|
125
|
+
# TODO:
|
|
126
|
+
rot = 0
|
|
97
127
|
|
|
98
128
|
# Square Bezier
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
"Curve type not straight and not squared/cubic Bezier, cannot compute mid point."
|
|
104
|
-
)
|
|
129
|
+
elif path.codes[-1] == mpl.path.Path.CURVE3:
|
|
130
|
+
coord = _evaluate_squared_bezier(path.vertices, 0.5)
|
|
131
|
+
# TODO:
|
|
132
|
+
rot = 0
|
|
105
133
|
|
|
134
|
+
else:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
"Curve type not straight and not squared/cubic Bezier, cannot compute mid point."
|
|
137
|
+
)
|
|
106
138
|
|
|
107
|
-
|
|
108
|
-
points,
|
|
109
|
-
transform,
|
|
110
|
-
vertexpadding=10,
|
|
111
|
-
):
|
|
112
|
-
"""Offset path for a group based on vertex padding.
|
|
139
|
+
return coord, rot
|
|
113
140
|
|
|
114
|
-
At the input, the structure is [v1, v1, v1, v2, v2, v2, ...]
|
|
115
|
-
"""
|
|
116
141
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
points = trans(points)
|
|
142
|
+
def _build_cmap_fun(values, cmap):
|
|
143
|
+
"""Map colormap on top of numerical values."""
|
|
144
|
+
cmap = mpl.cm._ensure_cmap(cmap)
|
|
121
145
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
mprev = atan2(points[0, 1] - vprev[1], points[0, 0] - vprev[0])
|
|
125
|
-
for i, vcur in enumerate(points[::3]):
|
|
126
|
-
vnext = points[(i + 1) * 3]
|
|
127
|
-
mnext = atan2(vnext[1] - vcur[1], vnext[0] - vcur[0])
|
|
146
|
+
if np.isscalar(values):
|
|
147
|
+
values = [values]
|
|
128
148
|
|
|
129
|
-
|
|
130
|
-
|
|
149
|
+
if isinstance(values, dict):
|
|
150
|
+
values = np.array(list(values.values()))
|
|
131
151
|
|
|
132
|
-
|
|
133
|
-
|
|
152
|
+
vmin = np.nanmin(values)
|
|
153
|
+
vmax = np.nanmax(values)
|
|
154
|
+
norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
|
|
134
155
|
|
|
135
|
-
|
|
136
|
-
return points
|
|
156
|
+
return lambda x: cmap(norm(x))
|
iplotx/utils/style.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from copy import copy
|
iplotx/version.py
CHANGED
iplotx/vertex.py
CHANGED
|
@@ -1,51 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module containing code to manipulate vertex visualisations, especially the VertexCollection class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import (
|
|
6
|
+
Optional,
|
|
7
|
+
Sequence,
|
|
8
|
+
Any,
|
|
9
|
+
Never,
|
|
10
|
+
)
|
|
11
|
+
import warnings
|
|
1
12
|
import numpy as np
|
|
2
|
-
|
|
13
|
+
import pandas as pd
|
|
14
|
+
import matplotlib as mpl
|
|
3
15
|
from matplotlib.collections import PatchCollection
|
|
4
16
|
from matplotlib.patches import (
|
|
17
|
+
Patch,
|
|
5
18
|
Ellipse,
|
|
6
19
|
Circle,
|
|
7
20
|
RegularPolygon,
|
|
8
21
|
Rectangle,
|
|
9
22
|
)
|
|
10
23
|
|
|
24
|
+
from .style import (
|
|
25
|
+
get_style,
|
|
26
|
+
rotate_style,
|
|
27
|
+
copy_with_deep_values,
|
|
28
|
+
)
|
|
29
|
+
from .utils.matplotlib import (
|
|
30
|
+
_get_label_width_height,
|
|
31
|
+
_build_cmap_fun,
|
|
32
|
+
_forwarder,
|
|
33
|
+
)
|
|
34
|
+
from .label import LabelCollection
|
|
35
|
+
|
|
11
36
|
|
|
37
|
+
@_forwarder(
|
|
38
|
+
(
|
|
39
|
+
"set_clip_path",
|
|
40
|
+
"set_clip_box",
|
|
41
|
+
"set_snap",
|
|
42
|
+
"set_sketch_params",
|
|
43
|
+
"set_animated",
|
|
44
|
+
"set_picker",
|
|
45
|
+
)
|
|
46
|
+
)
|
|
12
47
|
class VertexCollection(PatchCollection):
|
|
13
|
-
"""Collection of vertex patches for plotting.
|
|
48
|
+
"""Collection of vertex patches for plotting."""
|
|
14
49
|
|
|
15
|
-
|
|
50
|
+
_factor = 1.0
|
|
16
51
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
layout: pd.DataFrame,
|
|
55
|
+
*args,
|
|
56
|
+
layout_coordinate_system: str = "cartesian",
|
|
57
|
+
style: Optional[dict[str, Any]] = None,
|
|
58
|
+
labels: Optional[Sequence[str]] = None,
|
|
59
|
+
**kwargs,
|
|
60
|
+
):
|
|
61
|
+
"""Initialise the VertexCollection.
|
|
22
62
|
|
|
23
|
-
|
|
24
|
-
|
|
63
|
+
Parameters:
|
|
64
|
+
layout: The vertex layout.
|
|
65
|
+
layout_coordinate_system: The coordinate system for the layout, usually "cartesian").
|
|
66
|
+
style: The vertex style (subdictionary "vertex") to apply.
|
|
67
|
+
labels: The vertex labels, if present.
|
|
68
|
+
"""
|
|
25
69
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
70
|
+
self._index = layout.index
|
|
71
|
+
self._style = style
|
|
72
|
+
self._labels = labels
|
|
73
|
+
|
|
74
|
+
# Create patches from structured data
|
|
75
|
+
patches, offsets, sizes, kwargs2 = self._init_vertex_patches(
|
|
76
|
+
layout,
|
|
77
|
+
layout_coordinate_system=layout_coordinate_system,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
kwargs.update(kwargs2)
|
|
81
|
+
kwargs["offsets"] = offsets
|
|
82
|
+
kwargs["match_original"] = True
|
|
29
83
|
|
|
30
|
-
|
|
31
|
-
|
|
84
|
+
# Pass to PatchCollection constructor
|
|
85
|
+
super().__init__(patches, *args, **kwargs)
|
|
32
86
|
|
|
33
|
-
|
|
87
|
+
# Compute _transforms like in _CollectionWithScales for dpi issues
|
|
88
|
+
self.set_sizes(sizes)
|
|
34
89
|
|
|
35
|
-
|
|
90
|
+
if self._labels is not None:
|
|
91
|
+
self._compute_label_collection()
|
|
92
|
+
|
|
93
|
+
def get_children(self) -> tuple[mpl.artist.Artist]:
|
|
94
|
+
"""Get the children artists.
|
|
95
|
+
|
|
96
|
+
This can include the labels as a LabelCollection.
|
|
36
97
|
"""
|
|
37
|
-
|
|
98
|
+
children = []
|
|
99
|
+
if hasattr(self, "_label_collection"):
|
|
100
|
+
children.append(self._label_collection)
|
|
101
|
+
return tuple(children)
|
|
38
102
|
|
|
39
|
-
|
|
40
|
-
for
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
103
|
+
def set_figure(self, fig) -> Never:
|
|
104
|
+
"""Set the figure for this artist and all children."""
|
|
105
|
+
super().set_figure(fig)
|
|
106
|
+
self.set_sizes(self._sizes, self.get_figure(root=True).dpi)
|
|
107
|
+
for child in self.get_children():
|
|
108
|
+
child.set_figure(fig)
|
|
109
|
+
|
|
110
|
+
def get_index(self):
|
|
111
|
+
"""Get the VertexCollection index."""
|
|
112
|
+
return self._index
|
|
113
|
+
|
|
114
|
+
def get_vertex_id(self, index):
|
|
115
|
+
"""Get the id of a single vertex at a positional index."""
|
|
116
|
+
return self._index[index]
|
|
47
117
|
|
|
48
|
-
def
|
|
118
|
+
def get_sizes(self):
|
|
119
|
+
"""Get vertex sizes (max of width and height), not scaled by dpi."""
|
|
120
|
+
return self._sizes
|
|
121
|
+
|
|
122
|
+
def get_sizes_dpi(self):
|
|
123
|
+
"""Get vertex sizes (max of width and height), scaled by dpi."""
|
|
124
|
+
return self._transforms[:, 0, 0]
|
|
125
|
+
|
|
126
|
+
def set_sizes(self, sizes, dpi=72.0):
|
|
49
127
|
"""Set vertex sizes.
|
|
50
128
|
|
|
51
129
|
This rescales the current vertex symbol/path linearly, using this
|
|
@@ -53,26 +131,109 @@ class VertexCollection(PatchCollection):
|
|
|
53
131
|
|
|
54
132
|
@param sizes: A sequence of vertex sizes or a single size.
|
|
55
133
|
"""
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
134
|
+
if sizes is None:
|
|
135
|
+
self._sizes = np.array([])
|
|
136
|
+
self._transforms = np.empty((0, 3, 3))
|
|
137
|
+
else:
|
|
138
|
+
self._sizes = np.asarray(sizes)
|
|
139
|
+
self._transforms = np.zeros((len(self._sizes), 3, 3))
|
|
140
|
+
scale = self._sizes * dpi / 72.0 * self._factor
|
|
141
|
+
self._transforms[:, 0, 0] = scale
|
|
142
|
+
self._transforms[:, 1, 1] = scale
|
|
143
|
+
self._transforms[:, 2, 2] = 1.0
|
|
144
|
+
self.stale = True
|
|
145
|
+
|
|
146
|
+
get_size = get_sizes
|
|
147
|
+
set_size = set_sizes
|
|
148
|
+
|
|
149
|
+
def _init_vertex_patches(
|
|
150
|
+
self, vertex_layout_df, layout_coordinate_system="cartesian"
|
|
151
|
+
):
|
|
152
|
+
style = self._style or {}
|
|
153
|
+
if "cmap" in style:
|
|
154
|
+
cmap_fun = _build_cmap_fun(
|
|
155
|
+
style["facecolor"],
|
|
156
|
+
style["cmap"],
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
cmap_fun = None
|
|
160
|
+
|
|
161
|
+
if style.get("size", 20) == "label":
|
|
162
|
+
if self._labels is None:
|
|
163
|
+
warnings.warn(
|
|
164
|
+
"No labels found, cannot resize vertices based on labels."
|
|
165
|
+
)
|
|
166
|
+
style["size"] = get_style("default.vertex")["size"]
|
|
167
|
+
else:
|
|
168
|
+
vertex_labels = self._labels
|
|
169
|
+
|
|
170
|
+
if "cmap" in style:
|
|
171
|
+
colorarray = []
|
|
172
|
+
patches = []
|
|
173
|
+
offsets = []
|
|
174
|
+
sizes = []
|
|
175
|
+
for i, (vid, row) in enumerate(vertex_layout_df.iterrows()):
|
|
176
|
+
# Centre of the vertex
|
|
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":
|
|
187
|
+
# NOTE: it's ok to overwrite the dict here
|
|
188
|
+
style["size"] = _get_label_width_height(
|
|
189
|
+
str(vertex_labels[vid]), **style.get("label", {})
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
stylei = rotate_style(style, index=i, key=vid)
|
|
193
|
+
if cmap_fun is not None:
|
|
194
|
+
colorarray.append(style["facecolor"])
|
|
195
|
+
stylei["facecolor"] = cmap_fun(stylei["facecolor"])
|
|
196
|
+
|
|
197
|
+
# Shape of the vertex (Patch)
|
|
198
|
+
art, size = make_patch(**stylei)
|
|
199
|
+
patches.append(art)
|
|
67
200
|
sizes.append(size)
|
|
68
|
-
# Rescale the path for this vertex
|
|
69
|
-
path.vertices *= size / cursize
|
|
70
201
|
|
|
71
|
-
|
|
202
|
+
kwargs = {}
|
|
203
|
+
if "cmap" in style:
|
|
204
|
+
vmin = np.min(colorarray)
|
|
205
|
+
vmax = np.max(colorarray)
|
|
206
|
+
norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
|
|
207
|
+
kwargs["cmap"] = style["cmap"]
|
|
208
|
+
kwargs["norm"] = norm
|
|
209
|
+
|
|
210
|
+
return patches, offsets, sizes, kwargs
|
|
72
211
|
|
|
73
|
-
def
|
|
74
|
-
|
|
75
|
-
|
|
212
|
+
def _compute_label_collection(self):
|
|
213
|
+
transform = self.get_offset_transform()
|
|
214
|
+
|
|
215
|
+
style = (
|
|
216
|
+
copy_with_deep_values(self._style.get("label", None))
|
|
217
|
+
if self._style is not None
|
|
218
|
+
else {}
|
|
219
|
+
)
|
|
220
|
+
forbidden_props = ["hpadding", "vpadding"]
|
|
221
|
+
for prop in forbidden_props:
|
|
222
|
+
if prop in style:
|
|
223
|
+
del style[prop]
|
|
224
|
+
|
|
225
|
+
self._label_collection = LabelCollection(
|
|
226
|
+
self._labels,
|
|
227
|
+
style=style,
|
|
228
|
+
offsets=self._offsets,
|
|
229
|
+
transform=transform,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def get_labels(self):
|
|
233
|
+
if hasattr(self, "_label_collection"):
|
|
234
|
+
return self._label_collection
|
|
235
|
+
else:
|
|
236
|
+
return None
|
|
76
237
|
|
|
77
238
|
@property
|
|
78
239
|
def stale(self):
|
|
@@ -84,29 +245,63 @@ class VertexCollection(PatchCollection):
|
|
|
84
245
|
if val and hasattr(self, "stale_callback_post"):
|
|
85
246
|
self.stale_callback_post(self)
|
|
86
247
|
|
|
248
|
+
@mpl.artist.allow_rasterization
|
|
249
|
+
def draw(self, renderer):
|
|
250
|
+
if not self.get_visible():
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
# null graph, no need to draw anything
|
|
254
|
+
# NOTE: I would expect this to be already a clause in the superclass by oh well
|
|
255
|
+
if len(self.get_paths()) == 0:
|
|
256
|
+
return
|
|
87
257
|
|
|
88
|
-
|
|
258
|
+
self.set_sizes(self._sizes, self.get_figure(root=True).dpi)
|
|
259
|
+
|
|
260
|
+
# NOTE: This draws the vertices first, then the labels.
|
|
261
|
+
# The correct order would be vertex1->label1->vertex2->label2, etc.
|
|
262
|
+
# We might fix if we manage to find a way to do it.
|
|
263
|
+
super().draw(renderer)
|
|
264
|
+
for child in self.get_children():
|
|
265
|
+
child.draw(renderer)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def make_patch(
|
|
269
|
+
marker: str, size: float | Sequence[float], **kwargs
|
|
270
|
+
) -> tuple[Patch, float]:
|
|
89
271
|
"""Make a patch of the given marker shape and size."""
|
|
90
|
-
forbidden_props = ["label"]
|
|
272
|
+
forbidden_props = ["label", "cmap", "norm"]
|
|
91
273
|
for prop in forbidden_props:
|
|
92
274
|
if prop in kwargs:
|
|
93
275
|
kwargs.pop(prop)
|
|
94
276
|
|
|
95
|
-
if
|
|
277
|
+
if np.isscalar(size):
|
|
278
|
+
size = float(size)
|
|
96
279
|
size = (size, size)
|
|
97
280
|
|
|
98
|
-
|
|
99
|
-
|
|
281
|
+
# Size of vertices is determined in self._transforms, which scales with dpi, rather than here,
|
|
282
|
+
# 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 and height.
|
|
284
|
+
size = np.asarray(size, dtype=float)
|
|
285
|
+
size_max = size.max()
|
|
286
|
+
if size_max > 0:
|
|
287
|
+
size /= size_max
|
|
288
|
+
|
|
289
|
+
art: Patch
|
|
290
|
+
if marker in ("o", "c", "circle"):
|
|
291
|
+
art = Circle((0, 0), size[0] / 2, **kwargs)
|
|
100
292
|
elif marker in ("s", "square", "r", "rectangle"):
|
|
101
|
-
|
|
293
|
+
art = Rectangle((-size[0] / 2, -size[1] / 2), size[0], size[1], **kwargs)
|
|
102
294
|
elif marker in ("^", "triangle"):
|
|
103
|
-
|
|
295
|
+
art = RegularPolygon((0, 0), numVertices=3, radius=size[0] / 2, **kwargs)
|
|
104
296
|
elif marker in ("d", "diamond"):
|
|
105
|
-
|
|
297
|
+
art, _ = make_patch("s", size[0], angle=45, **kwargs)
|
|
106
298
|
elif marker in ("v", "triangle_down"):
|
|
107
|
-
|
|
299
|
+
art = RegularPolygon(
|
|
108
300
|
(0, 0), numVertices=3, radius=size[0] / 2, orientation=np.pi, **kwargs
|
|
109
301
|
)
|
|
110
302
|
elif marker in ("e", "ellipse"):
|
|
111
|
-
|
|
112
|
-
|
|
303
|
+
art = Ellipse((0, 0), size[0] / 2, size[1] / 2, **kwargs)
|
|
304
|
+
else:
|
|
305
|
+
raise KeyError(f"Unknown marker: {marker}")
|
|
306
|
+
|
|
307
|
+
return (art, size_max)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iplotx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Plot networkx from igraph and networkx.
|
|
5
5
|
Project-URL: Homepage, https://github.com/fabilab/iplotx
|
|
6
6
|
Project-URL: Documentation, https://readthedocs.org/iplotx
|
|
@@ -14,13 +14,14 @@ Keywords: graph,network,plotting,visualisation
|
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Classifier: Operating System :: OS Independent
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
20
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
-
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Requires-Python: >=3.11
|
|
22
21
|
Requires-Dist: matplotlib>=2.0.0
|
|
22
|
+
Requires-Dist: numpy>=2.0.0
|
|
23
23
|
Requires-Dist: pandas>=2.0.0
|
|
24
|
+
Requires-Dist: pylint>=3.3.7
|
|
24
25
|
Provides-Extra: igraph
|
|
25
26
|
Requires-Dist: igraph>=0.11.0; extra == 'igraph'
|
|
26
27
|
Provides-Extra: networkx
|
|
@@ -29,17 +30,45 @@ Description-Content-Type: text/markdown
|
|
|
29
30
|
|
|
30
31
|

|
|
31
32
|

|
|
33
|
+

|
|
32
34
|
|
|
33
35
|
# iplotx
|
|
34
36
|
Plotting networks from igraph and networkx.
|
|
35
37
|
|
|
36
|
-
**NOTE**: This is currently
|
|
38
|
+
**NOTE**: This is currently alpha quality software. The API and functionality will break constantly, so use at your own risk. That said, if you have things you would like to see improved, please open a GitHub issue.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
```bash
|
|
42
|
+
pip install iplotx
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
```python
|
|
47
|
+
import networkx as nx
|
|
48
|
+
import matplotlib.pyplot as plt
|
|
49
|
+
import iplotx as ipx
|
|
50
|
+
|
|
51
|
+
g = nx.Graph([(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)])
|
|
52
|
+
layout = nx.layout.circular_layout(g)
|
|
53
|
+
fig, ax = plt.subplots(figsize=(3, 3))
|
|
54
|
+
ipx.plot(g, ax=ax, layout=layout)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+

|
|
58
|
+
|
|
59
|
+
## Documentation
|
|
60
|
+
See [readthedocs](https://iplotx.readthedocs.io/en/latest/) for the full documentation.
|
|
61
|
+
|
|
62
|
+
## Gallery
|
|
63
|
+
See [gallery](https://iplotx.readthedocs.io/en/latest/gallery/index.html).
|
|
37
64
|
|
|
38
65
|
## Roadmap
|
|
39
66
|
- Plot networks from igraph and networkx interchangeably, using matplotlib as a backend. ✅
|
|
40
67
|
- Support interactive plotting, e.g. zooming and panning after the plot is created. ✅
|
|
41
|
-
- Support storing the plot to disk thanks to the many matplotlib backends (SVG, PNG, PDF, etc.).
|
|
42
|
-
-
|
|
68
|
+
- Support storing the plot to disk thanks to the many matplotlib backends (SVG, PNG, PDF, etc.). ✅
|
|
69
|
+
- Support flexible yet easy styling. ✅
|
|
70
|
+
- Efficient plotting of large graphs using matplotlib's collection functionality. ✅
|
|
71
|
+
- Support trees from special libraries such as ete3, biopython, etc. This will need a dedicated function and layouting. ✅
|
|
43
72
|
- Support animations, e.g. showing the evolution of a network over time. 🏗️
|
|
44
73
|
- Support uni- and bi-directional communication between graph object and plot object.🏗️
|
|
45
74
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
iplotx/__init__.py,sha256=DIOWUaEo9vgmz8B6hBlurV95BEYmjmUe5BU-jRWrtG4,418
|
|
2
|
+
iplotx/groups.py,sha256=QZ4iounIcoPQfxRIJamGJljpdKTsVJGM0DCfBPJKBm0,6106
|
|
3
|
+
iplotx/label.py,sha256=zS53rzA2j4yjo150CfjqbYfBTFWNG5tt9AkfdNWZlZ0,3884
|
|
4
|
+
iplotx/layout.py,sha256=IDj66gXUvAN8q1uBIhmob-ZOQFWO8GAlW-4gGprN7H0,3963
|
|
5
|
+
iplotx/network.py,sha256=VcrKdq0PzbXfufIAir-nyMgrpfGvvXpdqK5H8kPDf3I,9739
|
|
6
|
+
iplotx/plotting.py,sha256=1C6wenDeUohoYVl_vAcTs2aR-5LcRHFOcNb4ghIbkLI,7152
|
|
7
|
+
iplotx/style.py,sha256=m-sWbC4qjfdPr4fUns_vCPKl035-uJXLy6F4J-egQjM,11058
|
|
8
|
+
iplotx/tree.py,sha256=7o9BUltkl37wy9Gn8RepPoNlcbzZYALC1tZVLDJ0CVo,9372
|
|
9
|
+
iplotx/typing.py,sha256=dfiCkn_7plbQZ1tkBAdK8vCap8r9ZB6Skw2W4RYKVbo,1078
|
|
10
|
+
iplotx/version.py,sha256=yw3yV4Zl2O7TK5Iy1aB2-9BhAM-EcoMbba7u7G2n1TE,66
|
|
11
|
+
iplotx/vertex.py,sha256=h0B5pTDxT2dTd73fN8sXYsJdzanwaI38T12ZYpX_EgI,9505
|
|
12
|
+
iplotx/edge/__init__.py,sha256=1BE4qCIXJwcN6A1AQVY1PFxje9WNYR1PEMk36vTALkk,30285
|
|
13
|
+
iplotx/edge/arrow.py,sha256=dTlXMi1PsQDbpIktjRCGtubMGF72iOg1Gw9vC0FMOYo,11217
|
|
14
|
+
iplotx/edge/ports.py,sha256=UdK3ylEWOyaZkKAKqN-reL0euF6QBWHLTJG7Ts7uhz4,1126
|
|
15
|
+
iplotx/ingest/__init__.py,sha256=75Pml7X65tP8b2G3qaeZUdnDgwP6dclcCEEFl0BYSdo,4707
|
|
16
|
+
iplotx/ingest/heuristics.py,sha256=32AZ8iidM_uooaBKe2EMjNU_nJDbhA_28ADc0dpQy5A,6636
|
|
17
|
+
iplotx/ingest/typing.py,sha256=QEgCpLyfp-0v9czn6OaJ0_nuCo1AXv3lGS3aD6w-Ezw,3134
|
|
18
|
+
iplotx/ingest/providers/network/igraph.py,sha256=_J7lH-jrT0_1oSfwgT_mQMRhxNoFwHo8dyBLCbgqETQ,2766
|
|
19
|
+
iplotx/ingest/providers/network/networkx.py,sha256=u7NegapWZ0gWUj5n1PUVD-zZ92lKUiv6BLNTNIrXlRk,4233
|
|
20
|
+
iplotx/ingest/providers/tree/biopython.py,sha256=7ZVD_WwIaBOSl-at4r_Y4d2qHQq6BcvlV-yDxMVCWnw,3456
|
|
21
|
+
iplotx/ingest/providers/tree/cogent3.py,sha256=5O92zkdA43LWQ7h6r-uG_5X76EPHo-Yx9ODLb0hd_qc,3636
|
|
22
|
+
iplotx/ingest/providers/tree/ete4.py,sha256=sGm2363yLJWsWsrRn2iFx8qRiq57h_5dQWBD41xJmhY,3660
|
|
23
|
+
iplotx/ingest/providers/tree/skbio.py,sha256=iNZhY_TNLpzd55cK2nybjiftUfakeOrDDJ9dMBna_io,3629
|
|
24
|
+
iplotx/utils/geometry.py,sha256=K5ZBYPmz4-KNm64pDh2p0L6PF5-u57SCVaEd2eWeRv0,8956
|
|
25
|
+
iplotx/utils/internal.py,sha256=WWfcZDGK8Ut1y_tOHRGg9wSqY1bwSeLQO7dHM_8Tvwo,107
|
|
26
|
+
iplotx/utils/matplotlib.py,sha256=T37SMwKNSA-dRKgNkVw5H4fGr5NACtxvBPebDJGdhjk,4516
|
|
27
|
+
iplotx/utils/style.py,sha256=fEi14nc37HQqHxZTPeQaTnFFwbSneY1oDCyv2UbWfKk,22
|
|
28
|
+
iplotx-0.2.0.dist-info/METADATA,sha256=5Xey_j3r4NJ78C9f-cMvfyhZc9rPb6E6xwdBXbVjRUI,3055
|
|
29
|
+
iplotx-0.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
30
|
+
iplotx-0.2.0.dist-info/RECORD,,
|
iplotx/edge/common.py
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
from math import pi
|
|
2
|
-
import numpy as np
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
def _compute_loops_per_angle(nloops, angles):
|
|
6
|
-
if len(angles) == 0:
|
|
7
|
-
return [(0, 2 * pi, nloops)]
|
|
8
|
-
|
|
9
|
-
angles_sorted_closed = list(sorted(angles))
|
|
10
|
-
angles_sorted_closed.append(angles_sorted_closed[0] + 2 * pi)
|
|
11
|
-
deltas = np.diff(angles_sorted_closed)
|
|
12
|
-
|
|
13
|
-
# Now we have the deltas and the total number of loops
|
|
14
|
-
# 1. Assign all loops to the largest wedge
|
|
15
|
-
idx_dmax = deltas.argmax()
|
|
16
|
-
if nloops == 1:
|
|
17
|
-
return [
|
|
18
|
-
(angles_sorted_closed[idx_dmax], angles_sorted_closed[idx_dmax + 1], nloops)
|
|
19
|
-
]
|
|
20
|
-
|
|
21
|
-
# 2. Check if any other wedges are larger than this
|
|
22
|
-
# If not, we are done (this is the algo in igraph)
|
|
23
|
-
dsplit = deltas[idx_dmax] / nloops
|
|
24
|
-
if (deltas > dsplit).sum() < 2:
|
|
25
|
-
return [
|
|
26
|
-
(angles_sorted_closed[idx_dmax], angles_sorted_closed[idx_dmax + 1], nloops)
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
# 3. Check how small the second-largest wedge would become
|
|
30
|
-
idx_dsort = np.argsort(deltas)
|
|
31
|
-
return [
|
|
32
|
-
(
|
|
33
|
-
angles_sorted_closed[idx_dmax],
|
|
34
|
-
angles_sorted_closed[idx_dmax + 1],
|
|
35
|
-
nloops - 1,
|
|
36
|
-
),
|
|
37
|
-
(
|
|
38
|
-
angles_sorted_closed[idx_dsort[-2]],
|
|
39
|
-
angles_sorted_closed[idx_dsort[-2] + 1],
|
|
40
|
-
1,
|
|
41
|
-
),
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
## TODO: we should greedily iterate from this
|
|
45
|
-
## TODO: finish this
|
|
46
|
-
# dsplit_new = dsplit * nloops / (nloops - 1)
|
|
47
|
-
# dsplit2_new = deltas[idx_dsort[-2]]
|