Trajectree 0.0.1__py3-none-any.whl → 0.0.2__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.
- trajectree/__init__.py +0 -3
- trajectree/fock_optics/devices.py +1 -1
- trajectree/fock_optics/light_sources.py +2 -2
- trajectree/fock_optics/measurement.py +3 -3
- trajectree/fock_optics/utils.py +6 -6
- trajectree/trajectory.py +2 -2
- {trajectree-0.0.1.dist-info → trajectree-0.0.2.dist-info}/METADATA +2 -3
- trajectree-0.0.2.dist-info/RECORD +16 -0
- trajectree/quimb/docs/_pygments/_pygments_dark.py +0 -118
- trajectree/quimb/docs/_pygments/_pygments_light.py +0 -118
- trajectree/quimb/docs/conf.py +0 -158
- trajectree/quimb/docs/examples/ex_mpi_expm_evo.py +0 -62
- trajectree/quimb/quimb/__init__.py +0 -507
- trajectree/quimb/quimb/calc.py +0 -1491
- trajectree/quimb/quimb/core.py +0 -2279
- trajectree/quimb/quimb/evo.py +0 -712
- trajectree/quimb/quimb/experimental/__init__.py +0 -0
- trajectree/quimb/quimb/experimental/autojittn.py +0 -129
- trajectree/quimb/quimb/experimental/belief_propagation/__init__.py +0 -109
- trajectree/quimb/quimb/experimental/belief_propagation/bp_common.py +0 -397
- trajectree/quimb/quimb/experimental/belief_propagation/d1bp.py +0 -316
- trajectree/quimb/quimb/experimental/belief_propagation/d2bp.py +0 -653
- trajectree/quimb/quimb/experimental/belief_propagation/hd1bp.py +0 -571
- trajectree/quimb/quimb/experimental/belief_propagation/hv1bp.py +0 -775
- trajectree/quimb/quimb/experimental/belief_propagation/l1bp.py +0 -316
- trajectree/quimb/quimb/experimental/belief_propagation/l2bp.py +0 -537
- trajectree/quimb/quimb/experimental/belief_propagation/regions.py +0 -194
- trajectree/quimb/quimb/experimental/cluster_update.py +0 -286
- trajectree/quimb/quimb/experimental/merabuilder.py +0 -865
- trajectree/quimb/quimb/experimental/operatorbuilder/__init__.py +0 -15
- trajectree/quimb/quimb/experimental/operatorbuilder/operatorbuilder.py +0 -1631
- trajectree/quimb/quimb/experimental/schematic.py +0 -7
- trajectree/quimb/quimb/experimental/tn_marginals.py +0 -130
- trajectree/quimb/quimb/experimental/tnvmc.py +0 -1483
- trajectree/quimb/quimb/gates.py +0 -36
- trajectree/quimb/quimb/gen/__init__.py +0 -2
- trajectree/quimb/quimb/gen/operators.py +0 -1167
- trajectree/quimb/quimb/gen/rand.py +0 -713
- trajectree/quimb/quimb/gen/states.py +0 -479
- trajectree/quimb/quimb/linalg/__init__.py +0 -6
- trajectree/quimb/quimb/linalg/approx_spectral.py +0 -1109
- trajectree/quimb/quimb/linalg/autoblock.py +0 -258
- trajectree/quimb/quimb/linalg/base_linalg.py +0 -719
- trajectree/quimb/quimb/linalg/mpi_launcher.py +0 -397
- trajectree/quimb/quimb/linalg/numpy_linalg.py +0 -244
- trajectree/quimb/quimb/linalg/rand_linalg.py +0 -514
- trajectree/quimb/quimb/linalg/scipy_linalg.py +0 -293
- trajectree/quimb/quimb/linalg/slepc_linalg.py +0 -892
- trajectree/quimb/quimb/schematic.py +0 -1518
- trajectree/quimb/quimb/tensor/__init__.py +0 -401
- trajectree/quimb/quimb/tensor/array_ops.py +0 -610
- trajectree/quimb/quimb/tensor/circuit.py +0 -4824
- trajectree/quimb/quimb/tensor/circuit_gen.py +0 -411
- trajectree/quimb/quimb/tensor/contraction.py +0 -336
- trajectree/quimb/quimb/tensor/decomp.py +0 -1255
- trajectree/quimb/quimb/tensor/drawing.py +0 -1646
- trajectree/quimb/quimb/tensor/fitting.py +0 -385
- trajectree/quimb/quimb/tensor/geometry.py +0 -583
- trajectree/quimb/quimb/tensor/interface.py +0 -114
- trajectree/quimb/quimb/tensor/networking.py +0 -1058
- trajectree/quimb/quimb/tensor/optimize.py +0 -1818
- trajectree/quimb/quimb/tensor/tensor_1d.py +0 -4778
- trajectree/quimb/quimb/tensor/tensor_1d_compress.py +0 -1854
- trajectree/quimb/quimb/tensor/tensor_1d_tebd.py +0 -662
- trajectree/quimb/quimb/tensor/tensor_2d.py +0 -5954
- trajectree/quimb/quimb/tensor/tensor_2d_compress.py +0 -96
- trajectree/quimb/quimb/tensor/tensor_2d_tebd.py +0 -1230
- trajectree/quimb/quimb/tensor/tensor_3d.py +0 -2869
- trajectree/quimb/quimb/tensor/tensor_3d_tebd.py +0 -46
- trajectree/quimb/quimb/tensor/tensor_approx_spectral.py +0 -60
- trajectree/quimb/quimb/tensor/tensor_arbgeom.py +0 -3237
- trajectree/quimb/quimb/tensor/tensor_arbgeom_compress.py +0 -565
- trajectree/quimb/quimb/tensor/tensor_arbgeom_tebd.py +0 -1138
- trajectree/quimb/quimb/tensor/tensor_builder.py +0 -5411
- trajectree/quimb/quimb/tensor/tensor_core.py +0 -11179
- trajectree/quimb/quimb/tensor/tensor_dmrg.py +0 -1472
- trajectree/quimb/quimb/tensor/tensor_mera.py +0 -204
- trajectree/quimb/quimb/utils.py +0 -892
- trajectree/quimb/tests/__init__.py +0 -0
- trajectree/quimb/tests/test_accel.py +0 -501
- trajectree/quimb/tests/test_calc.py +0 -788
- trajectree/quimb/tests/test_core.py +0 -847
- trajectree/quimb/tests/test_evo.py +0 -565
- trajectree/quimb/tests/test_gen/__init__.py +0 -0
- trajectree/quimb/tests/test_gen/test_operators.py +0 -361
- trajectree/quimb/tests/test_gen/test_rand.py +0 -296
- trajectree/quimb/tests/test_gen/test_states.py +0 -261
- trajectree/quimb/tests/test_linalg/__init__.py +0 -0
- trajectree/quimb/tests/test_linalg/test_approx_spectral.py +0 -368
- trajectree/quimb/tests/test_linalg/test_base_linalg.py +0 -351
- trajectree/quimb/tests/test_linalg/test_mpi_linalg.py +0 -127
- trajectree/quimb/tests/test_linalg/test_numpy_linalg.py +0 -84
- trajectree/quimb/tests/test_linalg/test_rand_linalg.py +0 -134
- trajectree/quimb/tests/test_linalg/test_slepc_linalg.py +0 -283
- trajectree/quimb/tests/test_tensor/__init__.py +0 -0
- trajectree/quimb/tests/test_tensor/test_belief_propagation/__init__.py +0 -0
- trajectree/quimb/tests/test_tensor/test_belief_propagation/test_d1bp.py +0 -39
- trajectree/quimb/tests/test_tensor/test_belief_propagation/test_d2bp.py +0 -67
- trajectree/quimb/tests/test_tensor/test_belief_propagation/test_hd1bp.py +0 -64
- trajectree/quimb/tests/test_tensor/test_belief_propagation/test_hv1bp.py +0 -51
- trajectree/quimb/tests/test_tensor/test_belief_propagation/test_l1bp.py +0 -142
- trajectree/quimb/tests/test_tensor/test_belief_propagation/test_l2bp.py +0 -101
- trajectree/quimb/tests/test_tensor/test_circuit.py +0 -816
- trajectree/quimb/tests/test_tensor/test_contract.py +0 -67
- trajectree/quimb/tests/test_tensor/test_decomp.py +0 -40
- trajectree/quimb/tests/test_tensor/test_mera.py +0 -52
- trajectree/quimb/tests/test_tensor/test_optimizers.py +0 -488
- trajectree/quimb/tests/test_tensor/test_tensor_1d.py +0 -1171
- trajectree/quimb/tests/test_tensor/test_tensor_2d.py +0 -606
- trajectree/quimb/tests/test_tensor/test_tensor_2d_tebd.py +0 -144
- trajectree/quimb/tests/test_tensor/test_tensor_3d.py +0 -123
- trajectree/quimb/tests/test_tensor/test_tensor_arbgeom.py +0 -226
- trajectree/quimb/tests/test_tensor/test_tensor_builder.py +0 -441
- trajectree/quimb/tests/test_tensor/test_tensor_core.py +0 -2066
- trajectree/quimb/tests/test_tensor/test_tensor_dmrg.py +0 -388
- trajectree/quimb/tests/test_tensor/test_tensor_spectral_approx.py +0 -63
- trajectree/quimb/tests/test_tensor/test_tensor_tebd.py +0 -270
- trajectree/quimb/tests/test_utils.py +0 -85
- trajectree-0.0.1.dist-info/RECORD +0 -126
- {trajectree-0.0.1.dist-info → trajectree-0.0.2.dist-info}/WHEEL +0 -0
- {trajectree-0.0.1.dist-info → trajectree-0.0.2.dist-info}/licenses/LICENSE +0 -0
- {trajectree-0.0.1.dist-info → trajectree-0.0.2.dist-info}/top_level.txt +0 -0
|
@@ -1,1518 +0,0 @@
|
|
|
1
|
-
"""Draw psuedo-3D diagrams using matplotlib.
|
|
2
|
-
"""
|
|
3
|
-
|
|
4
|
-
import functools
|
|
5
|
-
import warnings
|
|
6
|
-
from math import atan2, cos, pi, sin
|
|
7
|
-
|
|
8
|
-
import matplotlib as mpl
|
|
9
|
-
import matplotlib.pyplot as plt
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class Drawing:
|
|
13
|
-
"""Draw 2D or pseudo-3D diagrams using matplotlib. This handles the
|
|
14
|
-
axonometric projection and the z-ordering of the elements, as well as named
|
|
15
|
-
preset styles for repeated elements, and the automatic adjustment of the
|
|
16
|
-
figure limits. It also has basic support for drawing smooth curves and
|
|
17
|
-
shaded areas around certain elements automatically.
|
|
18
|
-
|
|
19
|
-
Parameters
|
|
20
|
-
----------
|
|
21
|
-
background : color, optional
|
|
22
|
-
The background color of the figure, defaults to transparent.
|
|
23
|
-
drawcolor : color, optional
|
|
24
|
-
The default color to draw lines and text in.
|
|
25
|
-
shapecolor : color, optional
|
|
26
|
-
The default color to fill shapes with.
|
|
27
|
-
a : float
|
|
28
|
-
The axonometric angle of the x-axis in degrees.
|
|
29
|
-
b : float
|
|
30
|
-
The axonometric angle of the y-axis in degrees.
|
|
31
|
-
xscale : float
|
|
32
|
-
A factor to scale the x-axis by.
|
|
33
|
-
yscale : float
|
|
34
|
-
A factor to scale the y-axis by.
|
|
35
|
-
zscale : float
|
|
36
|
-
A factor to scale the z-axis by.
|
|
37
|
-
presets : dict
|
|
38
|
-
A dictionary of named style presets. When you add an element to the
|
|
39
|
-
drawing, you can specify a preset name to use as default styling.
|
|
40
|
-
ax : matplotlib.axes.Axes
|
|
41
|
-
The axes to draw on. If None, a new figure is created.
|
|
42
|
-
kwargs
|
|
43
|
-
Passed to ``plt.figure`` if ``ax`` is None.
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
def __init__(
|
|
47
|
-
self,
|
|
48
|
-
background=(0, 0, 0, 0),
|
|
49
|
-
drawcolor=(0.14, 0.15, 0.16, 1.0),
|
|
50
|
-
shapecolor=(0.45, 0.50, 0.55, 1.0),
|
|
51
|
-
a=50,
|
|
52
|
-
b=12,
|
|
53
|
-
xscale=1,
|
|
54
|
-
yscale=1,
|
|
55
|
-
zscale=1,
|
|
56
|
-
presets=None,
|
|
57
|
-
ax=None,
|
|
58
|
-
**kwargs,
|
|
59
|
-
):
|
|
60
|
-
if ax is None:
|
|
61
|
-
self.fig = plt.figure(**kwargs)
|
|
62
|
-
self.fig.set_facecolor(background)
|
|
63
|
-
self.ax = self.fig.add_subplot(111)
|
|
64
|
-
else:
|
|
65
|
-
self.ax = ax
|
|
66
|
-
self.fig = self.ax.figure
|
|
67
|
-
|
|
68
|
-
self.ax.set_axis_off()
|
|
69
|
-
self.ax.set_aspect("equal")
|
|
70
|
-
self.ax.set_clip_on(False)
|
|
71
|
-
|
|
72
|
-
self.drawcolor = drawcolor
|
|
73
|
-
self.shapecolor = shapecolor
|
|
74
|
-
|
|
75
|
-
self._xmin = None
|
|
76
|
-
self._xmax = None
|
|
77
|
-
self._ymin = None
|
|
78
|
-
self._ymax = None
|
|
79
|
-
self.presets = {} if presets is None else dict(presets)
|
|
80
|
-
self.presets.setdefault(None, {})
|
|
81
|
-
|
|
82
|
-
self._2d_project = functools.partial(
|
|
83
|
-
simple_scale,
|
|
84
|
-
xscale=xscale,
|
|
85
|
-
yscale=yscale,
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
self._3d_project = functools.partial(
|
|
89
|
-
axonometric_project,
|
|
90
|
-
a=a,
|
|
91
|
-
b=b,
|
|
92
|
-
xscale=xscale,
|
|
93
|
-
yscale=yscale,
|
|
94
|
-
zscale=zscale,
|
|
95
|
-
)
|
|
96
|
-
self._coo_to_zorder = functools.partial(
|
|
97
|
-
coo_to_zorder,
|
|
98
|
-
xscale=xscale,
|
|
99
|
-
yscale=yscale,
|
|
100
|
-
zscale=zscale,
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
def _adjust_lims(self, x, y):
|
|
104
|
-
xchange = ychange = False
|
|
105
|
-
if self._xmin is None or x < self._xmin:
|
|
106
|
-
xchange = True
|
|
107
|
-
self._xmin = x
|
|
108
|
-
|
|
109
|
-
if self._xmax is None or x > self._xmax:
|
|
110
|
-
xchange = True
|
|
111
|
-
self._xmax = x
|
|
112
|
-
|
|
113
|
-
if self._ymin is None or y < self._ymin:
|
|
114
|
-
ychange = True
|
|
115
|
-
self._ymin = y
|
|
116
|
-
|
|
117
|
-
if self._ymax is None or y > self._ymax:
|
|
118
|
-
ychange = True
|
|
119
|
-
self._ymax = y
|
|
120
|
-
|
|
121
|
-
if xchange and self._xmin != self._xmax:
|
|
122
|
-
dx = self._xmax - self._xmin
|
|
123
|
-
plot_xmin = self._xmin - dx * 0.01
|
|
124
|
-
plot_xmax = self._xmax + dx * 0.01
|
|
125
|
-
self.ax.set_xlim(plot_xmin, plot_xmax)
|
|
126
|
-
if ychange and self._ymin != self._ymax:
|
|
127
|
-
dy = self._ymax - self._ymin
|
|
128
|
-
plot_ymin = self._ymin - dy * 0.01
|
|
129
|
-
plot_ymax = self._ymax + dy * 0.01
|
|
130
|
-
self.ax.set_ylim(plot_ymin, plot_ymax)
|
|
131
|
-
|
|
132
|
-
def text(self, coo, text, preset=None, **kwargs):
|
|
133
|
-
"""Place text at the specified coordinate.
|
|
134
|
-
|
|
135
|
-
Parameters
|
|
136
|
-
----------
|
|
137
|
-
coo : tuple[int, int] or tuple[int, int, int]
|
|
138
|
-
The 2D or 3D coordinate of the text. If 3D, the coordinate will be
|
|
139
|
-
projected onto the 2D plane, and a z-order will be assigned.
|
|
140
|
-
text : str
|
|
141
|
-
The text to place.
|
|
142
|
-
preset : str, optional
|
|
143
|
-
A preset style to use for the text.
|
|
144
|
-
kwargs
|
|
145
|
-
Specific style options passed to ``matplotlib.axes.Axes.text``.
|
|
146
|
-
"""
|
|
147
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
148
|
-
style.setdefault("color", self.drawcolor)
|
|
149
|
-
style.setdefault("horizontalalignment", "center")
|
|
150
|
-
style.setdefault("verticalalignment", "center")
|
|
151
|
-
style.setdefault("clip_on", False)
|
|
152
|
-
|
|
153
|
-
if len(coo) == 2:
|
|
154
|
-
x, y = self._2d_project(*coo)
|
|
155
|
-
style.setdefault("zorder", +0.02)
|
|
156
|
-
else:
|
|
157
|
-
x, y = self._3d_project(*coo)
|
|
158
|
-
style.setdefault("zorder", self._coo_to_zorder(*coo) + 0.02)
|
|
159
|
-
|
|
160
|
-
self.ax.text(x, y, text, **style)
|
|
161
|
-
self._adjust_lims(x, y)
|
|
162
|
-
|
|
163
|
-
def text_between(self, cooa, coob, text, preset=None, **kwargs):
|
|
164
|
-
"""Place text between two coordinates.
|
|
165
|
-
|
|
166
|
-
Parameters
|
|
167
|
-
----------
|
|
168
|
-
cooa, coob : tuple[int, int] or tuple[int, int, int]
|
|
169
|
-
The 2D or 3D coordinates of the text endpoints. If 3D, the
|
|
170
|
-
coordinates will be projected onto the 2D plane, and a z-order
|
|
171
|
-
will be assigned based on average z-order of the endpoints.
|
|
172
|
-
text : str
|
|
173
|
-
The text to place.
|
|
174
|
-
center : float, optional
|
|
175
|
-
The position of the text along the line, where 0.0 is the start and
|
|
176
|
-
1.0 is the end. Default is 0.5.
|
|
177
|
-
preset : str, optional
|
|
178
|
-
A preset style to use for the text.
|
|
179
|
-
kwargs
|
|
180
|
-
Specific style options passed to ``matplotlib.axes.Axes.text``.
|
|
181
|
-
"""
|
|
182
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
183
|
-
style.setdefault("color", self.drawcolor)
|
|
184
|
-
style.setdefault("horizontalalignment", "center")
|
|
185
|
-
style.setdefault("verticalalignment", "center")
|
|
186
|
-
style.setdefault("clip_on", False)
|
|
187
|
-
center = style.pop("center", 0.5)
|
|
188
|
-
|
|
189
|
-
if len(cooa) == 2:
|
|
190
|
-
xa, ya = self._2d_project(*cooa)
|
|
191
|
-
xb, yb = self._2d_project(*coob)
|
|
192
|
-
style.setdefault("zorder", +0.02)
|
|
193
|
-
else:
|
|
194
|
-
style.setdefault(
|
|
195
|
-
"zorder",
|
|
196
|
-
mean(self._coo_to_zorder(*coo) for coo in [cooa, coob]) + 0.02,
|
|
197
|
-
)
|
|
198
|
-
xa, ya = self._3d_project(*cooa)
|
|
199
|
-
xb, yb = self._3d_project(*coob)
|
|
200
|
-
|
|
201
|
-
# compute midpoint
|
|
202
|
-
x = xa * (1 - center) + xb * center
|
|
203
|
-
y = ya * (1 - center) + yb * center
|
|
204
|
-
|
|
205
|
-
# compute angle
|
|
206
|
-
if xa <= xb:
|
|
207
|
-
angle = atan2(yb - ya, xb - xa) * 180 / pi
|
|
208
|
-
else:
|
|
209
|
-
angle = atan2(ya - yb, xa - xb) * 180 / pi
|
|
210
|
-
style.setdefault("rotation", angle)
|
|
211
|
-
|
|
212
|
-
self.ax.text(x, y, text, **style)
|
|
213
|
-
self._adjust_lims(x, y)
|
|
214
|
-
|
|
215
|
-
def label_ax(self, x, y, text, preset=None, **kwargs):
|
|
216
|
-
"""Place text at the specified location, using the axis coordinates
|
|
217
|
-
rather than 2D or 3D data coordinates.
|
|
218
|
-
|
|
219
|
-
Parameters
|
|
220
|
-
----------
|
|
221
|
-
x, y : float
|
|
222
|
-
The x and y positions of the text, relative to the axis.
|
|
223
|
-
text : str
|
|
224
|
-
The text to place.
|
|
225
|
-
preset : str, optional
|
|
226
|
-
A preset style to use for the text.
|
|
227
|
-
kwargs
|
|
228
|
-
Specific style options passed to ``matplotlib.axes.Axes.text``.
|
|
229
|
-
"""
|
|
230
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
231
|
-
style.setdefault("color", self.drawcolor)
|
|
232
|
-
style.setdefault("horizontalalignment", "center")
|
|
233
|
-
style.setdefault("verticalalignment", "center")
|
|
234
|
-
style.setdefault("transform", self.ax.transAxes)
|
|
235
|
-
self.ax.text(x, y, text, **style)
|
|
236
|
-
self._adjust_lims(x, y)
|
|
237
|
-
|
|
238
|
-
def label_fig(self, x, y, text, preset=None, **kwargs):
|
|
239
|
-
"""Place text at the specified location, using the figure coordinates
|
|
240
|
-
rather than 2D or 3D data coordinates.
|
|
241
|
-
|
|
242
|
-
Parameters
|
|
243
|
-
----------
|
|
244
|
-
x, y : float
|
|
245
|
-
The x and y positions of the text, relative to the figure.
|
|
246
|
-
text : str
|
|
247
|
-
The text to place.
|
|
248
|
-
preset : str, optional
|
|
249
|
-
A preset style to use for the text.
|
|
250
|
-
kwargs
|
|
251
|
-
Specific style options passed to ``matplotlib.axes.Axes.text``.
|
|
252
|
-
"""
|
|
253
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
254
|
-
style.setdefault("color", self.drawcolor)
|
|
255
|
-
style.setdefault("horizontalalignment", "center")
|
|
256
|
-
style.setdefault("verticalalignment", "center")
|
|
257
|
-
style.setdefault("transform", self.fig.transFigure)
|
|
258
|
-
self.ax.text(x, y, text, **style)
|
|
259
|
-
self._adjust_lims(x, y)
|
|
260
|
-
|
|
261
|
-
def _parse_style_for_marker(self, coo, preset=None, **kwargs):
|
|
262
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
263
|
-
if "color" in style:
|
|
264
|
-
# assume coloring whole shape
|
|
265
|
-
style.setdefault("facecolor", style.pop("color"))
|
|
266
|
-
style.setdefault("facecolor", self.shapecolor)
|
|
267
|
-
style.setdefault("edgecolor", darken_color(style["facecolor"]))
|
|
268
|
-
style.setdefault("linewidth", 1)
|
|
269
|
-
style.setdefault("radius", 0.25)
|
|
270
|
-
|
|
271
|
-
if len(coo) == 2:
|
|
272
|
-
x, y = self._2d_project(*coo)
|
|
273
|
-
style.setdefault("zorder", +0.01)
|
|
274
|
-
else:
|
|
275
|
-
x, y = self._3d_project(*coo)
|
|
276
|
-
style.setdefault("zorder", self._coo_to_zorder(*coo) + 0.01)
|
|
277
|
-
|
|
278
|
-
return x, y, style
|
|
279
|
-
|
|
280
|
-
def _adjust_lims_for_marker(self, x, y, r):
|
|
281
|
-
for x, y in [
|
|
282
|
-
(x - 1.1 * r, y),
|
|
283
|
-
(x + 1.1 * r, y),
|
|
284
|
-
(x, y - 1.1 * r),
|
|
285
|
-
(x, y + 1.1 * r),
|
|
286
|
-
]:
|
|
287
|
-
self._adjust_lims(x, y)
|
|
288
|
-
|
|
289
|
-
def circle(self, coo, preset=None, **kwargs):
|
|
290
|
-
"""Draw a circle at the specified coordinate.
|
|
291
|
-
|
|
292
|
-
Parameters
|
|
293
|
-
----------
|
|
294
|
-
coo : tuple[int, int] or tuple[int, int, int]
|
|
295
|
-
The 2D or 3D coordinate of the circle. If 3D, the coordinate will
|
|
296
|
-
be projected onto the 2D plane, and a z-order will be assigned.
|
|
297
|
-
preset : str, optional
|
|
298
|
-
A preset style to use for the circle.
|
|
299
|
-
kwargs
|
|
300
|
-
Specific style options passed to ``matplotlib.patches.Circle``.
|
|
301
|
-
"""
|
|
302
|
-
x, y, style = self._parse_style_for_marker(
|
|
303
|
-
coo, preset=preset, **kwargs
|
|
304
|
-
)
|
|
305
|
-
circle = mpl.patches.Circle((x, y), **style)
|
|
306
|
-
self.ax.add_artist(circle)
|
|
307
|
-
self._adjust_lims_for_marker(x, y, style["radius"])
|
|
308
|
-
|
|
309
|
-
def wedge(self, coo, theta1, theta2, preset=None, **kwargs):
|
|
310
|
-
"""Draw a wedge at the specified coordinate.
|
|
311
|
-
|
|
312
|
-
Parameters
|
|
313
|
-
----------
|
|
314
|
-
coo : tuple[int, int] or tuple[int, int, int]
|
|
315
|
-
The 2D or 3D coordinate of the wedge. If 3D, the coordinate will
|
|
316
|
-
be projected onto the 2D plane, and a z-order will be assigned.
|
|
317
|
-
theta1 : float
|
|
318
|
-
The angle in degrees of the first edge of the wedge.
|
|
319
|
-
theta2 : float
|
|
320
|
-
The angle in degrees of the second edge of the wedge.
|
|
321
|
-
preset : str, optional
|
|
322
|
-
A preset style to use for the wedge.
|
|
323
|
-
kwargs
|
|
324
|
-
Specific style options passed to ``matplotlib.patches.Wedge``.
|
|
325
|
-
"""
|
|
326
|
-
x, y, style = self._parse_style_for_marker(
|
|
327
|
-
coo, preset=preset, **kwargs
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
# wedge uses r, not radius
|
|
331
|
-
style["r"] = style.pop("radius")
|
|
332
|
-
# and is not filled by default
|
|
333
|
-
style.setdefault("fill", True)
|
|
334
|
-
|
|
335
|
-
wedge = mpl.patches.Wedge(
|
|
336
|
-
(x, y), theta1=theta1, theta2=theta2, **style
|
|
337
|
-
)
|
|
338
|
-
|
|
339
|
-
self.ax.add_artist(wedge)
|
|
340
|
-
self._adjust_lims_for_marker(x, y, style["r"])
|
|
341
|
-
|
|
342
|
-
def dot(self, coo, preset=None, **kwargs):
|
|
343
|
-
"""Draw a small circle with no border. Alias for circle with defaults
|
|
344
|
-
`radius=0.1` and `linewidth=0.0`.
|
|
345
|
-
|
|
346
|
-
Parameters
|
|
347
|
-
----------
|
|
348
|
-
coo : tuple[int, int] or tuple[int, int, int]
|
|
349
|
-
The 2D or 3D coordinate of the dot. If 3D, the coordinate will
|
|
350
|
-
be projected onto the 2D plane, and a z-order will be assigned.
|
|
351
|
-
preset : str, optional
|
|
352
|
-
A preset style to use for the dot.
|
|
353
|
-
kwargs
|
|
354
|
-
Specific style options passed to ``matplotlib.patches.Circle``.
|
|
355
|
-
"""
|
|
356
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
357
|
-
style.setdefault("radius", 0.1)
|
|
358
|
-
style.setdefault("linewidth", 0.0)
|
|
359
|
-
self.circle(coo, **style)
|
|
360
|
-
|
|
361
|
-
def regular_polygon(self, coo, preset=None, **kwargs):
|
|
362
|
-
"""Draw a regular polygon at the specified coordinate.
|
|
363
|
-
|
|
364
|
-
Parameters
|
|
365
|
-
----------
|
|
366
|
-
coo : tuple[int, int] or tuple[int, int, int]
|
|
367
|
-
The 2D or 3D coordinate of the polygon. If 3D, the coordinate will
|
|
368
|
-
be projected onto the 2D plane, and a z-order will be assigned.
|
|
369
|
-
n : int
|
|
370
|
-
The number of sides of the polygon.
|
|
371
|
-
orientation : float, optional
|
|
372
|
-
The orientation of the polygon in radians. Default is 0.0.
|
|
373
|
-
preset : str, optional
|
|
374
|
-
A preset style to use for the polygon.
|
|
375
|
-
kwargs
|
|
376
|
-
Specific style options passed to ``matplotlib.patches.Polygon``.
|
|
377
|
-
"""
|
|
378
|
-
x, y, style = self._parse_style_for_marker(
|
|
379
|
-
coo, preset=preset, **kwargs
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
n = style.pop("n", 3)
|
|
383
|
-
orientation = style.pop("orientation", 0.0)
|
|
384
|
-
|
|
385
|
-
rpoly = mpl.patches.RegularPolygon(
|
|
386
|
-
(x, y), numVertices=n, orientation=orientation, **style
|
|
387
|
-
)
|
|
388
|
-
self.ax.add_artist(rpoly)
|
|
389
|
-
self._adjust_lims_for_marker(x, y, style["radius"])
|
|
390
|
-
|
|
391
|
-
def marker(self, coo, preset=None, **kwargs):
|
|
392
|
-
"""Draw a 'marker' at the specified coordinate. This is a shorthand for
|
|
393
|
-
creating polygons with shape specified by a single character.
|
|
394
|
-
|
|
395
|
-
Parameters
|
|
396
|
-
----------
|
|
397
|
-
coo : tuple[int, int] or tuple[int, int, int]
|
|
398
|
-
The 2D or 3D coordinate of the marker. If 3D, the coordinate will
|
|
399
|
-
be projected onto the 2D plane, and a z-order will be assigned.
|
|
400
|
-
marker : str, optional
|
|
401
|
-
The marker shape to draw. One of ``"o.v^<>sDphH8"``.
|
|
402
|
-
preset : str, optional
|
|
403
|
-
A preset style to use for the marker.
|
|
404
|
-
kwargs
|
|
405
|
-
Specific style options.
|
|
406
|
-
"""
|
|
407
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
408
|
-
marker = style.pop("marker", "s")
|
|
409
|
-
if marker in ("o", "."):
|
|
410
|
-
return self.circle(coo, preset=preset, **style)
|
|
411
|
-
|
|
412
|
-
if isinstance(marker, int):
|
|
413
|
-
n = marker
|
|
414
|
-
orientation = 0.0
|
|
415
|
-
else:
|
|
416
|
-
n, orientation = {
|
|
417
|
-
"v": (3, pi / 3),
|
|
418
|
-
"^": (3, 0),
|
|
419
|
-
"<": (3, pi / 2),
|
|
420
|
-
">": (3, -pi / 2),
|
|
421
|
-
"s": (4, pi / 4),
|
|
422
|
-
"D": (4, 0),
|
|
423
|
-
"p": (5, 0),
|
|
424
|
-
"h": (6, 0),
|
|
425
|
-
"H": (6, pi / 2),
|
|
426
|
-
"8": (8, 0),
|
|
427
|
-
}[marker]
|
|
428
|
-
|
|
429
|
-
self.regular_polygon(
|
|
430
|
-
coo, preset=preset, n=n, orientation=orientation, **style
|
|
431
|
-
)
|
|
432
|
-
|
|
433
|
-
def square(self, coo, preset=None, **kwargs):
|
|
434
|
-
return self.marker(coo, preset=preset, marker="s", **kwargs)
|
|
435
|
-
|
|
436
|
-
def cube(self, coo, preset=None, **kwargs):
|
|
437
|
-
"""Draw a cube at the specified coordinate, which must be 3D.
|
|
438
|
-
|
|
439
|
-
Parameters
|
|
440
|
-
----------
|
|
441
|
-
coo : tuple[int, int, int]
|
|
442
|
-
The 3D coordinate of the cube. The coordinate will
|
|
443
|
-
be projected onto the 2D plane, and a z-order will be assigned.
|
|
444
|
-
preset : str, optional
|
|
445
|
-
A preset style to use for the cube.
|
|
446
|
-
kwargs
|
|
447
|
-
Specific style options passed to ``matplotlib.patches.Polygon``.
|
|
448
|
-
"""
|
|
449
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
450
|
-
|
|
451
|
-
r = style.pop("radius", 0.25)
|
|
452
|
-
x, y, z = coo
|
|
453
|
-
xm, xp = x - r, x + r
|
|
454
|
-
ym, yp = y - r, y + r
|
|
455
|
-
zm, zp = z - r, z + r
|
|
456
|
-
|
|
457
|
-
faces = [
|
|
458
|
-
((xm, ym, zm), (xm, ym, zp), (xm, yp, zp), (xm, yp, zm)),
|
|
459
|
-
((xp, ym, zm), (xp, ym, zp), (xp, yp, zp), (xp, yp, zm)),
|
|
460
|
-
((xp, ym, zm), (xp, ym, zp), (xm, ym, zp), (xm, ym, zm)),
|
|
461
|
-
((xp, yp, zm), (xp, yp, zp), (xm, yp, zp), (xm, yp, zm)),
|
|
462
|
-
((xp, ym, zm), (xp, yp, zm), (xm, yp, zm), (xm, ym, zm)),
|
|
463
|
-
((xp, ym, zp), (xp, yp, zp), (xm, yp, zp), (xm, ym, zp)),
|
|
464
|
-
]
|
|
465
|
-
for face in faces:
|
|
466
|
-
self.shape(face, preset=preset, **style)
|
|
467
|
-
|
|
468
|
-
def line(self, cooa, coob, preset=None, **kwargs):
|
|
469
|
-
"""Draw a line between two coordinates.
|
|
470
|
-
|
|
471
|
-
Parameters
|
|
472
|
-
----------
|
|
473
|
-
cooa, coob : tuple[int, int] or tuple[int, int, int]
|
|
474
|
-
The 2D or 3D coordinates of the line endpoints. If 3D, the
|
|
475
|
-
coordinates will be projected onto the 2D plane, and a z-order
|
|
476
|
-
will be assigned based on average z-order of the endpoints.
|
|
477
|
-
stretch : float
|
|
478
|
-
Stretch the line by this factor. 1.0 is no stretch, 0.5 is half
|
|
479
|
-
length, 2.0 is double length. Default is 1.0.
|
|
480
|
-
arrowhead : bool or dict, optional
|
|
481
|
-
Draw an arrowhead at the end of the line. Default is False. If a
|
|
482
|
-
dict, it is passed as keyword arguments to the arrowhead method.
|
|
483
|
-
text_between : str, optional
|
|
484
|
-
Add text along the line.
|
|
485
|
-
preset : str, optional
|
|
486
|
-
A preset style to use for the line.
|
|
487
|
-
kwargs
|
|
488
|
-
Specific style options passed to ``matplotlib.lines.Line2D``.
|
|
489
|
-
|
|
490
|
-
See Also
|
|
491
|
-
--------
|
|
492
|
-
Drawing.arrowhead
|
|
493
|
-
"""
|
|
494
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
495
|
-
style.setdefault("color", self.drawcolor)
|
|
496
|
-
style.setdefault("solid_capstyle", "round")
|
|
497
|
-
style.setdefault("stretch", 1.0)
|
|
498
|
-
style.setdefault("arrowhead", None)
|
|
499
|
-
style.setdefault("text", None)
|
|
500
|
-
stretch = style.pop("stretch")
|
|
501
|
-
arrowhead = style.pop("arrowhead")
|
|
502
|
-
text = style.pop("text")
|
|
503
|
-
|
|
504
|
-
if len(cooa) == 2:
|
|
505
|
-
xs, ys = zip(*[self._2d_project(*coo) for coo in [cooa, coob]])
|
|
506
|
-
style.setdefault("zorder", +0.0)
|
|
507
|
-
else:
|
|
508
|
-
style.setdefault(
|
|
509
|
-
"zorder",
|
|
510
|
-
mean(self._coo_to_zorder(*coo) for coo in [cooa, coob]),
|
|
511
|
-
)
|
|
512
|
-
xs, ys = zip(*[self._3d_project(*coo) for coo in [cooa, coob]])
|
|
513
|
-
|
|
514
|
-
if stretch != 1.0:
|
|
515
|
-
# shorten around center
|
|
516
|
-
center = mean(xs), mean(ys)
|
|
517
|
-
xs = [center[0] + stretch * (x - center[0]) for x in xs]
|
|
518
|
-
ys = [center[1] + stretch * (y - center[1]) for y in ys]
|
|
519
|
-
|
|
520
|
-
if arrowhead is not None:
|
|
521
|
-
if arrowhead is True:
|
|
522
|
-
arrowhead = {}
|
|
523
|
-
else:
|
|
524
|
-
arrowhead = dict(arrowhead)
|
|
525
|
-
self.arrowhead(cooa, coob, preset=preset, **(style | arrowhead))
|
|
526
|
-
|
|
527
|
-
line = mpl.lines.Line2D(xs, ys, **style)
|
|
528
|
-
self.ax.add_artist(line)
|
|
529
|
-
|
|
530
|
-
if text:
|
|
531
|
-
if isinstance(text, str):
|
|
532
|
-
text = {"text": text}
|
|
533
|
-
else:
|
|
534
|
-
text = dict(text)
|
|
535
|
-
|
|
536
|
-
# don't want to pass full style dict to text_between
|
|
537
|
-
text.setdefault("zorder", style["zorder"])
|
|
538
|
-
self.text_between(cooa, coob, **text)
|
|
539
|
-
|
|
540
|
-
for x, y in zip(xs, ys):
|
|
541
|
-
self._adjust_lims(x, y)
|
|
542
|
-
|
|
543
|
-
def line_offset(
|
|
544
|
-
self,
|
|
545
|
-
cooa,
|
|
546
|
-
coob,
|
|
547
|
-
offset,
|
|
548
|
-
midlength=0.5,
|
|
549
|
-
relative=True,
|
|
550
|
-
preset=None,
|
|
551
|
-
**kwargs,
|
|
552
|
-
):
|
|
553
|
-
"""Draw a line between two coordinates, but curving out by a given
|
|
554
|
-
offset perpendicular to the line.
|
|
555
|
-
|
|
556
|
-
Parameters
|
|
557
|
-
----------
|
|
558
|
-
cooa, coob : tuple[int, int] or tuple[int, int, int]
|
|
559
|
-
The 2D or 3D coordinates of the line endpoints. If 3D, the
|
|
560
|
-
coordinates will be projected onto the 2D plane, and a z-order
|
|
561
|
-
will be assigned based on average z-order of the endpoints.
|
|
562
|
-
offset : float
|
|
563
|
-
The offset of the curve from the line, as a fraction of the total
|
|
564
|
-
line length. This is always processed in the 2D projected plane.
|
|
565
|
-
midlength : float
|
|
566
|
-
The length of the middle straight section, as a fraction of the
|
|
567
|
-
total line length. Default is 0.5.
|
|
568
|
-
arrowhead : bool or dict, optional
|
|
569
|
-
Draw an arrowhead at the end of the line. Default is False. If a
|
|
570
|
-
dict, it is passed as keyword arguments to the arrowhead method.
|
|
571
|
-
text_between : str, optional
|
|
572
|
-
Add text along the line.
|
|
573
|
-
relative : bool, optional
|
|
574
|
-
If ``True`` (the default), then ``offset`` is taken as a fraction
|
|
575
|
-
of the line length, else in absolute units.
|
|
576
|
-
preset : str, optional
|
|
577
|
-
A preset style to use for the line.
|
|
578
|
-
kwargs
|
|
579
|
-
Specific style options passed to ``curve``.
|
|
580
|
-
"""
|
|
581
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
582
|
-
style.setdefault("arrowhead", None)
|
|
583
|
-
style.setdefault("text", None)
|
|
584
|
-
arrowhead = style.pop("arrowhead")
|
|
585
|
-
text = style.pop("text")
|
|
586
|
-
|
|
587
|
-
if len(cooa) == 2:
|
|
588
|
-
xs, ys = zip(*[self._2d_project(*coo) for coo in [cooa, coob]])
|
|
589
|
-
style.setdefault("zorder", +0.0)
|
|
590
|
-
else:
|
|
591
|
-
style.setdefault(
|
|
592
|
-
"zorder",
|
|
593
|
-
mean(self._coo_to_zorder(*coo) for coo in [cooa, coob]),
|
|
594
|
-
)
|
|
595
|
-
xs, ys = zip(*[self._3d_project(*coo) for coo in [cooa, coob]])
|
|
596
|
-
|
|
597
|
-
cooa = xs[0], ys[0]
|
|
598
|
-
coob = xs[1], ys[1]
|
|
599
|
-
forward, inverse = get_rotator_and_inverse(cooa, coob)
|
|
600
|
-
R = forward(*coob)[0]
|
|
601
|
-
|
|
602
|
-
if relative:
|
|
603
|
-
offset *= R
|
|
604
|
-
|
|
605
|
-
endlength = (1 - midlength) / 2
|
|
606
|
-
cooml = inverse(endlength * R, offset)
|
|
607
|
-
coomm = inverse(R / 2, offset)
|
|
608
|
-
coomr = inverse((1 - endlength) * R, offset)
|
|
609
|
-
curve_pts = [cooa, cooml, coomm, coomr, coob]
|
|
610
|
-
|
|
611
|
-
if arrowhead is not None:
|
|
612
|
-
if arrowhead is True:
|
|
613
|
-
arrowhead = {}
|
|
614
|
-
else:
|
|
615
|
-
arrowhead = dict(arrowhead)
|
|
616
|
-
|
|
617
|
-
# want to correct center for midlength
|
|
618
|
-
center = arrowhead.pop("center", 0.5)
|
|
619
|
-
arrowhead["center"] = min(
|
|
620
|
-
max(0.0, 0.5 + (center - 0.5) / midlength), 1.0
|
|
621
|
-
)
|
|
622
|
-
self.arrowhead(cooml, coomr, preset=preset, **(style | arrowhead))
|
|
623
|
-
|
|
624
|
-
self.curve(curve_pts, preset=preset, **style)
|
|
625
|
-
|
|
626
|
-
if text:
|
|
627
|
-
if isinstance(text, str):
|
|
628
|
-
text = {"text": text}
|
|
629
|
-
else:
|
|
630
|
-
text = dict(text)
|
|
631
|
-
# don't want to pass full style dict to text_between
|
|
632
|
-
text.setdefault("zorder", style["zorder"])
|
|
633
|
-
self.text_between(cooml, coomr, **text)
|
|
634
|
-
|
|
635
|
-
for coo in curve_pts:
|
|
636
|
-
self._adjust_lims(*coo)
|
|
637
|
-
|
|
638
|
-
def arrowhead(self, cooa, coob, preset=None, **kwargs):
|
|
639
|
-
"""Draw just a arrowhead on the line between ``cooa`` and ``coob``.
|
|
640
|
-
|
|
641
|
-
Parameters
|
|
642
|
-
----------
|
|
643
|
-
cooa, coob : tuple[int, int] or tuple[int, int, int]
|
|
644
|
-
The coordinates of the start and end of the line. If 3D, the
|
|
645
|
-
coordinates will be projected onto the 2D plane, and a z-order
|
|
646
|
-
will be assigned based on average z-order of the endpoints.
|
|
647
|
-
reverse : bool or "both", optional
|
|
648
|
-
Reverse the direction by switching ``cooa`` and ``coob``. If
|
|
649
|
-
``"both"``, draw an arrowhead in both directions. Default is
|
|
650
|
-
False.
|
|
651
|
-
center : float, optional
|
|
652
|
-
The position of the arrowhead along the line, where 0 is the start
|
|
653
|
-
and 1 is the end. Default is 0.5.
|
|
654
|
-
width : float, optional
|
|
655
|
-
The width of the arrowhead. Default is 0.05.
|
|
656
|
-
length : float, optional
|
|
657
|
-
The length of the arrowhead. Default is 0.1.
|
|
658
|
-
preset : str, optional
|
|
659
|
-
A preset style to use for the arrowhead, including the above
|
|
660
|
-
options.
|
|
661
|
-
kwargs
|
|
662
|
-
Specific style options passed to ``matplotlib.lines.Line2D``.
|
|
663
|
-
"""
|
|
664
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
665
|
-
style.setdefault("color", self.drawcolor)
|
|
666
|
-
style.setdefault("center", 0.5)
|
|
667
|
-
style.setdefault("width", 0.05)
|
|
668
|
-
style.setdefault("length", 0.1)
|
|
669
|
-
style.setdefault("reverse", False)
|
|
670
|
-
|
|
671
|
-
reverse = style.pop("reverse")
|
|
672
|
-
if reverse == "both":
|
|
673
|
-
self.arrowhead(cooa, coob, preset=preset, **style)
|
|
674
|
-
if reverse:
|
|
675
|
-
cooa, coob = coob, cooa
|
|
676
|
-
|
|
677
|
-
if len(cooa) != 2:
|
|
678
|
-
style.setdefault(
|
|
679
|
-
"zorder",
|
|
680
|
-
mean(self._coo_to_zorder(*coo) for coo in [cooa, coob]),
|
|
681
|
-
)
|
|
682
|
-
cooa = self._3d_project(*cooa)
|
|
683
|
-
coob = self._3d_project(*coob)
|
|
684
|
-
else:
|
|
685
|
-
cooa = self._2d_project(*cooa)
|
|
686
|
-
coob = self._2d_project(*coob)
|
|
687
|
-
|
|
688
|
-
forward, inverse = get_rotator_and_inverse(cooa, coob)
|
|
689
|
-
rb = forward(*coob)
|
|
690
|
-
|
|
691
|
-
center = style.pop("center")
|
|
692
|
-
width = style.pop("width")
|
|
693
|
-
length = style.pop("length")
|
|
694
|
-
lab = rb[0]
|
|
695
|
-
xc = center * lab
|
|
696
|
-
arrow_x = xc - length * lab
|
|
697
|
-
arrow_y = width * lab
|
|
698
|
-
|
|
699
|
-
aa, ab, ac = [
|
|
700
|
-
inverse(arrow_x, arrow_y),
|
|
701
|
-
inverse(xc, 0),
|
|
702
|
-
inverse(arrow_x, -arrow_y),
|
|
703
|
-
]
|
|
704
|
-
|
|
705
|
-
line = mpl.lines.Line2D(*zip(*(aa, ab, ac)), **style)
|
|
706
|
-
self.ax.add_artist(line)
|
|
707
|
-
for x, y in [aa, ab, ac]:
|
|
708
|
-
self._adjust_lims(x, y)
|
|
709
|
-
|
|
710
|
-
def curve(self, coos, preset=None, **kwargs):
|
|
711
|
-
"""Draw a smooth line through the given coordinates.
|
|
712
|
-
|
|
713
|
-
Parameters
|
|
714
|
-
----------
|
|
715
|
-
coos : Sequence[tuple[int, int]] or Sequence[tuple[int, int, int]]
|
|
716
|
-
The 2D or 3D coordinates of the line. If 3D, the coordinates will
|
|
717
|
-
be projected onto the 2D plane, and a z-order will be assigned
|
|
718
|
-
based on average z-order of the endpoints.
|
|
719
|
-
smoothing : float, optional
|
|
720
|
-
The amount of smoothing to apply to the curve. 0.0 is no smoothing,
|
|
721
|
-
1.0 is maximum smoothing. Default is 0.5.
|
|
722
|
-
preset : str, optional
|
|
723
|
-
A preset style to use for the curve.
|
|
724
|
-
kwargs
|
|
725
|
-
Specific style options passed to ``matplotlib.lines.Line2D``.
|
|
726
|
-
"""
|
|
727
|
-
from matplotlib.path import Path
|
|
728
|
-
|
|
729
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
730
|
-
if "color" in style:
|
|
731
|
-
# presume that edge color is being specified
|
|
732
|
-
style.setdefault("edgecolor", style.pop("color"))
|
|
733
|
-
style.setdefault("edgecolor", self.drawcolor)
|
|
734
|
-
style.setdefault("fill", False)
|
|
735
|
-
style.setdefault("capstyle", "round")
|
|
736
|
-
style.setdefault("smoothing", 1 / 2)
|
|
737
|
-
smoothing = style.pop("smoothing")
|
|
738
|
-
|
|
739
|
-
if len(coos[0]) != 2:
|
|
740
|
-
style.setdefault(
|
|
741
|
-
"zorder", mean(self._coo_to_zorder(*coo) for coo in coos)
|
|
742
|
-
)
|
|
743
|
-
coos = [self._3d_project(*coo) for coo in coos]
|
|
744
|
-
else:
|
|
745
|
-
coos = [self._2d_project(*coo) for coo in coos]
|
|
746
|
-
|
|
747
|
-
N = len(coos)
|
|
748
|
-
|
|
749
|
-
if N <= 2 or smoothing == 0.0:
|
|
750
|
-
path = coos
|
|
751
|
-
moves = [Path.MOVETO] + [Path.LINETO] * (N - 1)
|
|
752
|
-
control_pts = {}
|
|
753
|
-
else:
|
|
754
|
-
control_pts = {}
|
|
755
|
-
for i in range(1, N - 1):
|
|
756
|
-
control_pts[i, "l"], control_pts[i, "r"] = get_control_points(
|
|
757
|
-
coos[i - 1],
|
|
758
|
-
coos[i],
|
|
759
|
-
coos[i + 1],
|
|
760
|
-
spacing=smoothing / 2,
|
|
761
|
-
)
|
|
762
|
-
|
|
763
|
-
path = [coos[0], control_pts[1, "l"], coos[1]]
|
|
764
|
-
moves = [Path.MOVETO, Path.CURVE3, Path.CURVE3]
|
|
765
|
-
|
|
766
|
-
for i in range(1, N - 2):
|
|
767
|
-
path.extend(
|
|
768
|
-
(control_pts[i, "r"], control_pts[i + 1, "l"], coos[i + 1])
|
|
769
|
-
)
|
|
770
|
-
moves.extend((Path.CURVE4, Path.CURVE4, Path.CURVE4))
|
|
771
|
-
|
|
772
|
-
path.extend((control_pts[N - 2, "r"], coos[N - 1]))
|
|
773
|
-
moves.extend((Path.CURVE3, Path.CURVE3))
|
|
774
|
-
|
|
775
|
-
curve = mpl.patches.PathPatch(Path(path, moves), **style)
|
|
776
|
-
self.ax.add_patch(curve)
|
|
777
|
-
|
|
778
|
-
for coo in coos:
|
|
779
|
-
self._adjust_lims(*coo)
|
|
780
|
-
for coo in control_pts.values():
|
|
781
|
-
self._adjust_lims(*coo)
|
|
782
|
-
|
|
783
|
-
def shape(self, coos, preset=None, **kwargs):
|
|
784
|
-
"""Draw a closed shape with (sharp) corners at the given coordinates.
|
|
785
|
-
|
|
786
|
-
Parameters
|
|
787
|
-
----------
|
|
788
|
-
coos : sequence of coordinates
|
|
789
|
-
The coordinates of the corners' of the shape.
|
|
790
|
-
preset : str, optional
|
|
791
|
-
A preset style to use for the shape.
|
|
792
|
-
kwargs
|
|
793
|
-
Specific style options passed to ``matplotlib.patches.PathPatch``.
|
|
794
|
-
|
|
795
|
-
See Also
|
|
796
|
-
--------
|
|
797
|
-
Drawing.patch
|
|
798
|
-
"""
|
|
799
|
-
from matplotlib.path import Path
|
|
800
|
-
|
|
801
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
802
|
-
if "color" in style:
|
|
803
|
-
style.setdefault("facecolor", style.pop("color"))
|
|
804
|
-
style.setdefault("facecolor", self.shapecolor)
|
|
805
|
-
style.setdefault("edgecolor", darken_color(style["facecolor"]))
|
|
806
|
-
style.setdefault("linewidth", 1)
|
|
807
|
-
style.setdefault("joinstyle", "round")
|
|
808
|
-
|
|
809
|
-
if len(coos[0]) != 2:
|
|
810
|
-
style.setdefault(
|
|
811
|
-
"zorder", mean(self._coo_to_zorder(*coo) for coo in coos)
|
|
812
|
-
)
|
|
813
|
-
coos = [self._3d_project(*coo) for coo in coos]
|
|
814
|
-
else:
|
|
815
|
-
coos = [self._2d_project(*coo) for coo in coos]
|
|
816
|
-
|
|
817
|
-
path = [coos[0]]
|
|
818
|
-
moves = [Path.MOVETO]
|
|
819
|
-
for coo in coos[1:]:
|
|
820
|
-
path.append(coo)
|
|
821
|
-
moves.append(Path.LINETO)
|
|
822
|
-
path.append(coos[0])
|
|
823
|
-
moves.append(Path.CLOSEPOLY)
|
|
824
|
-
|
|
825
|
-
curve = mpl.patches.PathPatch(Path(path, moves), **style)
|
|
826
|
-
self.ax.add_patch(curve)
|
|
827
|
-
|
|
828
|
-
for coo in coos:
|
|
829
|
-
self._adjust_lims(*coo)
|
|
830
|
-
|
|
831
|
-
def rectangle(self, cooa, coob, preset=None, **kwargs):
|
|
832
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
833
|
-
radius = style.pop("radius", 0.25)
|
|
834
|
-
|
|
835
|
-
forward, inverse = get_rotator_and_inverse(cooa, coob)
|
|
836
|
-
|
|
837
|
-
# rotate both onto y=0
|
|
838
|
-
xa, _ = forward(*cooa)
|
|
839
|
-
xb, _ = forward(*coob)
|
|
840
|
-
points = [
|
|
841
|
-
(xa - radius, -radius),
|
|
842
|
-
(xa - radius, +radius),
|
|
843
|
-
(xb + radius, +radius),
|
|
844
|
-
(xb + radius, -radius),
|
|
845
|
-
]
|
|
846
|
-
points = [inverse(*coo) for coo in points]
|
|
847
|
-
self.shape(points, **style)
|
|
848
|
-
|
|
849
|
-
def patch(self, coos, preset=None, **kwargs):
|
|
850
|
-
"""Draw a closed smooth patch through given coordinates.
|
|
851
|
-
|
|
852
|
-
Parameters
|
|
853
|
-
----------
|
|
854
|
-
coos : sequence of coordinates
|
|
855
|
-
The coordinates of the 'corners' of the patch, the outline is
|
|
856
|
-
guaranteed to pass through these points.
|
|
857
|
-
smoothing : float
|
|
858
|
-
The smoothing factor, the higher the smoother. The default is
|
|
859
|
-
0.5.
|
|
860
|
-
preset : str, optional
|
|
861
|
-
A preset style to use for the patch.
|
|
862
|
-
kwargs
|
|
863
|
-
Specific style options passed to ``matplotlib.patches.PathPatch``.
|
|
864
|
-
|
|
865
|
-
See Also
|
|
866
|
-
--------
|
|
867
|
-
Drawing.shape, Drawing.curve
|
|
868
|
-
"""
|
|
869
|
-
from matplotlib.path import Path
|
|
870
|
-
|
|
871
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
872
|
-
style.setdefault("linestyle", ":")
|
|
873
|
-
style.setdefault("edgecolor", (0.5, 0.5, 0.5, 0.75))
|
|
874
|
-
style.setdefault("facecolor", (0.5, 0.5, 0.5, 0.25))
|
|
875
|
-
style.setdefault("smoothing", 1 / 2)
|
|
876
|
-
smoothing = style.pop("smoothing")
|
|
877
|
-
|
|
878
|
-
if len(coos[0]) != 2:
|
|
879
|
-
# use min so the patch appears *just* behind the elements
|
|
880
|
-
# its meant to highlight
|
|
881
|
-
style.setdefault(
|
|
882
|
-
"zorder", min(self._coo_to_zorder(*coo) for coo in coos) - 0.01
|
|
883
|
-
)
|
|
884
|
-
coos = [self._3d_project(*coo) for coo in coos]
|
|
885
|
-
else:
|
|
886
|
-
style.setdefault("zorder", -0.01)
|
|
887
|
-
coos = [self._2d_project(*coo) for coo in coos]
|
|
888
|
-
|
|
889
|
-
N = len(coos)
|
|
890
|
-
|
|
891
|
-
control_pts = {}
|
|
892
|
-
for i in range(N):
|
|
893
|
-
control_pts[i, "l"], control_pts[i, "r"] = get_control_points(
|
|
894
|
-
coos[(i - 1) % N],
|
|
895
|
-
coos[i],
|
|
896
|
-
coos[(i + 1) % N],
|
|
897
|
-
spacing=smoothing / 2,
|
|
898
|
-
)
|
|
899
|
-
|
|
900
|
-
path = [coos[0]]
|
|
901
|
-
moves = [Path.MOVETO]
|
|
902
|
-
for ia in range(N):
|
|
903
|
-
ib = (ia + 1) % N
|
|
904
|
-
path.append(control_pts[ia, "r"])
|
|
905
|
-
path.append(control_pts[ib, "l"])
|
|
906
|
-
path.append(coos[ib])
|
|
907
|
-
moves.append(Path.CURVE4)
|
|
908
|
-
moves.append(Path.CURVE4)
|
|
909
|
-
moves.append(Path.CURVE4)
|
|
910
|
-
|
|
911
|
-
curve = mpl.patches.PathPatch(Path(path, moves), **style)
|
|
912
|
-
self.ax.add_patch(curve)
|
|
913
|
-
|
|
914
|
-
for coo in coos:
|
|
915
|
-
self._adjust_lims(*coo)
|
|
916
|
-
for coo in control_pts.values():
|
|
917
|
-
self._adjust_lims(*coo)
|
|
918
|
-
|
|
919
|
-
def patch_around(self, coos, *, preset=None, **kwargs):
|
|
920
|
-
"""Draw a patch around the given coordinates, by contructing a convex
|
|
921
|
-
hull around the points, optionally including an extra uniform or per
|
|
922
|
-
coordinate radius.
|
|
923
|
-
|
|
924
|
-
Parameters
|
|
925
|
-
----------
|
|
926
|
-
coos : sequence[tuple[int, int]] or sequence[tuple[int, int, int]]
|
|
927
|
-
The coordinates of the points to draw the patch around. If 3D, the
|
|
928
|
-
coordinates will be projected onto the 2D plane, and a z-order will
|
|
929
|
-
be assigned based on *min* z-order of the endpoints.
|
|
930
|
-
radius : float or sequence[float], optional
|
|
931
|
-
The radius of the patch around each point. If a sequence, must be
|
|
932
|
-
the same length as ``coos``. Default is 0.0.
|
|
933
|
-
resolution : int, optional
|
|
934
|
-
The number of points to use pad around each point. Default is 12.
|
|
935
|
-
preset : str, optional
|
|
936
|
-
A preset style to use for the patch.
|
|
937
|
-
kwargs
|
|
938
|
-
Specific style options passed to ``matplotlib.patches.PathPatch``.
|
|
939
|
-
"""
|
|
940
|
-
import numpy as np
|
|
941
|
-
from scipy.spatial import ConvexHull
|
|
942
|
-
|
|
943
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
944
|
-
radius = style.pop("radius", 0.0)
|
|
945
|
-
resolution = style.pop("resolution", 12)
|
|
946
|
-
|
|
947
|
-
if isinstance(radius, (int, float)):
|
|
948
|
-
radius = [radius] * len(coos)
|
|
949
|
-
|
|
950
|
-
if len(coos[0]) != 2:
|
|
951
|
-
# use min so the patch appears *just* behind the elements
|
|
952
|
-
# its meant to highlight
|
|
953
|
-
style.setdefault(
|
|
954
|
-
"zorder", min(self._coo_to_zorder(*coo) for coo in coos) - 0.01
|
|
955
|
-
)
|
|
956
|
-
coos = [self._3d_project(*coo) for coo in coos]
|
|
957
|
-
else:
|
|
958
|
-
style.setdefault("zorder", -0.01)
|
|
959
|
-
|
|
960
|
-
expanded_pts = []
|
|
961
|
-
for coo, r in zip(coos, radius):
|
|
962
|
-
if r == 0:
|
|
963
|
-
expanded_pts.append(coo)
|
|
964
|
-
else:
|
|
965
|
-
expanded_pts.extend(gen_points_around(coo, r, resolution))
|
|
966
|
-
|
|
967
|
-
if len(expanded_pts) <= 3:
|
|
968
|
-
# need at least 3 points to make convex hull
|
|
969
|
-
boundary_pts = expanded_pts
|
|
970
|
-
else:
|
|
971
|
-
expanded_pts = np.array(expanded_pts)
|
|
972
|
-
hull = ConvexHull(expanded_pts)
|
|
973
|
-
boundary_pts = expanded_pts[hull.vertices]
|
|
974
|
-
|
|
975
|
-
self.patch(boundary_pts, **style)
|
|
976
|
-
|
|
977
|
-
def patch_around_circles(
|
|
978
|
-
self,
|
|
979
|
-
cooa,
|
|
980
|
-
ra,
|
|
981
|
-
coob,
|
|
982
|
-
rb,
|
|
983
|
-
padding=0.2,
|
|
984
|
-
pinch=True,
|
|
985
|
-
preset=None,
|
|
986
|
-
**kwargs,
|
|
987
|
-
):
|
|
988
|
-
"""Draw a smooth patch around two circles.
|
|
989
|
-
|
|
990
|
-
Parameters
|
|
991
|
-
----------
|
|
992
|
-
cooa : tuple[int, int] or tuple[int, int, int]
|
|
993
|
-
The coordinates of the center of the first circle. If 3D, the
|
|
994
|
-
coordinates will be projected onto the 2D plane, and a z-order
|
|
995
|
-
will be assigned based on average z-order of the endpoints.
|
|
996
|
-
ra : int
|
|
997
|
-
The radius of the first circle.
|
|
998
|
-
coob : tuple[int, int] or tuple[int, int, int]
|
|
999
|
-
The coordinates of the center of the second circle. If 3D, the
|
|
1000
|
-
coordinates will be projected onto the 2D plane, and a z-order
|
|
1001
|
-
will be assigned based on average z-order of the endpoints.
|
|
1002
|
-
rb : int
|
|
1003
|
-
The radius of the second circle.
|
|
1004
|
-
padding : float, optional
|
|
1005
|
-
The amount of padding to add around the circles. Default is 0.2.
|
|
1006
|
-
pinch : bool or float, optional
|
|
1007
|
-
If or how much to pinch the patch in between the circles.
|
|
1008
|
-
Default is to match the padding.
|
|
1009
|
-
preset : str, optional
|
|
1010
|
-
A preset style to use for the patch.
|
|
1011
|
-
kwargs
|
|
1012
|
-
Specific style options passed to ``matplotlib.patches.PathPatch``.
|
|
1013
|
-
|
|
1014
|
-
See Also
|
|
1015
|
-
--------
|
|
1016
|
-
Drawing.patch
|
|
1017
|
-
"""
|
|
1018
|
-
style = parse_style_preset(self.presets, preset, **kwargs)
|
|
1019
|
-
style.setdefault("smoothing", 1.0)
|
|
1020
|
-
|
|
1021
|
-
if pinch is True:
|
|
1022
|
-
pinch = 1 - padding
|
|
1023
|
-
|
|
1024
|
-
if len(cooa) != 2:
|
|
1025
|
-
style.setdefault(
|
|
1026
|
-
"zorder",
|
|
1027
|
-
min(self._coo_to_zorder(*coo) for coo in [cooa, coob]) - 0.01,
|
|
1028
|
-
)
|
|
1029
|
-
cooa = self._3d_project(*cooa)
|
|
1030
|
-
coob = self._3d_project(*coob)
|
|
1031
|
-
else:
|
|
1032
|
-
style.setdefault("zorder", -0.01)
|
|
1033
|
-
cooa = self._2d_project(*cooa)
|
|
1034
|
-
coob = self._2d_project(*coob)
|
|
1035
|
-
|
|
1036
|
-
forward, inverse = get_rotator_and_inverse(cooa, coob)
|
|
1037
|
-
xb = forward(*coob)[0]
|
|
1038
|
-
rl = (1 + padding) * ra
|
|
1039
|
-
rr = (1 + padding) * rb
|
|
1040
|
-
rcoos = [
|
|
1041
|
-
# left loop
|
|
1042
|
-
(0, -rl),
|
|
1043
|
-
(-rl * 2**-0.5, -rl * 2**-0.5),
|
|
1044
|
-
(-rl, 0),
|
|
1045
|
-
(-rl * 2**-0.5, +rl * 2**-0.5),
|
|
1046
|
-
(0, +rl),
|
|
1047
|
-
# above pinch point
|
|
1048
|
-
(xb / 2, (1 - float(pinch)) * (ra + rb)),
|
|
1049
|
-
# right loop
|
|
1050
|
-
(xb, +rr),
|
|
1051
|
-
(xb + rr * 2**-0.5, +rr * 2**-0.5),
|
|
1052
|
-
(xb + rr, 0),
|
|
1053
|
-
(xb + rr * 2**-0.5, -rr * 2**-0.5),
|
|
1054
|
-
(xb, -rr),
|
|
1055
|
-
# below pinch point
|
|
1056
|
-
(xb / 2, (float(pinch) - 1) * (ra + rb)),
|
|
1057
|
-
]
|
|
1058
|
-
|
|
1059
|
-
pcoos = [inverse(*rcoo) for rcoo in rcoos]
|
|
1060
|
-
self.patch(pcoos, preset=preset, **style)
|
|
1061
|
-
|
|
1062
|
-
def savefig(self, fname, dpi=300, bbox_inches="tight"):
|
|
1063
|
-
self.fig.savefig(fname, dpi=dpi, bbox_inches=bbox_inches)
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
def parse_style_preset(presets, preset, **kwargs):
|
|
1067
|
-
"""Parse a one or more style presets plus manual kwargs.
|
|
1068
|
-
|
|
1069
|
-
Parameters
|
|
1070
|
-
----------
|
|
1071
|
-
presets : dict
|
|
1072
|
-
The dictionary of presets.
|
|
1073
|
-
preset : str or sequence of str
|
|
1074
|
-
The name of the preset(s) to use. If multiple, later presets take
|
|
1075
|
-
precedence.
|
|
1076
|
-
kwargs
|
|
1077
|
-
Any additional manual keyword arguments are added to the style and
|
|
1078
|
-
override the presets.
|
|
1079
|
-
|
|
1080
|
-
Returns
|
|
1081
|
-
-------
|
|
1082
|
-
style : dict
|
|
1083
|
-
"""
|
|
1084
|
-
if (preset is None) or isinstance(preset, str):
|
|
1085
|
-
preset = (preset,)
|
|
1086
|
-
|
|
1087
|
-
style = {}
|
|
1088
|
-
for p in preset:
|
|
1089
|
-
if p not in presets:
|
|
1090
|
-
warnings.warn(f"Drawing has no preset '{p}' yet.")
|
|
1091
|
-
else:
|
|
1092
|
-
style.update(presets[p])
|
|
1093
|
-
style.update(kwargs)
|
|
1094
|
-
return style
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
def simple_scale(i, j, xscale=1, yscale=1):
|
|
1098
|
-
return i * xscale, j * yscale
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
def axonometric_project(
|
|
1102
|
-
i,
|
|
1103
|
-
j,
|
|
1104
|
-
k,
|
|
1105
|
-
a=50,
|
|
1106
|
-
b=12,
|
|
1107
|
-
xscale=1,
|
|
1108
|
-
yscale=1,
|
|
1109
|
-
zscale=1,
|
|
1110
|
-
):
|
|
1111
|
-
"""Project the 3D location ``(i, j, k)`` onto the 2D plane, using
|
|
1112
|
-
the axonometric projection with the given angles ``a`` and ``b``.
|
|
1113
|
-
|
|
1114
|
-
The ``xscale``, ``yscale`` and ``zscale`` parameters can be used to
|
|
1115
|
-
scale the axes, including flipping them.
|
|
1116
|
-
|
|
1117
|
-
Parameters
|
|
1118
|
-
----------
|
|
1119
|
-
i, j, k : float
|
|
1120
|
-
The 3D coordinates of the point to project.
|
|
1121
|
-
a, b : float
|
|
1122
|
-
The left and right angles to displace x and y axes, from horizontal,
|
|
1123
|
-
in degrees.
|
|
1124
|
-
xscale, yscale, zscale : float
|
|
1125
|
-
The scaling factor for the x, y and z axes. If negative, the axis
|
|
1126
|
-
is flipped.
|
|
1127
|
-
|
|
1128
|
-
Returns
|
|
1129
|
-
-------
|
|
1130
|
-
x, y : float
|
|
1131
|
-
The 2D coordinates of the projected point.
|
|
1132
|
-
"""
|
|
1133
|
-
i *= xscale * 0.8
|
|
1134
|
-
j *= yscale
|
|
1135
|
-
k *= zscale
|
|
1136
|
-
return (
|
|
1137
|
-
+i * cos(pi * a / 180) + j * cos(pi * b / 180),
|
|
1138
|
-
-i * sin(pi * a / 180) + j * sin(pi * b / 180) + k,
|
|
1139
|
-
)
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
def coo_to_zorder(i, j, k, xscale=1, yscale=1, zscale=1):
|
|
1143
|
-
"""Given the coordinates of a point in 3D space, return a z-order value
|
|
1144
|
-
that can be used to draw it on top of other elements in the diagram.
|
|
1145
|
-
Take into account the scaling of the axes, so that the z-ordering
|
|
1146
|
-
is correct even if the axes flipped.
|
|
1147
|
-
"""
|
|
1148
|
-
return (
|
|
1149
|
-
+i * xscale / abs(xscale)
|
|
1150
|
-
- j * yscale / abs(yscale)
|
|
1151
|
-
+ k * zscale / abs(zscale)
|
|
1152
|
-
)
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
# colorblind palettes by Okabe & Ito (https://jfly.uni-koeln.de/color/)
|
|
1156
|
-
|
|
1157
|
-
_COLORS_DEFAULT = {
|
|
1158
|
-
"blue": "#56B4E9", # light blue
|
|
1159
|
-
"orange": "#E69F00", # orange
|
|
1160
|
-
"green": "#009E73", # green
|
|
1161
|
-
"red": "#D55E00", # red
|
|
1162
|
-
"yellow": "#F0E442", # yellow
|
|
1163
|
-
"pink": "#CC79A7", # pink
|
|
1164
|
-
"bluedark": "#0072B2", # dark blue
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
def get_wong_color(
|
|
1169
|
-
which,
|
|
1170
|
-
alpha=None,
|
|
1171
|
-
hue_factor=0.0,
|
|
1172
|
-
sat_factor=1.0,
|
|
1173
|
-
val_factor=1.0,
|
|
1174
|
-
):
|
|
1175
|
-
"""Get a color by name, optionally modifying its alpha, hue, saturation
|
|
1176
|
-
or value.
|
|
1177
|
-
|
|
1178
|
-
These colorblind friendly colors were ppularized in an article by Wong
|
|
1179
|
-
(https://www.nature.com/articles/nmeth.1618) but originally come from
|
|
1180
|
-
Okabe & Ito (https://jfly.uni-koeln.de/color/).
|
|
1181
|
-
|
|
1182
|
-
Parameters
|
|
1183
|
-
----------
|
|
1184
|
-
which : {'blue', 'orange', 'green', 'red', 'yellow', 'pink', 'bluedark'}
|
|
1185
|
-
The name of the color to get.
|
|
1186
|
-
alpha : float, optional
|
|
1187
|
-
The alpha channel value to set for the color. Default is 1.0.
|
|
1188
|
-
hue_factor : float, optional
|
|
1189
|
-
The amount to shift the hue of the color. Default is 0.0.
|
|
1190
|
-
sat_factor : float, optional
|
|
1191
|
-
The amount to scale the saturation of the color. Default is 1.0.
|
|
1192
|
-
val_factor : float, optional
|
|
1193
|
-
The amount to scale the value of the color. Default is 1.0.
|
|
1194
|
-
|
|
1195
|
-
Returns
|
|
1196
|
-
-------
|
|
1197
|
-
color : tuple[float, float, float, float]
|
|
1198
|
-
The RGBA color as a tuple of floats.
|
|
1199
|
-
"""
|
|
1200
|
-
import matplotlib as mpl
|
|
1201
|
-
|
|
1202
|
-
h = _COLORS_DEFAULT[which]
|
|
1203
|
-
rgb = mpl.colors.to_rgb(h)
|
|
1204
|
-
h, s, v = mpl.colors.rgb_to_hsv(rgb)
|
|
1205
|
-
h = (h + hue_factor) % 1.0
|
|
1206
|
-
s = min(max(0.0, s * sat_factor), 1.0)
|
|
1207
|
-
v = min(max(0.0, v * val_factor), 1.0)
|
|
1208
|
-
r, g, b = mpl.colors.hsv_to_rgb((h, s, v))
|
|
1209
|
-
if alpha is not None:
|
|
1210
|
-
return (r, g, b, alpha)
|
|
1211
|
-
return r, g, b
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
_COLORS_SORTED = [
|
|
1215
|
-
_COLORS_DEFAULT["bluedark"],
|
|
1216
|
-
_COLORS_DEFAULT["blue"],
|
|
1217
|
-
_COLORS_DEFAULT["green"],
|
|
1218
|
-
_COLORS_DEFAULT["yellow"],
|
|
1219
|
-
_COLORS_DEFAULT["orange"],
|
|
1220
|
-
_COLORS_DEFAULT["red"],
|
|
1221
|
-
_COLORS_DEFAULT["pink"],
|
|
1222
|
-
]
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
def mod_sat(c, mod=None, alpha=None):
|
|
1226
|
-
"""Modify the luminosity of color ``c``, optionally set the ``alpha``
|
|
1227
|
-
channel, and return the final color as a RGBA tuple."""
|
|
1228
|
-
import matplotlib as mpl
|
|
1229
|
-
|
|
1230
|
-
r, g, b, a = mpl.colors.to_rgba(c)
|
|
1231
|
-
if alpha is None:
|
|
1232
|
-
alpha = a
|
|
1233
|
-
|
|
1234
|
-
if mod is None:
|
|
1235
|
-
return r, g, b, alpha
|
|
1236
|
-
|
|
1237
|
-
h, s, v = mpl.colors.rgb_to_hsv((r, g, b))
|
|
1238
|
-
return (*mpl.colors.hsv_to_rgb((h, mod * s, v)), alpha)
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
def auto_colors(nc, alpha=None, default_sequence=False):
|
|
1242
|
-
"""Generate a nice sequence of ``nc`` colors. By default this uses an
|
|
1243
|
-
interpolation between the colorblind friendly colors of Okabe & Ito in hue
|
|
1244
|
-
sorted order, with luminosity moderated by a sine function to increase
|
|
1245
|
-
local distinguishability.
|
|
1246
|
-
|
|
1247
|
-
Parameters
|
|
1248
|
-
----------
|
|
1249
|
-
nc : int
|
|
1250
|
-
The number of colors to generate.
|
|
1251
|
-
alpha : float, optional
|
|
1252
|
-
The alpha channel value to set for all colors. Default is 1.0.
|
|
1253
|
-
default_sequence : bool, optional
|
|
1254
|
-
If ``True``, take from the default sequence of 7 colors, un-sorted and
|
|
1255
|
-
un-modulated.
|
|
1256
|
-
|
|
1257
|
-
Returns
|
|
1258
|
-
-------
|
|
1259
|
-
colors : list[tuple[float, float, float, float]]
|
|
1260
|
-
"""
|
|
1261
|
-
import math
|
|
1262
|
-
|
|
1263
|
-
import numpy as np
|
|
1264
|
-
from matplotlib.colors import LinearSegmentedColormap
|
|
1265
|
-
|
|
1266
|
-
if default_sequence:
|
|
1267
|
-
if nc > 7:
|
|
1268
|
-
raise ValueError(
|
|
1269
|
-
"Can only generate 7 colors with default sequence"
|
|
1270
|
-
)
|
|
1271
|
-
return [
|
|
1272
|
-
mod_sat(c, alpha=alpha)
|
|
1273
|
-
for c in tuple(_COLORS_DEFAULT.values())[:nc]
|
|
1274
|
-
]
|
|
1275
|
-
|
|
1276
|
-
cmap = LinearSegmentedColormap.from_list("colorblind", _COLORS_SORTED)
|
|
1277
|
-
|
|
1278
|
-
xs = list(map(cmap, np.linspace(0, 1.0, nc)))
|
|
1279
|
-
|
|
1280
|
-
# modulate color saturation with sine to generate local distinguishability
|
|
1281
|
-
# ... but only turn on gradually for increasing number of nodes
|
|
1282
|
-
sat_mod_period = min(4, nc / 7)
|
|
1283
|
-
sat_mod_factor = max(0.0, 2 / 3 * math.tanh((nc - 7) / 4))
|
|
1284
|
-
|
|
1285
|
-
if alpha is None:
|
|
1286
|
-
alpha = 1.0
|
|
1287
|
-
|
|
1288
|
-
return [
|
|
1289
|
-
mod_sat(
|
|
1290
|
-
c,
|
|
1291
|
-
1 - sat_mod_factor * math.sin(math.pi * i / sat_mod_period) ** 2,
|
|
1292
|
-
alpha,
|
|
1293
|
-
)
|
|
1294
|
-
for i, c in enumerate(xs)
|
|
1295
|
-
]
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
def darken_color(color, factor=2 / 3):
|
|
1299
|
-
"""Take ``color`` and darken it by ``factor``."""
|
|
1300
|
-
rgba = mpl.colors.to_rgba(color)
|
|
1301
|
-
return tuple(factor * c for c in rgba[:3]) + rgba[3:]
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
def average_color(colors):
|
|
1305
|
-
"""Take a sequence of colors and return the RMS average in RGB space."""
|
|
1306
|
-
from matplotlib.colors import to_rgba
|
|
1307
|
-
|
|
1308
|
-
# first map to rgba
|
|
1309
|
-
colors = [to_rgba(c) for c in colors]
|
|
1310
|
-
|
|
1311
|
-
r, g, b, a = zip(*colors)
|
|
1312
|
-
|
|
1313
|
-
# then RMS average each channel
|
|
1314
|
-
rm = (sum(ri**2 for ri in r) / len(r)) ** 0.5
|
|
1315
|
-
gm = (sum(gi**2 for gi in g) / len(g)) ** 0.5
|
|
1316
|
-
bm = (sum(bi**2 for bi in b) / len(b)) ** 0.5
|
|
1317
|
-
am = sum(a) / len(a)
|
|
1318
|
-
|
|
1319
|
-
return (rm, gm, bm, am)
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
def jitter_color(color, factor=0.05):
|
|
1323
|
-
"""Take ``color`` and add a random offset to each of its components."""
|
|
1324
|
-
import random
|
|
1325
|
-
|
|
1326
|
-
rgba = mpl.colors.to_rgba(color)
|
|
1327
|
-
hsv = mpl.colors.rgb_to_hsv(rgba[:3])
|
|
1328
|
-
hsv = (
|
|
1329
|
-
hsv[0],
|
|
1330
|
-
min(max(0, hsv[1] + random.uniform(-factor / 2, factor / 2)), 1),
|
|
1331
|
-
min(max(0, hsv[2] + random.uniform(-factor / 2, factor / 2)), 1),
|
|
1332
|
-
)
|
|
1333
|
-
rgb = mpl.colors.hsv_to_rgb(hsv)
|
|
1334
|
-
return tuple(rgb) + rgba[3:]
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
COLORING_SEED = 8 # 8, 10
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
def set_coloring_seed(seed):
|
|
1342
|
-
"""Set the seed for the random color generator.
|
|
1343
|
-
|
|
1344
|
-
Parameters
|
|
1345
|
-
----------
|
|
1346
|
-
seed : int
|
|
1347
|
-
The seed to use.
|
|
1348
|
-
"""
|
|
1349
|
-
global COLORING_SEED
|
|
1350
|
-
COLORING_SEED = seed
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
def hash_to_nvalues(s, nval, seed=None):
|
|
1354
|
-
"""Hash the string ``s`` to ``nval`` different floats in the range [0, 1]."""
|
|
1355
|
-
import hashlib
|
|
1356
|
-
|
|
1357
|
-
if seed is None:
|
|
1358
|
-
seed = COLORING_SEED
|
|
1359
|
-
|
|
1360
|
-
m = hashlib.sha256()
|
|
1361
|
-
m.update(f"{seed}".encode())
|
|
1362
|
-
m.update(s.encode())
|
|
1363
|
-
hsh = m.hexdigest()
|
|
1364
|
-
|
|
1365
|
-
b = len(hsh) // nval
|
|
1366
|
-
if b == 0:
|
|
1367
|
-
raise ValueError(
|
|
1368
|
-
f"Can't extract {nval} values from hash of length {len(hsh)}"
|
|
1369
|
-
)
|
|
1370
|
-
return tuple(
|
|
1371
|
-
int(hsh[i * b : (i + 1) * b], 16) / 16**b for i in range(nval)
|
|
1372
|
-
)
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
def hash_to_color(
|
|
1376
|
-
s,
|
|
1377
|
-
hmin=0.0,
|
|
1378
|
-
hmax=1.0,
|
|
1379
|
-
smin=0.3,
|
|
1380
|
-
smax=0.8,
|
|
1381
|
-
vmin=0.8,
|
|
1382
|
-
vmax=0.9,
|
|
1383
|
-
):
|
|
1384
|
-
"""Generate a random color for a string ``s``.
|
|
1385
|
-
|
|
1386
|
-
Parameters
|
|
1387
|
-
----------
|
|
1388
|
-
s : str
|
|
1389
|
-
The string to generate a color for.
|
|
1390
|
-
hmin : float, optional
|
|
1391
|
-
The minimum hue value.
|
|
1392
|
-
hmax : float, optional
|
|
1393
|
-
The maximum hue value.
|
|
1394
|
-
smin : float, optional
|
|
1395
|
-
The minimum saturation value.
|
|
1396
|
-
smax : float, optional
|
|
1397
|
-
The maximum saturation value.
|
|
1398
|
-
vmin : float, optional
|
|
1399
|
-
The minimum value value.
|
|
1400
|
-
vmax : float, optional
|
|
1401
|
-
The maximum value value.
|
|
1402
|
-
|
|
1403
|
-
Returns
|
|
1404
|
-
-------
|
|
1405
|
-
color : tuple
|
|
1406
|
-
A tuple of floats in the range [0, 1] representing the RGB color.
|
|
1407
|
-
"""
|
|
1408
|
-
from matplotlib.colors import to_hex, hsv_to_rgb
|
|
1409
|
-
|
|
1410
|
-
h, s, v = hash_to_nvalues(s, 3)
|
|
1411
|
-
h = hmin + h * (hmax - hmin)
|
|
1412
|
-
s = smin + s * (smax - smin)
|
|
1413
|
-
v = vmin + v * (vmax - vmin)
|
|
1414
|
-
|
|
1415
|
-
rgb = hsv_to_rgb((h, s, v))
|
|
1416
|
-
return to_hex(rgb)
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
def mean(xs):
|
|
1420
|
-
"""Get the mean of a list of numbers."""
|
|
1421
|
-
s = 0
|
|
1422
|
-
i = 0
|
|
1423
|
-
for x in xs:
|
|
1424
|
-
s += x
|
|
1425
|
-
i += 1
|
|
1426
|
-
return s / i
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
def distance(pa, pb):
|
|
1430
|
-
"""Get the distance between two points, in arbtirary dimensions."""
|
|
1431
|
-
d = 0.0
|
|
1432
|
-
for a, b in zip(pa, pb):
|
|
1433
|
-
d += (a - b) ** 2
|
|
1434
|
-
return d**0.5
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
def get_angle(pa, pb):
|
|
1438
|
-
"""Get the angle between the line from p1 to p2 and the x-axis."""
|
|
1439
|
-
(xa, ya), (xb, yb) = pa, pb
|
|
1440
|
-
return atan2(yb - ya, xb - xa)
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
def get_rotator_and_inverse(pa, pb):
|
|
1444
|
-
"""Get a rotation matrix that rotates points by theta radians about
|
|
1445
|
-
the origin and then translates them by offset.
|
|
1446
|
-
"""
|
|
1447
|
-
theta = get_angle(pa, pb)
|
|
1448
|
-
dx, dy = pa
|
|
1449
|
-
|
|
1450
|
-
def forward(x, y):
|
|
1451
|
-
"""Rotate and translate a point."""
|
|
1452
|
-
x, y = x - dx, y - dy
|
|
1453
|
-
x, y = (
|
|
1454
|
-
x * cos(-theta) - y * sin(-theta),
|
|
1455
|
-
x * sin(-theta) + y * cos(-theta),
|
|
1456
|
-
)
|
|
1457
|
-
return x, y
|
|
1458
|
-
|
|
1459
|
-
def inverse(x, y):
|
|
1460
|
-
"""Rotate and translate a point."""
|
|
1461
|
-
x, y = x * cos(theta) - y * sin(theta), x * sin(theta) + y * cos(theta)
|
|
1462
|
-
return x + dx, y + dy
|
|
1463
|
-
|
|
1464
|
-
return forward, inverse
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
def get_control_points(pa, pb, pc, spacing=1 / 3):
|
|
1468
|
-
"""Get two points that can be used to construct a bezier curve that
|
|
1469
|
-
passes smoothly through the angle `pa`, `pb`, `pc`.
|
|
1470
|
-
"""
|
|
1471
|
-
# rotate onto x-axis (ra always (0, 0))
|
|
1472
|
-
forward, inverse = get_rotator_and_inverse(pa, pb)
|
|
1473
|
-
_, rb, rc = [forward(*p) for p in [pa, pb, pc]]
|
|
1474
|
-
|
|
1475
|
-
# flip so third point is always above axis
|
|
1476
|
-
flip_y = rc[1] < 0
|
|
1477
|
-
if flip_y:
|
|
1478
|
-
rc = rc[0], -rc[1]
|
|
1479
|
-
|
|
1480
|
-
phi = get_angle(rb, rc) / 2
|
|
1481
|
-
|
|
1482
|
-
# lengths of the two lines
|
|
1483
|
-
lab = rb[0]
|
|
1484
|
-
lbc = distance(rb, rc)
|
|
1485
|
-
|
|
1486
|
-
# lengths of perpendicular offsets
|
|
1487
|
-
oab = lab * cos(phi)
|
|
1488
|
-
obc = lbc * cos(phi)
|
|
1489
|
-
|
|
1490
|
-
dx_ab = spacing * oab * cos(phi)
|
|
1491
|
-
dy_ab = spacing * oab * sin(phi)
|
|
1492
|
-
|
|
1493
|
-
dx_bc = spacing * obc * cos(phi)
|
|
1494
|
-
dy_bc = spacing * obc * sin(phi)
|
|
1495
|
-
|
|
1496
|
-
# get control points in this reference frame
|
|
1497
|
-
rc_ab = rb[0] - dx_ab, rb[1] - dy_ab
|
|
1498
|
-
rc_bc = rb[0] + dx_bc, rb[1] + dy_bc
|
|
1499
|
-
|
|
1500
|
-
# unflip and un rotate
|
|
1501
|
-
if flip_y:
|
|
1502
|
-
rc_ab = rc_ab[0], -rc_ab[1]
|
|
1503
|
-
rc_bc = rc_bc[0], -rc_bc[1]
|
|
1504
|
-
|
|
1505
|
-
c_ab, c_bc = inverse(*rc_ab), inverse(*rc_bc)
|
|
1506
|
-
|
|
1507
|
-
return c_ab, c_bc
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
def gen_points_around(coo, radius=1, resolution=12):
|
|
1511
|
-
"""Generate points around a circle."""
|
|
1512
|
-
x, y = coo
|
|
1513
|
-
dphi = 2 * pi / resolution
|
|
1514
|
-
phis = (i * dphi for i in range(resolution))
|
|
1515
|
-
for phi in phis:
|
|
1516
|
-
xa = x + radius * cos(phi)
|
|
1517
|
-
ya = y + radius * sin(phi)
|
|
1518
|
-
yield xa, ya
|