iplotx 0.9.0__py3-none-any.whl → 0.11.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/art3d/edge/__init__.py +147 -0
- iplotx/art3d/edge/arrow.py +115 -0
- iplotx/art3d/edge/geometry.py +82 -0
- iplotx/art3d/vertex.py +87 -0
- iplotx/artists.py +4 -0
- iplotx/edge/__init__.py +30 -46
- iplotx/edge/arrow.py +27 -4
- iplotx/edge/geometry.py +10 -0
- iplotx/groups.py +19 -1
- iplotx/ingest/providers/network/igraph.py +9 -10
- iplotx/ingest/providers/network/networkx.py +12 -14
- iplotx/label.py +21 -0
- iplotx/network.py +57 -5
- iplotx/plotting.py +88 -41
- iplotx/version.py +1 -1
- iplotx/vertex.py +33 -3
- {iplotx-0.9.0.dist-info → iplotx-0.11.0.dist-info}/METADATA +1 -2
- {iplotx-0.9.0.dist-info → iplotx-0.11.0.dist-info}/RECORD +19 -15
- {iplotx-0.9.0.dist-info → iplotx-0.11.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module containing code to manipulate edge visualisations in 3D, especially the Edge3DCollection class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from mpl_toolkits.mplot3d import Axes3D
|
|
6
|
+
from mpl_toolkits.mplot3d.art3d import (
|
|
7
|
+
Line3DCollection,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from ...utils.matplotlib import (
|
|
11
|
+
_forwarder,
|
|
12
|
+
)
|
|
13
|
+
from ...edge import (
|
|
14
|
+
EdgeCollection,
|
|
15
|
+
)
|
|
16
|
+
from .arrow import (
|
|
17
|
+
arrow_collection_2d_to_3d,
|
|
18
|
+
)
|
|
19
|
+
from .geometry import (
|
|
20
|
+
_compute_edge_segments as _compute_single_edge_segments,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@_forwarder(
|
|
25
|
+
(
|
|
26
|
+
"set_clip_path",
|
|
27
|
+
"set_clip_box",
|
|
28
|
+
"set_snap",
|
|
29
|
+
"set_sketch_params",
|
|
30
|
+
"set_animated",
|
|
31
|
+
"set_picker",
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
class Edge3DCollection(Line3DCollection):
|
|
35
|
+
"""Collection of vertex patches for plotting."""
|
|
36
|
+
|
|
37
|
+
def get_children(self) -> tuple:
|
|
38
|
+
children = []
|
|
39
|
+
if hasattr(self, "_subedges"):
|
|
40
|
+
children.append(self._subedges)
|
|
41
|
+
if hasattr(self, "_arrows"):
|
|
42
|
+
children.append(self._arrows)
|
|
43
|
+
if hasattr(self, "_label_collection"):
|
|
44
|
+
children.append(self._label_collection)
|
|
45
|
+
return tuple(children)
|
|
46
|
+
|
|
47
|
+
def set_figure(self, fig) -> None:
|
|
48
|
+
super().set_figure(fig)
|
|
49
|
+
for child in self.get_children():
|
|
50
|
+
child.set_figure(fig)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def axes(self):
|
|
54
|
+
return Line3DCollection.axes.__get__(self)
|
|
55
|
+
|
|
56
|
+
@axes.setter
|
|
57
|
+
def axes(self, new_axes):
|
|
58
|
+
Line3DCollection.axes.__set__(self, new_axes)
|
|
59
|
+
for child in self.get_children():
|
|
60
|
+
child.axes = new_axes
|
|
61
|
+
|
|
62
|
+
_get_adjacent_vertices_info = EdgeCollection._get_adjacent_vertices_info
|
|
63
|
+
|
|
64
|
+
def _compute_edge_segments(self):
|
|
65
|
+
"""Compute the edge segments for all edges."""
|
|
66
|
+
vinfo = self._get_adjacent_vertices_info()
|
|
67
|
+
|
|
68
|
+
segments3d = []
|
|
69
|
+
for vcoord_data in vinfo["offsets"]:
|
|
70
|
+
segment = _compute_single_edge_segments(
|
|
71
|
+
vcoord_data,
|
|
72
|
+
)
|
|
73
|
+
segments3d.append(segment)
|
|
74
|
+
self.set_segments(segments3d)
|
|
75
|
+
|
|
76
|
+
def _update_before_draw(self) -> None:
|
|
77
|
+
"""Update the collection before drawing."""
|
|
78
|
+
if isinstance(self.axes, Axes3D) and hasattr(self, "do_3d_projection"):
|
|
79
|
+
self.do_3d_projection()
|
|
80
|
+
|
|
81
|
+
# TODO: Here's where we would shorten the edges to fit the vertex
|
|
82
|
+
# projections from 3D onto 2D, if we wanted to do that. Because edges
|
|
83
|
+
# in 3D are chains of segments rathen than splines, the shortening
|
|
84
|
+
# needs to be done in a different way to how it's done in 2D.
|
|
85
|
+
|
|
86
|
+
def draw(self, renderer) -> None:
|
|
87
|
+
"""Draw the collection of vertices in 3D.
|
|
88
|
+
|
|
89
|
+
Parameters:
|
|
90
|
+
renderer: The renderer to use for drawing.
|
|
91
|
+
"""
|
|
92
|
+
# Prepare the collection for drawing
|
|
93
|
+
self._update_before_draw()
|
|
94
|
+
|
|
95
|
+
# Render the Line3DCollection
|
|
96
|
+
# NOTE: we are NOT calling EdgeCollection.draw here
|
|
97
|
+
super().draw(renderer)
|
|
98
|
+
|
|
99
|
+
# This sets the labels offsets
|
|
100
|
+
# TODO: implement labels in 3D (one could copy the function from 2D,
|
|
101
|
+
# but would also need to promote the 2D labels into 3D labels similarly to
|
|
102
|
+
# how it's done for 3D vertices).
|
|
103
|
+
# self._update_labels()
|
|
104
|
+
|
|
105
|
+
# Now attempt to draw the arrows
|
|
106
|
+
for child in self.get_children():
|
|
107
|
+
child.draw(renderer)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def edge_collection_2d_to_3d(
|
|
111
|
+
col: EdgeCollection,
|
|
112
|
+
zdir: str = "z",
|
|
113
|
+
axlim_clip: bool = False,
|
|
114
|
+
):
|
|
115
|
+
"""Convert a 2D EdgeCollection to a 3D Edge3DCollection.
|
|
116
|
+
|
|
117
|
+
Parameters:
|
|
118
|
+
col: The 2D EdgeCollection to convert.
|
|
119
|
+
zs: The z coordinate(s) to use for the 3D vertices.
|
|
120
|
+
zdir: The axis to use as the z axis (default is "z").
|
|
121
|
+
depthshade: Whether to apply depth shading (default is True).
|
|
122
|
+
axlim_clip: Whether to clip the vertices to the axes limits (default is False).
|
|
123
|
+
"""
|
|
124
|
+
if not isinstance(col, EdgeCollection):
|
|
125
|
+
raise TypeError("vertices must be a VertexCollection")
|
|
126
|
+
|
|
127
|
+
# NOTE: after this line, none of the EdgeCollection methods will work
|
|
128
|
+
# It's become a static drawer now. It uses segments instead of paths.
|
|
129
|
+
col.__class__ = Edge3DCollection
|
|
130
|
+
col._compute_edge_segments()
|
|
131
|
+
|
|
132
|
+
col._axlim_clip = axlim_clip
|
|
133
|
+
|
|
134
|
+
# Convert the arrow collection if present
|
|
135
|
+
if hasattr(col, "_arrows"):
|
|
136
|
+
segments3d = col._segments3d
|
|
137
|
+
|
|
138
|
+
# Fix the x and y to the center of the target vertex (for now)
|
|
139
|
+
col._arrows._offsets[:] = [segment[-1][:2] for segment in segments3d]
|
|
140
|
+
zs = [segment[-1][2] for segment in segments3d]
|
|
141
|
+
arrow_collection_2d_to_3d(
|
|
142
|
+
col._arrows,
|
|
143
|
+
zs=zs,
|
|
144
|
+
zdir=zdir,
|
|
145
|
+
depthshade=False,
|
|
146
|
+
axlim_clip=axlim_clip,
|
|
147
|
+
)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module containing code to manipulate arrow visualisations in 3D, especially the EdgeArrow3DCollection class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import (
|
|
6
|
+
Sequence,
|
|
7
|
+
)
|
|
8
|
+
from math import atan2, cos, sin
|
|
9
|
+
import numpy as np
|
|
10
|
+
from matplotlib import (
|
|
11
|
+
cbook,
|
|
12
|
+
)
|
|
13
|
+
from mpl_toolkits.mplot3d import Axes3D
|
|
14
|
+
from mpl_toolkits.mplot3d.art3d import (
|
|
15
|
+
Path3DCollection,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from ...utils.matplotlib import (
|
|
19
|
+
_forwarder,
|
|
20
|
+
)
|
|
21
|
+
from ...edge.arrow import (
|
|
22
|
+
EdgeArrowCollection,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@_forwarder(
|
|
27
|
+
(
|
|
28
|
+
"set_clip_path",
|
|
29
|
+
"set_clip_box",
|
|
30
|
+
"set_snap",
|
|
31
|
+
"set_sketch_params",
|
|
32
|
+
"set_animated",
|
|
33
|
+
"set_picker",
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
class EdgeArrow3DCollection(EdgeArrowCollection, Path3DCollection):
|
|
37
|
+
"""Collection of vertex patches for plotting."""
|
|
38
|
+
|
|
39
|
+
def _update_before_draw(self) -> None:
|
|
40
|
+
"""Update the collection before drawing."""
|
|
41
|
+
if (
|
|
42
|
+
isinstance(self.axes, Axes3D)
|
|
43
|
+
and hasattr(self, "do_3d_projection")
|
|
44
|
+
and (self.axes.M is not None)
|
|
45
|
+
):
|
|
46
|
+
self.do_3d_projection()
|
|
47
|
+
|
|
48
|
+
# The original EdgeArrowCollection method for
|
|
49
|
+
# _update_before_draw cannot be used because it
|
|
50
|
+
# relies on paths, whereas edges are now a
|
|
51
|
+
# Line3DCollection which uses segments.
|
|
52
|
+
self.set_sizes(self._sizes, self.get_figure(root=True).dpi)
|
|
53
|
+
|
|
54
|
+
if (not hasattr(self, "_z_markers_idx")) or (
|
|
55
|
+
not isinstance(self._z_markers_idx, np.ndarray)
|
|
56
|
+
):
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
trans = self.get_offset_transform().transform
|
|
60
|
+
|
|
61
|
+
# The do_3d_projection method above reorders the
|
|
62
|
+
# arrow offsets in some way, so we might have to figure out
|
|
63
|
+
# what edge index corres
|
|
64
|
+
for i, ie in enumerate(self._z_markers_idx):
|
|
65
|
+
segments_2d = self._edge_collection.get_segments()[ie]
|
|
66
|
+
|
|
67
|
+
# We could reset the 3d projection here, might be a way to
|
|
68
|
+
# skip the function call above.
|
|
69
|
+
v2 = trans(segments_2d[-1])
|
|
70
|
+
v1 = trans(segments_2d[-2])
|
|
71
|
+
dv = v2 - v1
|
|
72
|
+
theta = atan2(*(dv[::-1]))
|
|
73
|
+
theta_old = self._angles[i]
|
|
74
|
+
dtheta = theta - theta_old
|
|
75
|
+
mrot = np.array([[cos(dtheta), sin(dtheta)], [-sin(dtheta), cos(dtheta)]])
|
|
76
|
+
|
|
77
|
+
apath = self._paths[i]
|
|
78
|
+
apath.vertices = apath.vertices @ mrot
|
|
79
|
+
self._angles[i] = theta
|
|
80
|
+
|
|
81
|
+
def draw(self, renderer) -> None:
|
|
82
|
+
"""Draw the collection of vertices in 3D.
|
|
83
|
+
|
|
84
|
+
Parameters:
|
|
85
|
+
renderer: The renderer to use for drawing.
|
|
86
|
+
"""
|
|
87
|
+
with self._use_zordered_offset():
|
|
88
|
+
with cbook._setattr_cm(self, _in_draw=True):
|
|
89
|
+
EdgeArrowCollection.draw(self, renderer)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def arrow_collection_2d_to_3d(
|
|
93
|
+
col: EdgeArrowCollection,
|
|
94
|
+
zs: np.ndarray | float | Sequence[float] = 0,
|
|
95
|
+
zdir: str = "z",
|
|
96
|
+
depthshade: bool = True,
|
|
97
|
+
axlim_clip: bool = False,
|
|
98
|
+
):
|
|
99
|
+
"""Convert a 2D EdgeArrowCollection to a 3D EdgeArrow3DCollection.
|
|
100
|
+
|
|
101
|
+
Parameters:
|
|
102
|
+
col: The 2D EdgeArrowCollection to convert.
|
|
103
|
+
zs: The z coordinate(s) to use for the 3D vertices.
|
|
104
|
+
zdir: The axis to use as the z axis (default is "z").
|
|
105
|
+
depthshade: Whether to apply depth shading (default is True).
|
|
106
|
+
axlim_clip: Whether to clip the vertices to the axes limits (default is False).
|
|
107
|
+
"""
|
|
108
|
+
if not isinstance(col, EdgeArrowCollection):
|
|
109
|
+
raise TypeError("vertices must be a EdgeArrowCollection")
|
|
110
|
+
|
|
111
|
+
col.__class__ = EdgeArrow3DCollection
|
|
112
|
+
col._offset_zordered = None
|
|
113
|
+
col._depthshade = depthshade
|
|
114
|
+
col._in_draw = False
|
|
115
|
+
col.set_3d_properties(zs, zdir, axlim_clip)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Support for computing edge paths in 3D.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import (
|
|
6
|
+
Optional,
|
|
7
|
+
Sequence,
|
|
8
|
+
)
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from ...typing import (
|
|
12
|
+
Pair,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _compute_edge_segments_straight(
|
|
17
|
+
vcoord_data,
|
|
18
|
+
layout_coordinate_system: str = "cartesian",
|
|
19
|
+
shrink: float = 0,
|
|
20
|
+
**kwargs,
|
|
21
|
+
):
|
|
22
|
+
"""Compute straight edge path between two vertices, in 3D.
|
|
23
|
+
|
|
24
|
+
Parameters:
|
|
25
|
+
vcoord_data: Vertex coordinates in data coordinates, shape (2, 3).
|
|
26
|
+
vpath_fig: Vertex path in figure coordinates.
|
|
27
|
+
vsize_fig: Vertex size in figure coordinates.
|
|
28
|
+
trans: Transformation from data to figure coordinates.
|
|
29
|
+
trans_inv: Inverse transformation from figure to data coordinates.
|
|
30
|
+
layout_coordinate_system: The coordinate system of the layout.
|
|
31
|
+
shrink: Amount to shorten the edge at each end, in figure coordinates.
|
|
32
|
+
**kwargs: Additional keyword arguments (not used).
|
|
33
|
+
Returns:
|
|
34
|
+
A pair with the path and a tuple of angles of exit and entry, in radians.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
if layout_coordinate_system not in ("cartesian"):
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"Layout coordinate system not supported for straight edges in 3D: {layout_coordinate_system}.",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
segments = [vcoord_data[0], vcoord_data[1]]
|
|
44
|
+
return segments
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _compute_edge_segments(
|
|
48
|
+
*args,
|
|
49
|
+
tension: float = 0,
|
|
50
|
+
waypoints: str | tuple[float, float] | Sequence[tuple[float, float]] | np.ndarray = "none",
|
|
51
|
+
ports: Pair[Optional[str]] = (None, None),
|
|
52
|
+
layout_coordinate_system: str = "cartesian",
|
|
53
|
+
**kwargs,
|
|
54
|
+
):
|
|
55
|
+
"""Compute the edge path in a few different ways."""
|
|
56
|
+
if (waypoints != "none") and (tension != 0):
|
|
57
|
+
raise ValueError("Waypoints not supported for curved edges.")
|
|
58
|
+
|
|
59
|
+
if waypoints != "none":
|
|
60
|
+
raise NotImplementedError("Waypoints not implemented for 3D edges.")
|
|
61
|
+
# return _compute_edge_path_waypoints(
|
|
62
|
+
# waypoints,
|
|
63
|
+
# *args,
|
|
64
|
+
# layout_coordinate_system=layout_coordinate_system,
|
|
65
|
+
# ports=ports,
|
|
66
|
+
# **kwargs,
|
|
67
|
+
# )
|
|
68
|
+
|
|
69
|
+
if np.isscalar(tension) and (tension == 0):
|
|
70
|
+
return _compute_edge_segments_straight(
|
|
71
|
+
*args,
|
|
72
|
+
layout_coordinate_system=layout_coordinate_system,
|
|
73
|
+
**kwargs,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
raise NotImplementedError("Curved edges not implemented for 3D edges.")
|
|
77
|
+
# return _compute_edge_path_curved(
|
|
78
|
+
# tension,
|
|
79
|
+
# *args,
|
|
80
|
+
# ports=ports,
|
|
81
|
+
# **kwargs,
|
|
82
|
+
# )
|
iplotx/art3d/vertex.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module containing code to manipulate vertex visualisations in 3D, especially the Vertex3DCollection class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import (
|
|
6
|
+
Sequence,
|
|
7
|
+
)
|
|
8
|
+
import numpy as np
|
|
9
|
+
from matplotlib import (
|
|
10
|
+
cbook,
|
|
11
|
+
)
|
|
12
|
+
from mpl_toolkits.mplot3d import Axes3D
|
|
13
|
+
from mpl_toolkits.mplot3d.art3d import (
|
|
14
|
+
Path3DCollection,
|
|
15
|
+
text_2d_to_3d,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from ..utils.matplotlib import (
|
|
19
|
+
_forwarder,
|
|
20
|
+
)
|
|
21
|
+
from ..vertex import (
|
|
22
|
+
VertexCollection,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@_forwarder(
|
|
27
|
+
(
|
|
28
|
+
"set_clip_path",
|
|
29
|
+
"set_clip_box",
|
|
30
|
+
"set_snap",
|
|
31
|
+
"set_sketch_params",
|
|
32
|
+
"set_animated",
|
|
33
|
+
"set_picker",
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
class Vertex3DCollection(VertexCollection, Path3DCollection):
|
|
37
|
+
"""Collection of vertex patches for plotting."""
|
|
38
|
+
|
|
39
|
+
def _update_before_draw(self) -> None:
|
|
40
|
+
"""Update the collection before drawing."""
|
|
41
|
+
# Set the sizes according to the current figure dpi
|
|
42
|
+
VertexCollection._update_before_draw(self)
|
|
43
|
+
|
|
44
|
+
if isinstance(self.axes, Axes3D) and hasattr(self, "do_3d_projection"):
|
|
45
|
+
self.do_3d_projection()
|
|
46
|
+
|
|
47
|
+
def draw(self, renderer) -> None:
|
|
48
|
+
"""Draw the collection of vertices in 3D.
|
|
49
|
+
|
|
50
|
+
Parameters:
|
|
51
|
+
renderer: The renderer to use for drawing.
|
|
52
|
+
"""
|
|
53
|
+
with self._use_zordered_offset():
|
|
54
|
+
with cbook._setattr_cm(self, _in_draw=True):
|
|
55
|
+
VertexCollection.draw(self, renderer)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def vertex_collection_2d_to_3d(
|
|
59
|
+
col: VertexCollection,
|
|
60
|
+
zs: np.ndarray | float | Sequence[float] = 0,
|
|
61
|
+
zdir: str = "z",
|
|
62
|
+
depthshade: bool = True,
|
|
63
|
+
axlim_clip: bool = False,
|
|
64
|
+
):
|
|
65
|
+
"""Convert a 2D VertexCollection to a 3D Vertex3DCollection.
|
|
66
|
+
|
|
67
|
+
Parameters:
|
|
68
|
+
col: The 2D VertexCollection to convert.
|
|
69
|
+
zs: The z coordinate(s) to use for the 3D vertices.
|
|
70
|
+
zdir: The axis to use as the z axis (default is "z").
|
|
71
|
+
depthshade: Whether to aply depth shading (default is True).
|
|
72
|
+
axlim_clip: Whether to clip the vertices to the axes limits (default is False).
|
|
73
|
+
"""
|
|
74
|
+
if not isinstance(col, VertexCollection):
|
|
75
|
+
raise TypeError("vertices must be a VertexCollection")
|
|
76
|
+
|
|
77
|
+
col.__class__ = Vertex3DCollection
|
|
78
|
+
col._offset_zordered = None
|
|
79
|
+
col._depthshade = depthshade
|
|
80
|
+
col._in_draw = False
|
|
81
|
+
col.set_3d_properties(zs, zdir, axlim_clip)
|
|
82
|
+
|
|
83
|
+
# Labels if present
|
|
84
|
+
if col.get_labels() is not None:
|
|
85
|
+
for z, art in zip(zs, col.get_labels()._labelartists):
|
|
86
|
+
# zdir=None means the text is always horizontal facing the camera
|
|
87
|
+
text_2d_to_3d(art, z, zdir=None, axlim_clip=axlim_clip)
|
iplotx/artists.py
CHANGED
|
@@ -10,6 +10,8 @@ from .label import LabelCollection
|
|
|
10
10
|
from .edge.arrow import EdgeArrowCollection
|
|
11
11
|
from .edge.leaf import LeafEdgeCollection
|
|
12
12
|
from .cascades import CascadeCollection
|
|
13
|
+
from .art3d.vertex import Vertex3DCollection
|
|
14
|
+
from .art3d.edge import Edge3DCollection
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
___all__ = (
|
|
@@ -21,4 +23,6 @@ ___all__ = (
|
|
|
21
23
|
LabelCollection,
|
|
22
24
|
EdgeArrowCollection,
|
|
23
25
|
CascadeCollection,
|
|
26
|
+
Vertex3DCollection,
|
|
27
|
+
Edge3DCollection,
|
|
24
28
|
)
|
iplotx/edge/__init__.py
CHANGED
|
@@ -9,7 +9,7 @@ from typing import (
|
|
|
9
9
|
Optional,
|
|
10
10
|
Any,
|
|
11
11
|
)
|
|
12
|
-
from math import
|
|
12
|
+
from math import pi
|
|
13
13
|
from collections import defaultdict
|
|
14
14
|
import numpy as np
|
|
15
15
|
import pandas as pd
|
|
@@ -181,17 +181,33 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
181
181
|
|
|
182
182
|
def set_figure(self, fig) -> None:
|
|
183
183
|
super().set_figure(fig)
|
|
184
|
-
self.
|
|
184
|
+
self._update_before_draw()
|
|
185
185
|
# NOTE: This sets the correct offsets in the arrows,
|
|
186
186
|
# but not the correct sizes (see below)
|
|
187
|
-
self.
|
|
187
|
+
self._update_labels()
|
|
188
188
|
for child in self.get_children():
|
|
189
189
|
# NOTE: This sets the sizes with correct dpi scaling in the arrows
|
|
190
190
|
child.set_figure(fig)
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
192
|
+
@property
|
|
193
|
+
def axes(self):
|
|
194
|
+
return mpl.artist.Artist.axes.__get__(self)
|
|
195
|
+
|
|
196
|
+
@axes.setter
|
|
197
|
+
def axes(self, new_axes):
|
|
198
|
+
mpl.artist.Artist.axes.__set__(self, new_axes)
|
|
199
|
+
for child in self.get_children():
|
|
200
|
+
child.axes = new_axes
|
|
201
|
+
|
|
202
|
+
def set_transform(self, transform: mpl.transforms.Transform) -> None:
|
|
203
|
+
"""Set the transform for the edges and their children."""
|
|
204
|
+
super().set_transform(transform)
|
|
205
|
+
if hasattr(self, "_subedges"):
|
|
206
|
+
self._subedges.set_transform(transform)
|
|
207
|
+
if hasattr(self, "_arrows"):
|
|
208
|
+
self._arrows.set_offset_transform(transform)
|
|
209
|
+
if hasattr(self, "_label_collection"):
|
|
210
|
+
self._label_collection.set_transform(transform)
|
|
195
211
|
|
|
196
212
|
@property
|
|
197
213
|
def directed(self) -> bool:
|
|
@@ -293,7 +309,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
293
309
|
"sizes": vsizes,
|
|
294
310
|
}
|
|
295
311
|
|
|
296
|
-
def
|
|
312
|
+
def _update_before_draw(self, transform=None):
|
|
297
313
|
"""Compute paths for the edges.
|
|
298
314
|
|
|
299
315
|
Loops split the largest wedge left open by other
|
|
@@ -502,42 +518,6 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
502
518
|
if not style.get("rotate", True):
|
|
503
519
|
self._label_collection.set_rotations(rotations)
|
|
504
520
|
|
|
505
|
-
def _update_arrows(
|
|
506
|
-
self,
|
|
507
|
-
) -> None:
|
|
508
|
-
"""Extract the start and/or end angles of the paths to compute arrows.
|
|
509
|
-
|
|
510
|
-
Parameters:
|
|
511
|
-
which: Which end of the edge to put an arrow on. Currently only "end" is accepted.
|
|
512
|
-
|
|
513
|
-
NOTE: This function does *not* update the arrow sizes/_transforms to the correct dpi
|
|
514
|
-
scaling. That's ok since the correct dpi scaling is set whenever there is a different
|
|
515
|
-
figure (before first draw) and whenever a draw is called.
|
|
516
|
-
"""
|
|
517
|
-
if not hasattr(self, "_arrows"):
|
|
518
|
-
return
|
|
519
|
-
|
|
520
|
-
transform = self.get_transform()
|
|
521
|
-
trans = transform.transform
|
|
522
|
-
|
|
523
|
-
for i, epath in enumerate(self.get_paths()):
|
|
524
|
-
# Offset the arrow to point to the end of the edge
|
|
525
|
-
self._arrows._offsets[i] = epath.vertices[-1]
|
|
526
|
-
|
|
527
|
-
# Rotate the arrow to point in the direction of the edge
|
|
528
|
-
apath = self._arrows._paths[i]
|
|
529
|
-
# NOTE: because the tip of the arrow is at (0, 0) in patch space,
|
|
530
|
-
# in theory it will rotate around that point already
|
|
531
|
-
v2 = trans(epath.vertices[-1])
|
|
532
|
-
v1 = trans(epath.vertices[-2])
|
|
533
|
-
dv = v2 - v1
|
|
534
|
-
theta = atan2(*(dv[::-1]))
|
|
535
|
-
theta_old = self._arrows._angles[i]
|
|
536
|
-
dtheta = theta - theta_old
|
|
537
|
-
mrot = np.array([[cos(dtheta), sin(dtheta)], [-sin(dtheta), cos(dtheta)]])
|
|
538
|
-
apath.vertices = apath.vertices @ mrot
|
|
539
|
-
self._arrows._angles[i] = theta
|
|
540
|
-
|
|
541
521
|
@_stale_wrapper
|
|
542
522
|
def draw(self, renderer):
|
|
543
523
|
# Visibility affects the children too
|
|
@@ -545,11 +525,15 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
545
525
|
return
|
|
546
526
|
|
|
547
527
|
# This includes the subedges if present
|
|
548
|
-
self.
|
|
549
|
-
# This sets the arrow offsets
|
|
550
|
-
self._update_children()
|
|
528
|
+
self._update_before_draw()
|
|
551
529
|
|
|
530
|
+
# Now you can draw the edges
|
|
552
531
|
super().draw(renderer)
|
|
532
|
+
|
|
533
|
+
# This sets the labels offsets
|
|
534
|
+
self._update_labels()
|
|
535
|
+
|
|
536
|
+
# Now you can draw arrows and labels
|
|
553
537
|
for child in self.get_children():
|
|
554
538
|
child.draw(renderer)
|
|
555
539
|
|
iplotx/edge/arrow.py
CHANGED
|
@@ -4,6 +4,7 @@ Module for edge arrows in iplotx.
|
|
|
4
4
|
|
|
5
5
|
from typing import Never, Optional
|
|
6
6
|
|
|
7
|
+
from math import atan2, cos, sin
|
|
7
8
|
import numpy as np
|
|
8
9
|
import matplotlib as mpl
|
|
9
10
|
from matplotlib.patches import PathPatch
|
|
@@ -91,9 +92,7 @@ class EdgeArrowCollection(mpl.collections.PatchCollection):
|
|
|
91
92
|
def set_figure(self, fig) -> None:
|
|
92
93
|
"""Set the figure for this artist and all children."""
|
|
93
94
|
super().set_figure(fig)
|
|
94
|
-
self.
|
|
95
|
-
for child in self.get_children():
|
|
96
|
-
child.set_figure(fig)
|
|
95
|
+
self._update_before_draw()
|
|
97
96
|
|
|
98
97
|
def get_offset_transform(self):
|
|
99
98
|
"""Get offset transform for the edge arrows. This sets the tip of each arrow."""
|
|
@@ -126,6 +125,30 @@ class EdgeArrowCollection(mpl.collections.PatchCollection):
|
|
|
126
125
|
|
|
127
126
|
return patches, sizes
|
|
128
127
|
|
|
128
|
+
def _update_before_draw(self) -> None:
|
|
129
|
+
"""Update the arrow paths and directions before drawing, based on the edge collection."""
|
|
130
|
+
self.set_sizes(self._sizes, self.get_figure(root=True).dpi)
|
|
131
|
+
|
|
132
|
+
trans = self.get_offset_transform().transform
|
|
133
|
+
|
|
134
|
+
for i, epath in enumerate(self._edge_collection.get_paths()):
|
|
135
|
+
# Offset the arrow to point to the end of the edge
|
|
136
|
+
self._offsets[i] = epath.vertices[-1]
|
|
137
|
+
|
|
138
|
+
# Rotate the arrow to point in the direction of the edge
|
|
139
|
+
apath = self._paths[i]
|
|
140
|
+
# NOTE: because the tip of the arrow is at (0, 0) in patch space,
|
|
141
|
+
# in theory it will rotate around that point already
|
|
142
|
+
v2 = trans(epath.vertices[-1])
|
|
143
|
+
v1 = trans(epath.vertices[-2])
|
|
144
|
+
dv = v2 - v1
|
|
145
|
+
theta = atan2(*(dv[::-1]))
|
|
146
|
+
theta_old = self._angles[i]
|
|
147
|
+
dtheta = theta - theta_old
|
|
148
|
+
mrot = np.array([[cos(dtheta), sin(dtheta)], [-sin(dtheta), cos(dtheta)]])
|
|
149
|
+
apath.vertices = apath.vertices @ mrot
|
|
150
|
+
self._angles[i] = theta
|
|
151
|
+
|
|
129
152
|
def set_array(self, A: np.ndarray) -> Never:
|
|
130
153
|
"""Set the array for cmap/norm coloring, but keep the facecolors as set (usually 'none')."""
|
|
131
154
|
raise ValueError("Setting an array for arrows directly is not supported.")
|
|
@@ -145,7 +168,7 @@ class EdgeArrowCollection(mpl.collections.PatchCollection):
|
|
|
145
168
|
|
|
146
169
|
@mpl.artist.allow_rasterization
|
|
147
170
|
def draw(self, renderer):
|
|
148
|
-
self.
|
|
171
|
+
self._update_before_draw()
|
|
149
172
|
super().draw(renderer)
|
|
150
173
|
|
|
151
174
|
|
iplotx/edge/geometry.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Support module with geometry- and path-related functions for edges.
|
|
3
|
+
|
|
4
|
+
3D geometry is in a separate module.
|
|
3
5
|
"""
|
|
4
6
|
|
|
5
7
|
from typing import (
|
|
@@ -65,6 +67,14 @@ def _compute_loops_per_angle(nloops, angles):
|
|
|
65
67
|
|
|
66
68
|
|
|
67
69
|
def _get_shorter_edge_coords(vpath, vsize, theta, shrink=0):
|
|
70
|
+
"""Get the coordinates of an edge tip such that it touches the vertex border.
|
|
71
|
+
|
|
72
|
+
Parameters:
|
|
73
|
+
vpath: the vertex path, in figure coordinates (so scaled by dpi).
|
|
74
|
+
vsize: the vertex max size, in figure coordinates (so scaled by dpi).
|
|
75
|
+
theta: the angle of the edge inpinging into the vertex, in radians, in figure coordinates.
|
|
76
|
+
shrink: additional shrinking of the edge, in figure coordinates (so scaled by dpi).
|
|
77
|
+
"""
|
|
68
78
|
# Bound theta from -pi to pi (why is that not guaranteed?)
|
|
69
79
|
theta = (theta + pi) % (2 * pi) - pi
|
|
70
80
|
|
iplotx/groups.py
CHANGED
|
@@ -4,6 +4,7 @@ Module for vertex groupings code, especially the GroupingArtist class.
|
|
|
4
4
|
|
|
5
5
|
from typing import Union
|
|
6
6
|
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
7
8
|
import matplotlib as mpl
|
|
8
9
|
from matplotlib.collections import PatchCollection
|
|
9
10
|
|
|
@@ -64,6 +65,9 @@ class GroupingArtist(PatchCollection):
|
|
|
64
65
|
self._points_per_curve = points_per_curve
|
|
65
66
|
|
|
66
67
|
network = kwargs.pop("network", None)
|
|
68
|
+
self.layout = normalise_layout(layout, network=network)
|
|
69
|
+
self.ndim = layout.shape[1]
|
|
70
|
+
|
|
67
71
|
patches, grouping, coords_hulls = self._create_patches(
|
|
68
72
|
grouping,
|
|
69
73
|
layout,
|
|
@@ -89,6 +93,21 @@ class GroupingArtist(PatchCollection):
|
|
|
89
93
|
self._compute_paths(self.get_figure(root=True).dpi)
|
|
90
94
|
return ret
|
|
91
95
|
|
|
96
|
+
@property
|
|
97
|
+
def axes(self):
|
|
98
|
+
return PatchCollection.axes.__get__(self)
|
|
99
|
+
|
|
100
|
+
@axes.setter
|
|
101
|
+
def axes(self, new_axes):
|
|
102
|
+
PatchCollection.axes.__set__(self, new_axes)
|
|
103
|
+
for child in self.get_children():
|
|
104
|
+
child.axes = new_axes
|
|
105
|
+
self.set_figure(new_axes.figure)
|
|
106
|
+
|
|
107
|
+
def get_layout(self) -> pd.DataFrame:
|
|
108
|
+
"""Get the layout used for this grouping."""
|
|
109
|
+
return self.layout
|
|
110
|
+
|
|
92
111
|
def get_vertexpadding(self) -> float:
|
|
93
112
|
"""Get the vertex padding of each group."""
|
|
94
113
|
return self._vertexpadding
|
|
@@ -98,7 +117,6 @@ class GroupingArtist(PatchCollection):
|
|
|
98
117
|
return self.get_vertexpadding() * dpi / 72.0 * self._factor
|
|
99
118
|
|
|
100
119
|
def _create_patches(self, grouping, layout, network, **kwargs):
|
|
101
|
-
layout = normalise_layout(layout, network=network)
|
|
102
120
|
grouping = normalise_grouping(grouping, layout)
|
|
103
121
|
style = get_style(".grouping")
|
|
104
122
|
style.pop("vertexpadding", None)
|
|
@@ -30,23 +30,22 @@ class IGraphDataProvider(NetworkDataProvider):
|
|
|
30
30
|
edge_labels: Optional[Sequence[str] | dict[str]] = None,
|
|
31
31
|
) -> NetworkData:
|
|
32
32
|
"""Create network data object for iplotx from an igraph object."""
|
|
33
|
-
network = self.network
|
|
34
|
-
directed = self.is_directed()
|
|
35
33
|
|
|
36
|
-
#
|
|
37
|
-
if np.isscalar(vertex_labels) and (not vertex_labels):
|
|
38
|
-
vertex_labels = None
|
|
39
|
-
|
|
40
|
-
# Vertices are ordered integers, no gaps
|
|
34
|
+
# Get layout
|
|
41
35
|
vertex_df = normalise_layout(
|
|
42
36
|
layout,
|
|
43
|
-
network=network,
|
|
37
|
+
network=self.network,
|
|
44
38
|
nvertices=self.number_of_vertices(),
|
|
45
39
|
)
|
|
46
40
|
ndim = vertex_df.shape[1]
|
|
47
41
|
vertex_df.columns = _make_layout_columns(ndim)
|
|
48
42
|
|
|
43
|
+
# Vertices are ordered integers, no gaps
|
|
44
|
+
|
|
49
45
|
# Vertex labels
|
|
46
|
+
# Recast vertex_labels=False as vertex_labels=None
|
|
47
|
+
if np.isscalar(vertex_labels) and (not vertex_labels):
|
|
48
|
+
vertex_labels = None
|
|
50
49
|
if vertex_labels is not None:
|
|
51
50
|
if np.isscalar(vertex_labels):
|
|
52
51
|
vertex_df["label"] = vertex_df.index.astype(str)
|
|
@@ -57,7 +56,7 @@ class IGraphDataProvider(NetworkDataProvider):
|
|
|
57
56
|
|
|
58
57
|
# Edges are a list of tuples, because of multiedges
|
|
59
58
|
tmp = []
|
|
60
|
-
for edge in network.es:
|
|
59
|
+
for edge in self.network.es:
|
|
61
60
|
row = {"_ipx_source": edge.source, "_ipx_target": edge.target}
|
|
62
61
|
row.update(edge.attributes())
|
|
63
62
|
tmp.append(row)
|
|
@@ -76,7 +75,7 @@ class IGraphDataProvider(NetworkDataProvider):
|
|
|
76
75
|
network_data = {
|
|
77
76
|
"vertex_df": vertex_df,
|
|
78
77
|
"edge_df": edge_df,
|
|
79
|
-
"directed":
|
|
78
|
+
"directed": self.is_directed(),
|
|
80
79
|
"ndim": ndim,
|
|
81
80
|
}
|
|
82
81
|
return network_data
|
|
@@ -33,25 +33,20 @@ class NetworkXDataProvider(NetworkDataProvider):
|
|
|
33
33
|
|
|
34
34
|
import networkx as nx
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
directed = self.is_directed()
|
|
39
|
-
|
|
40
|
-
# Recast vertex_labels=False as vertex_labels=None
|
|
41
|
-
if np.isscalar(vertex_labels) and (not vertex_labels):
|
|
42
|
-
vertex_labels = None
|
|
43
|
-
|
|
44
|
-
# Vertices are indexed by node ID
|
|
36
|
+
# Get layout
|
|
45
37
|
vertex_df = normalise_layout(
|
|
46
38
|
layout,
|
|
47
|
-
network=network,
|
|
39
|
+
network=self.network,
|
|
48
40
|
nvertices=self.number_of_vertices(),
|
|
49
|
-
)
|
|
41
|
+
)
|
|
50
42
|
ndim = vertex_df.shape[1]
|
|
51
43
|
vertex_df.columns = _make_layout_columns(ndim)
|
|
52
44
|
|
|
45
|
+
# Vertices are indexed by node ID
|
|
46
|
+
vertex_df = vertex_df.loc[pd.Index(self.network.nodes)]
|
|
47
|
+
|
|
53
48
|
# Vertex internal properties
|
|
54
|
-
tmp = pd.DataFrame(dict(network.nodes.data())).T
|
|
49
|
+
tmp = pd.DataFrame(dict(self.network.nodes.data())).T
|
|
55
50
|
# Arrays become a single column, which we have already anyway
|
|
56
51
|
if isinstance(layout, str) and (layout in tmp.columns):
|
|
57
52
|
del tmp[layout]
|
|
@@ -60,6 +55,9 @@ class NetworkXDataProvider(NetworkDataProvider):
|
|
|
60
55
|
del tmp
|
|
61
56
|
|
|
62
57
|
# Vertex labels
|
|
58
|
+
# Recast vertex_labels=False as vertex_labels=None
|
|
59
|
+
if np.isscalar(vertex_labels) and (not vertex_labels):
|
|
60
|
+
vertex_labels = None
|
|
63
61
|
if vertex_labels is None:
|
|
64
62
|
if "label" in vertex_df:
|
|
65
63
|
del vertex_df["label"]
|
|
@@ -78,7 +76,7 @@ class NetworkXDataProvider(NetworkDataProvider):
|
|
|
78
76
|
|
|
79
77
|
# Edges are a list of tuples, because of multiedges
|
|
80
78
|
tmp = []
|
|
81
|
-
for u, v, d in network.edges.data():
|
|
79
|
+
for u, v, d in self.network.edges.data():
|
|
82
80
|
row = {"_ipx_source": u, "_ipx_target": v}
|
|
83
81
|
row.update(d)
|
|
84
82
|
tmp.append(row)
|
|
@@ -112,7 +110,7 @@ class NetworkXDataProvider(NetworkDataProvider):
|
|
|
112
110
|
network_data = {
|
|
113
111
|
"vertex_df": vertex_df,
|
|
114
112
|
"edge_df": edge_df,
|
|
115
|
-
"directed":
|
|
113
|
+
"directed": self.is_directed(),
|
|
116
114
|
"ndim": ndim,
|
|
117
115
|
}
|
|
118
116
|
return network_data
|
iplotx/label.py
CHANGED
|
@@ -77,6 +77,27 @@ class LabelCollection(mpl.artist.Artist):
|
|
|
77
77
|
child.set_figure(fig)
|
|
78
78
|
self._update_offsets(dpi=fig.dpi)
|
|
79
79
|
|
|
80
|
+
@property
|
|
81
|
+
def axes(self):
|
|
82
|
+
return mpl.artist.Artist.axes.__get__(self)
|
|
83
|
+
|
|
84
|
+
@axes.setter
|
|
85
|
+
def axes(self, new_axes):
|
|
86
|
+
mpl.artist.Artist.axes.__set__(self, new_axes)
|
|
87
|
+
for child in self.get_children():
|
|
88
|
+
child.axes = new_axes
|
|
89
|
+
|
|
90
|
+
def set_transform(self, transform: mpl.transforms.Transform) -> None:
|
|
91
|
+
"""Set the transform for this artist and children.
|
|
92
|
+
|
|
93
|
+
Parameters:
|
|
94
|
+
transform: The transform to set.
|
|
95
|
+
"""
|
|
96
|
+
super().set_transform(transform)
|
|
97
|
+
if hasattr(self, "_labelartists"):
|
|
98
|
+
for art in self._labelartists:
|
|
99
|
+
art.set_transform(transform)
|
|
100
|
+
|
|
80
101
|
def get_texts(self):
|
|
81
102
|
"""Get the texts of the labels."""
|
|
82
103
|
return [child.get_text() for child in self.get_children()]
|
iplotx/network.py
CHANGED
|
@@ -30,6 +30,13 @@ from .edge import (
|
|
|
30
30
|
EdgeCollection,
|
|
31
31
|
make_stub_patch as make_undirected_edge_patch,
|
|
32
32
|
)
|
|
33
|
+
from .art3d.vertex import (
|
|
34
|
+
vertex_collection_2d_to_3d,
|
|
35
|
+
)
|
|
36
|
+
from .art3d.edge import (
|
|
37
|
+
Edge3DCollection,
|
|
38
|
+
edge_collection_2d_to_3d,
|
|
39
|
+
)
|
|
33
40
|
|
|
34
41
|
|
|
35
42
|
@_forwarder(
|
|
@@ -63,6 +70,10 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
63
70
|
will be drawn. If a list, the labels are taken from the list. If a dict, the keys
|
|
64
71
|
should be the vertex IDs and the values should be the labels.
|
|
65
72
|
edge_labels: The labels for the edges. If None, no edge labels will be drawn.
|
|
73
|
+
transform: The transform to use for the vertices. Default is IdentityTransform.
|
|
74
|
+
offset_transform: The transform to use as offset transform for the vertices and main
|
|
75
|
+
transform for the edges. Default is None, but this should eventually be set to
|
|
76
|
+
ax.transData once the artist is added to an Axes.
|
|
66
77
|
|
|
67
78
|
"""
|
|
68
79
|
self.network = network
|
|
@@ -111,7 +122,7 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
111
122
|
@classmethod
|
|
112
123
|
def from_edgecollection(
|
|
113
124
|
cls: "NetworkArtist", # NOTE: This is fixed in Python 3.14
|
|
114
|
-
edge_collection: EdgeCollection,
|
|
125
|
+
edge_collection: EdgeCollection | Edge3DCollection,
|
|
115
126
|
) -> Self:
|
|
116
127
|
"""Create a NetworkArtist from iplotx artists.
|
|
117
128
|
|
|
@@ -125,7 +136,7 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
125
136
|
vertex_collection = edge_collection._vertex_collection
|
|
126
137
|
layout = vertex_collection._layout
|
|
127
138
|
transform = vertex_collection.get_transform()
|
|
128
|
-
offset_transform =
|
|
139
|
+
offset_transform = vertex_collection.get_offset_transform()
|
|
129
140
|
|
|
130
141
|
# Follow the steps in the normal constructor
|
|
131
142
|
self = cls(
|
|
@@ -134,6 +145,7 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
134
145
|
transform=transform,
|
|
135
146
|
offset_transform=offset_transform,
|
|
136
147
|
)
|
|
148
|
+
# TODO: should we make copies here?
|
|
137
149
|
self._vertices = vertex_collection
|
|
138
150
|
self._edges = edge_collection
|
|
139
151
|
|
|
@@ -150,6 +162,17 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
150
162
|
for child in self.get_children():
|
|
151
163
|
child.set_figure(fig)
|
|
152
164
|
|
|
165
|
+
@property
|
|
166
|
+
def axes(self):
|
|
167
|
+
return mpl.artist.Artist.axes.__get__(self)
|
|
168
|
+
|
|
169
|
+
@axes.setter
|
|
170
|
+
def axes(self, new_axes):
|
|
171
|
+
mpl.artist.Artist.axes.__set__(self, new_axes)
|
|
172
|
+
for child in self.get_children():
|
|
173
|
+
child.axes = new_axes
|
|
174
|
+
self.set_figure(new_axes.figure)
|
|
175
|
+
|
|
153
176
|
def get_offset_transform(self):
|
|
154
177
|
"""Get the offset transform (for vertices/edges)."""
|
|
155
178
|
return self._offset_transform
|
|
@@ -157,6 +180,10 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
157
180
|
def set_offset_transform(self, offset_transform):
|
|
158
181
|
"""Set the offset transform (for vertices/edges)."""
|
|
159
182
|
self._offset_transform = offset_transform
|
|
183
|
+
if hasattr(self, "_vertices"):
|
|
184
|
+
self._vertices.set_offset_transform(offset_transform)
|
|
185
|
+
if hasattr(self, "_edges"):
|
|
186
|
+
self._edges.set_transform(offset_transform)
|
|
160
187
|
|
|
161
188
|
def get_vertices(self):
|
|
162
189
|
"""Get VertexCollection artist."""
|
|
@@ -210,10 +237,23 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
210
237
|
self.axes.autoscale_view(tight=tight)
|
|
211
238
|
|
|
212
239
|
def get_layout(self):
|
|
213
|
-
|
|
240
|
+
"""Get the vertex layout.
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
The vertex layout as a DataFrame.
|
|
244
|
+
"""
|
|
245
|
+
layout_columns = [f"_ipx_layout_{i}" for i in range(self.get_ndim())]
|
|
214
246
|
vertex_layout_df = self._ipx_internal_data["vertex_df"][layout_columns]
|
|
215
247
|
return vertex_layout_df
|
|
216
248
|
|
|
249
|
+
def get_ndim(self):
|
|
250
|
+
"""Get the dimensionality of the layout.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
The dimensionality of the layout (2 or 3).
|
|
254
|
+
"""
|
|
255
|
+
return self._ipx_internal_data["ndim"]
|
|
256
|
+
|
|
217
257
|
def _get_label_series(self, kind):
|
|
218
258
|
# Equivalence vertex/node
|
|
219
259
|
if kind == "node":
|
|
@@ -238,6 +278,13 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
238
278
|
offset_transform=self.get_offset_transform(),
|
|
239
279
|
)
|
|
240
280
|
|
|
281
|
+
if self.get_ndim() == 3:
|
|
282
|
+
vertex_collection_2d_to_3d(
|
|
283
|
+
self._vertices,
|
|
284
|
+
zs=self.get_layout().iloc[:, 2].values,
|
|
285
|
+
depthshade=False,
|
|
286
|
+
)
|
|
287
|
+
|
|
241
288
|
def _add_edges(self):
|
|
242
289
|
"""Add edges to the network artist.
|
|
243
290
|
|
|
@@ -319,6 +366,11 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
319
366
|
if "cmap" in edge_style:
|
|
320
367
|
self._edges.set_array(colorarray)
|
|
321
368
|
|
|
369
|
+
if self.get_ndim() == 3:
|
|
370
|
+
edge_collection_2d_to_3d(
|
|
371
|
+
self._edges,
|
|
372
|
+
)
|
|
373
|
+
|
|
322
374
|
@_stale_wrapper
|
|
323
375
|
def draw(self, renderer):
|
|
324
376
|
"""Draw each of the children, with some buffering mechanism."""
|
|
@@ -332,8 +384,8 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
332
384
|
# Handle zorder manually, just like in AxesBase in mpl
|
|
333
385
|
children = list(self.get_children())
|
|
334
386
|
children.sort(key=lambda x: x.zorder)
|
|
335
|
-
for
|
|
336
|
-
|
|
387
|
+
for child in children:
|
|
388
|
+
child.draw(renderer)
|
|
337
389
|
|
|
338
390
|
|
|
339
391
|
def _update_from_internal(style, row, kind):
|
iplotx/plotting.py
CHANGED
|
@@ -3,6 +3,7 @@ from contextlib import nullcontext
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
import pandas as pd
|
|
5
5
|
import matplotlib as mpl
|
|
6
|
+
from mpl_toolkits.mplot3d.axes3d import Axes3D
|
|
6
7
|
import matplotlib.pyplot as plt
|
|
7
8
|
|
|
8
9
|
from .typing import (
|
|
@@ -28,7 +29,7 @@ def network(
|
|
|
28
29
|
style: str | dict | Sequence[str | dict] = (),
|
|
29
30
|
title: Optional[str] = None,
|
|
30
31
|
aspect: Optional[str | float] = None,
|
|
31
|
-
margins: float | tuple[float, float] = 0,
|
|
32
|
+
margins: float | tuple[float, float] | tuple[float, float, float] = 0,
|
|
32
33
|
strip_axes: bool = True,
|
|
33
34
|
figsize: Optional[tuple[float, float]] = None,
|
|
34
35
|
**kwargs,
|
|
@@ -53,13 +54,15 @@ def network(
|
|
|
53
54
|
style: Apply this style for the objects to plot. This can be a sequence (e.g. list)
|
|
54
55
|
of styles and they will be applied in order.
|
|
55
56
|
title: If not None, set the axes title to this value.
|
|
56
|
-
aspect: If not None, set the aspect ratio of the axis to this value.
|
|
57
|
-
value is 1.0, which proportionates x- and y-axes.
|
|
57
|
+
aspect: If not None, set the aspect ratio of the axis to this value. In 2D, the most
|
|
58
|
+
common value is 1.0, which proportionates x- and y-axes. In 3D, only string
|
|
59
|
+
values are accepted (see the documentation of Axes.set_aspect).
|
|
58
60
|
margins: How much margin to leave around the plot. A higher value (e.g. 0.1) can be
|
|
59
61
|
used as a quick fix when some vertex shapes reach beyond the plot edge. This is
|
|
60
62
|
a fraction of the data limits, so 0.1 means 10% of the data limits will be left
|
|
61
|
-
as margin.
|
|
62
|
-
|
|
63
|
+
as margin. A pair (in 2D) or triplet (in 3D) of floats can also be provided and
|
|
64
|
+
applied to each axis separately.
|
|
65
|
+
strip_axes: If True, remove axis spines and ticks. In 3D, only ticks are removed.
|
|
63
66
|
figsize: If ax is None, a new matplotlib Figure is created. This argument specifies
|
|
64
67
|
the (width, height) dimension of the figure in inches. If ax is not None, this
|
|
65
68
|
argument is ignored. If None, the default matplotlib figure size is used.
|
|
@@ -83,9 +86,6 @@ def network(
|
|
|
83
86
|
if (network is None) and (grouping is None):
|
|
84
87
|
raise ValueError("At least one of network or grouping must be provided.")
|
|
85
88
|
|
|
86
|
-
if ax is None:
|
|
87
|
-
fig, ax = plt.subplots(figsize=figsize)
|
|
88
|
-
|
|
89
89
|
artists = []
|
|
90
90
|
if network is not None:
|
|
91
91
|
nwkart = NetworkArtist(
|
|
@@ -94,30 +94,58 @@ def network(
|
|
|
94
94
|
vertex_labels=vertex_labels,
|
|
95
95
|
edge_labels=edge_labels,
|
|
96
96
|
transform=mpl.transforms.IdentityTransform(),
|
|
97
|
-
offset_transform=ax.transData,
|
|
98
97
|
)
|
|
99
|
-
ax.add_artist(nwkart)
|
|
100
|
-
|
|
101
|
-
# Set the figure, which itself sets the dpi scale for vertices, edges,
|
|
102
|
-
# arrows, etc. Now data limits can be computed correctly
|
|
103
|
-
nwkart.set_figure(ax.figure)
|
|
104
|
-
|
|
105
98
|
artists.append(nwkart)
|
|
106
|
-
|
|
107
|
-
# Set normailsed layout since we have it by now
|
|
108
99
|
layout = nwkart.get_layout()
|
|
100
|
+
else:
|
|
101
|
+
nwkart = None
|
|
109
102
|
|
|
110
103
|
if grouping is not None:
|
|
111
104
|
grpart = GroupingArtist(
|
|
112
105
|
grouping,
|
|
113
106
|
layout,
|
|
114
107
|
network=network,
|
|
115
|
-
transform=ax.transData,
|
|
116
108
|
)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
grpart.set_figure(ax.figure)
|
|
109
|
+
layout = grpart.get_layout()
|
|
120
110
|
artists.append(grpart)
|
|
111
|
+
else:
|
|
112
|
+
grpart = None
|
|
113
|
+
|
|
114
|
+
if (nwkart is not None) or (grpart is not None):
|
|
115
|
+
ndim = layout.shape[1]
|
|
116
|
+
else:
|
|
117
|
+
ndim = None
|
|
118
|
+
|
|
119
|
+
if ax is None:
|
|
120
|
+
if ndim == 3:
|
|
121
|
+
fig = plt.figure(figsize=figsize)
|
|
122
|
+
ax = fig.add_subplot(111, projection="3d")
|
|
123
|
+
else:
|
|
124
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
125
|
+
ndim = 2
|
|
126
|
+
else:
|
|
127
|
+
# Check that the expected axis projection is used (3d for 3d layouts)
|
|
128
|
+
if ndim == 3:
|
|
129
|
+
assert isinstance(ax, Axes3D)
|
|
130
|
+
elif ndim == 2:
|
|
131
|
+
# NOTE: technically we probably want it to be cartesian (not polar, etc.)
|
|
132
|
+
# but let's be flexible for now and let that request bubble up from users
|
|
133
|
+
assert not isinstance(ax, Axes3D)
|
|
134
|
+
|
|
135
|
+
# This is used in 3D for autoscaling
|
|
136
|
+
had_data = ax.has_data()
|
|
137
|
+
|
|
138
|
+
if nwkart is not None:
|
|
139
|
+
# Set the figure, which itself sets the dpi scale for vertices, edges,
|
|
140
|
+
# arrows, etc. Now data limits can be computed correctly
|
|
141
|
+
nwkart.set_offset_transform(ax.transData)
|
|
142
|
+
ax.add_artist(nwkart)
|
|
143
|
+
nwkart.axes = ax
|
|
144
|
+
|
|
145
|
+
if grpart is not None:
|
|
146
|
+
grpart.set_transform(ax.transData)
|
|
147
|
+
ax.add_artist(grpart)
|
|
148
|
+
grpart.ax = ax
|
|
121
149
|
|
|
122
150
|
if title is not None:
|
|
123
151
|
ax.set_title(title)
|
|
@@ -125,11 +153,11 @@ def network(
|
|
|
125
153
|
if aspect is not None:
|
|
126
154
|
ax.set_aspect(aspect)
|
|
127
155
|
|
|
128
|
-
_postprocess_axes(ax, artists, strip=strip_axes)
|
|
156
|
+
_postprocess_axes(ax, artists, strip=strip_axes, had_data=had_data)
|
|
129
157
|
|
|
130
158
|
if np.isscalar(margins):
|
|
131
|
-
margins =
|
|
132
|
-
if (margins[0] != 0) or (margins[1] != 0):
|
|
159
|
+
margins = [margins] * ndim
|
|
160
|
+
if (margins[0] != 0) or (margins[1] != 0) or ((len(margins) == 3) and (margins[2] != 0)):
|
|
133
161
|
ax.margins(*margins)
|
|
134
162
|
|
|
135
163
|
return artists
|
|
@@ -223,7 +251,6 @@ def tree(
|
|
|
223
251
|
show_support=show_support,
|
|
224
252
|
)
|
|
225
253
|
ax.add_artist(artist)
|
|
226
|
-
|
|
227
254
|
artist.set_figure(ax.figure)
|
|
228
255
|
|
|
229
256
|
if title is not None:
|
|
@@ -243,26 +270,46 @@ def tree(
|
|
|
243
270
|
|
|
244
271
|
|
|
245
272
|
# INTERNAL ROUTINES
|
|
246
|
-
def _postprocess_axes(ax, artists, strip=True):
|
|
273
|
+
def _postprocess_axes(ax, artists, strip=True, had_data=None):
|
|
247
274
|
"""Postprocess axis after plotting."""
|
|
248
275
|
|
|
249
276
|
if strip:
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
277
|
+
if not isinstance(ax, Axes3D):
|
|
278
|
+
# Despine
|
|
279
|
+
ax.spines["right"].set_visible(False)
|
|
280
|
+
ax.spines["top"].set_visible(False)
|
|
281
|
+
ax.spines["left"].set_visible(False)
|
|
282
|
+
ax.spines["bottom"].set_visible(False)
|
|
255
283
|
|
|
256
284
|
# Remove axis ticks
|
|
257
285
|
ax.set_xticks([])
|
|
258
286
|
ax.set_yticks([])
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
ax
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
287
|
+
if isinstance(ax, Axes3D):
|
|
288
|
+
ax.set_zticks([])
|
|
289
|
+
|
|
290
|
+
# NOTE: bboxes appear to be not that well defined in 3D axes
|
|
291
|
+
# instead, there is a dedicated function that is a little
|
|
292
|
+
# pedestrian
|
|
293
|
+
if isinstance(ax, Axes3D):
|
|
294
|
+
for art in artists:
|
|
295
|
+
XYZ = art.get_layout().values.T
|
|
296
|
+
if ax._zmargin < 0.05 and XYZ[0].size > 0:
|
|
297
|
+
ax.set_zmargin(0.05)
|
|
298
|
+
ax.auto_scale_xyz(
|
|
299
|
+
*XYZ,
|
|
300
|
+
had_data=had_data,
|
|
301
|
+
)
|
|
302
|
+
# NOTE: breaking is not needed, worst case it will
|
|
303
|
+
# autoscale twice (for network and grouping), which
|
|
304
|
+
# is better, at this stage of development, than
|
|
305
|
+
# trying to be too clever by doing the math outselves
|
|
306
|
+
else:
|
|
307
|
+
# Set new data limits
|
|
308
|
+
bboxes = []
|
|
309
|
+
for art in artists:
|
|
310
|
+
bboxes.append(art.get_datalim(ax.transData))
|
|
311
|
+
bbox = mpl.transforms.Bbox.union(bboxes)
|
|
312
|
+
ax.update_datalim(bbox)
|
|
313
|
+
|
|
314
|
+
# Autoscale for x/y axis limits
|
|
315
|
+
ax.autoscale_view()
|
iplotx/version.py
CHANGED
iplotx/vertex.py
CHANGED
|
@@ -119,6 +119,16 @@ class VertexCollection(PatchCollection):
|
|
|
119
119
|
for child in self.get_children():
|
|
120
120
|
child.set_figure(fig)
|
|
121
121
|
|
|
122
|
+
@property
|
|
123
|
+
def axes(self):
|
|
124
|
+
return PatchCollection.axes.__get__(self)
|
|
125
|
+
|
|
126
|
+
@axes.setter
|
|
127
|
+
def axes(self, new_axes):
|
|
128
|
+
PatchCollection.axes.__set__(self, new_axes)
|
|
129
|
+
for child in self.get_children():
|
|
130
|
+
child.axes = new_axes
|
|
131
|
+
|
|
122
132
|
def get_index(self):
|
|
123
133
|
"""Get the VertexCollection index."""
|
|
124
134
|
return self._index
|
|
@@ -196,7 +206,9 @@ class VertexCollection(PatchCollection):
|
|
|
196
206
|
def _update_offsets_from_layout(self) -> None:
|
|
197
207
|
"""Update offsets in matplotlib coordinates from the layout DataFrame."""
|
|
198
208
|
if self._layout_coordinate_system == "cartesian":
|
|
199
|
-
|
|
209
|
+
# Make sure we accept 3D values and ignore the z component if present
|
|
210
|
+
# This makes life upstream a little more readable
|
|
211
|
+
self._offsets = self._layout.values[:, :2]
|
|
200
212
|
elif self._layout_coordinate_system == "polar":
|
|
201
213
|
# Convert polar coordinates (r, theta) to cartesian (x, y)
|
|
202
214
|
r = self._layout.iloc[:, 0].values
|
|
@@ -221,6 +233,16 @@ class VertexCollection(PatchCollection):
|
|
|
221
233
|
self._update_offsets_from_layout()
|
|
222
234
|
self.stale = True
|
|
223
235
|
|
|
236
|
+
def set_offset_transform(self, transform: mpl.transforms.Transform) -> None:
|
|
237
|
+
"""Set the offset transform for the vertices.
|
|
238
|
+
|
|
239
|
+
Parameters:
|
|
240
|
+
transform: The matplotlib transform to use for the offsets.
|
|
241
|
+
"""
|
|
242
|
+
super().set_offset_transform(transform)
|
|
243
|
+
if hasattr(self, "_label_collection"):
|
|
244
|
+
self._label_collection.set_transform(transform)
|
|
245
|
+
|
|
224
246
|
def get_style(self) -> Optional[dict[str, Any]]:
|
|
225
247
|
"""Get the style dictionary for the vertices."""
|
|
226
248
|
return self._style
|
|
@@ -293,6 +315,10 @@ class VertexCollection(PatchCollection):
|
|
|
293
315
|
transform=transform,
|
|
294
316
|
)
|
|
295
317
|
|
|
318
|
+
def get_ndim(self):
|
|
319
|
+
"""Get the number of dimensions of the layout."""
|
|
320
|
+
return self._layout.shape[1]
|
|
321
|
+
|
|
296
322
|
def get_labels(self):
|
|
297
323
|
"""Get the vertex labels.
|
|
298
324
|
|
|
@@ -339,6 +365,10 @@ class VertexCollection(PatchCollection):
|
|
|
339
365
|
rotations = np.arctan2(doffsets_fig[:, 1], doffsets_fig[:, 0])
|
|
340
366
|
self.get_labels().set_rotations(rotations)
|
|
341
367
|
|
|
368
|
+
def _update_before_draw(self) -> None:
|
|
369
|
+
"""Update the collection before drawing."""
|
|
370
|
+
self.set_sizes(self._sizes, self.get_figure(root=True).dpi)
|
|
371
|
+
|
|
342
372
|
@mpl.artist.allow_rasterization
|
|
343
373
|
def draw(self, renderer):
|
|
344
374
|
if not self.get_visible():
|
|
@@ -349,7 +379,8 @@ class VertexCollection(PatchCollection):
|
|
|
349
379
|
if len(self.get_paths()) == 0:
|
|
350
380
|
return
|
|
351
381
|
|
|
352
|
-
self.
|
|
382
|
+
self._update_before_draw()
|
|
383
|
+
super().draw(renderer)
|
|
353
384
|
|
|
354
385
|
# Set the label rotations already, hopefully this is not too early
|
|
355
386
|
self._update_children()
|
|
@@ -357,7 +388,6 @@ class VertexCollection(PatchCollection):
|
|
|
357
388
|
# NOTE: This draws the vertices first, then the labels.
|
|
358
389
|
# The correct order would be vertex1->label1->vertex2->label2, etc.
|
|
359
390
|
# We might fix if we manage to find a way to do it.
|
|
360
|
-
super().draw(renderer)
|
|
361
391
|
for child in self.get_children():
|
|
362
392
|
child.draw(renderer)
|
|
363
393
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iplotx
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.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
|
|
@@ -29,7 +29,6 @@ Requires-Python: >=3.11
|
|
|
29
29
|
Requires-Dist: matplotlib>=2.0.0
|
|
30
30
|
Requires-Dist: numpy>=2.0.0
|
|
31
31
|
Requires-Dist: pandas>=2.0.0
|
|
32
|
-
Requires-Dist: pylint>=3.3.7
|
|
33
32
|
Provides-Extra: igraph
|
|
34
33
|
Requires-Dist: igraph>=0.11.0; extra == 'igraph'
|
|
35
34
|
Provides-Extra: networkx
|
|
@@ -1,25 +1,29 @@
|
|
|
1
1
|
iplotx/__init__.py,sha256=RzSct91jO8abrxOIn33rKEnDUgYpu1oj4olbObgX_hs,489
|
|
2
|
-
iplotx/artists.py,sha256=
|
|
2
|
+
iplotx/artists.py,sha256=XNtRwuvQdKkZCAejILydLD3J5B87sg5xPXuZFv_Gkk8,654
|
|
3
3
|
iplotx/cascades.py,sha256=OPqF7Huls-HFmDA5MCF6DEZlUeRVaXsbQcHBoKAgNJs,8182
|
|
4
|
-
iplotx/groups.py,sha256=
|
|
5
|
-
iplotx/label.py,sha256=
|
|
4
|
+
iplotx/groups.py,sha256=X0G-EULkd7WBn1j82r-cBgpzZRd7gQ1cfqFoYNweLns,6775
|
|
5
|
+
iplotx/label.py,sha256=7eS8ByadrhdIFOZz19U4VrS-oXY_ndFYNB-D4RZbFqI,9573
|
|
6
6
|
iplotx/layout.py,sha256=KxmRLqjo8AYCBAmXez8rIiLU2sM34qhb6ox9AHYwRyE,4839
|
|
7
|
-
iplotx/network.py,sha256=
|
|
8
|
-
iplotx/plotting.py,sha256=
|
|
7
|
+
iplotx/network.py,sha256=ae5rZwzWxmcBQXx1Y0q24jaXcM1hT1kip-JKsyk11QY,13385
|
|
8
|
+
iplotx/plotting.py,sha256=icEefWJnS2lEGLp4t1LhDSP40JuvNKgOie3FDLOnTMk,13195
|
|
9
9
|
iplotx/tree.py,sha256=TxbNoBHS0CfswrcMIWCNtnOl_3e4-PwCrVo0goywC0U,28807
|
|
10
10
|
iplotx/typing.py,sha256=QLdzV358IiD1CFe88MVp0D77FSx5sSAVUmM_2WPPE8I,1463
|
|
11
|
-
iplotx/version.py,sha256=
|
|
12
|
-
iplotx/vertex.py,sha256=
|
|
13
|
-
iplotx/
|
|
14
|
-
iplotx/edge/
|
|
15
|
-
iplotx/edge/
|
|
11
|
+
iplotx/version.py,sha256=mharC6dtEtQmAi9lgWMRhn8D3jCoxBqPbGjIoeD7D9Y,67
|
|
12
|
+
iplotx/vertex.py,sha256=bjvAy9UciPWkA1J-SroWF9ZaTXRzNKtDZXBlZ80VM60,16026
|
|
13
|
+
iplotx/art3d/vertex.py,sha256=Xf8Um30X2doCd8KdNN7332F6BxC4k72Mb_GeRAuzQfQ,2545
|
|
14
|
+
iplotx/art3d/edge/__init__.py,sha256=EzzW06YEeyIu52gXormkGIobae-etwKevZ_PDBr-S9c,4624
|
|
15
|
+
iplotx/art3d/edge/arrow.py,sha256=14BFXY9kDOUGPZl2fMD9gRVGyaaN5kyd-l6ikBg6WHU,3601
|
|
16
|
+
iplotx/art3d/edge/geometry.py,sha256=76VUmpPG-4Mls7x_994dMwdDPrWWnjT7nHJsHfwK_hA,2467
|
|
17
|
+
iplotx/edge/__init__.py,sha256=wMKXD1h5SBaUv6HmebIc5wc9k8AuukaXzAOBu7epaqA,26341
|
|
18
|
+
iplotx/edge/arrow.py,sha256=U7vvBo7IMwo1qiyU9cyUEwraOaBcJLgdu9oU2OyoHL4,17453
|
|
19
|
+
iplotx/edge/geometry.py,sha256=jkTMvQC5425GjB_fmGLIPJeSDAr_7NZF8zZDLTrSj34,15541
|
|
16
20
|
iplotx/edge/leaf.py,sha256=SyGMv2PIOoH0pey8-aMVaZheK3hNe1Qz_okcyWbc4E4,4268
|
|
17
21
|
iplotx/edge/ports.py,sha256=BpkbiEhX4mPBBAhOv4jcKFG4Y8hxXz5GRtVLCC0jbtI,1235
|
|
18
22
|
iplotx/ingest/__init__.py,sha256=S0YfnXcFKseB7ZBQc4yRt0cNDsLlhqdom0TmSY3OY2E,4756
|
|
19
23
|
iplotx/ingest/heuristics.py,sha256=715VqgfKek5LOJnu1vTo7RqPgCl-Bb8Cf6o7_Tt57fA,5797
|
|
20
24
|
iplotx/ingest/typing.py,sha256=61LwNwrTHVh8eqqC778Gr81zPYcUKW61mDgGCCsuGSk,14181
|
|
21
|
-
iplotx/ingest/providers/network/igraph.py,sha256=
|
|
22
|
-
iplotx/ingest/providers/network/networkx.py,sha256=
|
|
25
|
+
iplotx/ingest/providers/network/igraph.py,sha256=WL9Yx2IF5QhUIoKMlozdyq5HWIZ-IJmNoeS8GOhL0KU,2945
|
|
26
|
+
iplotx/ingest/providers/network/networkx.py,sha256=ehCg4npL073HX-eAG-VoP6refLPsMb3lYG51xt_rNjA,4636
|
|
23
27
|
iplotx/ingest/providers/network/simple.py,sha256=e_aHhiHhN9DrMoNrt7tEMPURXGhQ1TYRPzsxDEptUlc,3766
|
|
24
28
|
iplotx/ingest/providers/tree/biopython.py,sha256=4N_54cVyHHPcASJZGr6pHKE2p5R3i8Cm307SLlSLHLA,1480
|
|
25
29
|
iplotx/ingest/providers/tree/cogent3.py,sha256=JmELbDK7LyybiJzFNbmeqZ4ySJoDajvFfJebpNfFKWo,1073
|
|
@@ -33,6 +37,6 @@ iplotx/utils/geometry.py,sha256=6RrC6qaB0-1vIk1LhGA4CfsiMd-9JNniSPyL_l9mshE,9245
|
|
|
33
37
|
iplotx/utils/internal.py,sha256=WWfcZDGK8Ut1y_tOHRGg9wSqY1bwSeLQO7dHM_8Tvwo,107
|
|
34
38
|
iplotx/utils/matplotlib.py,sha256=wELE73quQv10-1w9uA5eDTgkZkylJvjg7pd3K5tZPOo,6294
|
|
35
39
|
iplotx/utils/style.py,sha256=vyNP80nDYVinqm6_9ltCJCtjK35ZcGlHvOskNv3eQBc,4225
|
|
36
|
-
iplotx-0.
|
|
37
|
-
iplotx-0.
|
|
38
|
-
iplotx-0.
|
|
40
|
+
iplotx-0.11.0.dist-info/METADATA,sha256=yTnevMcILo2NHdvx7EOniBU6zX4vD4ujJWdA3RR7hVU,4880
|
|
41
|
+
iplotx-0.11.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
42
|
+
iplotx-0.11.0.dist-info/RECORD,,
|
|
File without changes
|