iplotx 0.0.1__py3-none-any.whl → 0.2.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/__init__.py +22 -1
- iplotx/edge/__init__.py +882 -0
- iplotx/edge/arrow.py +220 -10
- iplotx/edge/ports.py +42 -0
- iplotx/groups.py +79 -41
- 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 +127 -0
- iplotx/layout.py +139 -0
- iplotx/network.py +156 -375
- iplotx/plotting.py +157 -55
- iplotx/style.py +379 -0
- iplotx/tree.py +285 -0
- iplotx/typing.py +33 -38
- 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 +250 -55
- iplotx-0.2.0.dist-info/METADATA +76 -0
- iplotx-0.2.0.dist-info/RECORD +30 -0
- {iplotx-0.0.1.dist-info → iplotx-0.2.0.dist-info}/WHEEL +0 -1
- 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.0.1.dist-info/METADATA +0 -39
- iplotx-0.0.1.dist-info/RECORD +0 -20
iplotx/plotting.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Optional, Sequence
|
|
2
|
+
from contextlib import nullcontext
|
|
3
|
+
import numpy as np
|
|
2
4
|
import pandas as pd
|
|
3
5
|
import matplotlib as mpl
|
|
4
6
|
import matplotlib.pyplot as plt
|
|
@@ -7,77 +9,176 @@ from .typing import (
|
|
|
7
9
|
GraphType,
|
|
8
10
|
LayoutType,
|
|
9
11
|
GroupingType,
|
|
12
|
+
TreeType,
|
|
10
13
|
)
|
|
11
14
|
from .network import NetworkArtist
|
|
12
15
|
from .groups import GroupingArtist
|
|
13
|
-
from .
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
)
|
|
25
|
-
|
|
16
|
+
from .tree import TreeArtist
|
|
17
|
+
from .style import context
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def network(
|
|
21
|
+
network: Optional[GraphType] = None,
|
|
22
|
+
layout: Optional[LayoutType] = None,
|
|
23
|
+
grouping: Optional[GroupingType] = None,
|
|
24
|
+
vertex_labels: Optional[list | dict | pd.Series] = None,
|
|
25
|
+
edge_labels: Optional[Sequence] = None,
|
|
26
|
+
ax: Optional[mpl.axes.Axes] = None,
|
|
27
|
+
style: str | dict | Sequence[str | dict] = (),
|
|
28
|
+
title: Optional[str] = None,
|
|
29
|
+
aspect: Optional[str | float] = None,
|
|
30
|
+
margins: float | tuple[float, float] = 0,
|
|
31
|
+
**kwargs,
|
|
32
|
+
) -> list[mpl.artist.Artist]:
|
|
33
|
+
"""Plot this network and/or vertex grouping using the specified layout.
|
|
26
34
|
|
|
27
35
|
Parameters:
|
|
28
|
-
network
|
|
29
|
-
layout
|
|
30
|
-
|
|
36
|
+
network: The network to plot. Can be a networkx or igraph graph.
|
|
37
|
+
layout: The layout to use for plotting. If None, a layout will be looked for in the
|
|
38
|
+
network object and, if none is found, an exception is raised. Defaults to None.
|
|
39
|
+
vertex_labels: The labels for the vertices. If None or False, no vertex labels
|
|
31
40
|
will be drawn. If a list, the labels are taken from the list. If a dict, the keys
|
|
32
|
-
should be the vertex IDs and the values should be the labels.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
should be the vertex IDs and the values should be the labels. If True (a single
|
|
42
|
+
bool value), the vertex IDs will be used as labels.
|
|
43
|
+
edge_labels: The labels for the edges. If None, no edge labels will be drawn. Defaults
|
|
44
|
+
to None.
|
|
45
|
+
ax: The axis to plot on. If None, a new figure and axis will be created. Defaults to
|
|
46
|
+
None.
|
|
47
|
+
style: Apply this style for the objects to plot. This can be a sequence (e.g. list)
|
|
48
|
+
of styles and they will be applied in order.
|
|
49
|
+
title: If not None, set the axes title to this value.
|
|
50
|
+
aspect: If not None, set the aspect ratio of the axis to this value. The most common
|
|
51
|
+
value is 1.0, which proportionates x- and y-axes.
|
|
52
|
+
margins: How much margin to leave around the plot. A higher value (e.g. 0.1) can be
|
|
53
|
+
used as a quick fix when some vertex shapes reach beyond the plot edge. This is
|
|
54
|
+
a fraction of the data limits, so 0.1 means 10% of the data limits will be left
|
|
55
|
+
as margin.
|
|
56
|
+
**kwargs: Additional arguments are treated as an alternate way to specify style. If
|
|
57
|
+
both "style" and additional **kwargs are provided, they are both applied in that
|
|
58
|
+
order (style, then **kwargs).
|
|
36
59
|
|
|
37
60
|
Returns:
|
|
38
|
-
A
|
|
61
|
+
A list of mpl.artist.Artist objects, set as a direct child of the matplotlib Axes.
|
|
62
|
+
The list can have one or two elements, depending on whether you are requesting to
|
|
63
|
+
plot a network, a grouping, or both.
|
|
39
64
|
"""
|
|
40
|
-
if
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
65
|
+
stylecontext = context(style, **kwargs) if style or kwargs else nullcontext()
|
|
66
|
+
|
|
67
|
+
with stylecontext:
|
|
68
|
+
if (network is None) and (grouping is None):
|
|
69
|
+
raise ValueError("At least one of network or grouping must be provided.")
|
|
70
|
+
|
|
71
|
+
if ax is None:
|
|
72
|
+
fig, ax = plt.subplots()
|
|
73
|
+
|
|
74
|
+
artists = []
|
|
75
|
+
if network is not None:
|
|
76
|
+
nwkart = NetworkArtist(
|
|
77
|
+
network,
|
|
78
|
+
layout,
|
|
79
|
+
vertex_labels=vertex_labels,
|
|
46
80
|
edge_labels=edge_labels,
|
|
81
|
+
transform=mpl.transforms.IdentityTransform(),
|
|
82
|
+
offset_transform=ax.transData,
|
|
83
|
+
)
|
|
84
|
+
ax.add_artist(nwkart)
|
|
85
|
+
|
|
86
|
+
# Set the figure, which itself sets the dpi scale for vertices, edges,
|
|
87
|
+
# arrows, etc. Now data limits can be computed correctly
|
|
88
|
+
nwkart.set_figure(ax.figure)
|
|
89
|
+
|
|
90
|
+
artists.append(nwkart)
|
|
91
|
+
|
|
92
|
+
# Set normailsed layout since we have it by now
|
|
93
|
+
layout = nwkart.get_layout()
|
|
94
|
+
|
|
95
|
+
if grouping is not None:
|
|
96
|
+
grpart = GroupingArtist(
|
|
97
|
+
grouping,
|
|
98
|
+
layout,
|
|
99
|
+
network=network,
|
|
100
|
+
transform=ax.transData,
|
|
47
101
|
)
|
|
102
|
+
ax.add_artist(grpart)
|
|
103
|
+
|
|
104
|
+
grpart.set_figure(ax.figure)
|
|
105
|
+
artists.append(grpart)
|
|
106
|
+
|
|
107
|
+
if title is not None:
|
|
108
|
+
ax.set_title(title)
|
|
48
109
|
|
|
49
|
-
|
|
50
|
-
|
|
110
|
+
if aspect is not None:
|
|
111
|
+
ax.set_aspect(aspect)
|
|
51
112
|
|
|
52
|
-
|
|
53
|
-
fig, ax = plt.subplots()
|
|
113
|
+
_postprocess_axis(ax, artists)
|
|
54
114
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
115
|
+
if np.isscalar(margins):
|
|
116
|
+
margins = (margins, margins)
|
|
117
|
+
if (margins[0] != 0) or (margins[1] != 0):
|
|
118
|
+
ax.margins(*margins)
|
|
119
|
+
|
|
120
|
+
return artists
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def tree(
|
|
124
|
+
tree: Optional[TreeType] = None,
|
|
125
|
+
layout: str | LayoutType = "horizontal",
|
|
126
|
+
orientation: str = "right",
|
|
127
|
+
directed: bool | str = False,
|
|
128
|
+
vertex_labels: Optional[list | dict | pd.Series] = None,
|
|
129
|
+
ax: Optional[mpl.axes.Axes] = None,
|
|
130
|
+
style: str | dict | Sequence[str | dict] = "tree",
|
|
131
|
+
title: Optional[str] = None,
|
|
132
|
+
aspect: Optional[str | float] = None,
|
|
133
|
+
margins: float | tuple[float, float] = 0,
|
|
134
|
+
**kwargs,
|
|
135
|
+
) -> TreeArtist:
|
|
136
|
+
"""Plot a tree using the specified layout.
|
|
137
|
+
|
|
138
|
+
Parameters:
|
|
139
|
+
tree: The tree to plot. Can be a BioPython.Phylo.Tree object.
|
|
140
|
+
layout: The layout to use for plotting.
|
|
141
|
+
orientation: The orientation of the horizontal layout. Can be "right" or "left". Defaults to
|
|
142
|
+
"right".
|
|
143
|
+
directed: If False, donot draw arrows. If True or "child", draw arrows from parent to child
|
|
144
|
+
node. If "parent", draw arrows the other way around.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
A TreeArtist object, set as a direct child of the matplotlib Axes.
|
|
148
|
+
"""
|
|
149
|
+
stylecontext = context(style, **kwargs) if style or kwargs else nullcontext()
|
|
150
|
+
|
|
151
|
+
with stylecontext:
|
|
152
|
+
if ax is None:
|
|
153
|
+
fig, ax = plt.subplots()
|
|
154
|
+
|
|
155
|
+
artist = TreeArtist(
|
|
156
|
+
tree=tree,
|
|
157
|
+
layout=layout,
|
|
158
|
+
orientation=orientation,
|
|
159
|
+
directed=directed,
|
|
160
|
+
transform=mpl.transforms.IdentityTransform(),
|
|
161
|
+
offset_transform=ax.transData,
|
|
60
162
|
vertex_labels=vertex_labels,
|
|
61
|
-
edge_labels=edge_labels,
|
|
62
|
-
)
|
|
63
|
-
ax.add_artist(nwkart)
|
|
64
|
-
# Postprocess for things that require an axis (transform, etc.)
|
|
65
|
-
nwkart._process()
|
|
66
|
-
artists.append(nwkart)
|
|
67
|
-
|
|
68
|
-
if grouping is not None:
|
|
69
|
-
grpart = GroupingArtist(
|
|
70
|
-
grouping,
|
|
71
|
-
layout,
|
|
72
163
|
)
|
|
73
|
-
ax.add_artist(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
164
|
+
ax.add_artist(artist)
|
|
165
|
+
|
|
166
|
+
artist.set_figure(ax.figure)
|
|
167
|
+
|
|
168
|
+
if title is not None:
|
|
169
|
+
ax.set_title(title)
|
|
170
|
+
|
|
171
|
+
if aspect is not None:
|
|
172
|
+
ax.set_aspect(aspect)
|
|
173
|
+
|
|
174
|
+
_postprocess_axis(ax, [artist])
|
|
77
175
|
|
|
78
|
-
|
|
176
|
+
if np.isscalar(margins):
|
|
177
|
+
margins = (margins, margins)
|
|
178
|
+
if (margins[0] != 0) or (margins[1] != 0):
|
|
179
|
+
ax.margins(*margins)
|
|
79
180
|
|
|
80
|
-
return
|
|
181
|
+
return artist
|
|
81
182
|
|
|
82
183
|
|
|
83
184
|
# INTERNAL ROUTINES
|
|
@@ -98,7 +199,8 @@ def _postprocess_axis(ax, artists):
|
|
|
98
199
|
bboxes = []
|
|
99
200
|
for art in artists:
|
|
100
201
|
bboxes.append(art.get_datalim(ax.transData))
|
|
101
|
-
|
|
202
|
+
bbox = mpl.transforms.Bbox.union(bboxes)
|
|
203
|
+
ax.update_datalim(bbox)
|
|
102
204
|
|
|
103
205
|
# Autoscale for x/y axis limits
|
|
104
206
|
ax.autoscale_view()
|
iplotx/style.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
from typing import (
|
|
2
|
+
Any,
|
|
3
|
+
Never,
|
|
4
|
+
Optional,
|
|
5
|
+
Sequence,
|
|
6
|
+
)
|
|
7
|
+
from collections.abc import Hashable
|
|
8
|
+
import copy
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
style_leaves = (
|
|
15
|
+
"cmap",
|
|
16
|
+
"color",
|
|
17
|
+
"size",
|
|
18
|
+
"edgecolor",
|
|
19
|
+
"facecolor",
|
|
20
|
+
"linewidth",
|
|
21
|
+
"linestyle",
|
|
22
|
+
"alpha",
|
|
23
|
+
"zorder",
|
|
24
|
+
"tension",
|
|
25
|
+
"looptension",
|
|
26
|
+
"lloopmaxangle",
|
|
27
|
+
"rotate",
|
|
28
|
+
"marker",
|
|
29
|
+
"waypoints",
|
|
30
|
+
"horizontalalignment",
|
|
31
|
+
"verticalalignment",
|
|
32
|
+
"boxstyle",
|
|
33
|
+
"hpadding",
|
|
34
|
+
"vpadding",
|
|
35
|
+
"hmargin",
|
|
36
|
+
"vmargin",
|
|
37
|
+
"ports",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
default = {
|
|
42
|
+
"vertex": {
|
|
43
|
+
"size": 20,
|
|
44
|
+
"facecolor": "black",
|
|
45
|
+
"marker": "o",
|
|
46
|
+
"label": {
|
|
47
|
+
"color": "white",
|
|
48
|
+
"horizontalalignment": "center",
|
|
49
|
+
"verticalalignment": "center",
|
|
50
|
+
"hpadding": 18,
|
|
51
|
+
"vpadding": 12,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
"edge": {
|
|
55
|
+
"linewidth": 1.5,
|
|
56
|
+
"linestyle": "-",
|
|
57
|
+
"color": "black",
|
|
58
|
+
"curved": False,
|
|
59
|
+
"offset": 3,
|
|
60
|
+
"tension": 1,
|
|
61
|
+
"looptension": 4,
|
|
62
|
+
"loopmaxangle": 60,
|
|
63
|
+
"label": {
|
|
64
|
+
"horizontalalignment": "center",
|
|
65
|
+
"verticalalignment": "center",
|
|
66
|
+
"rotate": False,
|
|
67
|
+
"bbox": {
|
|
68
|
+
"boxstyle": "round",
|
|
69
|
+
"facecolor": "white",
|
|
70
|
+
"edgecolor": "none",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
"arrow": {
|
|
74
|
+
"marker": "|>",
|
|
75
|
+
"width": 8,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
"grouping": {
|
|
79
|
+
"facecolor": ["grey", "steelblue", "tomato"],
|
|
80
|
+
"edgecolor": "black",
|
|
81
|
+
"linewidth": 1.5,
|
|
82
|
+
"alpha": 0.5,
|
|
83
|
+
"vertexpadding": 18,
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def copy_with_deep_values(style):
|
|
89
|
+
"""Make a deep copy of the style dict but do not create copies of the keys."""
|
|
90
|
+
newdict = {}
|
|
91
|
+
for key, value in style.items():
|
|
92
|
+
if isinstance(value, dict):
|
|
93
|
+
newdict[key] = copy_with_deep_values(value)
|
|
94
|
+
else:
|
|
95
|
+
newdict[key] = copy.copy(value)
|
|
96
|
+
return newdict
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
hollow = copy_with_deep_values(default)
|
|
100
|
+
hollow["vertex"]["color"] = None
|
|
101
|
+
hollow["vertex"]["facecolor"] = "none"
|
|
102
|
+
hollow["vertex"]["edgecolor"] = "black"
|
|
103
|
+
hollow["vertex"]["linewidth"] = 1.5
|
|
104
|
+
hollow["vertex"]["marker"] = "r"
|
|
105
|
+
hollow["vertex"]["size"] = "label"
|
|
106
|
+
|
|
107
|
+
tree = copy_with_deep_values(default)
|
|
108
|
+
tree["vertex"]["size"] = 0
|
|
109
|
+
tree["vertex"]["alpha"] = 0
|
|
110
|
+
tree["edge"]["linewidth"] = 2.5
|
|
111
|
+
tree["vertex"]["label"]["bbox"] = {
|
|
112
|
+
"boxstyle": "square,pad=0.5",
|
|
113
|
+
"facecolor": "white",
|
|
114
|
+
"edgecolor": "none",
|
|
115
|
+
}
|
|
116
|
+
tree["vertex"]["label"]["color"] = "black"
|
|
117
|
+
tree["vertex"]["label"]["size"] = 12
|
|
118
|
+
tree["vertex"]["label"]["horizontalalignment"] = "left"
|
|
119
|
+
tree["vertex"]["label"]["hmargin"] = 5
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
styles = {
|
|
123
|
+
"default": default,
|
|
124
|
+
"hollow": hollow,
|
|
125
|
+
"tree": tree,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
stylename = "default"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
current = copy_with_deep_values(styles["default"])
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_stylename():
|
|
136
|
+
"""Return the name of the current iplotx style."""
|
|
137
|
+
return str(stylename)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def get_style(name: str = "") -> dict[str, Any]:
|
|
141
|
+
"""Get a *deep copy* of the chosen style.
|
|
142
|
+
|
|
143
|
+
Parameters:
|
|
144
|
+
name: The name of the style to get. If empty, the current style is returned.
|
|
145
|
+
Substyles can be obtained by using a dot notation, e.g. "default.vertex".
|
|
146
|
+
If "name" starts with a dot, it means a substyle of the current style.
|
|
147
|
+
Returns:
|
|
148
|
+
The requected style or substyle.
|
|
149
|
+
|
|
150
|
+
NOTE: The deep copy is a little different from standard deep copies. Here, keys
|
|
151
|
+
(which need to be hashables) are never copied, but values are. This can be
|
|
152
|
+
useful for hashables that change hash upon copying, such as Biopython's
|
|
153
|
+
tree nodes.
|
|
154
|
+
"""
|
|
155
|
+
namelist = name.split(".")
|
|
156
|
+
style = styles
|
|
157
|
+
for i, namei in enumerate(namelist):
|
|
158
|
+
if (i == 0) and (namei == ""):
|
|
159
|
+
style = current
|
|
160
|
+
else:
|
|
161
|
+
if namei in style:
|
|
162
|
+
style = style[namei]
|
|
163
|
+
# NOTE: if asking for a nonexistent, non-leaf style
|
|
164
|
+
# give the benefit of the doubt and set an empty dict
|
|
165
|
+
# which will not fail unless the uder tries to enter it
|
|
166
|
+
elif namei not in style_leaves:
|
|
167
|
+
style = {}
|
|
168
|
+
else:
|
|
169
|
+
raise KeyError(f"Style not found: {name}")
|
|
170
|
+
|
|
171
|
+
style = copy_with_deep_values(style)
|
|
172
|
+
return style
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# The following is inspired by matplotlib's style library
|
|
176
|
+
# https://github.com/matplotlib/matplotlib/blob/v3.10.3/lib/matplotlib/style/core.py#L45
|
|
177
|
+
def use(style: Optional[str | dict | Sequence] = None, **kwargs):
|
|
178
|
+
"""Use iplotx style setting for a style specification.
|
|
179
|
+
|
|
180
|
+
The style name of 'default' is reserved for reverting back to
|
|
181
|
+
the default style settings.
|
|
182
|
+
|
|
183
|
+
Parameters:
|
|
184
|
+
style: A style specification, currently either a name of an existing style
|
|
185
|
+
or a dict with specific parts of the style to override. The string
|
|
186
|
+
"default" resets the style to the default one. If this is a sequence,
|
|
187
|
+
each style is applied in order.
|
|
188
|
+
**kwargs: Additional style changes to be applied at the end of any style.
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
import networkx as nx
|
|
192
|
+
except ImportError:
|
|
193
|
+
nx = None
|
|
194
|
+
|
|
195
|
+
global current
|
|
196
|
+
|
|
197
|
+
def _sanitize_leaves(style: dict):
|
|
198
|
+
for key, value in style.items():
|
|
199
|
+
if key in style_leaves:
|
|
200
|
+
if nx is not None:
|
|
201
|
+
if isinstance(value, nx.classes.reportviews.NodeView):
|
|
202
|
+
style[key] = dict(value)
|
|
203
|
+
elif isinstance(value, nx.classes.reportviews.EdgeViewABC):
|
|
204
|
+
style[key] = [v for *e, v in value]
|
|
205
|
+
elif isinstance(value, dict):
|
|
206
|
+
_sanitize_leaves(value)
|
|
207
|
+
|
|
208
|
+
def _update(style: dict, current: dict):
|
|
209
|
+
for key, value in style.items():
|
|
210
|
+
if key not in current:
|
|
211
|
+
current[key] = value
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
# Style leaves are by definition not to be recurred into
|
|
215
|
+
if isinstance(value, dict) and (key not in style_leaves):
|
|
216
|
+
_update(value, current[key])
|
|
217
|
+
elif value is None:
|
|
218
|
+
del current[key]
|
|
219
|
+
else:
|
|
220
|
+
current[key] = value
|
|
221
|
+
|
|
222
|
+
old_style = copy_with_deep_values(current)
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
if style is None:
|
|
226
|
+
styles = []
|
|
227
|
+
elif isinstance(style, (dict, str)):
|
|
228
|
+
styles = [style]
|
|
229
|
+
else:
|
|
230
|
+
styles = list(style)
|
|
231
|
+
|
|
232
|
+
if kwargs:
|
|
233
|
+
styles.append(kwargs)
|
|
234
|
+
|
|
235
|
+
for style in styles:
|
|
236
|
+
if style == "default":
|
|
237
|
+
reset()
|
|
238
|
+
else:
|
|
239
|
+
if isinstance(style, str):
|
|
240
|
+
current = get_style(style)
|
|
241
|
+
else:
|
|
242
|
+
_sanitize_leaves(style)
|
|
243
|
+
unflatten_style(style)
|
|
244
|
+
_update(style, current)
|
|
245
|
+
except:
|
|
246
|
+
current = old_style
|
|
247
|
+
raise
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def reset() -> Never:
|
|
251
|
+
"""Reset to default style."""
|
|
252
|
+
global current
|
|
253
|
+
current = copy_with_deep_values(styles["default"])
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@contextmanager
|
|
257
|
+
def context(style: Optional[str | dict | Sequence] = None, **kwargs):
|
|
258
|
+
"""Create a style context for iplotx.
|
|
259
|
+
|
|
260
|
+
Parameters:
|
|
261
|
+
style: A single style specification or a list of style specifications, which are then
|
|
262
|
+
applied in order. Each style can be a string (for an existing style) or a dictionary
|
|
263
|
+
with the elements that are to change.
|
|
264
|
+
**kwargs: Additional style changes to be applied at the end of all styles.
|
|
265
|
+
|
|
266
|
+
Yields:
|
|
267
|
+
A context manager that applies the style and reverts it back to the previous one upon exit.
|
|
268
|
+
"""
|
|
269
|
+
current = get_style()
|
|
270
|
+
try:
|
|
271
|
+
use(style, **kwargs)
|
|
272
|
+
yield
|
|
273
|
+
finally:
|
|
274
|
+
use(["default", current])
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def unflatten_style(
|
|
278
|
+
style_flat: dict[str, str | dict | int | float],
|
|
279
|
+
) -> Never:
|
|
280
|
+
"""Convert a flat or semi-flat style into a fully structured dict.
|
|
281
|
+
|
|
282
|
+
Parameters:
|
|
283
|
+
style_flat: A flat dictionary where keys may contain underscores, which are taken to signify
|
|
284
|
+
subdictionaries.
|
|
285
|
+
|
|
286
|
+
NOTE: The dict is changed *in place*.
|
|
287
|
+
|
|
288
|
+
Example:
|
|
289
|
+
>>> style = {'vertex_size': 20}
|
|
290
|
+
>>> unflatten_style(style)
|
|
291
|
+
>>> print(style)
|
|
292
|
+
{'vertex': {'size': 20}}
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
def _inner(style_flat: dict):
|
|
296
|
+
keys = list(style_flat.keys())
|
|
297
|
+
|
|
298
|
+
for key in keys:
|
|
299
|
+
if "_" not in key:
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
keyhead, keytail = key.split("_", 1)
|
|
303
|
+
value = style_flat.pop(key)
|
|
304
|
+
if keyhead not in style_flat:
|
|
305
|
+
style_flat[keyhead] = {
|
|
306
|
+
keytail: value,
|
|
307
|
+
}
|
|
308
|
+
else:
|
|
309
|
+
style_flat[keyhead][keytail] = value
|
|
310
|
+
|
|
311
|
+
for key, value in style_flat.items():
|
|
312
|
+
if isinstance(value, dict) and (key not in style_leaves):
|
|
313
|
+
_inner(value)
|
|
314
|
+
|
|
315
|
+
# top-level adjustments
|
|
316
|
+
if "zorder" in style_flat:
|
|
317
|
+
style_flat["network_zorder"] = style_flat["grouping_zorder"] = style_flat.pop(
|
|
318
|
+
"zorder"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# Begin recursion
|
|
322
|
+
_inner(style_flat)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def rotate_style(
|
|
326
|
+
style: dict[str, Any],
|
|
327
|
+
index: Optional[int] = None,
|
|
328
|
+
key: Optional[Hashable] = None,
|
|
329
|
+
props: Sequence[str] = style_leaves,
|
|
330
|
+
) -> dict[str, Any]:
|
|
331
|
+
"""Rotate leaves of a style for a certain index or key.
|
|
332
|
+
|
|
333
|
+
Parameters:
|
|
334
|
+
style: The style to rotate.
|
|
335
|
+
index: The integer to rotate the style leaves into.
|
|
336
|
+
key: For dict-like leaves (e.g. vertex properties specified as a dict-like object over the
|
|
337
|
+
vertices themselves), the key to use for rotation (e.g. the vertex itself).
|
|
338
|
+
props: The properties to rotate, usually all leaf properties.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
A style with rotated leaves, which describes the properties of a single element (e.g. vertex).
|
|
342
|
+
|
|
343
|
+
Example:
|
|
344
|
+
>>> style = {'vertex': {'size': [10, 20]}}
|
|
345
|
+
>>> rotate_style(style, index=0)
|
|
346
|
+
{'vertex': {'size': 10}}
|
|
347
|
+
"""
|
|
348
|
+
if (index is None) and (key is None):
|
|
349
|
+
raise ValueError(
|
|
350
|
+
"At least one of 'index' or 'key' must be provided to rotate_style."
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
style = copy_with_deep_values(style)
|
|
354
|
+
|
|
355
|
+
for prop in props:
|
|
356
|
+
val = style.get(prop, None)
|
|
357
|
+
if val is None:
|
|
358
|
+
continue
|
|
359
|
+
# Try integer indexing for ordered types
|
|
360
|
+
if (index is not None) and isinstance(
|
|
361
|
+
val, (tuple, list, np.ndarray, pd.Index, pd.Series)
|
|
362
|
+
):
|
|
363
|
+
style[prop] = np.asarray(val)[index % len(val)]
|
|
364
|
+
# Try key indexing for unordered, dict-like types
|
|
365
|
+
if (
|
|
366
|
+
(key is not None)
|
|
367
|
+
and (not isinstance(val, (str, tuple, list, np.ndarray)))
|
|
368
|
+
and hasattr(val, "__getitem__")
|
|
369
|
+
):
|
|
370
|
+
# If only a subset of keys is provided, default the other ones
|
|
371
|
+
# to the empty type constructor (e.g. 0 for ints, 0.0 for floats,
|
|
372
|
+
# empty strings).
|
|
373
|
+
if key in val:
|
|
374
|
+
style[prop] = val[key]
|
|
375
|
+
else:
|
|
376
|
+
valtype = type(next(iter(val.values())))
|
|
377
|
+
style[prop] = valtype()
|
|
378
|
+
|
|
379
|
+
return style
|