iplotx 0.4.0__py3-none-any.whl → 0.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iplotx/cascades.py +19 -28
- iplotx/edge/__init__.py +74 -6
- iplotx/edge/arrow.py +10 -3
- iplotx/edge/geometry.py +5 -17
- iplotx/edge/ports.py +3 -2
- iplotx/groups.py +1 -3
- iplotx/ingest/__init__.py +5 -14
- iplotx/ingest/heuristics.py +1 -4
- iplotx/ingest/providers/network/igraph.py +4 -12
- iplotx/ingest/providers/network/networkx.py +6 -20
- iplotx/ingest/providers/network/simple.py +2 -9
- iplotx/ingest/providers/tree/biopython.py +2 -5
- iplotx/ingest/providers/tree/cogent3.py +2 -5
- iplotx/ingest/providers/tree/ete4.py +2 -5
- iplotx/ingest/providers/tree/simple.py +97 -0
- iplotx/ingest/providers/tree/skbio.py +2 -5
- iplotx/ingest/typing.py +5 -14
- iplotx/network.py +4 -7
- iplotx/plotting.py +1 -1
- iplotx/style/__init__.py +131 -106
- iplotx/style/leaf_info.py +3 -0
- iplotx/style/library.py +94 -0
- iplotx/tree.py +55 -42
- iplotx/typing.py +2 -0
- iplotx/utils/geometry.py +32 -40
- iplotx/utils/matplotlib.py +13 -10
- iplotx/utils/style.py +6 -1
- iplotx/version.py +1 -1
- iplotx/vertex.py +16 -23
- {iplotx-0.4.0.dist-info → iplotx-0.5.1.dist-info}/METADATA +33 -15
- iplotx-0.5.1.dist-info/RECORD +38 -0
- iplotx-0.4.0.dist-info/RECORD +0 -37
- {iplotx-0.4.0.dist-info → iplotx-0.5.1.dist-info}/WHEEL +0 -0
iplotx/tree.py
CHANGED
|
@@ -14,6 +14,7 @@ from .style import (
|
|
|
14
14
|
context,
|
|
15
15
|
get_style,
|
|
16
16
|
rotate_style,
|
|
17
|
+
merge_styles,
|
|
17
18
|
)
|
|
18
19
|
from .utils.matplotlib import (
|
|
19
20
|
_stale_wrapper,
|
|
@@ -63,9 +64,7 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
63
64
|
tree,
|
|
64
65
|
layout: Optional[str] = "horizontal",
|
|
65
66
|
directed: bool | str = False,
|
|
66
|
-
vertex_labels: Optional[
|
|
67
|
-
bool | list[str] | dict[Hashable, str] | pd.Series
|
|
68
|
-
] = None,
|
|
67
|
+
vertex_labels: Optional[bool | list[str] | dict[Hashable, str] | pd.Series] = None,
|
|
69
68
|
edge_labels: Optional[Sequence | dict[Hashable, str] | pd.Series] = None,
|
|
70
69
|
leaf_labels: Optional[Sequence | dict[Hashable, str]] | pd.Series = None,
|
|
71
70
|
transform: mpl.transforms.Transform = mpl.transforms.IdentityTransform(),
|
|
@@ -193,9 +192,7 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
193
192
|
|
|
194
193
|
def get_layout(self, kind="vertex"):
|
|
195
194
|
"""Get vertex or edge layout."""
|
|
196
|
-
layout_columns = [
|
|
197
|
-
f"_ipx_layout_{i}" for i in range(self._ipx_internal_data["ndim"])
|
|
198
|
-
]
|
|
195
|
+
layout_columns = [f"_ipx_layout_{i}" for i in range(self._ipx_internal_data["ndim"])]
|
|
199
196
|
|
|
200
197
|
if kind == "vertex":
|
|
201
198
|
layout = self._ipx_internal_data["vertex_df"][layout_columns]
|
|
@@ -264,9 +261,7 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
264
261
|
|
|
265
262
|
def get_leaf_vertices(self) -> Optional[VertexCollection]:
|
|
266
263
|
"""Get leaf VertexCollection artist."""
|
|
267
|
-
|
|
268
|
-
return self._leaf_vertices
|
|
269
|
-
return None
|
|
264
|
+
return self._leaf_vertices
|
|
270
265
|
|
|
271
266
|
def get_leaf_edges(self) -> Optional[LeafEdgeCollection]:
|
|
272
267
|
"""Get LeafEdgeCollection artist if present."""
|
|
@@ -284,12 +279,11 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
284
279
|
|
|
285
280
|
def get_leaf_labels(self) -> Optional[LabelCollection]:
|
|
286
281
|
"""Get the leaf label artist if present."""
|
|
287
|
-
|
|
288
|
-
return self._leaf_vertices.get_labels()
|
|
289
|
-
return None
|
|
282
|
+
return self._leaf_vertices.get_labels()
|
|
290
283
|
|
|
291
284
|
def get_leaf_edge_labels(self) -> Optional[LabelCollection]:
|
|
292
285
|
"""Get the leaf edge label artist if present."""
|
|
286
|
+
# TODO: leaf edge labels are basically unsupported as of now
|
|
293
287
|
if hasattr(self, "_leaf_edges"):
|
|
294
288
|
return self._leaf_edges.get_labels()
|
|
295
289
|
return None
|
|
@@ -312,8 +306,6 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
312
306
|
"""Add edges from the leaf to the max leaf depth."""
|
|
313
307
|
# If there are no leaves, no leaf labels, or leaves are not deep,
|
|
314
308
|
# skip leaf edges
|
|
315
|
-
if not hasattr(self, "_leaf_vertices"):
|
|
316
|
-
return
|
|
317
309
|
leaf_style = get_style(".leaf", {})
|
|
318
310
|
if ("deep" not in leaf_style) and self.get_leaf_labels() is None:
|
|
319
311
|
return
|
|
@@ -397,16 +389,14 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
397
389
|
if user_leaf_style.get("deep", True):
|
|
398
390
|
if layout_name == "radial":
|
|
399
391
|
leaf_layout.iloc[:, 0] = leaf_layout.iloc[:, 0].max()
|
|
400
|
-
elif layout_name == "horizontal":
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
else:
|
|
409
|
-
leaf_layout.iloc[:, 1] = leaf_layout.iloc[:, 1].max()
|
|
392
|
+
elif (layout_name, orientation) == ("horizontal", "right"):
|
|
393
|
+
leaf_layout.iloc[:, 0] = leaf_layout.iloc[:, 0].max()
|
|
394
|
+
elif (layout_name, orientation) == ("horizontal", "left"):
|
|
395
|
+
leaf_layout.iloc[:, 0] = leaf_layout.iloc[:, 0].min()
|
|
396
|
+
elif (layout_name, orientation) == ("vertical", "descending"):
|
|
397
|
+
leaf_layout.iloc[:, 1] = leaf_layout.iloc[:, 1].min()
|
|
398
|
+
elif (layout_name, orientation) == ("vertical", "ascending"):
|
|
399
|
+
leaf_layout.iloc[:, 1] = leaf_layout.iloc[:, 1].max()
|
|
410
400
|
else:
|
|
411
401
|
raise ValueError(
|
|
412
402
|
f"Layout and orientation not supported: {layout_name}, {orientation}."
|
|
@@ -471,7 +461,24 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
471
461
|
"""Add cascade patches."""
|
|
472
462
|
# NOTE: If leaf labels are present and the cascades are requested to wrap around them,
|
|
473
463
|
# we have to compute the max extend of the cascades from the leaf labels.
|
|
474
|
-
|
|
464
|
+
layout = self.get_layout()
|
|
465
|
+
layout_name = self._ipx_internal_data["layout_name"]
|
|
466
|
+
orientation = self._ipx_internal_data["orientation"]
|
|
467
|
+
maxdepth = 1e-10
|
|
468
|
+
if layout_name == "horizontal":
|
|
469
|
+
if orientation == "right":
|
|
470
|
+
maxdepth = layout.values[:, 0].max()
|
|
471
|
+
else:
|
|
472
|
+
maxdepth = layout.values[:, 0].min()
|
|
473
|
+
elif layout_name == "vertical":
|
|
474
|
+
if orientation == "descending":
|
|
475
|
+
maxdepth = layout.values[:, 1].min()
|
|
476
|
+
else:
|
|
477
|
+
maxdepth = layout.values[:, 1].max()
|
|
478
|
+
elif layout_name == "radial":
|
|
479
|
+
# layout values are: r, theta
|
|
480
|
+
maxdepth = layout.values[:, 0].max()
|
|
481
|
+
|
|
475
482
|
style_cascade = get_style(".cascade")
|
|
476
483
|
extend_to_labels = style_cascade.get("extend", False) == "leaf_labels"
|
|
477
484
|
has_leaf_labels = self.get_leaf_labels() is not None
|
|
@@ -483,9 +490,9 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
483
490
|
|
|
484
491
|
self._cascades = CascadeCollection(
|
|
485
492
|
tree=self.tree,
|
|
486
|
-
layout=
|
|
487
|
-
layout_name=
|
|
488
|
-
orientation=
|
|
493
|
+
layout=layout,
|
|
494
|
+
layout_name=layout_name,
|
|
495
|
+
orientation=orientation,
|
|
489
496
|
style=style_cascade,
|
|
490
497
|
provider=data_providers["tree"][self._ipx_internal_data["tree_library"]],
|
|
491
498
|
transform=self.get_offset_transform(),
|
|
@@ -496,9 +503,7 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
496
503
|
layout_name = self.get_layout_name()
|
|
497
504
|
if layout_name == "radial":
|
|
498
505
|
maxdepth = 0
|
|
499
|
-
bboxes = self.get_leaf_labels().get_datalims_children(
|
|
500
|
-
self.get_offset_transform()
|
|
501
|
-
)
|
|
506
|
+
bboxes = self.get_leaf_labels().get_datalims_children(self.get_offset_transform())
|
|
502
507
|
for bbox in bboxes:
|
|
503
508
|
r1 = np.linalg.norm([bbox.xmax, bbox.ymax])
|
|
504
509
|
r2 = np.linalg.norm([bbox.xmax, bbox.ymin])
|
|
@@ -541,9 +546,7 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
541
546
|
else:
|
|
542
547
|
cmap_fun = None
|
|
543
548
|
|
|
544
|
-
edge_df = self._ipx_internal_data["edge_df"].set_index(
|
|
545
|
-
["_ipx_source", "_ipx_target"]
|
|
546
|
-
)
|
|
549
|
+
edge_df = self._ipx_internal_data["edge_df"].set_index(["_ipx_source", "_ipx_target"])
|
|
547
550
|
|
|
548
551
|
if "cmap" in edge_style:
|
|
549
552
|
colorarray = []
|
|
@@ -551,7 +554,7 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
551
554
|
adjacent_vertex_ids = []
|
|
552
555
|
waypoints = []
|
|
553
556
|
for i, (vid1, vid2) in enumerate(edge_df.index):
|
|
554
|
-
edge_stylei = rotate_style(edge_style, index=i, key=
|
|
557
|
+
edge_stylei = rotate_style(edge_style, index=i, key=vid2)
|
|
555
558
|
|
|
556
559
|
# FIXME:: Improve this logic. We have three layers of priority:
|
|
557
560
|
# 1. Explicitely set in the style of "plot"
|
|
@@ -573,6 +576,8 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
573
576
|
|
|
574
577
|
# Tree layout determines waypoints
|
|
575
578
|
waypointsi = edge_stylei.pop("waypoints", None)
|
|
579
|
+
if isinstance(waypointsi, (bool, np.bool)):
|
|
580
|
+
waypointsi = ["none", None][int(waypointsi)]
|
|
576
581
|
if waypointsi is None:
|
|
577
582
|
layout_name = self._ipx_internal_data["layout_name"]
|
|
578
583
|
if layout_name == "horizontal":
|
|
@@ -582,7 +587,9 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
582
587
|
elif layout_name == "radial":
|
|
583
588
|
waypointsi = "r0a1"
|
|
584
589
|
else:
|
|
585
|
-
|
|
590
|
+
raise ValueError(
|
|
591
|
+
f"Layout not supported: {layout_name}. ",
|
|
592
|
+
)
|
|
586
593
|
waypoints.append(waypointsi)
|
|
587
594
|
|
|
588
595
|
# These are not the actual edges drawn, only stubs to establish
|
|
@@ -630,6 +637,7 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
630
637
|
self,
|
|
631
638
|
nodes: Sequence[Hashable],
|
|
632
639
|
style: Optional[dict[str, Any] | Sequence[str | dict[str, Any]]] = None,
|
|
640
|
+
**kwargs,
|
|
633
641
|
) -> None:
|
|
634
642
|
"""Style a subtree of the tree.
|
|
635
643
|
|
|
@@ -639,21 +647,26 @@ class TreeArtist(mpl.artist.Artist):
|
|
|
639
647
|
style: Style or sequence of styles to apply to the subtree. Each style can
|
|
640
648
|
be either a string, referring to an internal `iplotx` style, or a dictionary
|
|
641
649
|
with custom styling elements.
|
|
650
|
+
kwargs: Additional flat style elements. If both style and kwargs are provided,
|
|
651
|
+
kwargs is applied last.
|
|
642
652
|
"""
|
|
653
|
+
styles = []
|
|
654
|
+
if isinstance(style, (str, dict)):
|
|
655
|
+
styles = [style]
|
|
656
|
+
elif style is not None:
|
|
657
|
+
styles = list(style)
|
|
658
|
+
style = merge_styles(styles + [kwargs])
|
|
659
|
+
|
|
643
660
|
provider = data_providers["tree"][self._ipx_internal_data["tree_library"]]
|
|
644
661
|
|
|
645
662
|
# Get last (deepest) common ancestor of the requested nodes
|
|
646
663
|
root = provider(self.tree).get_lca(nodes)
|
|
647
664
|
|
|
648
665
|
# Populate a DataFrame with the array of properties to update
|
|
649
|
-
vertex_idx = {
|
|
650
|
-
node: i for i, node in enumerate(self._ipx_internal_data["vertex_df"].index)
|
|
651
|
-
}
|
|
666
|
+
vertex_idx = {node: i for i, node in enumerate(self._ipx_internal_data["vertex_df"].index)}
|
|
652
667
|
edge_idx = {
|
|
653
668
|
node: i
|
|
654
|
-
for i, node in enumerate(
|
|
655
|
-
self._ipx_internal_data["edge_df"]["_ipx_target"].values
|
|
656
|
-
)
|
|
669
|
+
for i, node in enumerate(self._ipx_internal_data["edge_df"]["_ipx_target"].values)
|
|
657
670
|
}
|
|
658
671
|
vertex_props = {}
|
|
659
672
|
edge_props = {}
|
iplotx/typing.py
CHANGED
|
@@ -32,12 +32,14 @@ LayoutType = Union[
|
|
|
32
32
|
Sequence[Sequence[float]],
|
|
33
33
|
np.ndarray,
|
|
34
34
|
pd.DataFrame,
|
|
35
|
+
dict[Hashable, Sequence[float] | tuple[float, float]],
|
|
35
36
|
# igraph.Layout,
|
|
36
37
|
]
|
|
37
38
|
GroupingType = Union[
|
|
38
39
|
Sequence[set],
|
|
39
40
|
Sequence[int],
|
|
40
41
|
Sequence[str],
|
|
42
|
+
dict[str, set],
|
|
41
43
|
# igraph.clustering.Clustering,
|
|
42
44
|
# igraph.clustering.VertexClustering,
|
|
43
45
|
# igraph.clustering.Cover,
|
iplotx/utils/geometry.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Sequence,
|
|
3
|
+
)
|
|
1
4
|
from math import atan2
|
|
2
5
|
import numpy as np
|
|
6
|
+
import matplotlib as mpl
|
|
3
7
|
|
|
4
8
|
|
|
5
9
|
# See also this link for the general answer (using scipy to compute coefficients):
|
|
@@ -13,28 +17,7 @@ def _evaluate_squared_bezier(points, t):
|
|
|
13
17
|
def _evaluate_cubic_bezier(points, t):
|
|
14
18
|
"""Evaluate a cubic Bezier curve at t."""
|
|
15
19
|
p0, p1, p2, p3 = points
|
|
16
|
-
return (
|
|
17
|
-
(1 - t) ** 3 * p0
|
|
18
|
-
+ 3 * (1 - t) ** 2 * t * p1
|
|
19
|
-
+ 3 * (1 - t) * t**2 * p2
|
|
20
|
-
+ t**3 * p3
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def _evaluate_cubic_bezier_derivative(points, t):
|
|
25
|
-
"""Evaluate the derivative of a cubic Bezier curve at t."""
|
|
26
|
-
p0, p1, p2, p3 = points
|
|
27
|
-
# (dx / dt, dy / dt) is the parametric gradient
|
|
28
|
-
# to get the angle from this, one can just atanh(dy/dt, dx/dt)
|
|
29
|
-
# This is equivalent to computing the actual bezier curve
|
|
30
|
-
# at low t, of course, which is the geometric interpretation
|
|
31
|
-
# (obviously, division by t is irrelenant)
|
|
32
|
-
return (
|
|
33
|
-
3 * p0 * (1 - t) ** 2
|
|
34
|
-
+ 3 * p1 * (1 - t) * (-3 * t + 1)
|
|
35
|
-
+ 3 * p2 * t * (2 - 3 * t)
|
|
36
|
-
+ 3 * p3 * t**2
|
|
37
|
-
)
|
|
20
|
+
return (1 - t) ** 3 * p0 + 3 * (1 - t) ** 2 * t * p1 + 3 * (1 - t) * t**2 * p2 + t**3 * p3
|
|
38
21
|
|
|
39
22
|
|
|
40
23
|
def convex_hull(points):
|
|
@@ -90,9 +73,7 @@ def _convex_hull_Graham_scan(points):
|
|
|
90
73
|
pivot_idx = miny_idx[points[miny_idx, 0].argmin()]
|
|
91
74
|
|
|
92
75
|
# Compute angles against that pivot, ensuring the pivot itself last
|
|
93
|
-
angles = np.arctan2(
|
|
94
|
-
points[:, 1] - points[pivot_idx, 1], points[:, 0] - points[pivot_idx, 0]
|
|
95
|
-
)
|
|
76
|
+
angles = np.arctan2(points[:, 1] - points[pivot_idx, 1], points[:, 0] - points[pivot_idx, 0])
|
|
96
77
|
angles[pivot_idx] = np.inf
|
|
97
78
|
|
|
98
79
|
# Sort points by angle
|
|
@@ -169,22 +150,36 @@ def _convex_hull_Graham_scan(points):
|
|
|
169
150
|
|
|
170
151
|
|
|
171
152
|
def _compute_group_path_with_vertex_padding(
|
|
172
|
-
hull,
|
|
173
|
-
points,
|
|
174
|
-
transform,
|
|
175
|
-
vertexpadding=10,
|
|
176
|
-
points_per_curve=30,
|
|
153
|
+
hull: np.ndarray | Sequence[int],
|
|
154
|
+
points: np.ndarray,
|
|
155
|
+
transform: mpl.transforms.Transform,
|
|
156
|
+
vertexpadding: int = 10,
|
|
177
157
|
# TODO: check how dpi affects this
|
|
178
|
-
dpi=72.0,
|
|
179
|
-
):
|
|
158
|
+
dpi: float = 72.0,
|
|
159
|
+
) -> np.ndarray:
|
|
180
160
|
"""Offset path for a group based on vertex padding.
|
|
181
161
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
162
|
+
Parameters:
|
|
163
|
+
hull: The coordinates (not indices!) of the convex hull.
|
|
164
|
+
points: This is the np.ndarray where the coordinates will be written to (output).
|
|
165
|
+
The length is some integer ppc * len(hull) + 1 because for each vertex, this
|
|
166
|
+
function wraps around it using a certain fixed ppc number of points, plus the
|
|
167
|
+
final point for CLOSEPOLY.
|
|
168
|
+
transform: The transform of the hull points.
|
|
169
|
+
vertexpadding: The padding to apply to the vertices, in figure coordinates.
|
|
170
|
+
dpi (WIP): The dpi of the figure renderer.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
None. The output is written to the `points` array in place. This ensures that the
|
|
174
|
+
length of this array is unchanged, which is important to ensure that the vertices
|
|
175
|
+
and SVG codes are in sync.
|
|
185
176
|
"""
|
|
186
|
-
|
|
187
|
-
|
|
177
|
+
if len(hull) == 0:
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
# Short form for point per curve
|
|
181
|
+
ppc = (len(points) - 1) // len(hull)
|
|
182
|
+
assert len(points) % ppc == 1
|
|
188
183
|
|
|
189
184
|
# No padding, set degenerate path
|
|
190
185
|
if vertexpadding == 0:
|
|
@@ -196,11 +191,9 @@ def _compute_group_path_with_vertex_padding(
|
|
|
196
191
|
# Transform into figure coordinates
|
|
197
192
|
trans = transform.transform
|
|
198
193
|
trans_inv = transform.inverted().transform
|
|
199
|
-
points = trans(points)
|
|
200
194
|
|
|
201
195
|
# Singleton: draw a circle around it
|
|
202
196
|
if len(hull) == 1:
|
|
203
|
-
|
|
204
197
|
# NOTE: linspace is double inclusive, which covers CLOSEPOLY
|
|
205
198
|
thetas = np.linspace(
|
|
206
199
|
-np.pi,
|
|
@@ -213,7 +206,6 @@ def _compute_group_path_with_vertex_padding(
|
|
|
213
206
|
|
|
214
207
|
# Doublet: draw two semicircles
|
|
215
208
|
if len(hull) == 2:
|
|
216
|
-
|
|
217
209
|
# Unit vector connecting the two points
|
|
218
210
|
dv = trans(hull[0]) - trans(hull[1])
|
|
219
211
|
dv = dv / np.sqrt((dv**2).sum())
|
iplotx/utils/matplotlib.py
CHANGED
|
@@ -127,32 +127,35 @@ def _get_label_width_height(text, hpadding=18, vpadding=12, dpi=72.0, **kwargs):
|
|
|
127
127
|
|
|
128
128
|
def _compute_mid_coord_and_rot(path, trans):
|
|
129
129
|
"""Compute mid point of an edge, straight or curved."""
|
|
130
|
-
#
|
|
130
|
+
# Straight path
|
|
131
131
|
if path.codes[-1] == mpl.path.Path.LINETO:
|
|
132
132
|
coord = path.vertices.mean(axis=0)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
vtr[-1, 1] - vtr[0, 1],
|
|
136
|
-
vtr[-1, 0] - vtr[0, 0],
|
|
137
|
-
)
|
|
133
|
+
v1 = path.vertices[0]
|
|
134
|
+
v2 = path.vertices[-1]
|
|
138
135
|
|
|
139
136
|
# Cubic Bezier
|
|
140
137
|
elif path.codes[-1] == mpl.path.Path.CURVE4:
|
|
141
138
|
coord = _evaluate_cubic_bezier(path.vertices, 0.5)
|
|
142
|
-
|
|
143
|
-
|
|
139
|
+
v1 = _evaluate_cubic_bezier(path.vertices, 0.475)
|
|
140
|
+
v2 = _evaluate_cubic_bezier(path.vertices, 0.525)
|
|
144
141
|
|
|
145
142
|
# Square Bezier
|
|
146
143
|
elif path.codes[-1] == mpl.path.Path.CURVE3:
|
|
147
144
|
coord = _evaluate_squared_bezier(path.vertices, 0.5)
|
|
148
|
-
|
|
149
|
-
|
|
145
|
+
v1 = _evaluate_squared_bezier(path.vertices, 0.475)
|
|
146
|
+
v2 = _evaluate_squared_bezier(path.vertices, 0.525)
|
|
150
147
|
|
|
151
148
|
else:
|
|
152
149
|
raise ValueError(
|
|
153
150
|
"Curve type not straight and not squared/cubic Bezier, cannot compute mid point."
|
|
154
151
|
)
|
|
155
152
|
|
|
153
|
+
v1 = trans(v1)
|
|
154
|
+
v2 = trans(v2)
|
|
155
|
+
rot = atan2(
|
|
156
|
+
v2[1] - v1[1],
|
|
157
|
+
v2[0] - v1[0],
|
|
158
|
+
)
|
|
156
159
|
return coord, rot
|
|
157
160
|
|
|
158
161
|
|
iplotx/utils/style.py
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import copy
|
|
2
|
+
from collections import defaultdict
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
def copy_with_deep_values(style):
|
|
5
6
|
"""Make a deep copy of the style dict but do not create copies of the keys."""
|
|
6
|
-
|
|
7
|
+
# Defaultdict should be respected
|
|
8
|
+
if hasattr(style, "default_factory"):
|
|
9
|
+
newdict = defaultdict(lambda: style.default_factory())
|
|
10
|
+
else:
|
|
11
|
+
newdict = {}
|
|
7
12
|
for key, value in style.items():
|
|
8
13
|
if isinstance(value, dict):
|
|
9
14
|
newdict[key] = copy_with_deep_values(value)
|
iplotx/version.py
CHANGED
iplotx/vertex.py
CHANGED
|
@@ -55,7 +55,7 @@ class VertexCollection(PatchCollection):
|
|
|
55
55
|
*args,
|
|
56
56
|
layout_coordinate_system: str = "cartesian",
|
|
57
57
|
style: Optional[dict[str, Any]] = None,
|
|
58
|
-
labels: Optional[Sequence[str]] = None,
|
|
58
|
+
labels: Optional[Sequence[str] | pd.Series] = None,
|
|
59
59
|
**kwargs,
|
|
60
60
|
):
|
|
61
61
|
"""Initialise the VertexCollection.
|
|
@@ -69,10 +69,13 @@ class VertexCollection(PatchCollection):
|
|
|
69
69
|
|
|
70
70
|
self._index = layout.index
|
|
71
71
|
self._style = style
|
|
72
|
-
self._labels = labels
|
|
73
72
|
self._layout = layout
|
|
74
73
|
self._layout_coordinate_system = layout_coordinate_system
|
|
75
74
|
|
|
75
|
+
if (labels is not None) and (not isinstance(labels, pd.Series)):
|
|
76
|
+
labels = pd.Series(labels, index=self._layout.index)
|
|
77
|
+
self._labels = labels
|
|
78
|
+
|
|
76
79
|
# Create patches from structured data
|
|
77
80
|
patches, sizes, kwargs2 = self._init_vertex_patches()
|
|
78
81
|
|
|
@@ -91,6 +94,10 @@ class VertexCollection(PatchCollection):
|
|
|
91
94
|
if self._labels is not None:
|
|
92
95
|
self._compute_label_collection()
|
|
93
96
|
|
|
97
|
+
def __len__(self):
|
|
98
|
+
"""Return the number of vertices in the collection."""
|
|
99
|
+
return len(self.get_paths())
|
|
100
|
+
|
|
94
101
|
def get_children(self) -> tuple[mpl.artist.Artist]:
|
|
95
102
|
"""Get the children artists.
|
|
96
103
|
|
|
@@ -227,9 +234,7 @@ class VertexCollection(PatchCollection):
|
|
|
227
234
|
|
|
228
235
|
if style.get("size", 20) == "label":
|
|
229
236
|
if self._labels is None:
|
|
230
|
-
warnings.warn(
|
|
231
|
-
"No labels found, cannot resize vertices based on labels."
|
|
232
|
-
)
|
|
237
|
+
warnings.warn("No labels found, cannot resize vertices based on labels.")
|
|
233
238
|
style["size"] = get_style("default.vertex")["size"]
|
|
234
239
|
|
|
235
240
|
if "cmap" in style:
|
|
@@ -265,9 +270,7 @@ class VertexCollection(PatchCollection):
|
|
|
265
270
|
transform = self.get_offset_transform()
|
|
266
271
|
|
|
267
272
|
style = (
|
|
268
|
-
copy_with_deep_values(self._style.get("label", None))
|
|
269
|
-
if self._style is not None
|
|
270
|
-
else {}
|
|
273
|
+
copy_with_deep_values(self._style.get("label", None)) if self._style is not None else {}
|
|
271
274
|
)
|
|
272
275
|
forbidden_props = ["hpadding", "vpadding"]
|
|
273
276
|
for prop in forbidden_props:
|
|
@@ -380,9 +383,7 @@ def make_patch(
|
|
|
380
383
|
elif marker in ("s", "square", "r", "rectangle"):
|
|
381
384
|
art = Rectangle((-size[0] / 2, -size[1] / 2), size[0], size[1], **kwargs)
|
|
382
385
|
elif marker in ("^", "triangle"):
|
|
383
|
-
art = RegularPolygon(
|
|
384
|
-
(0, 0), numVertices=3, radius=size[0] / np.sqrt(2), **kwargs
|
|
385
|
-
)
|
|
386
|
+
art = RegularPolygon((0, 0), numVertices=3, radius=size[0] / np.sqrt(2), **kwargs)
|
|
386
387
|
elif marker in ("v", "triangle_down"):
|
|
387
388
|
art = RegularPolygon(
|
|
388
389
|
(0, 0),
|
|
@@ -408,21 +409,13 @@ def make_patch(
|
|
|
408
409
|
**kwargs,
|
|
409
410
|
)
|
|
410
411
|
elif marker in ("d", "diamond"):
|
|
411
|
-
art = RegularPolygon(
|
|
412
|
-
(0, 0), numVertices=4, radius=size[0] / np.sqrt(2), **kwargs
|
|
413
|
-
)
|
|
412
|
+
art = RegularPolygon((0, 0), numVertices=4, radius=size[0] / np.sqrt(2), **kwargs)
|
|
414
413
|
elif marker in ("p", "pentagon"):
|
|
415
|
-
art = RegularPolygon(
|
|
416
|
-
(0, 0), numVertices=5, radius=size[0] / np.sqrt(2), **kwargs
|
|
417
|
-
)
|
|
414
|
+
art = RegularPolygon((0, 0), numVertices=5, radius=size[0] / np.sqrt(2), **kwargs)
|
|
418
415
|
elif marker in ("h", "hexagon"):
|
|
419
|
-
art = RegularPolygon(
|
|
420
|
-
(0, 0), numVertices=6, radius=size[0] / np.sqrt(2), **kwargs
|
|
421
|
-
)
|
|
416
|
+
art = RegularPolygon((0, 0), numVertices=6, radius=size[0] / np.sqrt(2), **kwargs)
|
|
422
417
|
elif marker in ("8", "octagon"):
|
|
423
|
-
art = RegularPolygon(
|
|
424
|
-
(0, 0), numVertices=8, radius=size[0] / np.sqrt(2), **kwargs
|
|
425
|
-
)
|
|
418
|
+
art = RegularPolygon((0, 0), numVertices=8, radius=size[0] / np.sqrt(2), **kwargs)
|
|
426
419
|
elif marker in ("e", "ellipse"):
|
|
427
420
|
art = Ellipse((0, 0), size[0], size[1], **kwargs)
|
|
428
421
|
elif marker in ("*", "star"):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iplotx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
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
|
|
@@ -39,12 +39,29 @@ Description-Content-Type: text/markdown
|
|
|
39
39
|

|
|
40
40
|

|
|
41
41
|

|
|
42
|
+
[](https://coveralls.io/github/fabilab/iplotx?branch=main)
|
|
42
43
|

|
|
43
44
|
|
|
44
45
|
# iplotx
|
|
45
|
-
|
|
46
|
+
[](https://iplotx.readthedocs.io/en/latest/gallery/index.html).
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
Visualise networks and trees in Python, with style.
|
|
49
|
+
|
|
50
|
+
Supports:
|
|
51
|
+
- **networks**:
|
|
52
|
+
- [networkx](https://networkx.org/)
|
|
53
|
+
- [igraph](igraph.readthedocs.io/)
|
|
54
|
+
- [minimal network data structure](https://iplotx.readthedocs.io/en/latest/gallery/plot_simplenetworkdataprovider.html#sphx-glr-gallery-plot-simplenetworkdataprovider-py) (for educational purposes)
|
|
55
|
+
- **trees**:
|
|
56
|
+
- [ETE4](https://etetoolkit.github.io/ete/)
|
|
57
|
+
- [cogent3](https://cogent3.org/)
|
|
58
|
+
- [Biopython](https://biopython.org/)
|
|
59
|
+
- [scikit-bio](https://scikit.bio)
|
|
60
|
+
- [minimal tree data structure](https://iplotx.readthedocs.io/en/latest/gallery/tree/plot_simpletreedataprovider.html#sphx-glr-gallery-tree-plot-simpletreedataprovider-py) (for educational purposes)
|
|
61
|
+
|
|
62
|
+
In addition to the above, *any* network or tree analysis library can register an [entry point](https://iplotx.readthedocs.io/en/latest/providers.html#creating-a-custom-data-provider) to gain compatibility with `iplotx` with no intervention from our side.
|
|
63
|
+
|
|
64
|
+
**NOTE**: This is currently late beta quality software. The API and functionality might break rarely.
|
|
48
65
|
|
|
49
66
|
## Installation
|
|
50
67
|
```bash
|
|
@@ -71,19 +88,20 @@ See [readthedocs](https://iplotx.readthedocs.io/en/latest/) for the full documen
|
|
|
71
88
|
## Gallery
|
|
72
89
|
See [gallery](https://iplotx.readthedocs.io/en/latest/gallery/index.html).
|
|
73
90
|
|
|
74
|
-
##
|
|
75
|
-
- Plot networks from
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
91
|
+
## Features
|
|
92
|
+
- Plot networks from multiple libraries including networkx and igraph, using matplotlib as a backend. ✅
|
|
93
|
+
- Plot trees from multiple libraries such as cogent3, ETE4, skbio, and biopython. ✅
|
|
94
|
+
- Flexible yet easy styling, including an internal library of styles ✅
|
|
95
|
+
- Interactive plotting, e.g. zooming and panning after the plot is created. ✅
|
|
96
|
+
- Store the plot to disk thanks to the many matplotlib backends (SVG, PNG, PDF, etc.). ✅
|
|
79
97
|
- Efficient plotting of large graphs using matplotlib's collection functionality. ✅
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
- Edit plotting elements after the plot is created, e.g. changing node colors, labels, etc. ✅
|
|
99
|
+
- Animations, e.g. showing the evolution of a network over time. ✅
|
|
100
|
+
- Mouse and keyboard interaction, e.g. hovering over nodes/edges to get information about them. ✅
|
|
101
|
+
- Node clustering and covers, e.g. showing communities in a network. ✅
|
|
102
|
+
- Choice of tree layouts and orientations. ✅
|
|
103
|
+
- Tree-specific options: cascades, subtree styling, split edges, etc. ✅
|
|
104
|
+
- (WIP) Support uni- and bi-directional communication between graph object and plot object.🏗️
|
|
87
105
|
|
|
88
106
|
## Authors
|
|
89
107
|
Fabio Zanini (https://fabilab.org)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
iplotx/__init__.py,sha256=MKb9UCXKgDHHkeATuJWxYdM-AotfBo2fbWy-Rkbn9Is,509
|
|
2
|
+
iplotx/artists.py,sha256=Bpn6NS8S_B_E4OW88JYW6aEu2bIuIQJmbs2paTmBAoY,522
|
|
3
|
+
iplotx/cascades.py,sha256=OPqF7Huls-HFmDA5MCF6DEZlUeRVaXsbQcHBoKAgNJs,8182
|
|
4
|
+
iplotx/groups.py,sha256=_9KdIiTAi1kXtd2mDywgBJCbqoRq2z-5fzOPf76Wgb8,6287
|
|
5
|
+
iplotx/label.py,sha256=i107wE-9kC_MVWsgWeYG6sRy_ZmyvITNm2laIij9SR0,8761
|
|
6
|
+
iplotx/layout.py,sha256=KxmRLqjo8AYCBAmXez8rIiLU2sM34qhb6ox9AHYwRyE,4839
|
|
7
|
+
iplotx/network.py,sha256=SlmDgc4tbCfvO08QWk-jUXrUfaz6S3xoXQVg6rP1910,11345
|
|
8
|
+
iplotx/plotting.py,sha256=RZj-E_2R8AbXoJmxr_qAC-g_nOudqep-TDSIV4QB9BM,7408
|
|
9
|
+
iplotx/tree.py,sha256=iILQRKUZzcDKIiwI1LheSuixi5y_3PAQrz61vdwi6DU,27448
|
|
10
|
+
iplotx/typing.py,sha256=QLdzV358IiD1CFe88MVp0D77FSx5sSAVUmM_2WPPE8I,1463
|
|
11
|
+
iplotx/version.py,sha256=dsB8xhRODrwa4OXOSbePgoZzy41j_DudRu80QRgFkpw,66
|
|
12
|
+
iplotx/vertex.py,sha256=OjDIkJCNU-IhZUVeZTSzGwTlHLrxu27lUThiUuEb6Qs,14497
|
|
13
|
+
iplotx/edge/__init__.py,sha256=0w-BDZpVyR4qM908PM5DzlNVXwwfxAeDNyHNXPWPgcc,26237
|
|
14
|
+
iplotx/edge/arrow.py,sha256=y8xMZY1eR5BXBmkX0_aDIn-3CeqaL6jwGGLw-ndUf50,12867
|
|
15
|
+
iplotx/edge/geometry.py,sha256=g9_z7nwlQhQm9Tvj2tme9dGboxkN-4jeUUg02gU-vOk,13285
|
|
16
|
+
iplotx/edge/leaf.py,sha256=SyGMv2PIOoH0pey8-aMVaZheK3hNe1Qz_okcyWbc4E4,4268
|
|
17
|
+
iplotx/edge/ports.py,sha256=BpkbiEhX4mPBBAhOv4jcKFG4Y8hxXz5GRtVLCC0jbtI,1235
|
|
18
|
+
iplotx/ingest/__init__.py,sha256=tsXDoa7Rs6Y1ulWtjCcUsO4tQIigeQ6ZMiU2PQDyhwQ,4751
|
|
19
|
+
iplotx/ingest/heuristics.py,sha256=715VqgfKek5LOJnu1vTo7RqPgCl-Bb8Cf6o7_Tt57fA,5797
|
|
20
|
+
iplotx/ingest/typing.py,sha256=pi-mn4ULkFjTo_fFdJPUjTHrWzbny4MNgoMylN4mNKM,13940
|
|
21
|
+
iplotx/ingest/providers/network/igraph.py,sha256=8dWeaQ_ZNdltC098V2YeLXsGdJHQnBa6shF1GAfl0Zg,2973
|
|
22
|
+
iplotx/ingest/providers/network/networkx.py,sha256=FIXMI3hXU1WtAzPVlQZcz47b-4V2omeHttnNTgS2gQw,4328
|
|
23
|
+
iplotx/ingest/providers/network/simple.py,sha256=yKILiE3-ZhBUGSs7eYuhV8tQDyueCosbbgovZZYpSPQ,3664
|
|
24
|
+
iplotx/ingest/providers/tree/biopython.py,sha256=4N_54cVyHHPcASJZGr6pHKE2p5R3i8Cm307SLlSLHLA,1480
|
|
25
|
+
iplotx/ingest/providers/tree/cogent3.py,sha256=JmELbDK7LyybiJzFNbmeqZ4ySJoDajvFfJebpNfFKWo,1073
|
|
26
|
+
iplotx/ingest/providers/tree/ete4.py,sha256=D7usSq0MOjzrk3EoLi834IlaDGwv7_qG6Qt0ptfKqfI,928
|
|
27
|
+
iplotx/ingest/providers/tree/simple.py,sha256=vOAlQbkm2HdlBTQab6s7mjAnLibVmeNOfc6y6UpBqzw,2533
|
|
28
|
+
iplotx/ingest/providers/tree/skbio.py,sha256=O1KUr8tYi28pZ3VVjapgO4Uj-YpMuix3GhOH5je8Lv4,822
|
|
29
|
+
iplotx/style/__init__.py,sha256=4K6EtAKOFth3zS_jdaDCvOEMeZxIgnMM_rtpH_G74io,12253
|
|
30
|
+
iplotx/style/leaf_info.py,sha256=2XckYhvE3FvNYUaQj_CY2HwtYfZA8FUQ7uBXe_ukaWU,938
|
|
31
|
+
iplotx/style/library.py,sha256=yryxQUSHMIwGgeS0Iq1BediVRRaFguJcjhXMj_vsHo8,8007
|
|
32
|
+
iplotx/utils/geometry.py,sha256=UH2gAcM5rYW7ADnJEm7HIJTpPF4UOm8P3vjSVCOGjqM,9192
|
|
33
|
+
iplotx/utils/internal.py,sha256=WWfcZDGK8Ut1y_tOHRGg9wSqY1bwSeLQO7dHM_8Tvwo,107
|
|
34
|
+
iplotx/utils/matplotlib.py,sha256=KpkuwXuSqpYbPVKbrlP8u_1Ry5gy5q60ZBrFiR-do_Q,5284
|
|
35
|
+
iplotx/utils/style.py,sha256=wMWxJykxBD-JmcN8-rSKlWcV6pMfwKgR4EzSpk_NX8k,547
|
|
36
|
+
iplotx-0.5.1.dist-info/METADATA,sha256=P1TOV3nMA2ihSTdEFQLNdRztBrKUmpVcIpxNFYPcOQE,4889
|
|
37
|
+
iplotx-0.5.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
38
|
+
iplotx-0.5.1.dist-info/RECORD,,
|