iplotx 0.4.0__py3-none-any.whl → 0.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iplotx/cascades.py +19 -28
- iplotx/edge/__init__.py +74 -6
- iplotx/edge/arrow.py +10 -3
- iplotx/edge/geometry.py +5 -17
- iplotx/edge/ports.py +3 -2
- iplotx/groups.py +1 -3
- iplotx/ingest/__init__.py +5 -14
- iplotx/ingest/heuristics.py +1 -4
- iplotx/ingest/providers/network/igraph.py +4 -12
- iplotx/ingest/providers/network/networkx.py +6 -20
- iplotx/ingest/providers/network/simple.py +2 -9
- iplotx/ingest/providers/tree/biopython.py +2 -5
- iplotx/ingest/providers/tree/cogent3.py +2 -5
- iplotx/ingest/providers/tree/ete4.py +2 -5
- iplotx/ingest/providers/tree/simple.py +97 -0
- iplotx/ingest/providers/tree/skbio.py +2 -5
- iplotx/ingest/typing.py +5 -14
- iplotx/network.py +4 -7
- iplotx/plotting.py +1 -1
- iplotx/style/__init__.py +131 -106
- iplotx/style/leaf_info.py +3 -0
- iplotx/style/library.py +94 -0
- iplotx/tree.py +55 -42
- iplotx/typing.py +2 -0
- iplotx/utils/geometry.py +32 -40
- iplotx/utils/matplotlib.py +13 -10
- iplotx/utils/style.py +6 -1
- iplotx/version.py +1 -1
- iplotx/vertex.py +16 -23
- {iplotx-0.4.0.dist-info → iplotx-0.5.1.dist-info}/METADATA +33 -15
- iplotx-0.5.1.dist-info/RECORD +38 -0
- iplotx-0.4.0.dist-info/RECORD +0 -37
- {iplotx-0.4.0.dist-info → iplotx-0.5.1.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Any,
|
|
3
|
+
Optional,
|
|
4
|
+
Sequence,
|
|
5
|
+
Iterable,
|
|
6
|
+
Self,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
from ...typing import (
|
|
10
|
+
TreeDataProvider,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SimpleTree:
|
|
15
|
+
"""A simple tree class for educational purposes.
|
|
16
|
+
|
|
17
|
+
Properties:
|
|
18
|
+
children: Children SimpleTree objects.
|
|
19
|
+
branch_length: Length of the branch leading to this node/tree.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
children: Sequence[Self] = []
|
|
23
|
+
branch_length: float = 1
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_dict(cls, data: dict) -> Self:
|
|
27
|
+
"""Create a SimpleTree from a dictionary.
|
|
28
|
+
|
|
29
|
+
Parameters:
|
|
30
|
+
data: A dictionary representation of the tree, with "children" as a list offset_transform
|
|
31
|
+
child nodes and an optional "branch_length" property (float).
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
An instance of SimpleTree constructed from the provided dictionary.
|
|
35
|
+
"""
|
|
36
|
+
tree = cls()
|
|
37
|
+
tree.branch_length = data.get("branch_length", 1)
|
|
38
|
+
tree.children = [cls.from_dict(child) for child in data.get("children", [])]
|
|
39
|
+
return tree
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SimpleTreeDataProvider(TreeDataProvider):
|
|
43
|
+
def is_rooted(self) -> bool:
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
def get_root(self) -> Any:
|
|
47
|
+
"""Get the root node of the tree."""
|
|
48
|
+
return self.tree
|
|
49
|
+
|
|
50
|
+
def preorder(self) -> Iterable[dict[dict | str, Any]]:
|
|
51
|
+
def _recur(node):
|
|
52
|
+
yield node
|
|
53
|
+
for child in node.children:
|
|
54
|
+
yield from _recur(child)
|
|
55
|
+
|
|
56
|
+
yield from _recur(self.tree)
|
|
57
|
+
|
|
58
|
+
def postorder(self) -> Iterable[dict[dict | str, Any]]:
|
|
59
|
+
def _recur(node):
|
|
60
|
+
for child in node.children:
|
|
61
|
+
yield from _recur(child)
|
|
62
|
+
yield node
|
|
63
|
+
|
|
64
|
+
yield from _recur(self.tree)
|
|
65
|
+
|
|
66
|
+
def get_leaves(self) -> Sequence[Any]:
|
|
67
|
+
def _recur(node):
|
|
68
|
+
if len(node.children) == 0:
|
|
69
|
+
yield node
|
|
70
|
+
else:
|
|
71
|
+
for child in node.children:
|
|
72
|
+
yield from _recur(child)
|
|
73
|
+
|
|
74
|
+
return list(_recur(self.tree))
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def get_children(node: Any) -> Sequence[Any]:
|
|
78
|
+
return node.children
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def get_branch_length(node: Any) -> Optional[float]:
|
|
82
|
+
return node.branch_length
|
|
83
|
+
|
|
84
|
+
@staticmethod
|
|
85
|
+
def check_dependencies() -> bool:
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def tree_type():
|
|
90
|
+
return SimpleTree
|
|
91
|
+
|
|
92
|
+
def get_support(self):
|
|
93
|
+
"""Get support/confidence values for all nodes."""
|
|
94
|
+
support_dict = {}
|
|
95
|
+
for node in self.preorder():
|
|
96
|
+
support_dict[node] = None
|
|
97
|
+
return support_dict
|
|
@@ -3,6 +3,7 @@ from typing import (
|
|
|
3
3
|
Optional,
|
|
4
4
|
Sequence,
|
|
5
5
|
)
|
|
6
|
+
import importlib
|
|
6
7
|
from ...typing import (
|
|
7
8
|
TreeDataProvider,
|
|
8
9
|
)
|
|
@@ -28,11 +29,7 @@ class SkbioDataProvider(TreeDataProvider):
|
|
|
28
29
|
|
|
29
30
|
@staticmethod
|
|
30
31
|
def check_dependencies() -> bool:
|
|
31
|
-
|
|
32
|
-
from skbio import TreeNode
|
|
33
|
-
except ImportError:
|
|
34
|
-
return False
|
|
35
|
-
return True
|
|
32
|
+
return importlib.util.find_spec("skbio") is not None
|
|
36
33
|
|
|
37
34
|
@staticmethod
|
|
38
35
|
def tree_type():
|
iplotx/ingest/typing.py
CHANGED
|
@@ -255,13 +255,9 @@ class TreeDataProvider(Protocol):
|
|
|
255
255
|
layout: str | LayoutType,
|
|
256
256
|
layout_style: Optional[dict[str, int | float | str]] = None,
|
|
257
257
|
directed: bool | str = False,
|
|
258
|
-
vertex_labels: Optional[
|
|
259
|
-
Sequence[str] | dict[Hashable, str] | pd.Series | bool
|
|
260
|
-
] = None,
|
|
258
|
+
vertex_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series | bool] = None,
|
|
261
259
|
edge_labels: Optional[Sequence[str] | dict] = None,
|
|
262
|
-
leaf_labels: Optional[
|
|
263
|
-
Sequence[str] | dict[Hashable, str] | pd.Series | bool
|
|
264
|
-
] = None,
|
|
260
|
+
leaf_labels: Optional[Sequence[str] | dict[Hashable, str] | pd.Series | bool] = None,
|
|
265
261
|
) -> TreeData:
|
|
266
262
|
"""Create tree data object for iplotx from ete4.core.tre.Tree classes.
|
|
267
263
|
|
|
@@ -344,17 +340,13 @@ class TreeDataProvider(Protocol):
|
|
|
344
340
|
# Apparently multiple supports are accepted in some XML format
|
|
345
341
|
support[key] = "/".join(str(int(np.round(v, 0))) for v in value)
|
|
346
342
|
|
|
347
|
-
tree_data["vertex_df"]["support"] = pd.Series(support).loc[
|
|
348
|
-
tree_data["vertex_df"].index
|
|
349
|
-
]
|
|
343
|
+
tree_data["vertex_df"]["support"] = pd.Series(support).loc[tree_data["vertex_df"].index]
|
|
350
344
|
|
|
351
345
|
# Add vertex labels
|
|
352
346
|
if vertex_labels is None:
|
|
353
347
|
vertex_labels = False
|
|
354
348
|
if np.isscalar(vertex_labels) and vertex_labels:
|
|
355
|
-
tree_data["vertex_df"]["label"] = [
|
|
356
|
-
x.name for x in tree_data["vertex_df"].index
|
|
357
|
-
]
|
|
349
|
+
tree_data["vertex_df"]["label"] = [x.name for x in tree_data["vertex_df"].index]
|
|
358
350
|
elif not np.isscalar(vertex_labels):
|
|
359
351
|
# If a dict-like object is passed, it can be incomplete (e.g. only the leaves):
|
|
360
352
|
# we fill the rest with empty strings which are not going to show up in the plot.
|
|
@@ -386,8 +378,7 @@ class TreeDataProvider(Protocol):
|
|
|
386
378
|
# Leaves are already in the dataframe in a certain order, so sequences are allowed
|
|
387
379
|
if isinstance(leaf_labels, (list, tuple, np.ndarray)):
|
|
388
380
|
leaf_labels = {
|
|
389
|
-
leaf: label
|
|
390
|
-
for leaf, label in zip(tree_data["leaf_df"].index, leaf_labels)
|
|
381
|
+
leaf: label for leaf, label in zip(tree_data["leaf_df"].index, leaf_labels)
|
|
391
382
|
}
|
|
392
383
|
# If a dict-like object is passed, it can be incomplete (e.g. only the leaves):
|
|
393
384
|
# we fill the rest with empty strings which are not going to show up in the plot.
|
iplotx/network.py
CHANGED
|
@@ -104,7 +104,8 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
104
104
|
"""
|
|
105
105
|
self = cls.from_edgecollection(other._edges)
|
|
106
106
|
self.network = other.network
|
|
107
|
-
|
|
107
|
+
if hasattr(other, "_ipx_internal_data"):
|
|
108
|
+
self._ipx_internal_data = other._ipx_internal_data
|
|
108
109
|
return self
|
|
109
110
|
|
|
110
111
|
@classmethod
|
|
@@ -200,9 +201,7 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
200
201
|
self.axes.autoscale_view(tight=tight)
|
|
201
202
|
|
|
202
203
|
def get_layout(self):
|
|
203
|
-
layout_columns = [
|
|
204
|
-
f"_ipx_layout_{i}" for i in range(self._ipx_internal_data["ndim"])
|
|
205
|
-
]
|
|
204
|
+
layout_columns = [f"_ipx_layout_{i}" for i in range(self._ipx_internal_data["ndim"])]
|
|
206
205
|
vertex_layout_df = self._ipx_internal_data["vertex_df"][layout_columns]
|
|
207
206
|
return vertex_layout_df
|
|
208
207
|
|
|
@@ -249,9 +248,7 @@ class NetworkArtist(mpl.artist.Artist):
|
|
|
249
248
|
else:
|
|
250
249
|
cmap_fun = None
|
|
251
250
|
|
|
252
|
-
edge_df = self._ipx_internal_data["edge_df"].set_index(
|
|
253
|
-
["_ipx_source", "_ipx_target"]
|
|
254
|
-
)
|
|
251
|
+
edge_df = self._ipx_internal_data["edge_df"].set_index(["_ipx_source", "_ipx_target"])
|
|
255
252
|
|
|
256
253
|
if "cmap" in edge_style:
|
|
257
254
|
colorarray = []
|
iplotx/plotting.py
CHANGED
|
@@ -21,7 +21,7 @@ def network(
|
|
|
21
21
|
network: Optional[GraphType] = None,
|
|
22
22
|
layout: Optional[LayoutType] = None,
|
|
23
23
|
grouping: Optional[GroupingType] = None,
|
|
24
|
-
vertex_labels: Optional[list | dict | pd.Series] = None,
|
|
24
|
+
vertex_labels: Optional[list | dict | pd.Series | bool] = None,
|
|
25
25
|
edge_labels: Optional[Sequence] = None,
|
|
26
26
|
ax: Optional[mpl.axes.Axes] = None,
|
|
27
27
|
style: str | dict | Sequence[str | dict] = (),
|
iplotx/style/__init__.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from typing import (
|
|
2
2
|
Any,
|
|
3
|
+
Iterable,
|
|
3
4
|
Optional,
|
|
4
5
|
Sequence,
|
|
5
6
|
)
|
|
7
|
+
from collections import defaultdict
|
|
6
8
|
from collections.abc import Hashable
|
|
7
9
|
from contextlib import contextmanager
|
|
8
10
|
import numpy as np
|
|
@@ -16,53 +18,8 @@ from .leaf_info import (
|
|
|
16
18
|
)
|
|
17
19
|
|
|
18
20
|
|
|
19
|
-
default
|
|
20
|
-
|
|
21
|
-
"size": 20,
|
|
22
|
-
"facecolor": "black",
|
|
23
|
-
"marker": "o",
|
|
24
|
-
"label": {
|
|
25
|
-
"color": "white",
|
|
26
|
-
"horizontalalignment": "center",
|
|
27
|
-
"verticalalignment": "center",
|
|
28
|
-
"hpadding": 18,
|
|
29
|
-
"vpadding": 12,
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
"edge": {
|
|
33
|
-
"linewidth": 1.5,
|
|
34
|
-
"linestyle": "-",
|
|
35
|
-
"color": "black",
|
|
36
|
-
"curved": False,
|
|
37
|
-
"tension": 1,
|
|
38
|
-
"looptension": 4,
|
|
39
|
-
"loopmaxangle": 60,
|
|
40
|
-
"paralleloffset": 3,
|
|
41
|
-
"label": {
|
|
42
|
-
"horizontalalignment": "center",
|
|
43
|
-
"verticalalignment": "center",
|
|
44
|
-
"rotate": False,
|
|
45
|
-
"bbox": {
|
|
46
|
-
"boxstyle": "round",
|
|
47
|
-
"facecolor": "white",
|
|
48
|
-
"edgecolor": "none",
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
"arrow": {
|
|
52
|
-
"marker": "|>",
|
|
53
|
-
"width": 8,
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
"grouping": {
|
|
57
|
-
"facecolor": ["grey", "steelblue", "tomato"],
|
|
58
|
-
"edgecolor": "black",
|
|
59
|
-
"linewidth": 1.5,
|
|
60
|
-
"alpha": 0.5,
|
|
61
|
-
"vertexpadding": 18,
|
|
62
|
-
},
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
21
|
+
# Prepopulate default style, it's used later as a backbone for everything else
|
|
22
|
+
default = style_library["default"]
|
|
66
23
|
styles = {
|
|
67
24
|
"default": default,
|
|
68
25
|
}
|
|
@@ -115,36 +72,33 @@ def get_style(name: str = "", *args) -> dict[str, Any]:
|
|
|
115
72
|
return style
|
|
116
73
|
|
|
117
74
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"""
|
|
122
|
-
|
|
123
|
-
The style name of 'default' is reserved for reverting back to
|
|
124
|
-
the default style settings.
|
|
75
|
+
def merge_styles(
|
|
76
|
+
styles: Sequence[str | dict[str, Any]] | Iterable[str | dict[str, Any]],
|
|
77
|
+
) -> dict[str, Any]:
|
|
78
|
+
"""Merge a sequence of styles into a single one.
|
|
125
79
|
|
|
126
80
|
Parameters:
|
|
127
|
-
|
|
128
|
-
or a dict with
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
**kwargs: Additional style changes to be applied at the end of any style.
|
|
81
|
+
styles: Sequence (list, tuple, etc.) of styles, each either the name of an internal
|
|
82
|
+
style or a dict-like with custom properties.
|
|
83
|
+
Returns:
|
|
84
|
+
The composite style as a dict.
|
|
132
85
|
"""
|
|
133
86
|
try:
|
|
134
87
|
import networkx as nx
|
|
135
88
|
except ImportError:
|
|
136
89
|
nx = None
|
|
137
90
|
|
|
138
|
-
global current
|
|
139
|
-
|
|
140
91
|
def _sanitize_leaves(style: dict):
|
|
141
92
|
for key, value in style.items():
|
|
142
93
|
if key in style_leaves:
|
|
94
|
+
# Networkx has a few lazy data structures
|
|
95
|
+
# TODO: move this code to provider
|
|
143
96
|
if nx is not None:
|
|
144
97
|
if isinstance(value, nx.classes.reportviews.NodeView):
|
|
145
98
|
style[key] = dict(value)
|
|
146
99
|
elif isinstance(value, nx.classes.reportviews.EdgeViewABC):
|
|
147
100
|
style[key] = [v for *e, v in value]
|
|
101
|
+
|
|
148
102
|
elif isinstance(value, dict):
|
|
149
103
|
_sanitize_leaves(value)
|
|
150
104
|
|
|
@@ -154,37 +108,97 @@ def use(style: Optional[str | dict | Sequence] = None, **kwargs):
|
|
|
154
108
|
current[key] = value
|
|
155
109
|
continue
|
|
156
110
|
|
|
157
|
-
# Style leaves are
|
|
158
|
-
if
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
111
|
+
# Style non-leaves are either recurred into or deleted
|
|
112
|
+
if key not in style_leaves:
|
|
113
|
+
if isinstance(value, dict):
|
|
114
|
+
_update(value, current[key])
|
|
115
|
+
elif value is None:
|
|
116
|
+
del current[key]
|
|
117
|
+
else:
|
|
118
|
+
raise ValueError(
|
|
119
|
+
f"Setting non-leaf style value to a non-dict: {key}, {value}",
|
|
120
|
+
)
|
|
162
121
|
else:
|
|
163
|
-
|
|
122
|
+
# Style leaves could be incomplete, ensure a sensible default
|
|
123
|
+
if value is None:
|
|
124
|
+
del current[key]
|
|
125
|
+
continue
|
|
164
126
|
|
|
165
|
-
|
|
127
|
+
if not isinstance(value, dict):
|
|
128
|
+
current[key] = value
|
|
129
|
+
continue
|
|
166
130
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
131
|
+
if hasattr(value, "default_factory"):
|
|
132
|
+
current[key] = value
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
if hasattr(current[key], "default_factory"):
|
|
136
|
+
default_value = current[key].default_factory()
|
|
137
|
+
else:
|
|
138
|
+
default_value = current[key]
|
|
139
|
+
current[key] = defaultdict(
|
|
140
|
+
lambda: default_value,
|
|
141
|
+
value,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
merged = {}
|
|
145
|
+
for style in styles:
|
|
146
|
+
if isinstance(style, str):
|
|
147
|
+
style = get_style(style)
|
|
172
148
|
else:
|
|
173
|
-
|
|
149
|
+
_sanitize_leaves(style)
|
|
150
|
+
unflatten_style(style)
|
|
151
|
+
_update(style, merged)
|
|
174
152
|
|
|
175
|
-
|
|
176
|
-
styles.append(kwargs)
|
|
153
|
+
return merged
|
|
177
154
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
155
|
+
|
|
156
|
+
# The following is inspired by matplotlib's style library
|
|
157
|
+
# https://github.com/matplotlib/matplotlib/blob/v3.10.3/lib/matplotlib/style/core.py#L45
|
|
158
|
+
def use(
|
|
159
|
+
style: Optional[
|
|
160
|
+
str | dict[str, Any] | Sequence[str | dict[str, Any]] | Iterable[str | dict[str, Any]]
|
|
161
|
+
] = None,
|
|
162
|
+
**kwargs,
|
|
163
|
+
):
|
|
164
|
+
"""Use iplotx style setting for a style specification.
|
|
165
|
+
|
|
166
|
+
The style name of 'default' is reserved for reverting back to
|
|
167
|
+
the default style settings.
|
|
168
|
+
|
|
169
|
+
Parameters:
|
|
170
|
+
style: A style specification, currently either a name of an existing style
|
|
171
|
+
or a dict with specific parts of the style to override. The string
|
|
172
|
+
"default" resets the style to the default one. If this is a sequence,
|
|
173
|
+
each style is applied in order.
|
|
174
|
+
**kwargs: Additional style changes to be applied at the end of any style.
|
|
175
|
+
"""
|
|
176
|
+
global current
|
|
177
|
+
|
|
178
|
+
styles = []
|
|
179
|
+
if isinstance(style, (dict, str)):
|
|
180
|
+
styles.append(style)
|
|
181
|
+
elif style is not None:
|
|
182
|
+
styles.extend(list(style))
|
|
183
|
+
if kwargs:
|
|
184
|
+
styles.append(kwargs)
|
|
185
|
+
|
|
186
|
+
# Discard empty styles for speed
|
|
187
|
+
styles = [style for style in styles if style]
|
|
188
|
+
|
|
189
|
+
if len(styles) == 0:
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
# If the first style is a string (internal style), apply it cold. If it's a
|
|
193
|
+
# dict, apply it hot (on top of the current style(. Any style after the first
|
|
194
|
+
# is always applied hot - otherwise it would invalidate previous styles.
|
|
195
|
+
if not isinstance(styles[0], str):
|
|
196
|
+
# hot insertion on top of current
|
|
197
|
+
styles.insert(0, current)
|
|
198
|
+
|
|
199
|
+
old_style = copy_with_deep_values(current)
|
|
200
|
+
try:
|
|
201
|
+
current = merge_styles(styles)
|
|
188
202
|
except:
|
|
189
203
|
current = old_style
|
|
190
204
|
raise
|
|
@@ -197,7 +211,12 @@ def reset() -> None:
|
|
|
197
211
|
|
|
198
212
|
|
|
199
213
|
@contextmanager
|
|
200
|
-
def context(
|
|
214
|
+
def context(
|
|
215
|
+
style: Optional[
|
|
216
|
+
str | dict[str, Any] | Sequence[str | dict[str, Any]] | Iterable[str | dict[str, Any]]
|
|
217
|
+
] = None,
|
|
218
|
+
**kwargs,
|
|
219
|
+
):
|
|
201
220
|
"""Create a style context for iplotx.
|
|
202
221
|
|
|
203
222
|
Parameters:
|
|
@@ -257,9 +276,7 @@ def unflatten_style(
|
|
|
257
276
|
|
|
258
277
|
# top-level adjustments
|
|
259
278
|
if "zorder" in style_flat:
|
|
260
|
-
style_flat["network_zorder"] = style_flat["grouping_zorder"] = style_flat.pop(
|
|
261
|
-
"zorder"
|
|
262
|
-
)
|
|
279
|
+
style_flat["network_zorder"] = style_flat["grouping_zorder"] = style_flat.pop("zorder")
|
|
263
280
|
|
|
264
281
|
# Begin recursion
|
|
265
282
|
_inner(style_flat)
|
|
@@ -269,6 +286,7 @@ def rotate_style(
|
|
|
269
286
|
style: dict[str, Any],
|
|
270
287
|
index: Optional[int] = None,
|
|
271
288
|
key: Optional[Hashable] = None,
|
|
289
|
+
key2: Optional[Hashable] = None,
|
|
272
290
|
props: Optional[Sequence[str]] = None,
|
|
273
291
|
) -> dict[str, Any]:
|
|
274
292
|
"""Rotate leaves of a style for a certain index or key.
|
|
@@ -278,6 +296,9 @@ def rotate_style(
|
|
|
278
296
|
index: The integer to rotate the style leaves into.
|
|
279
297
|
key: For dict-like leaves (e.g. vertex properties specified as a dict-like object over the
|
|
280
298
|
vertices themselves), the key to use for rotation (e.g. the vertex itself).
|
|
299
|
+
key2: For dict-like leaves, a backup key in case the first key fails. If this is None
|
|
300
|
+
or also a failure (i.e. KeyError), default to the empty type constructor for the
|
|
301
|
+
first value of the dict-like style leaf.
|
|
281
302
|
props: The properties to rotate, usually all leaf properties.
|
|
282
303
|
|
|
283
304
|
Returns:
|
|
@@ -290,9 +311,7 @@ def rotate_style(
|
|
|
290
311
|
{'vertex': {'size': 10}}
|
|
291
312
|
"""
|
|
292
313
|
if (index is None) and (key is None):
|
|
293
|
-
raise ValueError(
|
|
294
|
-
"At least one of 'index' or 'key' must be provided to rotate_style."
|
|
295
|
-
)
|
|
314
|
+
raise ValueError("At least one of 'index' or 'key' must be provided to rotate_style.")
|
|
296
315
|
|
|
297
316
|
if props is None:
|
|
298
317
|
props = tuple(prop for prop in style_leaves if prop not in nonrotating_leaves)
|
|
@@ -304,24 +323,29 @@ def rotate_style(
|
|
|
304
323
|
if val is None:
|
|
305
324
|
continue
|
|
306
325
|
# Try integer indexing for ordered types
|
|
307
|
-
if (index is not None) and isinstance(
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
style[prop] =
|
|
326
|
+
if (index is not None) and isinstance(val, (tuple, list, np.ndarray, pd.Index, pd.Series)):
|
|
327
|
+
# NOTE: cannot cast to ndarray because rotation might involve
|
|
328
|
+
# cross-type lists (e.g. ["none", False])
|
|
329
|
+
style[prop] = list(val)[index % len(val)]
|
|
311
330
|
# Try key indexing for unordered, dict-like types
|
|
312
331
|
if (
|
|
313
332
|
(key is not None)
|
|
314
333
|
and (not isinstance(val, (str, tuple, list, np.ndarray)))
|
|
315
334
|
and hasattr(val, "__getitem__")
|
|
316
335
|
):
|
|
317
|
-
# If only a subset of keys is provided,
|
|
318
|
-
#
|
|
319
|
-
# empty strings).
|
|
336
|
+
# If only a subset of keys is provided, try the default value a la
|
|
337
|
+
# defaultdict. If that fails, use an empty constructor
|
|
320
338
|
if key in val:
|
|
321
|
-
|
|
339
|
+
newval = val[key]
|
|
340
|
+
elif key2 is not None and (key2 in val):
|
|
341
|
+
newval = val[key2]
|
|
322
342
|
else:
|
|
323
|
-
|
|
324
|
-
|
|
343
|
+
try:
|
|
344
|
+
newval = val[key]
|
|
345
|
+
except KeyError:
|
|
346
|
+
valtype = type(next(iter(val.values())))
|
|
347
|
+
newval = valtype()
|
|
348
|
+
style[prop] = newval
|
|
325
349
|
|
|
326
350
|
return style
|
|
327
351
|
|
|
@@ -337,7 +361,8 @@ def add_style(name: str, style: dict[str, Any]) -> None:
|
|
|
337
361
|
styles[name] = get_style()
|
|
338
362
|
|
|
339
363
|
|
|
340
|
-
# Populate style library
|
|
364
|
+
# Populate style library (default is already there)
|
|
341
365
|
for name, style in style_library.items():
|
|
342
|
-
|
|
343
|
-
|
|
366
|
+
if name != "default":
|
|
367
|
+
add_style(name, style)
|
|
368
|
+
del name, style
|
iplotx/style/leaf_info.py
CHANGED
|
@@ -23,6 +23,8 @@ rotating_leaves = (
|
|
|
23
23
|
"hmargin",
|
|
24
24
|
"vmargin",
|
|
25
25
|
"ports",
|
|
26
|
+
"width",
|
|
27
|
+
"height",
|
|
26
28
|
)
|
|
27
29
|
|
|
28
30
|
# These properties are also terminal style properties, but they cannot be rotated.
|
|
@@ -35,6 +37,7 @@ nonrotating_leaves = (
|
|
|
35
37
|
"extend",
|
|
36
38
|
"deep",
|
|
37
39
|
"angular",
|
|
40
|
+
"curved",
|
|
38
41
|
)
|
|
39
42
|
|
|
40
43
|
# Union of all style leaves (rotating and nonrotating)
|
iplotx/style/library.py
CHANGED
|
@@ -1,4 +1,49 @@
|
|
|
1
1
|
style_library = {
|
|
2
|
+
"default": {
|
|
3
|
+
"vertex": {
|
|
4
|
+
"size": 20,
|
|
5
|
+
"facecolor": "black",
|
|
6
|
+
"marker": "o",
|
|
7
|
+
"label": {
|
|
8
|
+
"color": "white",
|
|
9
|
+
"horizontalalignment": "center",
|
|
10
|
+
"verticalalignment": "center",
|
|
11
|
+
"hpadding": 18,
|
|
12
|
+
"vpadding": 12,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
"edge": {
|
|
16
|
+
"linewidth": 1.5,
|
|
17
|
+
"linestyle": "-",
|
|
18
|
+
"color": "black",
|
|
19
|
+
"curved": False,
|
|
20
|
+
"tension": 1,
|
|
21
|
+
"looptension": 4,
|
|
22
|
+
"loopmaxangle": 60,
|
|
23
|
+
"paralleloffset": 3,
|
|
24
|
+
"label": {
|
|
25
|
+
"horizontalalignment": "center",
|
|
26
|
+
"verticalalignment": "center",
|
|
27
|
+
"rotate": False,
|
|
28
|
+
"bbox": {
|
|
29
|
+
"boxstyle": "round",
|
|
30
|
+
"facecolor": "white",
|
|
31
|
+
"edgecolor": "none",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
"arrow": {
|
|
35
|
+
"marker": "|>",
|
|
36
|
+
"width": 8,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
"grouping": {
|
|
40
|
+
"facecolor": ["grey", "steelblue", "tomato"],
|
|
41
|
+
"edgecolor": "black",
|
|
42
|
+
"linewidth": 1.5,
|
|
43
|
+
"alpha": 0.5,
|
|
44
|
+
"vertexpadding": 18,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
2
47
|
# Hollow style for organization charts et similar
|
|
3
48
|
"hollow": {
|
|
4
49
|
"vertex": {
|
|
@@ -54,6 +99,55 @@ style_library = {
|
|
|
54
99
|
"deep": False,
|
|
55
100
|
},
|
|
56
101
|
},
|
|
102
|
+
# Dashed depth tree branches
|
|
103
|
+
"dashdepth": {
|
|
104
|
+
"vertex": {
|
|
105
|
+
"size": 2,
|
|
106
|
+
"label": {
|
|
107
|
+
"color": "black",
|
|
108
|
+
"size": 10,
|
|
109
|
+
"verticalalignment": "center",
|
|
110
|
+
"bbox": {
|
|
111
|
+
"facecolor": "none",
|
|
112
|
+
"edgecolor": "none",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
"edge": {
|
|
117
|
+
"linewidth": 1.5,
|
|
118
|
+
"split": {
|
|
119
|
+
"linestyle": ":",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
"leaf": {
|
|
123
|
+
"deep": False,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
# Dashed depth tree branches
|
|
127
|
+
"dashwidth": {
|
|
128
|
+
"vertex": {
|
|
129
|
+
"size": 2,
|
|
130
|
+
"label": {
|
|
131
|
+
"color": "black",
|
|
132
|
+
"size": 10,
|
|
133
|
+
"verticalalignment": "center",
|
|
134
|
+
"bbox": {
|
|
135
|
+
"facecolor": "none",
|
|
136
|
+
"edgecolor": "none",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
"edge": {
|
|
141
|
+
"linewidth": 1.5,
|
|
142
|
+
"linestyle": ":",
|
|
143
|
+
"split": {
|
|
144
|
+
"linestyle": "-",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
"leaf": {
|
|
148
|
+
"deep": False,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
57
151
|
# Greyscale style
|
|
58
152
|
"greyscale": {
|
|
59
153
|
"vertex": {
|