iplotx 0.1.0__py3-none-any.whl → 0.2.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/__init__.py +22 -1
- iplotx/edge/__init__.py +623 -0
- iplotx/edge/arrow.py +220 -10
- iplotx/edge/geometry.py +392 -0
- iplotx/edge/ports.py +47 -0
- iplotx/groups.py +93 -45
- iplotx/ingest/__init__.py +155 -0
- iplotx/ingest/heuristics.py +209 -0
- iplotx/ingest/providers/network/igraph.py +96 -0
- iplotx/ingest/providers/network/networkx.py +133 -0
- iplotx/ingest/providers/tree/biopython.py +105 -0
- iplotx/ingest/providers/tree/cogent3.py +112 -0
- iplotx/ingest/providers/tree/ete4.py +112 -0
- iplotx/ingest/providers/tree/skbio.py +112 -0
- iplotx/ingest/typing.py +100 -0
- iplotx/label.py +162 -0
- iplotx/layout.py +139 -0
- iplotx/network.py +161 -379
- iplotx/plotting.py +157 -56
- iplotx/style.py +391 -0
- iplotx/tree.py +312 -0
- iplotx/typing.py +55 -41
- iplotx/utils/geometry.py +128 -81
- iplotx/utils/internal.py +3 -0
- iplotx/utils/matplotlib.py +58 -38
- iplotx/utils/style.py +1 -0
- iplotx/version.py +5 -1
- iplotx/vertex.py +305 -55
- iplotx-0.2.1.dist-info/METADATA +88 -0
- iplotx-0.2.1.dist-info/RECORD +31 -0
- iplotx/edge/common.py +0 -47
- iplotx/edge/directed.py +0 -149
- iplotx/edge/label.py +0 -50
- iplotx/edge/undirected.py +0 -447
- iplotx/heuristics.py +0 -114
- iplotx/importing.py +0 -13
- iplotx/styles.py +0 -186
- iplotx-0.1.0.dist-info/METADATA +0 -47
- iplotx-0.1.0.dist-info/RECORD +0 -20
- {iplotx-0.1.0.dist-info → iplotx-0.2.1.dist-info}/WHEEL +0 -0
iplotx/ingest/typing.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Typing module for data/object ingestion. This module described the abstract data types that providers need to comply with to be compatible with iplotx.
|
|
3
|
+
|
|
4
|
+
Networkx and trees are treated separately for practical reasons: many tree analysis libraries rely heavily on recursive data structures, which do not
|
|
5
|
+
work as well on general networks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import (
|
|
9
|
+
NotRequired,
|
|
10
|
+
TypedDict,
|
|
11
|
+
Protocol,
|
|
12
|
+
Optional,
|
|
13
|
+
Sequence,
|
|
14
|
+
)
|
|
15
|
+
from collections.abc import Hashable
|
|
16
|
+
import pandas as pd
|
|
17
|
+
from ..typing import (
|
|
18
|
+
GraphType,
|
|
19
|
+
LayoutType,
|
|
20
|
+
TreeType,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class NetworkData(TypedDict):
|
|
25
|
+
"""Network data structure for iplotx."""
|
|
26
|
+
|
|
27
|
+
directed: bool
|
|
28
|
+
vertex_df: pd.DataFrame
|
|
29
|
+
edge_df: pd.DataFrame
|
|
30
|
+
ndim: int
|
|
31
|
+
network_library: NotRequired[str]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class NetworkDataProvider(Protocol):
|
|
35
|
+
"""Protocol for network data ingestion provider for iplotx."""
|
|
36
|
+
|
|
37
|
+
def __call__(
|
|
38
|
+
self,
|
|
39
|
+
network: GraphType,
|
|
40
|
+
layout: Optional[LayoutType] = None,
|
|
41
|
+
vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
|
|
42
|
+
edge_labels: Optional[Sequence[str] | dict] = None,
|
|
43
|
+
) -> NetworkData:
|
|
44
|
+
"""Create network data object for iplotx from any provider."""
|
|
45
|
+
raise NotImplementedError("Network data providers must implement this method.")
|
|
46
|
+
|
|
47
|
+
def check_dependencies(
|
|
48
|
+
self,
|
|
49
|
+
):
|
|
50
|
+
"""Check whether the dependencies for this provider are installed."""
|
|
51
|
+
raise NotImplementedError("Network data providers must implement this method.")
|
|
52
|
+
|
|
53
|
+
def graph_type(
|
|
54
|
+
self,
|
|
55
|
+
):
|
|
56
|
+
"""Return the graph type from this provider to check for instances."""
|
|
57
|
+
raise NotImplementedError("Network data providers must implement this method.")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TreeData(TypedDict):
|
|
61
|
+
"""Tree data structure for iplotx."""
|
|
62
|
+
|
|
63
|
+
rooted: bool
|
|
64
|
+
directed: bool | str
|
|
65
|
+
root: Optional[Hashable]
|
|
66
|
+
leaves: list[Hashable]
|
|
67
|
+
vertex_df: dict[Hashable, tuple[float, float]]
|
|
68
|
+
edge_df: dict[Hashable, Sequence[tuple[float, float]]]
|
|
69
|
+
layout_coordinate_system: str
|
|
70
|
+
layout_name: str
|
|
71
|
+
ndim: int
|
|
72
|
+
tree_library: NotRequired[str]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TreeDataProvider(Protocol):
|
|
76
|
+
"""Protocol for tree data ingestion provider for iplotx."""
|
|
77
|
+
|
|
78
|
+
def __call__(
|
|
79
|
+
self,
|
|
80
|
+
tree: TreeType,
|
|
81
|
+
layout: str | LayoutType,
|
|
82
|
+
orientation: Optional[str] = None,
|
|
83
|
+
directed: bool | str = False,
|
|
84
|
+
vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series] = None,
|
|
85
|
+
edge_labels: Optional[Sequence[str] | dict] = None,
|
|
86
|
+
) -> TreeData:
|
|
87
|
+
"""Create tree data object for iplotx from any provider."""
|
|
88
|
+
raise NotImplementedError("Tree data providers must implement this method.")
|
|
89
|
+
|
|
90
|
+
def check_dependencies(
|
|
91
|
+
self,
|
|
92
|
+
):
|
|
93
|
+
"""Check whether the dependencies for this provider are installed."""
|
|
94
|
+
raise NotImplementedError("Tree data providers must implement this method.")
|
|
95
|
+
|
|
96
|
+
def tree_type(
|
|
97
|
+
self,
|
|
98
|
+
):
|
|
99
|
+
"""Return the tree type from this provider to check for instances."""
|
|
100
|
+
raise NotImplementedError("Tree data providers must implement this method.")
|
iplotx/label.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for label collection in iplotx.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import (
|
|
6
|
+
Optional,
|
|
7
|
+
Sequence,
|
|
8
|
+
)
|
|
9
|
+
import numpy as np
|
|
10
|
+
import matplotlib as mpl
|
|
11
|
+
|
|
12
|
+
from .style import (
|
|
13
|
+
rotate_style,
|
|
14
|
+
copy_with_deep_values,
|
|
15
|
+
)
|
|
16
|
+
from .utils.matplotlib import (
|
|
17
|
+
_stale_wrapper,
|
|
18
|
+
_forwarder,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@_forwarder(
|
|
23
|
+
(
|
|
24
|
+
"set_clip_path",
|
|
25
|
+
"set_clip_box",
|
|
26
|
+
"set_snap",
|
|
27
|
+
"set_sketch_params",
|
|
28
|
+
"set_animated",
|
|
29
|
+
"set_picker",
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
class LabelCollection(mpl.artist.Artist):
|
|
33
|
+
"""Collection of labels for iplotx with styles.
|
|
34
|
+
|
|
35
|
+
NOTE: This class is not a subclass of `mpl.collections.Collection`, although in some ways items
|
|
36
|
+
behaves like one. It is named LabelCollection quite literally to indicate it contains a list of
|
|
37
|
+
labels for vertices, edges, etc.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
labels: Sequence[str],
|
|
43
|
+
style: Optional[dict[str, dict]] = None,
|
|
44
|
+
offsets: Optional[np.ndarray] = None,
|
|
45
|
+
transform: mpl.transforms.Transform = mpl.transforms.IdentityTransform(),
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Initialize a collection of labels.
|
|
48
|
+
|
|
49
|
+
Parameters:
|
|
50
|
+
labels: A sequence of labels to be displayed.
|
|
51
|
+
style: A dictionary of styles to apply to the labels. The keys are style properties.
|
|
52
|
+
offsets: A sequence of offsets for each label, specifying the position of the label.
|
|
53
|
+
transform: A transform to apply to the labels. This is usually ax.transData.
|
|
54
|
+
"""
|
|
55
|
+
self._labels = labels
|
|
56
|
+
self._offsets = offsets if offsets is not None else np.zeros((len(labels), 2))
|
|
57
|
+
self._style = style
|
|
58
|
+
super().__init__()
|
|
59
|
+
|
|
60
|
+
self.set_transform(transform)
|
|
61
|
+
self._create_artists()
|
|
62
|
+
|
|
63
|
+
def get_children(self) -> tuple[mpl.artist.Artist]:
|
|
64
|
+
"""Get the children of this artist, which are the label artists."""
|
|
65
|
+
return tuple(self._labelartists)
|
|
66
|
+
|
|
67
|
+
def set_figure(self, fig) -> None:
|
|
68
|
+
"""Set the figure of this artist.
|
|
69
|
+
|
|
70
|
+
Parameters:
|
|
71
|
+
fig: The figure to set.
|
|
72
|
+
"""
|
|
73
|
+
super().set_figure(fig)
|
|
74
|
+
for child in self.get_children():
|
|
75
|
+
child.set_figure(fig)
|
|
76
|
+
self._update_offsets(dpi=fig.dpi)
|
|
77
|
+
|
|
78
|
+
def _get_margins_with_dpi(self, dpi: float = 72.0) -> np.ndarray:
|
|
79
|
+
return self._margins * dpi / 72.0
|
|
80
|
+
|
|
81
|
+
def _create_artists(self) -> None:
|
|
82
|
+
style = copy_with_deep_values(self._style) if self._style is not None else {}
|
|
83
|
+
transform = self.get_transform()
|
|
84
|
+
|
|
85
|
+
margins = []
|
|
86
|
+
|
|
87
|
+
forbidden_props = ["rotate"]
|
|
88
|
+
for prop in forbidden_props:
|
|
89
|
+
if prop in style:
|
|
90
|
+
del style[prop]
|
|
91
|
+
|
|
92
|
+
arts = []
|
|
93
|
+
for i, (anchor_id, label) in enumerate(self._labels.items()):
|
|
94
|
+
stylei = rotate_style(style, index=i, key=anchor_id)
|
|
95
|
+
# Margins are handled separately
|
|
96
|
+
hmargin = stylei.pop("hmargin", 0.0)
|
|
97
|
+
vmargin = stylei.pop("vmargin", 0.0)
|
|
98
|
+
margins.append((hmargin, vmargin))
|
|
99
|
+
|
|
100
|
+
art = mpl.text.Text(
|
|
101
|
+
self._offsets[i][0],
|
|
102
|
+
self._offsets[i][1],
|
|
103
|
+
label,
|
|
104
|
+
transform=transform,
|
|
105
|
+
**stylei,
|
|
106
|
+
)
|
|
107
|
+
arts.append(art)
|
|
108
|
+
self._labelartists = arts
|
|
109
|
+
self._margins = np.array(margins)
|
|
110
|
+
|
|
111
|
+
def _update_offsets(self, dpi: float = 72.0) -> None:
|
|
112
|
+
"""Update offsets including margins."""
|
|
113
|
+
offsets = self._adjust_offsets_for_margins(self._offsets, dpi=dpi)
|
|
114
|
+
self.set_offsets(offsets)
|
|
115
|
+
|
|
116
|
+
def get_offsets(self) -> np.ndarray:
|
|
117
|
+
"""Get the positions (offsets) of the labels."""
|
|
118
|
+
return self._offsets
|
|
119
|
+
|
|
120
|
+
def _adjust_offsets_for_margins(self, offsets, dpi=72.0):
|
|
121
|
+
margins = self._get_margins_with_dpi(dpi=dpi)
|
|
122
|
+
if (margins != 0).any():
|
|
123
|
+
transform = self.get_transform()
|
|
124
|
+
trans = transform.transform
|
|
125
|
+
trans_inv = transform.inverted().transform
|
|
126
|
+
offsets = trans_inv(trans(offsets) + margins)
|
|
127
|
+
return offsets
|
|
128
|
+
|
|
129
|
+
def set_offsets(self, offsets) -> None:
|
|
130
|
+
"""Set positions (offsets) of the labels.
|
|
131
|
+
|
|
132
|
+
Parameters:
|
|
133
|
+
offsets: A sequence of offsets for each label, specifying the position of the label.
|
|
134
|
+
"""
|
|
135
|
+
self._offsets = np.asarray(offsets)
|
|
136
|
+
for art, offset in zip(self._labelartists, self._offsets):
|
|
137
|
+
art.set_position((offset[0], offset[1]))
|
|
138
|
+
|
|
139
|
+
def set_rotations(self, rotations: Sequence[float]) -> None:
|
|
140
|
+
"""Set the rotations of the labels.
|
|
141
|
+
|
|
142
|
+
Parameters:
|
|
143
|
+
rotations: A sequence of rotations in radians for each label.
|
|
144
|
+
"""
|
|
145
|
+
for art, rotation in zip(self._labelartists, rotations):
|
|
146
|
+
rot_deg = 180.0 / np.pi * rotation
|
|
147
|
+
# Force the font size to be upwards
|
|
148
|
+
rot_deg = ((rot_deg + 90) % 180) - 90
|
|
149
|
+
art.set_rotation(rot_deg)
|
|
150
|
+
|
|
151
|
+
@_stale_wrapper
|
|
152
|
+
def draw(self, renderer) -> None:
|
|
153
|
+
"""Draw each of the children, with some buffering mechanism."""
|
|
154
|
+
if not self.get_visible():
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
self._update_offsets(dpi=renderer.dpi)
|
|
158
|
+
|
|
159
|
+
# We should manage zorder ourselves, but we need to compute
|
|
160
|
+
# the new offsets and angles of arrows from the edges before drawing them
|
|
161
|
+
for art in self.get_children():
|
|
162
|
+
art.draw(renderer)
|
iplotx/layout.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Layout functions, currently limited to trees.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from collections.abc import Hashable
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def compute_tree_layout(
|
|
11
|
+
tree,
|
|
12
|
+
layout: str,
|
|
13
|
+
orientation: str,
|
|
14
|
+
**kwargs,
|
|
15
|
+
) -> dict[Hashable, list[float]]:
|
|
16
|
+
"""Compute the layout for a tree.
|
|
17
|
+
|
|
18
|
+
Parameters:
|
|
19
|
+
tree: The tree to compute the layout for.
|
|
20
|
+
layout: The name of the layout, e.g. "horizontal" or "radial".
|
|
21
|
+
orientation: The orientation of the layout, e.g. "right", "left", "descending", or
|
|
22
|
+
"ascending".
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A layout dictionary with node positions.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
if layout == "radial":
|
|
29
|
+
layout_dict = _circular_tree_layout(tree, orientation=orientation, **kwargs)
|
|
30
|
+
elif layout == "horizontal":
|
|
31
|
+
layout_dict = _horizontal_tree_layout(tree, orientation=orientation, **kwargs)
|
|
32
|
+
elif layout == "vertical":
|
|
33
|
+
layout_dict = _vertical_tree_layout(tree, orientation=orientation, **kwargs)
|
|
34
|
+
else:
|
|
35
|
+
raise ValueError(f"Tree layout not available: {layout}")
|
|
36
|
+
|
|
37
|
+
return layout_dict
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _horizontal_tree_layout_right(
|
|
41
|
+
tree,
|
|
42
|
+
root_fun: callable,
|
|
43
|
+
preorder_fun: callable,
|
|
44
|
+
postorder_fun: callable,
|
|
45
|
+
children_fun: callable,
|
|
46
|
+
branch_length_fun: callable,
|
|
47
|
+
) -> dict[Hashable, list[float]]:
|
|
48
|
+
"""Build a tree layout horizontally, left to right.
|
|
49
|
+
|
|
50
|
+
The strategy is the usual one:
|
|
51
|
+
1. Compute the y values for the leaves, from 0 upwards.
|
|
52
|
+
2. Compute the y values for the internal nodes, bubbling up (postorder).
|
|
53
|
+
3. Set the x value for the root as 0.
|
|
54
|
+
4. Compute the x value of all nodes, trickling down (BFS/preorder).
|
|
55
|
+
5. Compute the edges from the end nodes.
|
|
56
|
+
"""
|
|
57
|
+
layout = {}
|
|
58
|
+
|
|
59
|
+
# Set the y values for vertices
|
|
60
|
+
i = 0
|
|
61
|
+
for node in postorder_fun(tree):
|
|
62
|
+
children = children_fun(node)
|
|
63
|
+
if len(children) == 0:
|
|
64
|
+
layout[node] = [None, i]
|
|
65
|
+
i += 1
|
|
66
|
+
else:
|
|
67
|
+
layout[node] = [
|
|
68
|
+
None,
|
|
69
|
+
np.mean([layout[child][1] for child in children]),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
# Set the x values for vertices
|
|
73
|
+
layout[root_fun(tree)][0] = 0
|
|
74
|
+
for node in preorder_fun(tree):
|
|
75
|
+
for child in children_fun(node):
|
|
76
|
+
bl = branch_length_fun(child)
|
|
77
|
+
if bl is None:
|
|
78
|
+
bl = 1.0
|
|
79
|
+
layout[child][0] = layout[node][0] + bl
|
|
80
|
+
|
|
81
|
+
return layout
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _horizontal_tree_layout(
|
|
85
|
+
tree,
|
|
86
|
+
orientation="right",
|
|
87
|
+
**kwargs,
|
|
88
|
+
) -> dict[Hashable, list[float]]:
|
|
89
|
+
"""Horizontal tree layout."""
|
|
90
|
+
if orientation not in ("right", "left"):
|
|
91
|
+
raise ValueError("Orientation must be 'right' or 'left'.")
|
|
92
|
+
|
|
93
|
+
layout = _horizontal_tree_layout_right(tree, **kwargs)
|
|
94
|
+
|
|
95
|
+
if orientation == "left":
|
|
96
|
+
for key in layout:
|
|
97
|
+
layout[key][0] *= -1
|
|
98
|
+
return layout
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _vertical_tree_layout(
|
|
102
|
+
tree,
|
|
103
|
+
orientation="descending",
|
|
104
|
+
**kwargs,
|
|
105
|
+
) -> dict[Hashable, list[float]]:
|
|
106
|
+
"""Vertical tree layout."""
|
|
107
|
+
sign = 1 if orientation == "descending" else -1
|
|
108
|
+
layout = _horizontal_tree_layout(tree, **kwargs)
|
|
109
|
+
for key, value in layout.items():
|
|
110
|
+
# Invert x and y
|
|
111
|
+
layout[key] = value[::-1]
|
|
112
|
+
# Orient vertically
|
|
113
|
+
layout[key][1] *= sign
|
|
114
|
+
return layout
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _circular_tree_layout(
|
|
118
|
+
tree,
|
|
119
|
+
orientation="right",
|
|
120
|
+
starting_angle=0,
|
|
121
|
+
angular_span=360,
|
|
122
|
+
**kwargs,
|
|
123
|
+
) -> dict[Hashable, list[float]]:
|
|
124
|
+
"""Circular tree layout."""
|
|
125
|
+
# Short form
|
|
126
|
+
th = starting_angle * np.pi / 180
|
|
127
|
+
th_span = angular_span * np.pi / 180
|
|
128
|
+
sign = 1 if orientation == "right" else -1
|
|
129
|
+
|
|
130
|
+
layout = _horizontal_tree_layout_right(tree, **kwargs)
|
|
131
|
+
ymax = max(point[1] for point in layout.values())
|
|
132
|
+
for key, (x, y) in layout.items():
|
|
133
|
+
r = x
|
|
134
|
+
theta = sign * th_span * y / (ymax + 1) + th
|
|
135
|
+
# We export r and theta to ensure theta does not
|
|
136
|
+
# modulo 2pi if we take the tan and then arctan later.
|
|
137
|
+
layout[key] = (r, theta)
|
|
138
|
+
|
|
139
|
+
return layout
|