cdxcore 0.1.5__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.
Potentially problematic release.
This version of cdxcore might be problematic. Click here for more details.
- cdxcore/__init__.py +15 -0
- cdxcore/config.py +1633 -0
- cdxcore/crman.py +105 -0
- cdxcore/deferred.py +220 -0
- cdxcore/dynaplot.py +1155 -0
- cdxcore/filelock.py +430 -0
- cdxcore/jcpool.py +411 -0
- cdxcore/logger.py +319 -0
- cdxcore/np.py +1098 -0
- cdxcore/npio.py +270 -0
- cdxcore/prettydict.py +388 -0
- cdxcore/prettyobject.py +64 -0
- cdxcore/sharedarray.py +285 -0
- cdxcore/subdir.py +2963 -0
- cdxcore/uniquehash.py +970 -0
- cdxcore/util.py +1041 -0
- cdxcore/verbose.py +403 -0
- cdxcore/version.py +402 -0
- cdxcore-0.1.5.dist-info/METADATA +1418 -0
- cdxcore-0.1.5.dist-info/RECORD +30 -0
- cdxcore-0.1.5.dist-info/WHEEL +5 -0
- cdxcore-0.1.5.dist-info/licenses/LICENSE +21 -0
- cdxcore-0.1.5.dist-info/top_level.txt +4 -0
- conda/conda_exists.py +10 -0
- conda/conda_modify_yaml.py +42 -0
- tests/_cdxbasics.py +1086 -0
- tests/test_uniquehash.py +469 -0
- tests/test_util.py +329 -0
- up/git_message.py +7 -0
- up/pip_modify_setup.py +55 -0
cdxcore/dynaplot.py
ADDED
|
@@ -0,0 +1,1155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dynaplot
|
|
3
|
+
Dynamic matplotlib in jupyer notebooks
|
|
4
|
+
|
|
5
|
+
from dynaplot import figure
|
|
6
|
+
|
|
7
|
+
fig = figure()
|
|
8
|
+
ax = fig.add_subplot()
|
|
9
|
+
ax.plot(x,y)
|
|
10
|
+
ax = fig.add_subplot()
|
|
11
|
+
ax.plot(x,z)
|
|
12
|
+
fig.close()
|
|
13
|
+
|
|
14
|
+
Hans Buehler 2022
|
|
15
|
+
"""
|
|
16
|
+
import matplotlib.pyplot as plt
|
|
17
|
+
from matplotlib.gridspec import GridSpec#NOQA
|
|
18
|
+
import matplotlib.colors as mcolors
|
|
19
|
+
from matplotlib.artist import Artist
|
|
20
|
+
from matplotlib.axes import Axes
|
|
21
|
+
from IPython import display
|
|
22
|
+
import io as io
|
|
23
|
+
import gc as gc
|
|
24
|
+
import types as types
|
|
25
|
+
import numpy as np
|
|
26
|
+
from .deferred import Deferred
|
|
27
|
+
from .util import fmt as txtfmt
|
|
28
|
+
from collections.abc import Collection
|
|
29
|
+
import warnings as warnings
|
|
30
|
+
|
|
31
|
+
def error( text, *args, exception = RuntimeError, **kwargs ):
|
|
32
|
+
raise exception( txtfmt(text, *args, **kwargs) )
|
|
33
|
+
def verify( cond, text, *args, exception = RuntimeError, **kwargs ):
|
|
34
|
+
if not cond:
|
|
35
|
+
error( text, *args, **kwargs, exception=exception )
|
|
36
|
+
def warn( text, *args, warning=warnings.RuntimeWarning, stack_level=1, **kwargs ):
|
|
37
|
+
warnings.warn( txtfmt(text, *args, **kwargs), warning, stack_level=stack_level )
|
|
38
|
+
def warn_if( cond, text, *args, warning=warnings.RuntimeWarning, stack_level=1, **kwargs ):
|
|
39
|
+
if cond:
|
|
40
|
+
warn( text, *args, warning=warning, stack_level=stack_level, **kwargs )
|
|
41
|
+
|
|
42
|
+
class AutoLimits( object ):
|
|
43
|
+
"""
|
|
44
|
+
Max/Min limit manger for dynamic figures.
|
|
45
|
+
|
|
46
|
+
limits = MinMaxLimit( 0.05, 0.95 )
|
|
47
|
+
ax.add_subplot( x,y ,.. )
|
|
48
|
+
limits.update(x, y)
|
|
49
|
+
ax.add_subplot( x,z,.. )
|
|
50
|
+
limits.update(x, z)
|
|
51
|
+
limits.set_lims(ax)
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, low_quantile, high_quantile, min_length : int = 10, lookback : int = None ):
|
|
55
|
+
"""
|
|
56
|
+
Initialize MinMaxLimit.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
low_quantile : float
|
|
61
|
+
Lower quantile to use for computing a 'min' y value. Set to 0 to use 'min'.
|
|
62
|
+
high_quantile : float
|
|
63
|
+
Higher quantile to use for computing a 'min' y value. Set to 1 to use 'max'.
|
|
64
|
+
min_length : int
|
|
65
|
+
Minimum length data must have to use quantile(). If less data is presented,
|
|
66
|
+
use min/max, respectively.
|
|
67
|
+
lookback : int
|
|
68
|
+
How many steps to lookback for any calculation. None to use all steps
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
verify( low_quantile >=0., "'low_quantile' must not be negative", exception=ValueError )
|
|
72
|
+
verify( high_quantile <=1., "'high_quantile' must not exceed 1", exception=ValueError )
|
|
73
|
+
verify( low_quantile<=high_quantile, "'low_quantile' not exceed 'high_quantile'", exception=ValueError )
|
|
74
|
+
self.lo_q = low_quantile
|
|
75
|
+
self.hi_q = high_quantile
|
|
76
|
+
self.min_length = int(min_length)
|
|
77
|
+
self.lookback = int(lookback) if not lookback is None else None
|
|
78
|
+
self.max_y = None
|
|
79
|
+
self.min_y = None
|
|
80
|
+
self.min_x = None
|
|
81
|
+
self.max_x = None
|
|
82
|
+
|
|
83
|
+
def update(self, *args, axis=None ):
|
|
84
|
+
"""
|
|
85
|
+
Add a data set to the min/max calc
|
|
86
|
+
|
|
87
|
+
If the x axis is ordinal first dimension of 'y':
|
|
88
|
+
update(y, axis=axis )
|
|
89
|
+
In this case x = np.linspace(1,y.shape[0],y.shape[0])
|
|
90
|
+
|
|
91
|
+
Specifcy x axis
|
|
92
|
+
update(x, y, axis=axis )
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
*args:
|
|
97
|
+
Either y or x,y
|
|
98
|
+
axis:
|
|
99
|
+
along which axis to compute min/max/quantiles
|
|
100
|
+
"""
|
|
101
|
+
assert len(args) in [1,2], ("'args' must be 1 or 2", len(args))
|
|
102
|
+
|
|
103
|
+
y = args[-1]
|
|
104
|
+
x = args[0] if len(args) > 1 else None
|
|
105
|
+
|
|
106
|
+
if len(y) == 0:
|
|
107
|
+
return
|
|
108
|
+
if axis is None:
|
|
109
|
+
axis = None if len(y.shape) <= 1 else tuple(list(y.shape)[1])
|
|
110
|
+
|
|
111
|
+
y_len = y.shape[0]
|
|
112
|
+
if not self.lookback is None:
|
|
113
|
+
y = y[-self.lookback:,...]
|
|
114
|
+
x = x[-self.lookback:,...] if not x is None else None
|
|
115
|
+
|
|
116
|
+
min_y = np.min( np.quantile( y, self.lo_q, axis=axis ) ) if self.lo_q > 0. and len(y) > self.min_length else np.min( y )
|
|
117
|
+
max_y = np.max( np.quantile( y, self.hi_q, axis=axis ) ) if self.hi_q < 1. and len(y) > self.min_length else np.max( y )
|
|
118
|
+
assert min_y <= max_y, ("Internal error", min_y, max_y, y)
|
|
119
|
+
self.min_y = min_y if self.min_y is None else min( self.min_y, min_y )
|
|
120
|
+
self.max_y = max_y if self.max_y is None else max( self.max_y, max_y )
|
|
121
|
+
|
|
122
|
+
if x is None:
|
|
123
|
+
self.min_x = 1
|
|
124
|
+
self.max_x = y_len if self.max_x is None else max( y_len, self.max_x )
|
|
125
|
+
else:
|
|
126
|
+
min_ = np.min( x )
|
|
127
|
+
max_ = np.max( x )
|
|
128
|
+
self.min_x = min_ if self.max_x is None else min( self.min_x, min_ )
|
|
129
|
+
self.max_x = max_ if self.max_x is None else max( self.max_x, max_ )
|
|
130
|
+
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
def set( self, *, min_x = None, max_x = None,
|
|
134
|
+
min_y = None, max_y = None ) -> type:
|
|
135
|
+
"""
|
|
136
|
+
Overwrite any of the extrema.
|
|
137
|
+
Imposing an extrema also sets the other side if this would violate the requrest eg if min_x is set the function also floors self.max_x at min_x
|
|
138
|
+
Returns 'self.'
|
|
139
|
+
"""
|
|
140
|
+
verify( min_x is None or max_x is None or min_x <= max_x, "'min_x' and 'max_'x are in wrong order", min_x, max_x, exception=ValueError )
|
|
141
|
+
if not min_x is None:
|
|
142
|
+
self.min_x = min_x
|
|
143
|
+
self.max_x = min( self.max_x, min_x )
|
|
144
|
+
if not max_x is None:
|
|
145
|
+
self.min_x = max( self.min_x, max_x )
|
|
146
|
+
self.max_x = max_x
|
|
147
|
+
|
|
148
|
+
verify( min_y is None or max_y is None or min_y <= max_y, "'min_y' and 'max_'x are in wrong order", min_y, max_y, exception=ValueError )
|
|
149
|
+
if not min_y is None:
|
|
150
|
+
self.min_y = min_y
|
|
151
|
+
self.max_y = min( self.max_y, min_y )
|
|
152
|
+
if not max_y is None:
|
|
153
|
+
self.min_y = max( self.min_y, max_y )
|
|
154
|
+
self.max_y = max_y
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
def bound( self, *, min_x_at_least = None, max_x_at_most = None, # <= boundary limits
|
|
158
|
+
min_y_at_least = None, max_y_at_most = None,
|
|
159
|
+
):
|
|
160
|
+
"""
|
|
161
|
+
Bound extrema
|
|
162
|
+
"""
|
|
163
|
+
verify( min_x_at_least is None or max_x_at_most is None or min_x_at_least <= max_x_at_most, "'min_x_at_least' and 'max_x_at_most'x are in wrong order", min_x_at_least, max_x_at_most, exception=ValueError )
|
|
164
|
+
if not min_x_at_least is None:
|
|
165
|
+
self.min_x = max( self.min_x, min_x_at_least )
|
|
166
|
+
self.max_x = max( self.max_x, min_x_at_least )
|
|
167
|
+
if not max_x_at_most is None:
|
|
168
|
+
self.min_x = min( self.min_x, max_x_at_most )
|
|
169
|
+
self.max_x = min( self.max_x, max_x_at_most )
|
|
170
|
+
|
|
171
|
+
verify( min_y_at_least is None or max_y_at_most is None or min_y_at_least <= max_y_at_most, "'min_y_at_least' and 'max_y_at_most'x are in wrong order", min_y_at_least, max_y_at_most, exception=ValueError )
|
|
172
|
+
if not min_y_at_least is None:
|
|
173
|
+
self.min_y = max( self.min_y, min_y_at_least )
|
|
174
|
+
self.max_y = max( self.max_y, min_y_at_least )
|
|
175
|
+
if not max_y_at_most is None:
|
|
176
|
+
self.min_y = min( self.min_y, max_y_at_most )
|
|
177
|
+
self.max_y = min( self.max_y, max_y_at_most )
|
|
178
|
+
return self
|
|
179
|
+
|
|
180
|
+
def set_a_lim( self, ax,*, is_x,
|
|
181
|
+
min_d,
|
|
182
|
+
rspace,
|
|
183
|
+
min_set = None,
|
|
184
|
+
max_set = None,
|
|
185
|
+
min_at_least = None,
|
|
186
|
+
max_at_most = None ):
|
|
187
|
+
""" Utility function """
|
|
188
|
+
min_ = self.min_x if is_x else self.min_y
|
|
189
|
+
max_ = self.max_x if is_x else self.max_y
|
|
190
|
+
ax_scale = (ax.get_xaxis() if is_x else ax.get_yaxis()).get_scale()
|
|
191
|
+
label = "x" if is_x else "y"
|
|
192
|
+
f = ax.set_xlim if is_x else ax.set_ylim
|
|
193
|
+
|
|
194
|
+
if min_ is None or max_ is None:
|
|
195
|
+
warn( "No data recevied yet; ignoring call" )
|
|
196
|
+
return
|
|
197
|
+
assert min_ <= max_, ("Internal error (1): min and max are not in order", label, min_, max_)
|
|
198
|
+
|
|
199
|
+
verify( min_set is None or max_set is None or min_set <= max_set, "'min_set_%s' exceeds 'max_set_%s': found %g and %g, respectively", label, label, min_set, max_set, exception=RuntimeError )
|
|
200
|
+
verify( min_at_least is None or max_at_most is None or min_at_least <= max_at_most, "'min_at_least_%s' exceeds 'max_at_most_%s': found %g and %g, respectively", label, label, min_at_least, max_at_most, exception=RuntimeError )
|
|
201
|
+
|
|
202
|
+
if not min_set is None:
|
|
203
|
+
min_ = min_set
|
|
204
|
+
max_ = max(min_set, max_)
|
|
205
|
+
if not max_set is None:
|
|
206
|
+
min_ = min(min_, max_set)
|
|
207
|
+
max_ = max_set
|
|
208
|
+
if not min_at_least is None:
|
|
209
|
+
min_ = max( min_, min_at_least )
|
|
210
|
+
max_ = max( max_, min_at_least )
|
|
211
|
+
if not max_at_most is None:
|
|
212
|
+
min_ = min( min_, max_at_most )
|
|
213
|
+
max_ = min( max_, max_at_most )
|
|
214
|
+
|
|
215
|
+
assert min_ <= max_, ("Internal error (2): min and max are not in order", label, min_, max_)
|
|
216
|
+
|
|
217
|
+
if isinstance( max_, int ):
|
|
218
|
+
verify( ax_scale == "linear", "Only 'linear' %s axis supported for integer based %s coordinates; found '%s'", label, label, ax_scale, exception=AttributeError )
|
|
219
|
+
max_ = max(max_, min_+1)
|
|
220
|
+
f( min_, max_ )
|
|
221
|
+
else:
|
|
222
|
+
d = max( max_-min_, min_d ) * rspace
|
|
223
|
+
if ax_scale == "linear":
|
|
224
|
+
f( min_ - d, max_ + d )
|
|
225
|
+
else:
|
|
226
|
+
verify( ax_scale == "log", "Only 'linear' and 'log' %s axis scales are supported; found '%s'", label, ax_scale, exception=AttributeError )
|
|
227
|
+
verify( min_ > 0., "Minimum for 'log' %s axis must be positive; found %g", label, min_)
|
|
228
|
+
rdx = np.exp( d )
|
|
229
|
+
f( min_ / rdx, max_ * rdx )
|
|
230
|
+
return self
|
|
231
|
+
|
|
232
|
+
def set_ylim(self, ax, *, min_dy : float = 1E-4, yrspace : float = 0.001, min_set_y = None, max_set_y = None, min_y_at_least = None, max_y_at_most = None ):
|
|
233
|
+
"""
|
|
234
|
+
Set x limits for 'ax'. See set_lims()
|
|
235
|
+
"""
|
|
236
|
+
return self.set_a_lim( ax, is_x=False, min_d=min_dy, rspace=yrspace, min_set=min_set_y, max_set=max_set_y, min_at_least=min_y_at_least, max_at_most=max_y_at_most )
|
|
237
|
+
|
|
238
|
+
def set_xlim(self, ax, *, min_dx : float = 1E-4, xrspace : float = 0.001, min_set_x = None, max_set_x = None, min_x_at_least = None, max_x_at_most = None ):
|
|
239
|
+
"""
|
|
240
|
+
Set x limits for 'ax'. See set_lims()
|
|
241
|
+
"""
|
|
242
|
+
return self.set_a_lim( ax, is_x=True, min_d=min_dx, rspace=xrspace, min_set=min_set_x, max_set=max_set_x, min_at_least=min_x_at_least, max_at_most=max_x_at_most )
|
|
243
|
+
|
|
244
|
+
def set_lims( self, ax, *, x : bool = True, y : bool = True,
|
|
245
|
+
min_dx : float = 1E-4, min_dy = 1E-4, xrspace = 0.001, yrspace = 0.001,
|
|
246
|
+
min_set_x = None, max_set_x = None, min_x_at_least = None, max_x_at_most = None,
|
|
247
|
+
min_set_y = None, max_set_y = None, min_y_at_least = None, max_y_at_most = None):
|
|
248
|
+
"""
|
|
249
|
+
Set x and/or y limits for 'ax'.
|
|
250
|
+
|
|
251
|
+
For example for the x axis: let
|
|
252
|
+
dx := max( max_x - min_x, min_dx )*xrspace
|
|
253
|
+
|
|
254
|
+
For linear axes:
|
|
255
|
+
set_xlim( min_x - dy, max_x + dx )
|
|
256
|
+
|
|
257
|
+
For logarithmic axes
|
|
258
|
+
set_xlim( min_x * exp(-dx), max_x * exp(dx) )
|
|
259
|
+
|
|
260
|
+
Parameters
|
|
261
|
+
----------
|
|
262
|
+
ax :
|
|
263
|
+
matplotlib plot
|
|
264
|
+
x, y: bool
|
|
265
|
+
Whether to apply x and y limits.
|
|
266
|
+
min_dx, min_dy:
|
|
267
|
+
Minimum distance
|
|
268
|
+
xrspace, yspace:
|
|
269
|
+
How much of the distance to add to left and right.
|
|
270
|
+
The actual distance added to max_x is dx:=max(min_dx,max_x-min_x)*xrspace
|
|
271
|
+
min_set_x, max_set_x, min_set_y, max_set_y:
|
|
272
|
+
If not None, set the respective min/max accordingly.
|
|
273
|
+
min_x_at_least, max_x_at_most, min_y_at_least, max_y_at_most:
|
|
274
|
+
If not None, bound the respecitve min/max accordingly.
|
|
275
|
+
"""
|
|
276
|
+
if x: self.set_xlim(ax, min_dx=min_dx, xrspace=xrspace, min_set_x=min_set_x, max_set_x=max_set_x, min_x_at_least=min_x_at_least, max_x_at_most=max_x_at_most)
|
|
277
|
+
if y: self.set_ylim(ax, min_dy=min_dy, yrspace=yrspace, min_set_y=min_set_y, max_set_y=max_set_y, min_y_at_least=min_y_at_least, max_y_at_most=max_y_at_most)
|
|
278
|
+
return self
|
|
279
|
+
|
|
280
|
+
class DynamicAx(Deferred):
|
|
281
|
+
"""
|
|
282
|
+
Wrapper around a matplotlib axis returned by DynamicFig (which in turn is returned by figure()).
|
|
283
|
+
|
|
284
|
+
All calls to the returned axis are delegated to matplotlib.
|
|
285
|
+
The results of deferred function calls are again deferred objects, allowing (mostly) to keep working in deferred mode.
|
|
286
|
+
|
|
287
|
+
DynamicAx has a number of additional features:
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
Example
|
|
291
|
+
-------
|
|
292
|
+
fig = figure()
|
|
293
|
+
str = figure.store()
|
|
294
|
+
ax = fig.add_subplot()
|
|
295
|
+
str += ax.plot( x, y, ":" ) # the matplotlib plot() calls is deferred
|
|
296
|
+
fig.render() # renders the figure with the correct plots
|
|
297
|
+
# and executes plot() which returns a list of Line2Ds
|
|
298
|
+
str.clear() # clear previous line
|
|
299
|
+
str += ax.plot( x, y2, ":") # draw new line
|
|
300
|
+
fig.render() # update graph
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
def __init__(self, *,
|
|
304
|
+
fig_id : str,
|
|
305
|
+
fig_list : list,
|
|
306
|
+
row : int,
|
|
307
|
+
col : int,
|
|
308
|
+
spec_pos,
|
|
309
|
+
title : str,
|
|
310
|
+
args : list,
|
|
311
|
+
kwargs : dict):
|
|
312
|
+
""" Creates internal object which defers the creation of various graphics to a later point """
|
|
313
|
+
if row is None:
|
|
314
|
+
assert col is None, "Consistency error"
|
|
315
|
+
assert not args is None or not spec_pos is None, "Consistency error"
|
|
316
|
+
else:
|
|
317
|
+
assert not col is None and args is None, "Consistency error"
|
|
318
|
+
|
|
319
|
+
Deferred.__init__(self,f"subplot({row},{col})" if not row is None else "axes()")
|
|
320
|
+
self.fig_id = fig_id
|
|
321
|
+
self.fig_list = fig_list
|
|
322
|
+
self.row = row
|
|
323
|
+
self.col = col
|
|
324
|
+
self.spec_pos = spec_pos
|
|
325
|
+
self.title = title
|
|
326
|
+
self.plots = {}
|
|
327
|
+
self.args = args
|
|
328
|
+
self.kwargs = kwargs
|
|
329
|
+
self.ax = None
|
|
330
|
+
self.__auto_lims = None
|
|
331
|
+
assert not self in fig_list
|
|
332
|
+
fig_list.append( self )
|
|
333
|
+
|
|
334
|
+
def initialize( self, plt_fig, rows : int, cols : int):
|
|
335
|
+
"""
|
|
336
|
+
Creates the plot by calling all 'caught' functions calls in sequece for the figure 'fig'.
|
|
337
|
+
'rows' and 'cols' count the columns and rows specified by add_subplot() and are ignored by add_axes()
|
|
338
|
+
"""
|
|
339
|
+
assert self.ax is None, "Internal error; function called twice?"
|
|
340
|
+
|
|
341
|
+
def handle_kw_share( kw ):
|
|
342
|
+
v = self.kwargs.pop(kw, None)
|
|
343
|
+
if v is None:
|
|
344
|
+
return
|
|
345
|
+
if isinstance( v, Axes ):
|
|
346
|
+
self.kwargs[kw] = v
|
|
347
|
+
assert isinstance( v, DynamicAx ), ("Cannot",kw,"with type:", type(v))
|
|
348
|
+
assert not v.ax is None, ("Cannot", kw, "with provided axis: it has bnot been creatred yet. That usually means that you mnixed up the order of the plots")
|
|
349
|
+
self.kwargs[kw] = v.ax
|
|
350
|
+
|
|
351
|
+
handle_kw_share("sharex")
|
|
352
|
+
handle_kw_share("sharey")
|
|
353
|
+
|
|
354
|
+
if not self.row is None:
|
|
355
|
+
# add_axes
|
|
356
|
+
num = 1 + self.col + self.row*cols
|
|
357
|
+
self.ax = plt_fig.add_subplot( rows, cols, num, **self.kwargs )
|
|
358
|
+
elif not self.spec_pos is None:
|
|
359
|
+
# add_subplot with grid spec
|
|
360
|
+
self.ax = plt_fig.add_subplot( self.spec_pos.cdx_deferred_result, **self.kwargs )
|
|
361
|
+
else:
|
|
362
|
+
# add_subplot with auto-numbering
|
|
363
|
+
self.ax = plt_fig.add_axes( *self.args, **self.kwargs )
|
|
364
|
+
|
|
365
|
+
if not self.title is None:
|
|
366
|
+
self.ax.set_title(self.title)
|
|
367
|
+
|
|
368
|
+
# handle common functions which expect 'axis' as argument
|
|
369
|
+
# Handle sharex() and sharey() for the moment.
|
|
370
|
+
ref_ax = self.ax
|
|
371
|
+
ax_sharex = ref_ax.sharex
|
|
372
|
+
def sharex(self, other):
|
|
373
|
+
if isinstance(other, DynamicAx):
|
|
374
|
+
verify( not other.ax is None, "Cannot sharex() with provided axis: 'other' has not been created yet. That usually means that you have mixed up the order of the plots")
|
|
375
|
+
other = other.ax
|
|
376
|
+
return ax_sharex(other)
|
|
377
|
+
ref_ax.sharex = types.MethodType(sharex,ref_ax)
|
|
378
|
+
|
|
379
|
+
ax_sharey = ref_ax.sharey
|
|
380
|
+
def sharey(self, other):
|
|
381
|
+
if isinstance(other, DynamicAx):
|
|
382
|
+
verify( not other.ax is None, "Cannot sharey() with provided axis: 'other' has not been created yet. That usually means that you have mixed up the order of the plots")
|
|
383
|
+
other = other.ax
|
|
384
|
+
return ax_sharey(other)
|
|
385
|
+
ref_ax.sharey = types.MethodType(sharey,ref_ax)
|
|
386
|
+
|
|
387
|
+
# call all deferred operations
|
|
388
|
+
self._dereference( self.ax )
|
|
389
|
+
|
|
390
|
+
def remove(self):
|
|
391
|
+
""" Equivalent of the respective Axes remove() function """
|
|
392
|
+
assert self in self.fig_list, ("Internal error: axes not contained in figure list")
|
|
393
|
+
self.fig_list.remove(self)
|
|
394
|
+
self.ax.remove()
|
|
395
|
+
self.ax = None
|
|
396
|
+
gc.collect()
|
|
397
|
+
|
|
398
|
+
def __eq__(self, ax):
|
|
399
|
+
if type(ax).__name__ != type(self).__name__:
|
|
400
|
+
return False
|
|
401
|
+
return self.fig_id == ax.fig_id and self.row == ax.row and self.col == ax.col
|
|
402
|
+
|
|
403
|
+
# automatic limit handling
|
|
404
|
+
# -------------------------
|
|
405
|
+
|
|
406
|
+
def plot(self, *args, scalex=True, scaley=True, data=None, **kwargs ):
|
|
407
|
+
"""
|
|
408
|
+
Wrapper around matplotlib.axes.plot()
|
|
409
|
+
https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html
|
|
410
|
+
If automatic limits are not used, this is a simple pass-through.
|
|
411
|
+
|
|
412
|
+
If automatic limits are used, then this will process the limits accordingly.
|
|
413
|
+
Does not support the 'data' interface into matplotlib.axes.plot()
|
|
414
|
+
"""
|
|
415
|
+
plot = Deferred.__getattr__(self,"plot")
|
|
416
|
+
if self.__auto_lims is None:
|
|
417
|
+
return plot( *args, scalex=scalex, scaley=scaley, data=data, **kwargs )
|
|
418
|
+
|
|
419
|
+
assert data is None, ("Cannot use 'data' for automatic limits yet")
|
|
420
|
+
assert len(args) > 0, "Must have at least one position argument (the data)"
|
|
421
|
+
|
|
422
|
+
def add(x,y,fmt):
|
|
423
|
+
assert not y is None
|
|
424
|
+
if x is None:
|
|
425
|
+
self.limits.update(y, scalex=scalex, scaley=scaley)
|
|
426
|
+
else:
|
|
427
|
+
self.limits.update(x,y, scalex=scalex, scaley=scaley)
|
|
428
|
+
|
|
429
|
+
type_str = [ type(_).__name__ for _ in args ]
|
|
430
|
+
my_args = list(args)
|
|
431
|
+
while len(my_args) > 0:
|
|
432
|
+
assert not isinstance(my_args[0], str), ("Fmt string at the wrong position", my_args[0], "Argument types", type_str)
|
|
433
|
+
if len(my_args) == 1:
|
|
434
|
+
add( x=None, y=my_args[0], fmt=None )
|
|
435
|
+
my_args = my_args[1:]
|
|
436
|
+
elif isinstance(my_args[1], str):
|
|
437
|
+
add( x=None, y=my_args[0], fmt=my_args[1] )
|
|
438
|
+
my_args = my_args[2:]
|
|
439
|
+
elif len(my_args) == 2:
|
|
440
|
+
add( x=my_args[0], y=my_args[1], fmt=None )
|
|
441
|
+
my_args = my_args[2:]
|
|
442
|
+
elif isinstance(my_args[2], str):
|
|
443
|
+
add( x=my_args[0], y=my_args[1], fmt=my_args[2] )
|
|
444
|
+
my_args = my_args[3:]
|
|
445
|
+
else:
|
|
446
|
+
add( x=my_args[0], y=my_args[1], fmt=None )
|
|
447
|
+
my_args = my_args[2:]
|
|
448
|
+
return plot( *args, scalex=scalex, scaley=scaley, data=data, **kwargs )
|
|
449
|
+
|
|
450
|
+
def auto_limits( self, low_quantile, high_quantile, min_length : int = 10, lookback : int = None ):
|
|
451
|
+
"""
|
|
452
|
+
Add automatic limits
|
|
453
|
+
|
|
454
|
+
Parameters
|
|
455
|
+
----------
|
|
456
|
+
low_quantile : float
|
|
457
|
+
Lower quantile to use for computing a 'min' y value. Set to 0 to use 'min'.
|
|
458
|
+
high_quantile : float
|
|
459
|
+
Higher quantile to use for computing a 'min' y value. Set to 1 to use 'max'.
|
|
460
|
+
min_length : int
|
|
461
|
+
Minimum length data must have to use quantile().
|
|
462
|
+
If less data is presented, use min/max, respectively.
|
|
463
|
+
lookback : int
|
|
464
|
+
How many steps to lookback for any calculation. None to use all steps
|
|
465
|
+
"""
|
|
466
|
+
assert self.__auto_lims is None, ("Automatic limits already set")
|
|
467
|
+
self.__auto_lims = AutoLimits( low_quantile=low_quantile, high_quantile=high_quantile, min_length=min_length, lookback=lookback )
|
|
468
|
+
return self
|
|
469
|
+
|
|
470
|
+
def set_auto_lims(self, *args, **kwargs):
|
|
471
|
+
"""
|
|
472
|
+
Apply automatic limits to this axes.
|
|
473
|
+
See AutoLimits.set_lims() for parameter description
|
|
474
|
+
"""
|
|
475
|
+
assert not self.__auto_lims is None, ("Automatic limits not set. Use auto_limits()")
|
|
476
|
+
self.__auto_lims.set_lims( *args, ax=self, **kwargs)
|
|
477
|
+
|
|
478
|
+
class DynamicGridSpec(Deferred):
|
|
479
|
+
""" Deferred GridSpec """
|
|
480
|
+
|
|
481
|
+
def __init__(self, nrows, ncols, kwargs):
|
|
482
|
+
Deferred.__init__(self,f"gridspec({nrows},{ncols})")
|
|
483
|
+
self.grid = None
|
|
484
|
+
self.nrows = nrows
|
|
485
|
+
self.ncols = ncols
|
|
486
|
+
self.kwargs = dict(kwargs)
|
|
487
|
+
|
|
488
|
+
def initialize( self, plt_fig ):
|
|
489
|
+
""" Lazy initialization """
|
|
490
|
+
assert self.grid is None, ("Initialized twice?")
|
|
491
|
+
if len(self.kwargs) == 0:
|
|
492
|
+
self.grid = plt_fig.add_gridspec( nrows=self.nrows, ncols=self.ncols )
|
|
493
|
+
else:
|
|
494
|
+
# wired error in my distribution
|
|
495
|
+
try:
|
|
496
|
+
self.grid = plt_fig.add_gridspec( nrows=self.nrows, ncols=self.ncols, **self.kwargs )
|
|
497
|
+
except TypeError as e:
|
|
498
|
+
estr = str(e)
|
|
499
|
+
print(estr)
|
|
500
|
+
if estr != "GridSpec.__init__() got an unexpected keyword argument 'kwargs'":
|
|
501
|
+
raise e
|
|
502
|
+
warn("Error calling matplotlib GridSpec() with **kwargs: %s; will attempt to ignore any kwargs.", estr)
|
|
503
|
+
self.grid = plt_fig.add_gridspec( nrows=self.nrows, ncols=self.ncols )
|
|
504
|
+
self._dereference( self.grid )
|
|
505
|
+
|
|
506
|
+
class DynamicFig(Deferred):
|
|
507
|
+
"""
|
|
508
|
+
Figure.
|
|
509
|
+
Wraps matplotlib figures.
|
|
510
|
+
Main classic use are the functions
|
|
511
|
+
|
|
512
|
+
add_subplot():
|
|
513
|
+
notice that the call signatue is now different.
|
|
514
|
+
No more need to keep track of the number of plots
|
|
515
|
+
we will use
|
|
516
|
+
|
|
517
|
+
render():
|
|
518
|
+
Use instead of plt.show().
|
|
519
|
+
Elements of the figure may still be modified ("animated") after this point.
|
|
520
|
+
|
|
521
|
+
close():
|
|
522
|
+
Closes the figure. No further calls to elements of the figure allowed.
|
|
523
|
+
Call this to avoud duplicate images in jupyter.
|
|
524
|
+
|
|
525
|
+
next_row()
|
|
526
|
+
Skip to next row, if not already in the first column.
|
|
527
|
+
|
|
528
|
+
Example
|
|
529
|
+
-------
|
|
530
|
+
Simple add_subplot() without the need to pre-specify axes positions.
|
|
531
|
+
|
|
532
|
+
fig = dynaplot.figure()
|
|
533
|
+
ax = fig.add_subplot("1")
|
|
534
|
+
ax.plot(x,y)
|
|
535
|
+
ax = fig.add_subplot("2")
|
|
536
|
+
ax.plot(x,y)
|
|
537
|
+
fig.render()
|
|
538
|
+
|
|
539
|
+
Example with Grid Spec
|
|
540
|
+
-----------------------
|
|
541
|
+
https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.add_gridspec.html#matplotlib.figure.Figure.add_gridspec
|
|
542
|
+
|
|
543
|
+
fig = dynaplot.figure()
|
|
544
|
+
gs = fig.add_gridspec(2,2)
|
|
545
|
+
ax = fig.add_subplot( gs[:,0] )
|
|
546
|
+
ax.plot(x,y)
|
|
547
|
+
ax = fig.add_subplot( gs[:,1] )
|
|
548
|
+
ax.plot(x,y)
|
|
549
|
+
fig.render()
|
|
550
|
+
|
|
551
|
+
The object will also defer all other function calls to the figure
|
|
552
|
+
object; most useful for: suptitle, supxlabel, supylabel
|
|
553
|
+
https://matplotlib.org/stable/gallery/subplots_axes_and_figures/figure_title.html
|
|
554
|
+
|
|
555
|
+
Note that this wrapper will have its own 'axes' funtions.
|
|
556
|
+
"""
|
|
557
|
+
|
|
558
|
+
MODE = 'hdisplay' # switch to 'canvas' if it doesn't work
|
|
559
|
+
|
|
560
|
+
def __init__(self, title : str = None, *,
|
|
561
|
+
row_size : int = 5,
|
|
562
|
+
col_size : int = 4,
|
|
563
|
+
col_nums : int = 5,
|
|
564
|
+
tight : bool = True,
|
|
565
|
+
**fig_kwargs ):
|
|
566
|
+
"""
|
|
567
|
+
Setup object with a given output geometry.
|
|
568
|
+
By default the "figsize" of the figure will be derived from the number of plots vs col_nums, row_size and col_size.
|
|
569
|
+
If 'figsize' is specificed as part of fig_kwargs, then ow_size and col_size are ignored.
|
|
570
|
+
|
|
571
|
+
Once the figure is constructed,
|
|
572
|
+
1) Use add_subplot() to add plots
|
|
573
|
+
2) Call render() to place those plots. Post render, plots can be updated ("animated").
|
|
574
|
+
3) Call close() to close the figure and avoid duplicate copies in jupyter.
|
|
575
|
+
|
|
576
|
+
Parameters
|
|
577
|
+
----------
|
|
578
|
+
title : str, optional
|
|
579
|
+
An optional title which will be passed to suptitle()
|
|
580
|
+
row_size : int, optional
|
|
581
|
+
Size for a row for matplot lib. Default is 5.
|
|
582
|
+
This is ignored if 'figsize' is specified as part of fig_kwargs
|
|
583
|
+
col_size : int, optional
|
|
584
|
+
Size for a column for matplot lib. Default is 4
|
|
585
|
+
This is ignored if 'figsize' is specified as part of fig_kwargs
|
|
586
|
+
col_nums : int, optional
|
|
587
|
+
How many columns to use when add_subplot() is used.
|
|
588
|
+
If omitted, and grid_spec is not specified in fig_kwargs, then the default is 5.
|
|
589
|
+
This is ignored if 'figsize' is specified as part of fig_kwargs
|
|
590
|
+
tight : bool, optional (False)
|
|
591
|
+
Short cut for tight_layout
|
|
592
|
+
|
|
593
|
+
fig_kwargs :
|
|
594
|
+
matplotlib oarameters for creating the figure
|
|
595
|
+
https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.figure.html#
|
|
596
|
+
|
|
597
|
+
By default, 'figsize' is derived from col_size and row_size. If 'figsize' is specified,
|
|
598
|
+
those two values are ignored.
|
|
599
|
+
"""
|
|
600
|
+
Deferred.__init__(self, "figure")
|
|
601
|
+
self.hdisplay = None
|
|
602
|
+
self.axes = []
|
|
603
|
+
self.grid_specs = []
|
|
604
|
+
self.fig = None
|
|
605
|
+
self.row_size = int(row_size)
|
|
606
|
+
self.col_size = int(col_size)
|
|
607
|
+
self.col_nums = int(col_nums)
|
|
608
|
+
self.tight = bool(tight)
|
|
609
|
+
self.tight_para = None
|
|
610
|
+
self.fig_kwargs = dict(fig_kwargs)
|
|
611
|
+
if self.tight:
|
|
612
|
+
self.fig_kwargs['tight_layout'] = True
|
|
613
|
+
verify( self.row_size > 0 and self.col_size > 0 and self.col_nums > 0, "Invalid input.")
|
|
614
|
+
self.this_row = 0
|
|
615
|
+
self.this_col = 0
|
|
616
|
+
self.max_col = 0
|
|
617
|
+
self.fig_title = title
|
|
618
|
+
self.closed = False
|
|
619
|
+
|
|
620
|
+
def __del__(self): # NOQA
|
|
621
|
+
""" Ensure the figure is closed """
|
|
622
|
+
self.close()
|
|
623
|
+
|
|
624
|
+
def add_subplot(self, title : str = None, *,
|
|
625
|
+
new_row : bool = None,
|
|
626
|
+
spec_pos = None,
|
|
627
|
+
**kwargs) -> DynamicAx:
|
|
628
|
+
"""
|
|
629
|
+
Add a subplot.
|
|
630
|
+
This function will return a wrapper which defers the creation of the actual sub plot until self.render() or self.close() is called.
|
|
631
|
+
Thus function cannot be called after render() was called. Use add_axes() in that case.
|
|
632
|
+
|
|
633
|
+
Parameters
|
|
634
|
+
----------
|
|
635
|
+
title : str, options
|
|
636
|
+
Optional title for the plot.
|
|
637
|
+
new_row : bool, optional
|
|
638
|
+
Whether to force a new row. Default is False
|
|
639
|
+
spec_pos : optional
|
|
640
|
+
Grid spec position
|
|
641
|
+
kwargs :
|
|
642
|
+
other arguments to be passed to matplotlib's add_subplot https://matplotlib.org/stable/api/figure_api.html#matplotlib.figure.Figure.add_subplot
|
|
643
|
+
Common use cases
|
|
644
|
+
projection='3d'
|
|
645
|
+
subplotspec='...' when using https://matplotlib.org/stable/api/_as_gen/matplotlib.gridspec.GridSpec.html
|
|
646
|
+
|
|
647
|
+
"""
|
|
648
|
+
verify( not self.closed, "Cannot call add_subplot() after close() was called")
|
|
649
|
+
verify( self.fig is None, "Cannot call add_subplot() after render() was called. Use add_axes() instead")
|
|
650
|
+
|
|
651
|
+
# backward compatibility:
|
|
652
|
+
# previous versions has "new_row" first.
|
|
653
|
+
assert title is None or isinstance(title, str), ("'title' must be a string or None, not", type(title))
|
|
654
|
+
title = str(title) if not title is None else None
|
|
655
|
+
|
|
656
|
+
if not spec_pos is None:
|
|
657
|
+
assert new_row is None, ("Cannot specify 'new_row' when 'spec_pos' is specified")
|
|
658
|
+
ax = DynamicAx( fig_id=hash(self), fig_list=self.axes, row=None, col=None, title=title, spec_pos=spec_pos, args=None, kwargs=dict(kwargs) )
|
|
659
|
+
|
|
660
|
+
else:
|
|
661
|
+
new_row = bool(new_row) if not new_row is None else False
|
|
662
|
+
if (self.this_col >= self.col_nums) or ( new_row and not self.this_col == 0 ):
|
|
663
|
+
self.this_col = 0
|
|
664
|
+
self.this_row = self.this_row + 1
|
|
665
|
+
if self.max_col < self.this_col:
|
|
666
|
+
self.max_col = self.this_col
|
|
667
|
+
ax = DynamicAx( fig_id=hash(self), fig_list=self.axes, row=self.this_row, col=self.this_col, spec_pos=None, title=title, args=None, kwargs=dict(kwargs) )
|
|
668
|
+
self.this_col += 1
|
|
669
|
+
assert ax in self.axes
|
|
670
|
+
return ax
|
|
671
|
+
|
|
672
|
+
add_plot = add_subplot
|
|
673
|
+
|
|
674
|
+
def add_axes( self, title : str = None, *args, **kwargs ):
|
|
675
|
+
"""
|
|
676
|
+
Add axes.
|
|
677
|
+
This function will return a wrapper which defers the creation of the actual axes until self.render() or self.close() is called.
|
|
678
|
+
Unlike add_subplot() you can add axes after render() was called.
|
|
679
|
+
|
|
680
|
+
Parameters
|
|
681
|
+
----------
|
|
682
|
+
title : str, options
|
|
683
|
+
Optional title for the plot.
|
|
684
|
+
kwargs :
|
|
685
|
+
keyword arguments to be passed to matplotlib's add_axes https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.add_axes.html#matplotlib.figure.Figure.add_axes
|
|
686
|
+
"""
|
|
687
|
+
verify( not self.closed, "Cannot call add_subplot() after close() was called")
|
|
688
|
+
|
|
689
|
+
title = str(title) if not title is None else None
|
|
690
|
+
|
|
691
|
+
ax = DynamicAx( fig_id=hash(self), fig_list=self.axes, row=None, col=None, title=title, spec_pos=None, args=list(args), kwargs=dict(kwargs) )
|
|
692
|
+
assert ax in self.axes
|
|
693
|
+
if not self.fig is None:
|
|
694
|
+
ax.initialize( self.fig, rows=self.this_row+1, cols=self.max_col+1 )
|
|
695
|
+
return ax
|
|
696
|
+
|
|
697
|
+
def add_gridspec(self, ncols=1, nrows=1, **kwargs):
|
|
698
|
+
"""
|
|
699
|
+
Wrapper for https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.add_gridspec.html#matplotlib.figure.Figure.add_gridspec
|
|
700
|
+
"""
|
|
701
|
+
grid = DynamicGridSpec( ncols=ncols, nrows=nrows, kwargs=kwargs )
|
|
702
|
+
self.grid_specs.append( grid )
|
|
703
|
+
return grid
|
|
704
|
+
|
|
705
|
+
def next_row(self):
|
|
706
|
+
""" Skip to next row """
|
|
707
|
+
verify( self.fig is None, "Cannot call next_row() after render() was called")
|
|
708
|
+
if self.this_col == 0:
|
|
709
|
+
return
|
|
710
|
+
self.this_col = 0
|
|
711
|
+
self.this_row = self.this_row + 1
|
|
712
|
+
|
|
713
|
+
def render(self, draw : bool = True):
|
|
714
|
+
"""
|
|
715
|
+
Plot all axes.
|
|
716
|
+
Once called, no further plots can be added, but the plots can be updated in place
|
|
717
|
+
|
|
718
|
+
Parameters
|
|
719
|
+
----------
|
|
720
|
+
draw : bool
|
|
721
|
+
If False, then the figure is created, but not drawn.
|
|
722
|
+
This is used in savefig() and to_buytes().
|
|
723
|
+
"""
|
|
724
|
+
verify( not self.closed, "Cannot call render() after close() was called")
|
|
725
|
+
if self.this_row == 0 and self.this_col == 0:
|
|
726
|
+
return
|
|
727
|
+
if self.fig is None:
|
|
728
|
+
# create figure
|
|
729
|
+
if not 'figsize' in self.fig_kwargs:
|
|
730
|
+
self.fig_kwargs['figsize'] = ( self.col_size*(self.max_col+1), self.row_size*(self.this_row+1))
|
|
731
|
+
self.fig = plt.figure( **self.fig_kwargs )
|
|
732
|
+
if self.tight:
|
|
733
|
+
self.fig.tight_layout()
|
|
734
|
+
self.fig.set_tight_layout(True)
|
|
735
|
+
if not self.fig_title is None:
|
|
736
|
+
self.fig.suptitle( self.fig_title )
|
|
737
|
+
# create all grid specs
|
|
738
|
+
for gs in self.grid_specs:
|
|
739
|
+
gs.initialize( self.fig )
|
|
740
|
+
# create all axes
|
|
741
|
+
for ax in self.axes:
|
|
742
|
+
ax.initialize( self.fig, rows=self.this_row+1, cols=self.max_col+1 )
|
|
743
|
+
# execute all deferred calls to fig()
|
|
744
|
+
self._dereference( self.fig )
|
|
745
|
+
|
|
746
|
+
if not draw:
|
|
747
|
+
return
|
|
748
|
+
if self.MODE == 'hdisplay':
|
|
749
|
+
if self.hdisplay is None:
|
|
750
|
+
self.hdisplay = display.display(display_id=True)
|
|
751
|
+
verify( not self.hdisplay is None, "Could not optain current IPython display ID from IPython.display.display(). Set DynamicFig.MODE = 'canvas' for an alternative mode")
|
|
752
|
+
self.hdisplay.update(self.fig)
|
|
753
|
+
elif self.MODE == 'canvas_idle':
|
|
754
|
+
self.fig.canvas.draw_idle()
|
|
755
|
+
else:
|
|
756
|
+
verify( self.MODE == "canvas", "DynamicFig.MODE must be 'hdisplay', 'canvas_idle' or 'canvas'. Found %s", self.MODE, exception=ValueError )
|
|
757
|
+
self.fig.canvas.draw()
|
|
758
|
+
gc.collect() # for some unknown reason this is required in VSCode
|
|
759
|
+
|
|
760
|
+
def savefig(self, fname, silent_close : bool = True, **kwargs ):
|
|
761
|
+
"""
|
|
762
|
+
Saves the figure to a file.
|
|
763
|
+
https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.savefig.html
|
|
764
|
+
|
|
765
|
+
Parameters
|
|
766
|
+
----------
|
|
767
|
+
fname : filename or file-like object, c.f. https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.savefig.html
|
|
768
|
+
silent_close : if True, call close(). Unless the figure was drawn before, this means that the figure will not be displayed in jupyter.
|
|
769
|
+
kwargs : to be passed to https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.savefig.html
|
|
770
|
+
"""
|
|
771
|
+
verify( not self.closed, "Cannot call savefig() after close() was called")
|
|
772
|
+
if self.fig is None:
|
|
773
|
+
self.render(draw=False)
|
|
774
|
+
self.fig.savefig( fname, **kwargs )
|
|
775
|
+
if silent_close:
|
|
776
|
+
self.close(render=False)
|
|
777
|
+
|
|
778
|
+
def to_bytes(self, silent_close : bool = True ) -> bytes:
|
|
779
|
+
"""
|
|
780
|
+
Convert figure to a byte stream
|
|
781
|
+
This stream can be used to generate a IPython image using
|
|
782
|
+
|
|
783
|
+
from IPython.display import Image, display
|
|
784
|
+
bytes = fig.to_bytes()
|
|
785
|
+
image = Image(data=byes)
|
|
786
|
+
display(image)
|
|
787
|
+
|
|
788
|
+
Parameters
|
|
789
|
+
----------
|
|
790
|
+
silent_close : if True, call close(). Unless the figure was drawn before, this means that the figure will not be displayed in jupyter.
|
|
791
|
+
"""
|
|
792
|
+
verify( not self.closed, "Cannot call savefig() after close() was called")
|
|
793
|
+
img_buf = io.BytesIO()
|
|
794
|
+
if self.fig is None:
|
|
795
|
+
self.render(draw=False)
|
|
796
|
+
self.fig.savefig( img_buf )
|
|
797
|
+
if silent_close:
|
|
798
|
+
self.close(render=False)
|
|
799
|
+
data = img_buf.getvalue()
|
|
800
|
+
img_buf.close()
|
|
801
|
+
return data
|
|
802
|
+
|
|
803
|
+
@staticmethod
|
|
804
|
+
def store():
|
|
805
|
+
""" Create a FigStore(). Such a store allows managing graphical elements (artists) dynamically """
|
|
806
|
+
return FigStore()
|
|
807
|
+
|
|
808
|
+
def close(self, render : bool = True,
|
|
809
|
+
clear : bool = False):
|
|
810
|
+
"""
|
|
811
|
+
Closes the figure. Does not clear the figure.
|
|
812
|
+
Call this to avoid a duplicate in jupyter output cells.
|
|
813
|
+
|
|
814
|
+
Parameters
|
|
815
|
+
----------
|
|
816
|
+
render : if True, this function will call render() before closing the figure.
|
|
817
|
+
clear : if True, all axes will be cleared.
|
|
818
|
+
"""
|
|
819
|
+
if not self.closed:
|
|
820
|
+
# magic wand to avoid printing an empty figure message
|
|
821
|
+
if clear:
|
|
822
|
+
if not self.fig is None:
|
|
823
|
+
def repr_magic(self):
|
|
824
|
+
return type(self)._repr_html_(self) if len(self.axes) > 0 else "</HTML>"
|
|
825
|
+
self.fig._repr_html_ = types.MethodType(repr_magic,self.fig)
|
|
826
|
+
self.delaxes( self.axes, render=render )
|
|
827
|
+
elif render:
|
|
828
|
+
self.render()
|
|
829
|
+
if not self.fig is None:
|
|
830
|
+
plt.close(self.fig)
|
|
831
|
+
self.fig = None
|
|
832
|
+
self.closed = True
|
|
833
|
+
self.hdisplay = None
|
|
834
|
+
gc.collect()
|
|
835
|
+
|
|
836
|
+
def get_axes(self) -> list:
|
|
837
|
+
""" Equivalent to self.axes """
|
|
838
|
+
verify( not self.closed, "Cannot call render() after close() was called")
|
|
839
|
+
return self.axes
|
|
840
|
+
|
|
841
|
+
def remove_all_axes(self, *, render : bool = False):
|
|
842
|
+
""" Calles remove() for all axes """
|
|
843
|
+
while len(self.axes) > 0:
|
|
844
|
+
self.axes[0].remove()
|
|
845
|
+
if render:
|
|
846
|
+
self.render()
|
|
847
|
+
|
|
848
|
+
def delaxes( self, ax : DynamicAx, *, render : bool = False ):
|
|
849
|
+
"""
|
|
850
|
+
Equivalent of https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.delaxes.html#matplotlib.figure.Figure.delaxes
|
|
851
|
+
Can also take a list
|
|
852
|
+
"""
|
|
853
|
+
verify( not self.closed, "Cannot call render() after close() was called")
|
|
854
|
+
if isinstance( ax, Collection ):
|
|
855
|
+
ax = list(ax)
|
|
856
|
+
for x in ax:
|
|
857
|
+
x.remove()
|
|
858
|
+
else:
|
|
859
|
+
assert ax in self.axes, ("Cannot delete axes which wasn't created by this figure")
|
|
860
|
+
ax.remove()
|
|
861
|
+
if render:
|
|
862
|
+
self.render()
|
|
863
|
+
|
|
864
|
+
def figure( title : str = None, *,
|
|
865
|
+
row_size : int = 5,
|
|
866
|
+
col_size : int = 4,
|
|
867
|
+
col_nums : int = 5,
|
|
868
|
+
tight : bool = True,
|
|
869
|
+
**fig_kwargs ) -> DynamicFig:
|
|
870
|
+
"""
|
|
871
|
+
Generates a 'DynamicFig' dynamic figure using matplot lib.
|
|
872
|
+
It has the following main functions
|
|
873
|
+
|
|
874
|
+
add_subplot():
|
|
875
|
+
Used to create a sub plot. No need to provide the customary
|
|
876
|
+
rows, cols, and total number as this will computed for you.
|
|
877
|
+
|
|
878
|
+
All calls to the returned 'ax' are delegated to
|
|
879
|
+
matplotlib with the amendmend that if any such function
|
|
880
|
+
returs a list with one member, it will just return
|
|
881
|
+
this member.
|
|
882
|
+
This caters for the very common use case plot() where
|
|
883
|
+
x,y are vectors. Assume y2 is an updated data set
|
|
884
|
+
In this case we can use
|
|
885
|
+
|
|
886
|
+
fig = figure()
|
|
887
|
+
ax = fig.add_subplot()
|
|
888
|
+
lns = ax.plot( x, y, ":" )
|
|
889
|
+
fig.render() # --> draw graph
|
|
890
|
+
|
|
891
|
+
# "animate"
|
|
892
|
+
lns.set_ydata( y2 )
|
|
893
|
+
fig.render() # --> change graph
|
|
894
|
+
|
|
895
|
+
render():
|
|
896
|
+
Draws the figure as it is.
|
|
897
|
+
Call repeatedly if the underlying graphs are modified
|
|
898
|
+
as per example above.
|
|
899
|
+
No further add_subplots() are recommended
|
|
900
|
+
|
|
901
|
+
close():
|
|
902
|
+
Close the figure.
|
|
903
|
+
Call this to avoid duplicate copies of the figure in jupyter
|
|
904
|
+
|
|
905
|
+
The object will also defer all other function calls to the figure
|
|
906
|
+
object; most useful for: suptitle, supxlabel, supylabel
|
|
907
|
+
https://matplotlib.org/stable/gallery/subplots_axes_and_figures/figure_title.html
|
|
908
|
+
|
|
909
|
+
By default the "figsize" of the figure will be derived from the number of plots vs col_nums, row_size and col_size.
|
|
910
|
+
If 'figsize' is specificed as part of fig_kwargs, then ow_size and col_size are ignored.
|
|
911
|
+
|
|
912
|
+
Paraneters
|
|
913
|
+
----------
|
|
914
|
+
title : str, optional
|
|
915
|
+
An optional title which will be passed to suptitle()
|
|
916
|
+
row_size : int, optional
|
|
917
|
+
Size for a row for matplot lib. Default is 5.
|
|
918
|
+
This is ignored if 'figsize' is specified as part of fig_kwargs
|
|
919
|
+
col_size : int, optional
|
|
920
|
+
Size for a column for matplot lib. Default is 4
|
|
921
|
+
This is ignored if 'figsize' is specified as part of fig_kwargs
|
|
922
|
+
col_nums : int, optional
|
|
923
|
+
How many columns to use when add_subplot() is used.
|
|
924
|
+
If omitted, and grid_spec is not specified in fig_kwargs, then the default is 5.
|
|
925
|
+
This is ignored if 'figsize' is specified as part of fig_kwargs
|
|
926
|
+
tight : bool, optional (False)
|
|
927
|
+
Short cut for tight_layout
|
|
928
|
+
|
|
929
|
+
fig_kwargs :
|
|
930
|
+
matplotlib oarameters for creating the figure
|
|
931
|
+
https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.figure.html#
|
|
932
|
+
|
|
933
|
+
By default, 'figsize' is derived from col_size and row_size. If 'figsize' is specified,
|
|
934
|
+
those two values are ignored.
|
|
935
|
+
|
|
936
|
+
Returns
|
|
937
|
+
-------
|
|
938
|
+
DynamicFig
|
|
939
|
+
A figure wrapper; see above.
|
|
940
|
+
"""
|
|
941
|
+
return DynamicFig( title=title, row_size=row_size, col_size=col_size, col_nums=col_nums, tight=tight, **fig_kwargs )
|
|
942
|
+
|
|
943
|
+
# ----------------------------------------------------------------------------------
|
|
944
|
+
# Utility class for animated content
|
|
945
|
+
# ----------------------------------------------------------------------------------
|
|
946
|
+
|
|
947
|
+
class FigStore( object ):
|
|
948
|
+
"""
|
|
949
|
+
Utility class to manage dynamic content by removing old graphical elements (instead of using element-specifc update).
|
|
950
|
+
Allows implementing a fairly cheap dynamic pattern:
|
|
951
|
+
|
|
952
|
+
from cdxbasics.dynaplot import figure
|
|
953
|
+
import time as time
|
|
954
|
+
|
|
955
|
+
fig = figure()
|
|
956
|
+
ax = fig.add_subplot()
|
|
957
|
+
store = fig.store()
|
|
958
|
+
|
|
959
|
+
x = np.linspace(-2.,+2,21)
|
|
960
|
+
|
|
961
|
+
for i in range(10):
|
|
962
|
+
store.remove()
|
|
963
|
+
store += ax.plot( x, np.sin(x+float(i)) )
|
|
964
|
+
fig.render()
|
|
965
|
+
time.sleep(1)
|
|
966
|
+
|
|
967
|
+
fig.close()
|
|
968
|
+
"""
|
|
969
|
+
|
|
970
|
+
def __init__(self):
|
|
971
|
+
""" Create FigStore() objecy """
|
|
972
|
+
self._elements = []
|
|
973
|
+
|
|
974
|
+
def add(self, element : Artist):
|
|
975
|
+
"""
|
|
976
|
+
Add an element to the store.
|
|
977
|
+
The same operation is available using +=
|
|
978
|
+
|
|
979
|
+
Parameters
|
|
980
|
+
----------
|
|
981
|
+
element :
|
|
982
|
+
Graphical matplot element derived from matplotlib.artist.Artist, e.g. Line2D
|
|
983
|
+
or
|
|
984
|
+
Collection of the above
|
|
985
|
+
or
|
|
986
|
+
None
|
|
987
|
+
|
|
988
|
+
Returns
|
|
989
|
+
-------
|
|
990
|
+
self, such that a.add(x).add(y).add(z) works
|
|
991
|
+
"""
|
|
992
|
+
if element is None:
|
|
993
|
+
return self
|
|
994
|
+
if isinstance(element, Artist):
|
|
995
|
+
self._elements.append( element )
|
|
996
|
+
return self
|
|
997
|
+
if isinstance(element, Deferred):
|
|
998
|
+
self._elements.append( element )
|
|
999
|
+
return self
|
|
1000
|
+
if not isinstance(element,Collection):
|
|
1001
|
+
raise ValueError("Cannot add element of type '{type(element).__name__}' as it is not derived from matplotlib.artist.Artist, nor is it a Collection")
|
|
1002
|
+
for l in element:
|
|
1003
|
+
self += l
|
|
1004
|
+
return self
|
|
1005
|
+
|
|
1006
|
+
def __iadd__(self, element : Artist):
|
|
1007
|
+
""" += operator replacement for 'add' """
|
|
1008
|
+
return self.add(element)
|
|
1009
|
+
|
|
1010
|
+
def remove(self):
|
|
1011
|
+
"""
|
|
1012
|
+
Removes all elements by calling their remove() function:
|
|
1013
|
+
https://matplotlib.org/stable/api/_as_gen/matplotlib.artist.Artist.remove.html#matplotlib.artist.Artist.remove
|
|
1014
|
+
"""
|
|
1015
|
+
def rem(e):
|
|
1016
|
+
if isinstance(e, Artist):
|
|
1017
|
+
e.remove()
|
|
1018
|
+
return
|
|
1019
|
+
if isinstance(e,Collection):
|
|
1020
|
+
for l in e:
|
|
1021
|
+
rem(l)
|
|
1022
|
+
return
|
|
1023
|
+
if isinstance(e, Deferred):
|
|
1024
|
+
if not e._was_executed:
|
|
1025
|
+
raise RuntimeError("Error: remove() was called before the figure was rendered. Call figure.render() before removing elements.")
|
|
1026
|
+
rem( e.cdx_deferred_result )
|
|
1027
|
+
return
|
|
1028
|
+
if not e is None:
|
|
1029
|
+
raise RuntimeError("Cannot remove() element of type '{type(e).__name__}' as it is not derived from matplotlib.artist.Artist, nor is it a Collection")
|
|
1030
|
+
|
|
1031
|
+
while len(self._elements) > 0:
|
|
1032
|
+
rem( self._elements.pop(0) )
|
|
1033
|
+
self._elements = []
|
|
1034
|
+
gc.collect()
|
|
1035
|
+
|
|
1036
|
+
def clear(self):
|
|
1037
|
+
"""
|
|
1038
|
+
Alias for remove(): removes all elements by calling their remove() function:
|
|
1039
|
+
https://matplotlib.org/stable/api/_as_gen/matplotlib.artist.Artist.remove.html#matplotlib.artist.Artist.remove
|
|
1040
|
+
"""
|
|
1041
|
+
return self.remove()
|
|
1042
|
+
|
|
1043
|
+
def store():
|
|
1044
|
+
""" Creates a FigStore which can be used to dynamically update a figure """
|
|
1045
|
+
return FigStore()
|
|
1046
|
+
figure.store = store
|
|
1047
|
+
|
|
1048
|
+
# ----------------------------------------------------------------------------------
|
|
1049
|
+
# color management
|
|
1050
|
+
# ----------------------------------------------------------------------------------
|
|
1051
|
+
|
|
1052
|
+
def color_css4(i : int):
|
|
1053
|
+
""" Returns the i'th css4 color """
|
|
1054
|
+
names = list(mcolors.CSS4_COLORS)
|
|
1055
|
+
name = names[i % len(names)]
|
|
1056
|
+
return mcolors.CSS4_COLORS[name]
|
|
1057
|
+
|
|
1058
|
+
def color_base(i : int):
|
|
1059
|
+
""" Returns the i'th base color """
|
|
1060
|
+
names = list(mcolors.BASE_COLORS)
|
|
1061
|
+
name = names[i % len(names)]
|
|
1062
|
+
return mcolors.BASE_COLORS[name]
|
|
1063
|
+
|
|
1064
|
+
def color_tableau(i : int):
|
|
1065
|
+
""" Returns the i'th tableau color """
|
|
1066
|
+
names = list(mcolors.TABLEAU_COLORS)
|
|
1067
|
+
name = names[i % len(names)]
|
|
1068
|
+
return mcolors.TABLEAU_COLORS[name]
|
|
1069
|
+
|
|
1070
|
+
def color_xkcd(i : int):
|
|
1071
|
+
""" Returns the i'th xkcd color """
|
|
1072
|
+
names = list(mcolors.XKCD_COLORS)
|
|
1073
|
+
name = names[i % len(names)]
|
|
1074
|
+
return mcolors.XKCD_COLORS[name]
|
|
1075
|
+
|
|
1076
|
+
def color(i : int, table : str ="css4"):
|
|
1077
|
+
"""
|
|
1078
|
+
Returns a color with a given index to allow consistent colouring
|
|
1079
|
+
Use case is using the same colors by nominal index, e.g.
|
|
1080
|
+
|
|
1081
|
+
fig = figure()
|
|
1082
|
+
ax = fig.add_subplot()
|
|
1083
|
+
for i in range(N):
|
|
1084
|
+
ax.plot( x, y1[i], "-", color=color(i) )
|
|
1085
|
+
ax.plot( x, y2[i], ":", color=color(i) )
|
|
1086
|
+
fig.render()
|
|
1087
|
+
|
|
1088
|
+
Parameters
|
|
1089
|
+
----------
|
|
1090
|
+
i : int
|
|
1091
|
+
Integer number. Colors will be rotated
|
|
1092
|
+
table : str, default "css4""
|
|
1093
|
+
Which color table from matplotlib.colors to use: css4, base, tableau, xkcd
|
|
1094
|
+
Returns
|
|
1095
|
+
-------
|
|
1096
|
+
Color
|
|
1097
|
+
"""
|
|
1098
|
+
if table == "css4":
|
|
1099
|
+
return color_css4(i)
|
|
1100
|
+
if table == "base":
|
|
1101
|
+
return color_base(i)
|
|
1102
|
+
if table == "tableau":
|
|
1103
|
+
return color_tableau(i)
|
|
1104
|
+
verify( table == "xkcd", "Invalid color code '%s'. Must be 'css4' (the default), 'base', 'tableau', or 'xkcd'", table, exception=ValueError )
|
|
1105
|
+
return color_xkcd(i)
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def colors(table : str = "css4"):
|
|
1109
|
+
"""
|
|
1110
|
+
Returns a generator for the colors of the specified table
|
|
1111
|
+
|
|
1112
|
+
fig = figure()
|
|
1113
|
+
ax = fig.add_subplot()
|
|
1114
|
+
for label, color in zip( lables, colors() ):
|
|
1115
|
+
ax.plot( x, y1[i], "-", color=color )
|
|
1116
|
+
ax.plot( x, y2[i], ":", color=color )
|
|
1117
|
+
fig.render()
|
|
1118
|
+
|
|
1119
|
+
fig = figure()
|
|
1120
|
+
ax = fig.add_subplot()
|
|
1121
|
+
color = colors()
|
|
1122
|
+
for label in labels:
|
|
1123
|
+
color_ = next(color)
|
|
1124
|
+
ax.plot( x, y1[i], "-", color=color_ )
|
|
1125
|
+
ax.plot( x, y2[i], ":", color=color_ )
|
|
1126
|
+
fig.render()
|
|
1127
|
+
Parameters
|
|
1128
|
+
----------
|
|
1129
|
+
table : str, default "css4""
|
|
1130
|
+
Which color table from matplotlib.colors to use: css4, base, tableau, xkcd
|
|
1131
|
+
Returns
|
|
1132
|
+
-------
|
|
1133
|
+
Generator for colors. Use next() or iterate.
|
|
1134
|
+
"""
|
|
1135
|
+
num = 0
|
|
1136
|
+
while True:
|
|
1137
|
+
yield color(num,table)
|
|
1138
|
+
num = num + 1
|
|
1139
|
+
|
|
1140
|
+
def colors_css4():
|
|
1141
|
+
""" Iterator for css4 matplotlib colors """
|
|
1142
|
+
return colors("css4")
|
|
1143
|
+
|
|
1144
|
+
def colors_base():
|
|
1145
|
+
""" Iterator for base matplotlib colors """
|
|
1146
|
+
return colors("base")
|
|
1147
|
+
|
|
1148
|
+
def colors_tableau():
|
|
1149
|
+
""" Iterator for tableau matplotlib colors """
|
|
1150
|
+
return colors("tableau")
|
|
1151
|
+
|
|
1152
|
+
def colors_xkcd():
|
|
1153
|
+
""" Iterator for xkcd matplotlib colors """
|
|
1154
|
+
return colors("xkcd")
|
|
1155
|
+
|