iplotx 0.10.0__py3-none-any.whl → 0.11.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/art3d/edge/__init__.py +147 -0
- iplotx/art3d/edge/arrow.py +115 -0
- iplotx/{edge/geometry3d.py → art3d/edge/geometry.py} +6 -37
- iplotx/art3d/vertex.py +20 -2
- iplotx/edge/__init__.py +20 -46
- iplotx/edge/arrow.py +27 -4
- iplotx/edge/geometry.py +6 -29
- iplotx/groups.py +1 -1
- iplotx/label.py +10 -0
- iplotx/network.py +2 -4
- iplotx/version.py +1 -1
- iplotx/vertex.py +10 -2
- {iplotx-0.10.0.dist-info → iplotx-0.11.1.dist-info}/METADATA +1 -1
- {iplotx-0.10.0.dist-info → iplotx-0.11.1.dist-info}/RECORD +15 -14
- iplotx/art3d/edge.py +0 -65
- {iplotx-0.10.0.dist-info → iplotx-0.11.1.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)
|
|
@@ -7,19 +7,14 @@ from typing import (
|
|
|
7
7
|
Sequence,
|
|
8
8
|
)
|
|
9
9
|
import numpy as np
|
|
10
|
-
import matplotlib as mpl
|
|
11
10
|
|
|
12
|
-
from
|
|
11
|
+
from ...typing import (
|
|
13
12
|
Pair,
|
|
14
13
|
)
|
|
15
14
|
|
|
16
15
|
|
|
17
|
-
def
|
|
16
|
+
def _compute_edge_segments_straight(
|
|
18
17
|
vcoord_data,
|
|
19
|
-
vpath_fig,
|
|
20
|
-
vsize_fig,
|
|
21
|
-
trans,
|
|
22
|
-
trans_inv,
|
|
23
18
|
layout_coordinate_system: str = "cartesian",
|
|
24
19
|
shrink: float = 0,
|
|
25
20
|
**kwargs,
|
|
@@ -45,37 +40,11 @@ def _compute_edge_path_straight(
|
|
|
45
40
|
f"Layout coordinate system not supported for straight edges in 3D: {layout_coordinate_system}.",
|
|
46
41
|
)
|
|
47
42
|
|
|
48
|
-
|
|
43
|
+
segments = [vcoord_data[0], vcoord_data[1]]
|
|
44
|
+
return segments
|
|
49
45
|
|
|
50
|
-
# Coordinates in figure (default) coords
|
|
51
|
-
vcoord_fig = trans(vcoord_data_cart)
|
|
52
46
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# Angles of the straight line
|
|
56
|
-
# FIXME: In 2D, this is only used to make space for loops
|
|
57
|
-
# let's ignore for now
|
|
58
|
-
# theta = atan2(*((vcoord_fig[1] - vcoord_fig[0])[::-1]))
|
|
59
|
-
theta = 0
|
|
60
|
-
|
|
61
|
-
# TODO: Shorten at starting vertex (?)
|
|
62
|
-
vs = vcoord_fig[0]
|
|
63
|
-
points.append(vs)
|
|
64
|
-
|
|
65
|
-
# TODO: Shorten at end vertex (?)
|
|
66
|
-
ve = vcoord_fig[1]
|
|
67
|
-
points.append(ve)
|
|
68
|
-
|
|
69
|
-
codes = ["MOVETO", "LINETO"]
|
|
70
|
-
path = mpl.path.Path(
|
|
71
|
-
points,
|
|
72
|
-
codes=[getattr(mpl.path.Path, x) for x in codes],
|
|
73
|
-
)
|
|
74
|
-
path.vertices = trans_inv(path.vertices)
|
|
75
|
-
return path, (theta, theta + np.pi)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def _compute_edge_path_3d(
|
|
47
|
+
def _compute_edge_segments(
|
|
79
48
|
*args,
|
|
80
49
|
tension: float = 0,
|
|
81
50
|
waypoints: str | tuple[float, float] | Sequence[tuple[float, float]] | np.ndarray = "none",
|
|
@@ -98,7 +67,7 @@ def _compute_edge_path_3d(
|
|
|
98
67
|
# )
|
|
99
68
|
|
|
100
69
|
if np.isscalar(tension) and (tension == 0):
|
|
101
|
-
return
|
|
70
|
+
return _compute_edge_segments_straight(
|
|
102
71
|
*args,
|
|
103
72
|
layout_coordinate_system=layout_coordinate_system,
|
|
104
73
|
**kwargs,
|
iplotx/art3d/vertex.py
CHANGED
|
@@ -9,7 +9,11 @@ import numpy as np
|
|
|
9
9
|
from matplotlib import (
|
|
10
10
|
cbook,
|
|
11
11
|
)
|
|
12
|
-
from mpl_toolkits.mplot3d
|
|
12
|
+
from mpl_toolkits.mplot3d import Axes3D
|
|
13
|
+
from mpl_toolkits.mplot3d.art3d import (
|
|
14
|
+
Path3DCollection,
|
|
15
|
+
text_2d_to_3d,
|
|
16
|
+
)
|
|
13
17
|
|
|
14
18
|
from ..utils.matplotlib import (
|
|
15
19
|
_forwarder,
|
|
@@ -32,6 +36,14 @@ from ..vertex import (
|
|
|
32
36
|
class Vertex3DCollection(VertexCollection, Path3DCollection):
|
|
33
37
|
"""Collection of vertex patches for plotting."""
|
|
34
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
|
+
|
|
35
47
|
def draw(self, renderer) -> None:
|
|
36
48
|
"""Draw the collection of vertices in 3D.
|
|
37
49
|
|
|
@@ -56,7 +68,7 @@ def vertex_collection_2d_to_3d(
|
|
|
56
68
|
col: The 2D VertexCollection to convert.
|
|
57
69
|
zs: The z coordinate(s) to use for the 3D vertices.
|
|
58
70
|
zdir: The axis to use as the z axis (default is "z").
|
|
59
|
-
depthshade: Whether to
|
|
71
|
+
depthshade: Whether to aply depth shading (default is True).
|
|
60
72
|
axlim_clip: Whether to clip the vertices to the axes limits (default is False).
|
|
61
73
|
"""
|
|
62
74
|
if not isinstance(col, VertexCollection):
|
|
@@ -67,3 +79,9 @@ def vertex_collection_2d_to_3d(
|
|
|
67
79
|
col._depthshade = depthshade
|
|
68
80
|
col._in_draw = False
|
|
69
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/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,23 @@ 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
|
|
195
201
|
|
|
196
202
|
def set_transform(self, transform: mpl.transforms.Transform) -> None:
|
|
197
203
|
"""Set the transform for the edges and their children."""
|
|
@@ -303,7 +309,7 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
303
309
|
"sizes": vsizes,
|
|
304
310
|
}
|
|
305
311
|
|
|
306
|
-
def
|
|
312
|
+
def _update_before_draw(self, transform=None):
|
|
307
313
|
"""Compute paths for the edges.
|
|
308
314
|
|
|
309
315
|
Loops split the largest wedge left open by other
|
|
@@ -512,42 +518,6 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
512
518
|
if not style.get("rotate", True):
|
|
513
519
|
self._label_collection.set_rotations(rotations)
|
|
514
520
|
|
|
515
|
-
def _update_arrows(
|
|
516
|
-
self,
|
|
517
|
-
) -> None:
|
|
518
|
-
"""Extract the start and/or end angles of the paths to compute arrows.
|
|
519
|
-
|
|
520
|
-
Parameters:
|
|
521
|
-
which: Which end of the edge to put an arrow on. Currently only "end" is accepted.
|
|
522
|
-
|
|
523
|
-
NOTE: This function does *not* update the arrow sizes/_transforms to the correct dpi
|
|
524
|
-
scaling. That's ok since the correct dpi scaling is set whenever there is a different
|
|
525
|
-
figure (before first draw) and whenever a draw is called.
|
|
526
|
-
"""
|
|
527
|
-
if not hasattr(self, "_arrows"):
|
|
528
|
-
return
|
|
529
|
-
|
|
530
|
-
transform = self.get_transform()
|
|
531
|
-
trans = transform.transform
|
|
532
|
-
|
|
533
|
-
for i, epath in enumerate(self.get_paths()):
|
|
534
|
-
# Offset the arrow to point to the end of the edge
|
|
535
|
-
self._arrows._offsets[i] = epath.vertices[-1]
|
|
536
|
-
|
|
537
|
-
# Rotate the arrow to point in the direction of the edge
|
|
538
|
-
apath = self._arrows._paths[i]
|
|
539
|
-
# NOTE: because the tip of the arrow is at (0, 0) in patch space,
|
|
540
|
-
# in theory it will rotate around that point already
|
|
541
|
-
v2 = trans(epath.vertices[-1])
|
|
542
|
-
v1 = trans(epath.vertices[-2])
|
|
543
|
-
dv = v2 - v1
|
|
544
|
-
theta = atan2(*(dv[::-1]))
|
|
545
|
-
theta_old = self._arrows._angles[i]
|
|
546
|
-
dtheta = theta - theta_old
|
|
547
|
-
mrot = np.array([[cos(dtheta), sin(dtheta)], [-sin(dtheta), cos(dtheta)]])
|
|
548
|
-
apath.vertices = apath.vertices @ mrot
|
|
549
|
-
self._arrows._angles[i] = theta
|
|
550
|
-
|
|
551
521
|
@_stale_wrapper
|
|
552
522
|
def draw(self, renderer):
|
|
553
523
|
# Visibility affects the children too
|
|
@@ -555,11 +525,15 @@ class EdgeCollection(mpl.collections.PatchCollection):
|
|
|
555
525
|
return
|
|
556
526
|
|
|
557
527
|
# This includes the subedges if present
|
|
558
|
-
self.
|
|
559
|
-
# This sets the arrow offsets
|
|
560
|
-
self._update_children()
|
|
528
|
+
self._update_before_draw()
|
|
561
529
|
|
|
530
|
+
# Now you can draw the edges
|
|
562
531
|
super().draw(renderer)
|
|
532
|
+
|
|
533
|
+
# This sets the labels offsets
|
|
534
|
+
self._update_labels()
|
|
535
|
+
|
|
536
|
+
# Now you can draw arrows and labels
|
|
563
537
|
for child in self.get_children():
|
|
564
538
|
child.draw(renderer)
|
|
565
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,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Support module with geometry- and path-related functions for edges.
|
|
3
3
|
|
|
4
|
-
3D geometry is in
|
|
4
|
+
3D geometry is in a separate module.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from typing import (
|
|
@@ -16,7 +16,6 @@ from ..typing import (
|
|
|
16
16
|
Pair,
|
|
17
17
|
)
|
|
18
18
|
from .ports import _get_port_unit_vector
|
|
19
|
-
from .geometry3d import _compute_edge_path_3d
|
|
20
19
|
|
|
21
20
|
|
|
22
21
|
def _compute_loops_per_angle(nloops, angles):
|
|
@@ -71,10 +70,10 @@ def _get_shorter_edge_coords(vpath, vsize, theta, shrink=0):
|
|
|
71
70
|
"""Get the coordinates of an edge tip such that it touches the vertex border.
|
|
72
71
|
|
|
73
72
|
Parameters:
|
|
74
|
-
vpath:
|
|
75
|
-
vsize:
|
|
76
|
-
theta:
|
|
77
|
-
shrink:
|
|
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).
|
|
78
77
|
"""
|
|
79
78
|
# Bound theta from -pi to pi (why is that not guaranteed?)
|
|
80
79
|
theta = (theta + pi) % (2 * pi) - pi
|
|
@@ -479,7 +478,7 @@ def _compute_edge_path_curved(
|
|
|
479
478
|
return path, tuple(thetas)
|
|
480
479
|
|
|
481
480
|
|
|
482
|
-
def
|
|
481
|
+
def _compute_edge_path(
|
|
483
482
|
*args,
|
|
484
483
|
tension: float = 0,
|
|
485
484
|
waypoints: str | tuple[float, float] | Sequence[tuple[float, float]] | np.ndarray = "none",
|
|
@@ -513,25 +512,3 @@ def _compute_edge_path_2d(
|
|
|
513
512
|
ports=ports,
|
|
514
513
|
**kwargs,
|
|
515
514
|
)
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
def _compute_edge_path(
|
|
519
|
-
vcoord_data,
|
|
520
|
-
*args,
|
|
521
|
-
**kwargs,
|
|
522
|
-
):
|
|
523
|
-
"""Compute the edge path in either 2D or 3D.
|
|
524
|
-
|
|
525
|
-
Parameters:
|
|
526
|
-
vcoord_data: The vertex coordinates in data coordinates. This is used to
|
|
527
|
-
determine the dimensionality of the layout.
|
|
528
|
-
*args: Additional arguments passed to the internal functions.
|
|
529
|
-
**kwargs: Additional keyword arguments passed to the internal functions.
|
|
530
|
-
|
|
531
|
-
Returns:
|
|
532
|
-
The computed edge path and the angles at the start and end of the edge.
|
|
533
|
-
"""
|
|
534
|
-
ndim = len(vcoord_data[0])
|
|
535
|
-
if ndim == 2:
|
|
536
|
-
return _compute_edge_path_2d(vcoord_data, *args, **kwargs)
|
|
537
|
-
return _compute_edge_path_3d(vcoord_data, *args, **kwargs)
|
iplotx/groups.py
CHANGED
|
@@ -65,7 +65,7 @@ class GroupingArtist(PatchCollection):
|
|
|
65
65
|
self._points_per_curve = points_per_curve
|
|
66
66
|
|
|
67
67
|
network = kwargs.pop("network", None)
|
|
68
|
-
self.layout = normalise_layout(layout, network=network)
|
|
68
|
+
self.layout = layout = normalise_layout(layout, network=network)
|
|
69
69
|
self.ndim = layout.shape[1]
|
|
70
70
|
|
|
71
71
|
patches, grouping, coords_hulls = self._create_patches(
|
iplotx/label.py
CHANGED
|
@@ -77,6 +77,16 @@ 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
|
+
|
|
80
90
|
def set_transform(self, transform: mpl.transforms.Transform) -> None:
|
|
81
91
|
"""Set the transform for this artist and children.
|
|
82
92
|
|
iplotx/network.py
CHANGED
|
@@ -384,10 +384,8 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
384
384
|
# Handle zorder manually, just like in AxesBase in mpl
|
|
385
385
|
children = list(self.get_children())
|
|
386
386
|
children.sort(key=lambda x: x.zorder)
|
|
387
|
-
for
|
|
388
|
-
|
|
389
|
-
art.do_3d_projection()
|
|
390
|
-
art.draw(renderer)
|
|
387
|
+
for child in children:
|
|
388
|
+
child.draw(renderer)
|
|
391
389
|
|
|
392
390
|
|
|
393
391
|
def _update_from_internal(style, row, kind):
|
iplotx/version.py
CHANGED
iplotx/vertex.py
CHANGED
|
@@ -315,6 +315,10 @@ class VertexCollection(PatchCollection):
|
|
|
315
315
|
transform=transform,
|
|
316
316
|
)
|
|
317
317
|
|
|
318
|
+
def get_ndim(self):
|
|
319
|
+
"""Get the number of dimensions of the layout."""
|
|
320
|
+
return self._layout.shape[1]
|
|
321
|
+
|
|
318
322
|
def get_labels(self):
|
|
319
323
|
"""Get the vertex labels.
|
|
320
324
|
|
|
@@ -361,6 +365,10 @@ class VertexCollection(PatchCollection):
|
|
|
361
365
|
rotations = np.arctan2(doffsets_fig[:, 1], doffsets_fig[:, 0])
|
|
362
366
|
self.get_labels().set_rotations(rotations)
|
|
363
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
|
+
|
|
364
372
|
@mpl.artist.allow_rasterization
|
|
365
373
|
def draw(self, renderer):
|
|
366
374
|
if not self.get_visible():
|
|
@@ -371,7 +379,8 @@ class VertexCollection(PatchCollection):
|
|
|
371
379
|
if len(self.get_paths()) == 0:
|
|
372
380
|
return
|
|
373
381
|
|
|
374
|
-
self.
|
|
382
|
+
self._update_before_draw()
|
|
383
|
+
super().draw(renderer)
|
|
375
384
|
|
|
376
385
|
# Set the label rotations already, hopefully this is not too early
|
|
377
386
|
self._update_children()
|
|
@@ -379,7 +388,6 @@ class VertexCollection(PatchCollection):
|
|
|
379
388
|
# NOTE: This draws the vertices first, then the labels.
|
|
380
389
|
# The correct order would be vertex1->label1->vertex2->label2, etc.
|
|
381
390
|
# We might fix if we manage to find a way to do it.
|
|
382
|
-
super().draw(renderer)
|
|
383
391
|
for child in self.get_children():
|
|
384
392
|
child.draw(renderer)
|
|
385
393
|
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
iplotx/__init__.py,sha256=RzSct91jO8abrxOIn33rKEnDUgYpu1oj4olbObgX_hs,489
|
|
2
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=g6ahm61BSBmd2weIjr40MvPi_GcNRgvNb9YklQsiza4,6784
|
|
5
|
+
iplotx/label.py,sha256=7eS8ByadrhdIFOZz19U4VrS-oXY_ndFYNB-D4RZbFqI,9573
|
|
6
6
|
iplotx/layout.py,sha256=KxmRLqjo8AYCBAmXez8rIiLU2sM34qhb6ox9AHYwRyE,4839
|
|
7
|
-
iplotx/network.py,sha256=
|
|
7
|
+
iplotx/network.py,sha256=ae5rZwzWxmcBQXx1Y0q24jaXcM1hT1kip-JKsyk11QY,13385
|
|
8
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/art3d/
|
|
14
|
-
iplotx/art3d/
|
|
15
|
-
iplotx/edge/
|
|
16
|
-
iplotx/edge/
|
|
17
|
-
iplotx/edge/
|
|
18
|
-
iplotx/edge/
|
|
11
|
+
iplotx/version.py,sha256=BVfqBj50ae0jogJGXsQXi_fML0WjuC4GLFRHZQWawYY,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
|
|
19
20
|
iplotx/edge/leaf.py,sha256=SyGMv2PIOoH0pey8-aMVaZheK3hNe1Qz_okcyWbc4E4,4268
|
|
20
21
|
iplotx/edge/ports.py,sha256=BpkbiEhX4mPBBAhOv4jcKFG4Y8hxXz5GRtVLCC0jbtI,1235
|
|
21
22
|
iplotx/ingest/__init__.py,sha256=S0YfnXcFKseB7ZBQc4yRt0cNDsLlhqdom0TmSY3OY2E,4756
|
|
@@ -36,6 +37,6 @@ iplotx/utils/geometry.py,sha256=6RrC6qaB0-1vIk1LhGA4CfsiMd-9JNniSPyL_l9mshE,9245
|
|
|
36
37
|
iplotx/utils/internal.py,sha256=WWfcZDGK8Ut1y_tOHRGg9wSqY1bwSeLQO7dHM_8Tvwo,107
|
|
37
38
|
iplotx/utils/matplotlib.py,sha256=wELE73quQv10-1w9uA5eDTgkZkylJvjg7pd3K5tZPOo,6294
|
|
38
39
|
iplotx/utils/style.py,sha256=vyNP80nDYVinqm6_9ltCJCtjK35ZcGlHvOskNv3eQBc,4225
|
|
39
|
-
iplotx-0.
|
|
40
|
-
iplotx-0.
|
|
41
|
-
iplotx-0.
|
|
40
|
+
iplotx-0.11.1.dist-info/METADATA,sha256=1F0wH64PAw1_Bx-Qv7RSTgz6nwoQfcbRDJjmlR3KYfc,4880
|
|
41
|
+
iplotx-0.11.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
42
|
+
iplotx-0.11.1.dist-info/RECORD,,
|
iplotx/art3d/edge.py
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Module containing code to manipulate edge visualisations in 3D, especially the Edge3DCollection class.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from mpl_toolkits.mplot3d.art3d import (
|
|
6
|
-
Line3DCollection,
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
from ..utils.matplotlib import (
|
|
10
|
-
_forwarder,
|
|
11
|
-
)
|
|
12
|
-
from ..edge import (
|
|
13
|
-
EdgeCollection,
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
@_forwarder(
|
|
18
|
-
(
|
|
19
|
-
"set_clip_path",
|
|
20
|
-
"set_clip_box",
|
|
21
|
-
"set_snap",
|
|
22
|
-
"set_sketch_params",
|
|
23
|
-
"set_animated",
|
|
24
|
-
"set_picker",
|
|
25
|
-
)
|
|
26
|
-
)
|
|
27
|
-
class Edge3DCollection(Line3DCollection):
|
|
28
|
-
"""Collection of vertex patches for plotting."""
|
|
29
|
-
|
|
30
|
-
pass
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def edge_collection_2d_to_3d(
|
|
34
|
-
col: EdgeCollection,
|
|
35
|
-
zdir: str = "z",
|
|
36
|
-
depthshade: bool = True,
|
|
37
|
-
axlim_clip: bool = False,
|
|
38
|
-
):
|
|
39
|
-
"""Convert a 2D EdgeCollection to a 3D Edge3DCollection.
|
|
40
|
-
|
|
41
|
-
Parameters:
|
|
42
|
-
col: The 2D EdgeCollection to convert.
|
|
43
|
-
zs: The z coordinate(s) to use for the 3D vertices.
|
|
44
|
-
zdir: The axis to use as the z axis (default is "z").
|
|
45
|
-
depthshade: Whether to apply depth shading (default is True).
|
|
46
|
-
axlim_clip: Whether to clip the vertices to the axes limits (default is False).
|
|
47
|
-
"""
|
|
48
|
-
if not isinstance(col, EdgeCollection):
|
|
49
|
-
raise TypeError("vertices must be a VertexCollection")
|
|
50
|
-
|
|
51
|
-
# TODO: if we make Edge3DCollection a dynamic drawer, this will need to change
|
|
52
|
-
# fundamentally. Also, this currently does not handle labels properly.
|
|
53
|
-
vinfo = col._get_adjacent_vertices_info()
|
|
54
|
-
|
|
55
|
-
segments3d = []
|
|
56
|
-
for offset1, offset2 in vinfo["offsets"]:
|
|
57
|
-
segment = [tuple(offset1), tuple(offset2)]
|
|
58
|
-
segments3d.append(segment)
|
|
59
|
-
|
|
60
|
-
# NOTE: after this line, none of the EdgeCollection methods will work
|
|
61
|
-
# It's become a static drawer now
|
|
62
|
-
col.__class__ = Edge3DCollection
|
|
63
|
-
|
|
64
|
-
col.set_segments(segments3d)
|
|
65
|
-
col._axlim_clip = axlim_clip
|
|
File without changes
|