cdxcore 0.1.31__py3-none-any.whl → 0.1.33__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 +1 -1
- cdxcore/dynaplot.py +435 -292
- cdxcore/subdir.py +95 -44
- cdxcore/uniquehash.py +1 -1
- {cdxcore-0.1.31.dist-info → cdxcore-0.1.33.dist-info}/METADATA +1 -1
- {cdxcore-0.1.31.dist-info → cdxcore-0.1.33.dist-info}/RECORD +9 -9
- {cdxcore-0.1.31.dist-info → cdxcore-0.1.33.dist-info}/WHEEL +0 -0
- {cdxcore-0.1.31.dist-info → cdxcore-0.1.33.dist-info}/licenses/LICENSE +0 -0
- {cdxcore-0.1.31.dist-info → cdxcore-0.1.33.dist-info}/top_level.txt +0 -0
cdxcore/dynaplot.py
CHANGED
|
@@ -7,14 +7,16 @@ animated visualization with :mod:`matplotlib`,
|
|
|
7
7
|
for example during training with machine learing kits such as
|
|
8
8
|
*pyTorch*.
|
|
9
9
|
|
|
10
|
+
It also makes the creation of subplots more streamlined.
|
|
11
|
+
|
|
10
12
|
This has been tested with Anaconda's
|
|
11
13
|
JupyterHub and ``%matplotlib inline``.
|
|
12
14
|
|
|
13
15
|
Overview
|
|
14
16
|
--------
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
Animated Graphs, Simple
|
|
19
|
+
^^^^^^^^^^^^^^^^^^^^^^^
|
|
18
20
|
|
|
19
21
|
The package now contains a lazy method to manage updates to graphs (animations).
|
|
20
22
|
This is implemented as follows:
|
|
@@ -37,12 +39,56 @@ This is implemented as follows:
|
|
|
37
39
|
|
|
38
40
|
If you do not call close, you will likely see
|
|
39
41
|
unwanted copies of your plots in Jupyter.
|
|
42
|
+
|
|
43
|
+
If possible, use the ``with figure(...) as fig`` pattern which will ennsure that :meth:`cdxcore.dynaplot.DynaFig.close`
|
|
44
|
+
is called.
|
|
40
45
|
|
|
41
46
|
Here is an example of animated line plots using :func:`cdxcore.dynaplot.DynaFig.store`::
|
|
42
47
|
|
|
43
48
|
%matplotlib inline
|
|
44
49
|
import numpy as np
|
|
45
|
-
|
|
50
|
+
import time
|
|
51
|
+
from cdxcore.dynaplot import figure, MODE
|
|
52
|
+
|
|
53
|
+
x = np.linspace(0,1,100)
|
|
54
|
+
pm = 0.2
|
|
55
|
+
|
|
56
|
+
with figure(col_size=10) as fig:
|
|
57
|
+
ax = fig.add_subplot()
|
|
58
|
+
ax2 = fig.add_subplot()
|
|
59
|
+
ax2.sharey(ax)
|
|
60
|
+
store = fig.store()
|
|
61
|
+
fig.render()
|
|
62
|
+
|
|
63
|
+
for i in range(10):
|
|
64
|
+
y = np.random.random(size=(100,))
|
|
65
|
+
ax.set_title(f"Test {i}")
|
|
66
|
+
ax2.set_title(f"Test {i}")
|
|
67
|
+
|
|
68
|
+
store.remove() # delete all prviously stored elements
|
|
69
|
+
store += ax.plot(x,y,":", label=f"data {i}")
|
|
70
|
+
store += ax2.plot(x,y,"-",color="red", label=f"data {i}")
|
|
71
|
+
store += ax2.fill_between( x, y-pm, y+pm, color="blue", alpha=0.2 )
|
|
72
|
+
store += ax.legend()
|
|
73
|
+
|
|
74
|
+
fig.render()
|
|
75
|
+
time.sleep(0.5)
|
|
76
|
+
|
|
77
|
+
.. image:: /_static/dynaplot.gif
|
|
78
|
+
|
|
79
|
+
In above example we used the ``with`` context of a :class:`cdxcore.dynaplot.DynaFig` figure.
|
|
80
|
+
The point of using ``with`` where convenient is that it will call :meth:`cdxcore.dynaplot.DynaFig.close`
|
|
81
|
+
which will avoid duplicate copies of the figure in jupyter.
|
|
82
|
+
|
|
83
|
+
To do close the figure manually, call :meth:`cdxcore.dynaplot.DynaFig.close` directy:
|
|
84
|
+
|
|
85
|
+
.. code-block:: python
|
|
86
|
+
:emphasize-lines: 9, 30
|
|
87
|
+
|
|
88
|
+
%matplotlib inline
|
|
89
|
+
import numpy as np
|
|
90
|
+
import time
|
|
91
|
+
from cdxcore.dynaplot import figure, MODE
|
|
46
92
|
|
|
47
93
|
x = np.linspace(0,1,100)
|
|
48
94
|
pm = 0.2
|
|
@@ -51,17 +97,15 @@ Here is an example of animated line plots using :func:`cdxcore.dynaplot.DynaFig.
|
|
|
51
97
|
ax = fig.add_subplot()
|
|
52
98
|
ax2 = fig.add_subplot()
|
|
53
99
|
ax2.sharey(ax)
|
|
54
|
-
store = fig.store()
|
|
55
|
-
|
|
100
|
+
store = fig.store()
|
|
56
101
|
fig.render()
|
|
57
102
|
|
|
58
|
-
|
|
59
|
-
for i in range(5):
|
|
103
|
+
for i in range(10):
|
|
60
104
|
y = np.random.random(size=(100,))
|
|
61
105
|
ax.set_title(f"Test {i}")
|
|
62
106
|
ax2.set_title(f"Test {i}")
|
|
63
107
|
|
|
64
|
-
store.remove() # delete all
|
|
108
|
+
store.remove() # delete all prviously stored elements
|
|
65
109
|
store += ax.plot(x,y,":", label=f"data {i}")
|
|
66
110
|
store += ax2.plot(x,y,"-",color="red", label=f"data {i}")
|
|
67
111
|
store += ax2.fill_between( x, y-pm, y+pm, color="blue", alpha=0.2 )
|
|
@@ -69,48 +113,43 @@ Here is an example of animated line plots using :func:`cdxcore.dynaplot.DynaFig.
|
|
|
69
113
|
|
|
70
114
|
fig.render()
|
|
71
115
|
time.sleep(0.5)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.. image:: /_static/dynaplot.gif
|
|
75
|
-
|
|
76
|
-
This example shows the use of ``store`` with different elements.
|
|
116
|
+
|
|
117
|
+
fig.close()
|
|
77
118
|
|
|
78
119
|
Here is an example with
|
|
79
120
|
animated 3D plots, calling :meth:`matplotlib.axes.Axes.remove` manually::
|
|
80
121
|
|
|
81
122
|
%matplotlib inline
|
|
82
123
|
import numpy as np
|
|
83
|
-
from cdxcore.dynaplot import figure
|
|
124
|
+
from cdxcore.dynaplot import figure
|
|
84
125
|
import math
|
|
85
126
|
|
|
86
127
|
x = np.linspace(0.,2.*math.pi,51)
|
|
87
128
|
y = x
|
|
88
129
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
fig.render()
|
|
101
|
-
r1 = None
|
|
102
|
-
r2 = None
|
|
103
|
-
import time
|
|
104
|
-
for i in range(50):
|
|
105
|
-
time.sleep(0.01)
|
|
106
|
-
z = np.cos( float(i)/10.+x )+np.sin( float(i)/2.+y )
|
|
107
|
-
if not r1 is None: r1.remove()
|
|
108
|
-
if not r2 is None: r2.remove()
|
|
109
|
-
r1 = ax1.scatter( x,y,z, color="blue" )
|
|
110
|
-
r2 = ax2.scatter( 2.*math.pi-x,math.pi*(1.+np.sin( float(i)/2.+y )),z )
|
|
130
|
+
with figure() as fig:
|
|
131
|
+
ax1 = fig.add_subplot(projection='3d')
|
|
132
|
+
ax2 = fig.add_subplot(projection='3d')
|
|
133
|
+
ax1.set_xlim(0.,2.*math.pi)
|
|
134
|
+
ax1.set_ylim(0.,2.*math.pi)
|
|
135
|
+
ax1.set_zlim(-2,+2)
|
|
136
|
+
ax1.set_title("Color specified")
|
|
137
|
+
ax2.set_xlim(0.,2.*math.pi)
|
|
138
|
+
ax2.set_ylim(0.,2.*math.pi)
|
|
139
|
+
ax2.set_zlim(-2,+2)
|
|
140
|
+
ax2.set_title("Color not specified")
|
|
111
141
|
fig.render()
|
|
112
|
-
|
|
113
|
-
|
|
142
|
+
r1 = None
|
|
143
|
+
r2 = None
|
|
144
|
+
import time
|
|
145
|
+
for i in range(50):
|
|
146
|
+
time.sleep(0.01)
|
|
147
|
+
z = np.cos( float(i)/10.+x )+np.sin( float(i)/2.+y )
|
|
148
|
+
if not r1 is None: r1.remove()
|
|
149
|
+
if not r2 is None: r2.remove()
|
|
150
|
+
r1 = ax1.scatter( x,y,z, color="blue" )
|
|
151
|
+
r2 = ax2.scatter( 2.*math.pi-x,math.pi*(1.+np.sin( float(i)/2.+y )),z )
|
|
152
|
+
fig.render()
|
|
114
153
|
|
|
115
154
|
.. image:: /_static/dynaplot3D.gif
|
|
116
155
|
|
|
@@ -145,24 +184,35 @@ The example also shows that we can specify titles for subplots and figures easil
|
|
|
145
184
|
|
|
146
185
|
%matplotlib inline
|
|
147
186
|
import numpy as np
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
ref_ax = None
|
|
187
|
+
import time
|
|
188
|
+
from cdxcore.dynaplot import figure
|
|
189
|
+
|
|
152
190
|
x = np.linspace(0,1,100)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
ax.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
191
|
+
|
|
192
|
+
with figure(title="Multi Graph", fig_size=(10,6), columns=4) as fig:
|
|
193
|
+
lines = []
|
|
194
|
+
ref_ax = None
|
|
195
|
+
for k in range(9):
|
|
196
|
+
ax = fig.add_subplot()
|
|
197
|
+
ax.set_title("Test %ld" % k)
|
|
198
|
+
y = np.random.random(size=(100,1))
|
|
199
|
+
l = ax.plot(x,y,":",color="red", label="data")
|
|
200
|
+
lines.append(l)
|
|
201
|
+
ax.legend()
|
|
202
|
+
|
|
203
|
+
if not ref_ax is None:
|
|
204
|
+
ax.sharey(ref_ax)
|
|
205
|
+
ax.sharex(ref_ax)
|
|
206
|
+
else:
|
|
207
|
+
ref_ax = ax
|
|
208
|
+
fig.render()
|
|
209
|
+
|
|
210
|
+
for i in range(5):
|
|
211
|
+
time.sleep(0.2)
|
|
212
|
+
for l in lines:
|
|
213
|
+
y = np.random.random(size=(100,1))
|
|
214
|
+
l[0].set_ydata( y )
|
|
215
|
+
fig.render()
|
|
166
216
|
.. image:: /_static/multi.gif
|
|
167
217
|
|
|
168
218
|
Grid Spec
|
|
@@ -178,18 +228,16 @@ Example::
|
|
|
178
228
|
%matplotlib inline
|
|
179
229
|
from cdxcore.dynaplot import figure
|
|
180
230
|
import numpy as np
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
ax.plot(x,y)
|
|
192
|
-
fig.close()
|
|
231
|
+
|
|
232
|
+
x = np.linspace(-2.,+2,21)
|
|
233
|
+
|
|
234
|
+
with figure(tight=False) as fig:
|
|
235
|
+
ax = fig.add_subplot()
|
|
236
|
+
ax.plot( x, np.sin(x) )
|
|
237
|
+
fig.render()
|
|
238
|
+
|
|
239
|
+
ax = fig.add_axes( (0.5,0.5,0.3,0.3), "axes" )
|
|
240
|
+
ax.plot( x, np.cos(x) )
|
|
193
241
|
|
|
194
242
|
.. image:: /_static/gridspec.gif
|
|
195
243
|
|
|
@@ -211,48 +259,59 @@ Example of using the same colors by order in two plots::
|
|
|
211
259
|
|
|
212
260
|
x = np.linspace(0.,2.*math.pi,51)
|
|
213
261
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
# draw 10 lines in the second sub plot.
|
|
224
|
-
# use the same colors for the same scaling of 'x'
|
|
225
|
-
ax = fig.add_subplot("Cos")
|
|
262
|
+
with figure(fig_size=(14,6)) as fig:
|
|
263
|
+
ax = fig.add_subplot("Sin")
|
|
264
|
+
store = fig.store()
|
|
265
|
+
# draw 10 lines in the first sub plot, and add a legend
|
|
266
|
+
for i in range(10):
|
|
267
|
+
y = np.sin(x/(i+1))
|
|
268
|
+
ax.plot( x, y, color=color_base(i), label=f"f(x/{i+1})" )
|
|
269
|
+
ax.legend(loc="lower right")
|
|
226
270
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
fig.render()
|
|
271
|
+
# draw 10 lines in the second sub plot.
|
|
272
|
+
# use the same colors for the same scaling of 'x'
|
|
273
|
+
ax = fig.add_subplot("Cos")
|
|
231
274
|
|
|
232
|
-
# animiate, again with the same colors
|
|
233
|
-
for p in np.linspace(0.,4.,11,endpoint=False):
|
|
234
|
-
time.sleep(0.1)
|
|
235
|
-
store.clear() # alias for 'remove'
|
|
236
275
|
for i in range(10):
|
|
237
|
-
z = np.cos(
|
|
276
|
+
z = np.cos(x/(i+1))
|
|
238
277
|
store += ax.plot( x, z, color=color_base(i) )
|
|
239
278
|
fig.render()
|
|
240
279
|
|
|
241
|
-
|
|
242
|
-
|
|
280
|
+
# animiate, again with the same colors
|
|
281
|
+
for p in np.linspace(0.,4.,11,endpoint=False):
|
|
282
|
+
time.sleep(0.1)
|
|
283
|
+
store.clear() # alias for 'remove'
|
|
284
|
+
for i in range(10):
|
|
285
|
+
z = np.cos((x+p)/(i+1))
|
|
286
|
+
store += ax.plot( x, z, color=color_base(i) )
|
|
287
|
+
fig.render()
|
|
288
|
+
|
|
243
289
|
.. image:: /_static/colors.gif
|
|
244
290
|
|
|
245
|
-
Here is a view of the first 20 colors of the four supported maps
|
|
291
|
+
Here is a view of the first 20 colors of the four supported maps, computed with::
|
|
246
292
|
|
|
293
|
+
%matplotlib inline
|
|
294
|
+
from cdxcore.dynaplot import figure, color_names, color
|
|
295
|
+
import numpy as np
|
|
296
|
+
x = np.linspace(-2.*np.pi,+2*np.pi,101)
|
|
297
|
+
N = 20
|
|
298
|
+
with figure(f"Color tables up to #{N}", figsize=(20,15), columns=2) as fig:
|
|
299
|
+
for color_name in color_names:
|
|
300
|
+
ax = fig.add_subplot(color_name)
|
|
301
|
+
for i in range(N):
|
|
302
|
+
r =1./(i+1)
|
|
303
|
+
ax.plot( x, np.sin(x*r), color
|
|
304
|
+
|
|
247
305
|
.. image:: /_static/colormap.gif
|
|
248
306
|
|
|
249
307
|
The classes :class:`cdxcore.dynaplot.colors_css4`, :class:`cdxcore.dynaplot.colors_base`, :class:`cdxcore.dynaplot.colors_tableau`, :class:`cdxcore.dynaplot.colors_xkcd`
|
|
250
308
|
are generators for the same colors.
|
|
251
309
|
|
|
310
|
+
|
|
252
311
|
Known Issues
|
|
253
312
|
^^^^^^^^^^^^
|
|
254
313
|
|
|
255
|
-
Some users reported that the package does not
|
|
314
|
+
Some users reported that the package does not update figures consistently in some versions of Jupyter, in particular with VSCode.
|
|
256
315
|
In this case, please try changing the ``draw_mode`` parameter when calling :func:`cdxcore.dynaplot.figure`.
|
|
257
316
|
|
|
258
317
|
Import
|
|
@@ -273,6 +332,7 @@ from IPython import display
|
|
|
273
332
|
import io as io
|
|
274
333
|
import gc as gc
|
|
275
334
|
import types as types
|
|
335
|
+
import numpy as np
|
|
276
336
|
from collections.abc import Collection
|
|
277
337
|
from .deferred import Deferred
|
|
278
338
|
from .util import verify, warn
|
|
@@ -288,20 +348,18 @@ class MODE:
|
|
|
288
348
|
""" Call :func:`IPython.display.display`. """
|
|
289
349
|
|
|
290
350
|
CANVAS_IDLE = 0x02
|
|
291
|
-
""" Call
|
|
351
|
+
""" Call `matplotlib.pyplot.figure.canvas.draw_idle <https://matplotlib.org/stable/api/backend_bases_api.html#matplotlib.backend_bases.FigureCanvasBase.draw_idle>`__ """
|
|
292
352
|
|
|
293
353
|
CANVAS_DRAW = 0x04
|
|
294
|
-
""" Call
|
|
354
|
+
""" Call `matplotlib.pyplot.figure.canvas.draw_idle <https://matplotlib.org/stable/api/backend_bases_api.html#matplotlib.backend_bases.FigureCanvasBase.draw>`__ """
|
|
295
355
|
|
|
296
356
|
PLT_SHOW = 0x80
|
|
297
357
|
""" Call :func:`matplotlib.pyplot.show`. """
|
|
298
358
|
|
|
299
|
-
|
|
300
|
-
""" Setting which works for Jupyter lab as far as we can tell. """
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
""" Setting which works for VSCode it seems. Feedback welome. """
|
|
304
|
-
|
|
359
|
+
DEFAULT = HDISPLAY
|
|
360
|
+
""" Setting which works for Jupyter lab and VSCode with ``%matplotlib inline`` as far as we can tell. """
|
|
361
|
+
|
|
362
|
+
|
|
305
363
|
class _DynaDeferred( Deferred ):
|
|
306
364
|
""" Internal class which implements the required deferral method """
|
|
307
365
|
__setitem__ = Deferred._deferred_handle("__setitem__", num_args=2, fmt="{parent}[{arg0}]={arg1}")
|
|
@@ -315,9 +373,9 @@ class _DynaDeferred( Deferred ):
|
|
|
315
373
|
|
|
316
374
|
class DynaAx(_DynaDeferred):
|
|
317
375
|
"""
|
|
318
|
-
Deferred wrapper around a :class:`matplotlib.
|
|
376
|
+
Deferred wrapper around a :class:`matplotlib.axes.Axes` objects returned by :meth:`cdxcore.dynaplot.DynaFig.add_subplot` or similar.
|
|
319
377
|
|
|
320
|
-
*You should not need to know that this object is not actually a* :class:`matplotlib.
|
|
378
|
+
*You should not need to know that this object is not actually a* :class:`matplotlib.axes.Axes`.
|
|
321
379
|
*If you receive error messages which you do not understand, please contact the authors of this
|
|
322
380
|
module.*
|
|
323
381
|
"""
|
|
@@ -439,17 +497,17 @@ class DynaAx(_DynaDeferred):
|
|
|
439
497
|
self.fig_list.remove(self)
|
|
440
498
|
self.ax.remove()
|
|
441
499
|
self.ax = None
|
|
442
|
-
gc.collect()
|
|
500
|
+
#gc.collect()
|
|
443
501
|
|
|
444
502
|
# automatic limit handling
|
|
445
503
|
# -------------------------
|
|
446
504
|
|
|
447
505
|
def plot(self, *args, scalex=True, scaley=True, data=None, **kwargs ):
|
|
448
506
|
"""
|
|
449
|
-
Wrapper around :
|
|
507
|
+
Wrapper around :meth:`matplotlib.axes.Axes.plot`.
|
|
450
508
|
|
|
451
509
|
This function wrapper does not support the ``data`` interface
|
|
452
|
-
of :
|
|
510
|
+
of :meth:`matplotlib.axes.Axes.plot`.
|
|
453
511
|
|
|
454
512
|
If automatic limits are not used, this is a wrapper with deferred pass-through.
|
|
455
513
|
If automatic limits are used, then this function will update
|
|
@@ -458,7 +516,7 @@ class DynaAx(_DynaDeferred):
|
|
|
458
516
|
Parameters
|
|
459
517
|
----------
|
|
460
518
|
args, scalex, scaley, data, kwargs : ...
|
|
461
|
-
See :
|
|
519
|
+
See :meth:`matplotlib.axes.plot`.
|
|
462
520
|
|
|
463
521
|
Returns
|
|
464
522
|
-------
|
|
@@ -568,53 +626,59 @@ class _DynaGridSpec(_DynaDeferred):
|
|
|
568
626
|
|
|
569
627
|
class DynaFig(_DynaDeferred):
|
|
570
628
|
"""
|
|
571
|
-
Deferred wrapper around :class:`matplotlib.
|
|
629
|
+
Deferred wrapper around :class:`matplotlib.figure.Figure`.
|
|
572
630
|
|
|
573
|
-
|
|
631
|
+
Provides a simple :meth:`cdxcore.dynaplot.DynaFig.add_subplot` without the need to pre-specify axes positions
|
|
574
632
|
as is common for :mod:`matplotlib`.
|
|
575
633
|
|
|
576
|
-
|
|
634
|
+
Construct elements of this class with :func:`cdxcore.dynaplot.figure`.
|
|
577
635
|
"""
|
|
578
636
|
|
|
579
|
-
def __init__(self, title : str = None, *,
|
|
637
|
+
def __init__(self, title : str|None = None, *,
|
|
580
638
|
row_size : int = 5,
|
|
581
639
|
col_size : int = 4,
|
|
582
|
-
fig_size : tuple[int] = None,
|
|
640
|
+
fig_size : tuple[int]|None = None,
|
|
583
641
|
columns : int = 5,
|
|
584
642
|
tight : bool = True,
|
|
585
|
-
draw_mode: int = MODE.
|
|
643
|
+
draw_mode: int = MODE.DEFAULT,
|
|
586
644
|
**fig_kwargs ):
|
|
587
645
|
"""
|
|
588
|
-
|
|
646
|
+
__init__
|
|
589
647
|
"""
|
|
590
|
-
self.hdisplay = None
|
|
591
|
-
self.axes = [] #: List of axes. Until; :meth:`cdxcore.dynaplot.DynaFig.render` is called, these are :class:`cdxcore.dynaplot.DynaAx` objects;
|
|
592
|
-
#: afterwards, these are :class:`matplotlib.pyplot.axis` objects.
|
|
593
|
-
self.grid_specs = []
|
|
594
|
-
self.fig = None
|
|
595
|
-
self.row_size = int(row_size)
|
|
596
|
-
self.col_size = int(col_size)
|
|
597
|
-
self.cols = int(columns)
|
|
598
|
-
self.tight = bool(tight)
|
|
599
|
-
self.tight_para = None
|
|
600
|
-
self.fig_kwargs = dict(fig_kwargs)
|
|
601
|
-
if self.tight:
|
|
602
|
-
self.fig_kwargs['tight_layout'] = True
|
|
603
|
-
verify( self.row_size > 0 and self.col_size > 0 and self.cols > 0, "Invalid input.", exception=ValueError)
|
|
604
|
-
self.this_row = 0
|
|
605
|
-
self.this_col = 0
|
|
606
|
-
self.max_col = 0
|
|
607
|
-
self.fig_title = title
|
|
608
|
-
self.closed = False
|
|
609
|
-
self.draw_mode = draw_mode
|
|
610
|
-
|
|
611
|
-
verify( not 'cols' in fig_kwargs, "Unknown keyword 'cols'. Did you mean 'columns'?", exception=ValueError)
|
|
612
|
-
|
|
613
648
|
if not fig_size is None:
|
|
614
649
|
verify( not 'figsize' in fig_kwargs, "Cannot specify both `figsize` and `fig_size`", exception=ValueError)
|
|
615
650
|
fig_kwargs['figsize'] = fig_size
|
|
651
|
+
|
|
652
|
+
self._hdisplay = None
|
|
653
|
+
self._axes = [] #:
|
|
654
|
+
#:
|
|
655
|
+
self._grid_specs = []
|
|
656
|
+
self._fig = None
|
|
657
|
+
self._row_size = int(row_size)
|
|
658
|
+
self._col_size = int(col_size)
|
|
659
|
+
self._cols = int(columns)
|
|
660
|
+
self._tight = bool(tight)
|
|
661
|
+
self._tight_para = None
|
|
662
|
+
self._fig_kwargs = dict(fig_kwargs)
|
|
663
|
+
if self._tight:
|
|
664
|
+
self._fig_kwargs['tight_layout'] = True
|
|
665
|
+
verify( self._row_size > 0 and self._col_size > 0 and self._cols > 0, "Invalid input.", exception=ValueError)
|
|
666
|
+
self._this_row = 0
|
|
667
|
+
self._this_col = 0
|
|
668
|
+
self._max_col = 0
|
|
669
|
+
self._fig_title = title
|
|
670
|
+
self._closed = False
|
|
671
|
+
self._enter_count = 0
|
|
672
|
+
self.draw_mode = draw_mode
|
|
673
|
+
#: A combination of :class:`cdxcore.dynaplot.MODE` flags on how to draw plots
|
|
674
|
+
#: once they were rendered. The required function call differs by IPython platform.
|
|
675
|
+
#: The default, :attr:`cdxcore.dynaplot.MODE.DEFAULT` draws well on Jupyter notebooks
|
|
676
|
+
#: and VSCode if :func:`cdxcore.dynaplot.dynamic_backend` was called
|
|
677
|
+
#: (which sets ``%matplotlib inline``).
|
|
678
|
+
|
|
679
|
+
verify( not 'cols' in fig_kwargs, "Unknown keyword 'cols'. Did you mean 'columns'?", exception=ValueError)
|
|
616
680
|
|
|
617
|
-
dyna_title = title if len(title) <= 20 else ( title[:17] + "..." ) if not title is None else None
|
|
681
|
+
dyna_title = ( title if len(title) <= 20 else ( title[:17] + "..." ) ) if not title is None else None
|
|
618
682
|
_DynaDeferred.__init__(self, f"figure('{dyna_title}')" if not title is None else "figure()" )
|
|
619
683
|
|
|
620
684
|
def __str__(self):
|
|
@@ -624,10 +688,45 @@ class DynaFig(_DynaDeferred):
|
|
|
624
688
|
""" Ensure the figure is closed """
|
|
625
689
|
self.close()
|
|
626
690
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
691
|
+
# properties
|
|
692
|
+
# ----------
|
|
693
|
+
|
|
694
|
+
@property
|
|
695
|
+
def axes(self) -> list[DynaAx]:
|
|
696
|
+
"""
|
|
697
|
+
List of axes. Until :meth:`cdxcore.dynaplot.DynaFig.render` is called, these are :class:`cdxcore.dynaplot.DynaAx` objects;
|
|
698
|
+
afterwards, these are :class:`matplotlib.axes.Axes` objects.
|
|
699
|
+
"""
|
|
700
|
+
return self._axes
|
|
701
|
+
|
|
702
|
+
@property
|
|
703
|
+
def fig(self) -> plt.Figure|None:
|
|
704
|
+
"""
|
|
705
|
+
Returns the figure or ``None`` if it was not yet rendered.
|
|
706
|
+
"""
|
|
707
|
+
return self._fig
|
|
708
|
+
|
|
709
|
+
@property
|
|
710
|
+
def hdisplay(self) -> display.DisplayHandle|None:
|
|
711
|
+
"""
|
|
712
|
+
Returns the :class:`IPython.display.DisplayHandle` for the current display, if
|
|
713
|
+
``MODE.HDISPLAY`` was used for ``draw_mode`` when the figure was constructed, and if the figure
|
|
714
|
+
was rendered yet. Otherwise returns ``None``.
|
|
715
|
+
"""
|
|
716
|
+
return self._hdisplay
|
|
717
|
+
|
|
718
|
+
@property
|
|
719
|
+
def is_closed(self) -> bool:
|
|
720
|
+
""" Returrns whether the figure was closed """
|
|
721
|
+
return self._closed
|
|
722
|
+
|
|
723
|
+
# functionality
|
|
724
|
+
# -------------
|
|
725
|
+
|
|
726
|
+
def add_subplot(self, title : str|None = None, *,
|
|
727
|
+
new_row : bool|None = None,
|
|
728
|
+
spec_pos : type|None = None,
|
|
729
|
+
projection: str|None = None,
|
|
631
730
|
**kwargs) -> DynaAx:
|
|
632
731
|
"""
|
|
633
732
|
Adds a subplot.
|
|
@@ -640,29 +739,35 @@ class DynaFig(_DynaDeferred):
|
|
|
640
739
|
|
|
641
740
|
This function returns a wrapper which defers the creation of the actual sub plot until
|
|
642
741
|
:meth:`cdxcore.dynaplot.DynaFig.render` or :meth:`cdxcore.dynaplot.DynaFig.close` is called.
|
|
742
|
+
|
|
643
743
|
Thus this function cannot be called after :meth:`cdxcore.dynaplot.DynaFig.render` was called as then the geometry of the plots
|
|
644
|
-
is set. Use :meth:`cdxcore.dynaplot.DynaFig.add_axes` to draw plots
|
|
744
|
+
is set. Use :meth:`cdxcore.dynaplot.DynaFig.add_axes` to draw plots at any time/.
|
|
645
745
|
|
|
646
746
|
Parameters
|
|
647
747
|
----------
|
|
648
|
-
title : str,
|
|
748
|
+
title : str | None, default ``None``
|
|
649
749
|
Optional title for the plot.
|
|
650
750
|
|
|
651
|
-
new_row : bool,
|
|
751
|
+
new_row : bool | None, default ``None``
|
|
652
752
|
Whether to force a new row and place this polt in the first column. Default is ``False``.
|
|
653
753
|
|
|
654
|
-
spec_pos :
|
|
655
|
-
Grid spec position
|
|
754
|
+
spec_pos : grid spec | None, default ``None``
|
|
755
|
+
Grid spec position from :meth:`cdxcore.dynaplot.DynaFig.add_gridspec`, or ``None``.
|
|
656
756
|
|
|
657
|
-
projection : str,
|
|
757
|
+
projection : str | None, default ``None``
|
|
658
758
|
What ``projection`` to use. The default ``None`` matches the default choice for
|
|
659
759
|
:meth:`matplotlib.figure.Figure.add_subplot`.
|
|
660
760
|
|
|
661
761
|
kwargs : dict
|
|
662
762
|
other arguments to be passed to matplotlib's :meth:`matplotlib.figure.Figure.add_subplot`.
|
|
763
|
+
|
|
764
|
+
Returns
|
|
765
|
+
-------
|
|
766
|
+
Axis : :class:`cdxcore.dynaplot.DynaAx`
|
|
767
|
+
A wrapper around an matplotlib axis.
|
|
663
768
|
"""
|
|
664
|
-
verify( not self.
|
|
665
|
-
verify( self.
|
|
769
|
+
verify( not self._closed, "Cannot call add_subplot() after close() was called")
|
|
770
|
+
verify( self._fig is None, "Cannot call add_subplot() after render() was called. Use add_axes() instead")
|
|
666
771
|
|
|
667
772
|
# backward compatibility:
|
|
668
773
|
# previous versions has "new_row" first.
|
|
@@ -672,7 +777,7 @@ class DynaFig(_DynaDeferred):
|
|
|
672
777
|
if not spec_pos is None:
|
|
673
778
|
assert new_row is None, ("Cannot specify 'new_row' when 'spec_pos' is specified")
|
|
674
779
|
ax = DynaAx( fig_id=hash(self),
|
|
675
|
-
fig_list=self.
|
|
780
|
+
fig_list=self._axes,
|
|
676
781
|
row=None,
|
|
677
782
|
col=None,
|
|
678
783
|
title=title,
|
|
@@ -683,30 +788,49 @@ class DynaFig(_DynaDeferred):
|
|
|
683
788
|
|
|
684
789
|
else:
|
|
685
790
|
new_row = bool(new_row) if not new_row is None else False
|
|
686
|
-
if (self.
|
|
687
|
-
self.
|
|
688
|
-
self.
|
|
689
|
-
if self.
|
|
690
|
-
self.
|
|
791
|
+
if (self._this_col >= self._cols) or ( new_row and not self._this_col == 0 ):
|
|
792
|
+
self._this_col = 0
|
|
793
|
+
self._this_row = self._this_row + 1
|
|
794
|
+
if self._max_col < self._this_col:
|
|
795
|
+
self._max_col = self._this_col
|
|
691
796
|
ax = DynaAx( fig_id=hash(self),
|
|
692
|
-
fig_list=self.
|
|
693
|
-
row=self.
|
|
694
|
-
col=self.
|
|
797
|
+
fig_list=self._axes,
|
|
798
|
+
row=self._this_row,
|
|
799
|
+
col=self._this_col,
|
|
695
800
|
spec_pos=None,
|
|
696
801
|
rect=None,
|
|
697
802
|
title=title,
|
|
698
803
|
projection=projection,
|
|
699
804
|
kwargs=dict(kwargs) )
|
|
700
|
-
self.
|
|
701
|
-
assert ax in self.
|
|
805
|
+
self._this_col += 1
|
|
806
|
+
assert ax in self._axes
|
|
702
807
|
return ax
|
|
703
808
|
|
|
704
809
|
add_plot = add_subplot
|
|
810
|
+
|
|
811
|
+
def add_subplots( self, titles : list[str]|int ) -> tuple[DynaAx]:
|
|
812
|
+
"""
|
|
813
|
+
Generate a number of sub-plots in one function call.
|
|
814
|
+
|
|
815
|
+
Parameters
|
|
816
|
+
----------
|
|
817
|
+
titles : list[str] | int
|
|
818
|
+
Either a list of plot titles, or a number.
|
|
819
|
+
|
|
820
|
+
Returns
|
|
821
|
+
-------
|
|
822
|
+
Sub-plots: tuple
|
|
823
|
+
A tuple of sub-plots, one for each ``title``.
|
|
824
|
+
"""
|
|
825
|
+
if isinstance( titles, int ):
|
|
826
|
+
return tuple( self.add_subplot() for _ in range(titles) )
|
|
827
|
+
else:
|
|
828
|
+
return tuple( self.add_subplot(title) for title in titles )
|
|
705
829
|
|
|
706
830
|
def add_axes( self,
|
|
707
831
|
rect : tuple,
|
|
708
832
|
title : str|None = None, *,
|
|
709
|
-
projection: str = None,
|
|
833
|
+
projection: str|None = None,
|
|
710
834
|
**kwargs ) -> DynaAx:
|
|
711
835
|
"""
|
|
712
836
|
Add a freely placed sub plot.
|
|
@@ -726,22 +850,31 @@ class DynaFig(_DynaDeferred):
|
|
|
726
850
|
The dimensions (left, bottom, width, height) of the new plot.
|
|
727
851
|
All quantities are in fractions of figure width and height.
|
|
728
852
|
|
|
729
|
-
title : str,
|
|
853
|
+
title : str | None, default ``None``
|
|
730
854
|
Title for the plot, or ``None`` for no plot.
|
|
731
855
|
|
|
732
|
-
projection : str,
|
|
856
|
+
projection : str | None, default ``None``
|
|
733
857
|
What ``projection`` to use. The default ``None`` matches the default choice for
|
|
734
858
|
:meth:`matplotlib.figure.Figure.add_axes`
|
|
735
|
-
|
|
859
|
+
|
|
736
860
|
args, kwargs :
|
|
737
861
|
keyword arguments to be passed to :meth:`matplotlib.figure.Figure.add_axes`.
|
|
738
|
-
"""
|
|
739
|
-
verify( not self.closed, "Cannot call add_subplot() after close() was called")
|
|
740
862
|
|
|
863
|
+
Returns
|
|
864
|
+
-------
|
|
865
|
+
Axis : :class:`cdxcore.dynaplot.DynaAx`
|
|
866
|
+
A wrapper around an matplotlib axis.
|
|
867
|
+
"""
|
|
868
|
+
verify( not self._closed, "Cannot call add_subplot() after close() was called")
|
|
869
|
+
verify( not isinstance(rect,str), "'rect' is a string ... did you mix up the order of 'title' and 'rect'?")
|
|
741
870
|
title = str(title) if not title is None else None
|
|
871
|
+
if not isinstance(rect, tuple):
|
|
872
|
+
rect = tuple(rect)
|
|
873
|
+
verify( len(rect)==4, lambda:f"'rect' must be a tuple of length 4. Found '{rect}'")
|
|
874
|
+
verify( np.isfinite(rect).all(), lambda:f"'rect' has infinite elements: {rect}")
|
|
742
875
|
|
|
743
876
|
ax = DynaAx( fig_id=hash(self),
|
|
744
|
-
fig_list=self.
|
|
877
|
+
fig_list=self._axes,
|
|
745
878
|
row=None,
|
|
746
879
|
col=None,
|
|
747
880
|
title=title,
|
|
@@ -749,17 +882,17 @@ class DynaFig(_DynaDeferred):
|
|
|
749
882
|
rect=rect,
|
|
750
883
|
projection=projection,
|
|
751
884
|
kwargs=dict(kwargs) )
|
|
752
|
-
assert ax in self.
|
|
753
|
-
if not self.
|
|
754
|
-
ax._initialize( self.
|
|
885
|
+
assert ax in self._axes
|
|
886
|
+
if not self._fig is None:
|
|
887
|
+
ax._initialize( self._fig, rows=None, cols=None )
|
|
755
888
|
return ax
|
|
756
889
|
|
|
757
890
|
def add_gridspec(self, ncols=1, nrows=1, **kwargs):
|
|
758
891
|
"""
|
|
759
892
|
Wrapper for :meth:`matplotlib.figure.Figure.add_gridspec`, returning a defered ``GridSpec``.
|
|
760
893
|
"""
|
|
761
|
-
grid = _DynaGridSpec( ncols=ncols, nrows=nrows, cnt=len(self.
|
|
762
|
-
self.
|
|
894
|
+
grid = _DynaGridSpec( ncols=ncols, nrows=nrows, cnt=len(self._grid_specs), kwargs=kwargs )
|
|
895
|
+
self._grid_specs.append( grid )
|
|
763
896
|
return grid
|
|
764
897
|
|
|
765
898
|
def next_row(self):
|
|
@@ -769,11 +902,11 @@ class DynaFig(_DynaDeferred):
|
|
|
769
902
|
The next plot generated by :meth:`cdxcore.dynaplot.DynaFig.add_subplot` will
|
|
770
903
|
appears in the first column of the next row.
|
|
771
904
|
"""
|
|
772
|
-
verify( self.
|
|
773
|
-
if self.
|
|
905
|
+
verify( self._fig is None, "Cannot call next_row() after render() was called")
|
|
906
|
+
if self._this_col == 0:
|
|
774
907
|
return
|
|
775
|
-
self.
|
|
776
|
-
self.
|
|
908
|
+
self._this_col = 0
|
|
909
|
+
self._this_row = self._this_row + 1
|
|
777
910
|
|
|
778
911
|
def render(self, draw : bool = True ):
|
|
779
912
|
"""
|
|
@@ -786,47 +919,47 @@ class DynaFig(_DynaDeferred):
|
|
|
786
919
|
|
|
787
920
|
Parameters
|
|
788
921
|
----------
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
922
|
+
draw : bool, default ``True``
|
|
923
|
+
If False, then the figure is created, but not drawn.
|
|
924
|
+
You usually use ``False`` when planning to use
|
|
925
|
+
:func:`cdxcore.dynaplot.DynaFig.savefig` or :func:`cdxcore.dynaplot.DynaFig.to_bytes`.
|
|
793
926
|
"""
|
|
794
|
-
verify( not self.
|
|
795
|
-
if len(self.
|
|
927
|
+
verify( not self._closed, "Cannot call render() after close() was called")
|
|
928
|
+
if len(self._axes) == 0:
|
|
796
929
|
return
|
|
797
|
-
if self.
|
|
930
|
+
if self._fig is None:
|
|
798
931
|
# create figure
|
|
799
|
-
if not 'figsize' in self.
|
|
800
|
-
self.
|
|
801
|
-
self.
|
|
802
|
-
if self.
|
|
803
|
-
self.
|
|
804
|
-
self.
|
|
805
|
-
if not self.
|
|
806
|
-
self.
|
|
932
|
+
if not 'figsize' in self._fig_kwargs:
|
|
933
|
+
self._fig_kwargs['figsize'] = ( self._col_size*(self._max_col+1), self._row_size*(self._this_row+1))
|
|
934
|
+
self._fig = plt.figure( **self._fig_kwargs )
|
|
935
|
+
if self._tight:
|
|
936
|
+
self._fig.tight_layout()
|
|
937
|
+
self._fig.set_tight_layout(True)
|
|
938
|
+
if not self._fig_title is None:
|
|
939
|
+
self._fig.suptitle( self._fig_title )
|
|
807
940
|
# create all grid specs
|
|
808
|
-
for gs in self.
|
|
809
|
-
gs._initialize( self.
|
|
941
|
+
for gs in self._grid_specs:
|
|
942
|
+
gs._initialize( self._fig )
|
|
810
943
|
# create all axes
|
|
811
|
-
for ax in self.
|
|
812
|
-
ax._initialize( self.
|
|
944
|
+
for ax in self._axes:
|
|
945
|
+
ax._initialize( self._fig, rows=self._this_row+1, cols=self._max_col+1 )
|
|
813
946
|
# execute all deferred calls to fig()
|
|
814
|
-
self.deferred_resolve( self.
|
|
947
|
+
self.deferred_resolve( self._fig )
|
|
815
948
|
|
|
816
949
|
if not draw:
|
|
817
950
|
return
|
|
818
951
|
if self.draw_mode & MODE.HDISPLAY:
|
|
819
|
-
if self.
|
|
820
|
-
self.
|
|
821
|
-
verify( not self.
|
|
822
|
-
self.
|
|
952
|
+
if self._hdisplay is None:
|
|
953
|
+
self._hdisplay = display.display(display_id=True)
|
|
954
|
+
verify( not self._hdisplay is None, "Could not optain current IPython display ID from IPython.display.display(). Set DynaFig.MODE = 'canvas' for an alternative mode")
|
|
955
|
+
self._hdisplay.update(self._fig)
|
|
823
956
|
if self.draw_mode & MODE.CANVAS_IDLE:
|
|
824
|
-
self.
|
|
957
|
+
self._fig.canvas.draw_idle()
|
|
825
958
|
if self.draw_mode & MODE.CANVAS_DRAW:
|
|
826
|
-
self.
|
|
959
|
+
self._fig.canvas.draw()
|
|
827
960
|
if self.draw_mode & MODE.PLT_SHOW:
|
|
828
961
|
plt.show()
|
|
829
|
-
gc.collect() # for some unknown reason this is required in VSCode
|
|
962
|
+
#gc.collect() # for some unknown reason this is required in VSCode
|
|
830
963
|
|
|
831
964
|
def savefig(self, fname : str,
|
|
832
965
|
silent_close : bool = True,
|
|
@@ -834,26 +967,27 @@ class DynaFig(_DynaDeferred):
|
|
|
834
967
|
"""
|
|
835
968
|
Saves the figure to a file.
|
|
836
969
|
|
|
837
|
-
Wrapper around :func:`matplotlib.pyplot.savefig`. Essentially, this function
|
|
970
|
+
Wrapper around :func:`matplotlib.pyplot.savefig`. Essentially, `this function <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.savefig.html>`__
|
|
971
|
+
writes the figure to a file
|
|
838
972
|
rather than displaying itl.
|
|
839
973
|
|
|
840
974
|
Parameters
|
|
841
975
|
----------
|
|
842
|
-
|
|
843
|
-
|
|
976
|
+
fname : str
|
|
977
|
+
`filename or file-like object <https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.savefig.html>`__
|
|
844
978
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
979
|
+
silent_close : bool, default ``True``
|
|
980
|
+
If ``True`` (the default), call :meth:`cdxcore.dynaplot.DynaFig.close` once the figure was saved to disk.
|
|
981
|
+
Unless the figure was drawn before, this means that the figure will not be displayed in jupyter, and
|
|
982
|
+
subsequent activity is blocked.
|
|
983
|
+
|
|
984
|
+
kwargs : dict
|
|
985
|
+
These arguments will be passed to :meth:`matplotlib.pyplot.savefig`.
|
|
852
986
|
"""
|
|
853
|
-
verify( not self.
|
|
854
|
-
if self.
|
|
987
|
+
verify( not self._closed, "Cannot call savefig() after close() was called")
|
|
988
|
+
if self._fig is None:
|
|
855
989
|
self.render(draw=False)
|
|
856
|
-
self.
|
|
990
|
+
self._fig.savefig( fname, **kwargs )
|
|
857
991
|
if silent_close:
|
|
858
992
|
self.close(render=False)
|
|
859
993
|
|
|
@@ -870,21 +1004,21 @@ class DynaFig(_DynaDeferred):
|
|
|
870
1004
|
|
|
871
1005
|
Parameters
|
|
872
1006
|
----------
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
1007
|
+
silent_close : bool, default ``True``
|
|
1008
|
+
If ``True``, call :meth:`cdxcore.dynaplot.DynaFig.close` after this genersating the byte streanm.
|
|
1009
|
+
Unless the figure was drawn before, this means that the figure will not be displayed in jupyter, and
|
|
1010
|
+
subsequent activity is blocked.
|
|
877
1011
|
|
|
878
1012
|
Returns
|
|
879
1013
|
-------
|
|
880
|
-
|
|
881
|
-
|
|
1014
|
+
image : bytes
|
|
1015
|
+
Buyte stream of the image.
|
|
882
1016
|
"""
|
|
883
|
-
verify( not self.
|
|
1017
|
+
verify( not self._closed, "Cannot call savefig() after close() was called")
|
|
884
1018
|
img_buf = io.BytesIO()
|
|
885
|
-
if self.
|
|
1019
|
+
if self._fig is None:
|
|
886
1020
|
self.render(draw=False)
|
|
887
|
-
self.
|
|
1021
|
+
self._fig.savefig( img_buf )
|
|
888
1022
|
if silent_close:
|
|
889
1023
|
self.close(render=False)
|
|
890
1024
|
data = img_buf.getvalue()
|
|
@@ -893,7 +1027,8 @@ class DynaFig(_DynaDeferred):
|
|
|
893
1027
|
|
|
894
1028
|
@staticmethod
|
|
895
1029
|
def store():
|
|
896
|
-
""" Create a FigStore
|
|
1030
|
+
""" Create a :class:`cdxcore.dynaplot.FigStore`. Such a store allows managing graphical elements (artists) dynamically. See the examples
|
|
1031
|
+
in the introduction. """
|
|
897
1032
|
return FigStore()
|
|
898
1033
|
|
|
899
1034
|
def close(self, render : bool = True,
|
|
@@ -902,55 +1037,50 @@ class DynaFig(_DynaDeferred):
|
|
|
902
1037
|
Closes the figure.
|
|
903
1038
|
|
|
904
1039
|
Call this to avoid a duplicate in jupyter output cells.
|
|
905
|
-
By dault this function will call :meth:`cdxcore.dynaplot.DynaFig
|
|
1040
|
+
By dault this function will call :meth:`cdxcore.dynaplot.DynaFig.render` to draw the figure, and then close it.
|
|
906
1041
|
|
|
907
1042
|
Parameters
|
|
908
1043
|
----------
|
|
909
|
-
render : bool,
|
|
910
|
-
If
|
|
911
|
-
clear :
|
|
1044
|
+
render : bool, default ``True``
|
|
1045
|
+
If ``True``, the default, this function will call :meth:`cdxcore.dynaplot.DynaFig.render` and therefore renders the figure before closing the figure.
|
|
1046
|
+
clear : bool, default ``False``
|
|
912
1047
|
If ``True``, all axes will be cleared. *This is experimental.* The default is ``False``.
|
|
913
1048
|
"""
|
|
914
|
-
if not self.
|
|
1049
|
+
if not self._closed:
|
|
915
1050
|
# magic wand to avoid printing an empty figure message
|
|
916
1051
|
if clear:
|
|
917
|
-
if not self.
|
|
1052
|
+
if not self._fig is None:
|
|
918
1053
|
def repr_magic(self):
|
|
919
|
-
return type(self)._repr_html_(self) if len(self.
|
|
920
|
-
self.
|
|
921
|
-
self.delaxes( self.
|
|
1054
|
+
return type(self)._repr_html_(self) if len(self._axes) > 0 else "</HTML>"
|
|
1055
|
+
self._fig._repr_html_ = types.MethodType(repr_magic,self._fig)
|
|
1056
|
+
self.delaxes( self._axes, render=render )
|
|
922
1057
|
elif render:
|
|
923
1058
|
self.render(draw=True)
|
|
924
|
-
if not self.
|
|
925
|
-
plt.close(self.
|
|
926
|
-
self.
|
|
927
|
-
self.
|
|
928
|
-
self.
|
|
1059
|
+
if not self._fig is None:
|
|
1060
|
+
plt.close(self._fig)
|
|
1061
|
+
self._fig = None
|
|
1062
|
+
self._closed = True
|
|
1063
|
+
self._hdisplay = None
|
|
929
1064
|
gc.collect()
|
|
930
1065
|
|
|
931
|
-
def get_axes(self) -> list:
|
|
932
|
-
""" Equivalent to ``self.axes`` """
|
|
933
|
-
verify( not self.closed, "Cannot call render() after close() was called")
|
|
934
|
-
return self.axes
|
|
935
|
-
|
|
936
1066
|
def remove_all_axes(self, *, render : bool = False):
|
|
937
|
-
"""
|
|
938
|
-
while len(self.
|
|
939
|
-
self.
|
|
1067
|
+
""" Calls :meth:`cdxcore.dynaplot.DynaAx.remove` for all :attr:`cdxcore.dynaplot.DynaFig.axes` """
|
|
1068
|
+
while len(self._axes) > 0:
|
|
1069
|
+
self._axes[0].remove()
|
|
940
1070
|
if render:
|
|
941
1071
|
self.render(draw=True)
|
|
942
1072
|
|
|
943
1073
|
def delaxes( self, ax : DynaAx, *, render : bool = False ):
|
|
944
1074
|
"""
|
|
945
|
-
Equivalent of :
|
|
1075
|
+
Equivalent of :meth:`matplotlib.figure.Figure.delaxes`, but this function can also take a list.
|
|
946
1076
|
"""
|
|
947
|
-
verify( not self.
|
|
1077
|
+
verify( not self._closed, "Cannot call render() after close() was called")
|
|
948
1078
|
if isinstance( ax, Collection ):
|
|
949
1079
|
ax = list(ax)
|
|
950
1080
|
for x in ax:
|
|
951
1081
|
x.remove()
|
|
952
1082
|
else:
|
|
953
|
-
assert ax in self.
|
|
1083
|
+
assert ax in self._axes, ("Cannot delete axes which wasn't created by this figure")
|
|
954
1084
|
ax.remove()
|
|
955
1085
|
if render:
|
|
956
1086
|
self.render()
|
|
@@ -959,24 +1089,28 @@ class DynaFig(_DynaDeferred):
|
|
|
959
1089
|
# -----------------------
|
|
960
1090
|
|
|
961
1091
|
def __enter__(self):
|
|
1092
|
+
self._enter_count += 1
|
|
962
1093
|
return self
|
|
963
1094
|
|
|
964
1095
|
def __exit__(self, *args, **kwargs):
|
|
965
|
-
self.
|
|
1096
|
+
self._enter_count -= 1
|
|
1097
|
+
if self._enter_count <= 0:
|
|
1098
|
+
self.close()
|
|
966
1099
|
return False
|
|
967
1100
|
|
|
968
|
-
def figure( title : str = None, *,
|
|
1101
|
+
def figure( title : str|None = None, *,
|
|
969
1102
|
row_size : int = 5,
|
|
970
1103
|
col_size : int = 4,
|
|
971
|
-
fig_size : tuple[int] = None,
|
|
1104
|
+
fig_size : tuple[int]|None = None,
|
|
972
1105
|
columns : int = 5,
|
|
973
1106
|
tight : bool = True,
|
|
974
|
-
draw_mode: int = MODE.
|
|
1107
|
+
draw_mode: int = MODE.DEFAULT,
|
|
1108
|
+
add_plots: int|list|None = None,
|
|
975
1109
|
**fig_kwargs ):
|
|
976
1110
|
"""
|
|
977
1111
|
Creates a dynamic figure of type :class:`cdxcore.dynaplot.DynaFig`.
|
|
978
1112
|
|
|
979
|
-
By default the ``fig_size`` of the underlying :class:`matplotlib.
|
|
1113
|
+
By default the ``fig_size`` of the underlying :class:`matplotlib.figure.Figure`
|
|
980
1114
|
will be derived from the number of plots vs ``cols``, ``row_size`` and ``col_size``
|
|
981
1115
|
as ``(col_size* (N%col_num), row_size (N//col_num))``.
|
|
982
1116
|
|
|
@@ -988,7 +1122,8 @@ def figure( title : str = None, *,
|
|
|
988
1122
|
|
|
989
1123
|
2) Call :meth:`cdxcore.dynaplot.DynaFig.render` to place those plots.
|
|
990
1124
|
|
|
991
|
-
3) Call :meth:`cdxcore.dynaplot.DynaFig.close` to close the figure and avoid duplicate copies in Jupyter
|
|
1125
|
+
3) Call :meth:`cdxcore.dynaplot.DynaFig.close` to close the figure and avoid duplicate copies in Jupyter; a convient wrapper is to use
|
|
1126
|
+
``with`` to ensure ``close()`` gets called. This pattern is used below.
|
|
992
1127
|
|
|
993
1128
|
**Examples:**
|
|
994
1129
|
|
|
@@ -996,23 +1131,21 @@ def figure( title : str = None, *,
|
|
|
996
1131
|
matplotlib need to pre-specify axes positions::
|
|
997
1132
|
|
|
998
1133
|
from cdxcore.dynaplot import figure
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
fig.render()
|
|
1134
|
+
with dynaplot.figure("Two plots") as fig:
|
|
1135
|
+
ax = fig.add_subplot("1")
|
|
1136
|
+
ax.plot(x,y)
|
|
1137
|
+
ax = fig.add_subplot("2")
|
|
1138
|
+
ax.plot(x,y)
|
|
1005
1139
|
|
|
1006
1140
|
Here is an example using :meth:`matplotlib.figure.Figure.add_gridspec`::
|
|
1007
1141
|
|
|
1008
1142
|
from cdxcore.dynaplot import figure
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
fig.render()
|
|
1143
|
+
with dynaplot.figure() as fig:
|
|
1144
|
+
gs = fig.add_gridspec(2,2)
|
|
1145
|
+
ax = fig.add_subplot( gs[:,0] )
|
|
1146
|
+
ax.plot(x,y)
|
|
1147
|
+
ax = fig.add_subplot( gs[:,1] )
|
|
1148
|
+
ax.plot(x,y)
|
|
1016
1149
|
|
|
1017
1150
|
**Important Functions:**
|
|
1018
1151
|
|
|
@@ -1020,6 +1153,8 @@ def figure( title : str = None, *,
|
|
|
1020
1153
|
defer all other function calls to the figure
|
|
1021
1154
|
object until :meth:`cdxcore.dynaplot.DynaFig.render`
|
|
1022
1155
|
or :meth:`cdxcore.dynaplot.DynaFig.close` are called.
|
|
1156
|
+
Whenever you use ``with`` at the end of the context window :meth:`cdxcore.dynaplot.DynaFig.close`
|
|
1157
|
+
will be called for you.
|
|
1023
1158
|
|
|
1024
1159
|
The following direct members are important for using the framework:
|
|
1025
1160
|
|
|
@@ -1040,39 +1175,43 @@ def figure( title : str = None, *,
|
|
|
1040
1175
|
|
|
1041
1176
|
Parameters
|
|
1042
1177
|
----------
|
|
1043
|
-
title : str,
|
|
1178
|
+
title : str, default ``None``
|
|
1044
1179
|
An optional title which will be passed to :meth:`matplotlib.pyplot.suptitle`.
|
|
1045
1180
|
|
|
1046
|
-
fig_size : tuple[int],
|
|
1047
|
-
By default the ``fig_size`` of the underlying :
|
|
1181
|
+
fig_size : tuple[int] | None, default ``None``
|
|
1182
|
+
By default the ``fig_size`` of the underlying :func:`matplotlib.pyplot.figure`
|
|
1048
1183
|
will be derived from the number of plots vs ``cols``, ``row_size`` and ``col_size``
|
|
1049
1184
|
as ``(col_size* (N%col_num), row_size (N//col_num))``.
|
|
1050
1185
|
|
|
1051
1186
|
If ``fig_size`` is specified then ``row_size`` and ``col_size`` are ignored.
|
|
1052
1187
|
|
|
1053
|
-
row_size : int,
|
|
1188
|
+
row_size : int, default ``5``
|
|
1054
1189
|
Size for a row for matplot lib. Default is 5.
|
|
1055
1190
|
This is ignored if ``fig_size`` is specified.
|
|
1056
1191
|
|
|
1057
|
-
col_size : int,
|
|
1192
|
+
col_size : int, default ``4``
|
|
1058
1193
|
Size for a column for matplot lib. Default is 4.
|
|
1059
1194
|
This is ignored if ``fig_size`` is specified.
|
|
1060
1195
|
|
|
1061
|
-
columns : int,
|
|
1196
|
+
columns : int, default ``5``
|
|
1062
1197
|
How many columns to use when :meth:`cdxcore.dynaplot.DynaFig.add_subplot` is used.
|
|
1063
1198
|
If omitted then the default is 5.
|
|
1064
1199
|
|
|
1065
|
-
tight : bool,
|
|
1200
|
+
tight : bool, default ``True``
|
|
1066
1201
|
Short cut for :meth:`matplotlib.figure.Figure.set_tight_layout`. The default is ``True``.
|
|
1067
1202
|
|
|
1068
1203
|
Note that when ``tight`` is ``True`` and :meth:`cdxcore.dynaplot.DynaFig.add_axes`
|
|
1069
1204
|
is called a :class:`UserWarning` is generated. Turn ``tight`` off to avoid this.
|
|
1070
|
-
|
|
1071
|
-
draw_mode : int,
|
|
1205
|
+
|
|
1206
|
+
draw_mode : int, default ``MODE.DEFAULT``
|
|
1072
1207
|
A combination of :class:`cdxcore.dynaplot.MODE` flags on how to draw plots
|
|
1073
1208
|
once they were rendered. The required function call differs by IPython platform.
|
|
1074
|
-
The default, :attr:`cdxcore.dynaplot.MODE.
|
|
1075
|
-
|
|
1209
|
+
The default, :attr:`cdxcore.dynaplot.MODE.DEFAULT` draws well on Jupyter notebooks
|
|
1210
|
+
and in VSCode if ``%matplotlin inline`` or ``widget`` is used. The latter requires the
|
|
1211
|
+
packages ``ipympl`` and ``ipywidgets``.
|
|
1212
|
+
|
|
1213
|
+
Use the function :func:`cdxcore.dynaplot.dynamic_backend` to
|
|
1214
|
+
set the ``widget`` mode if possible.
|
|
1076
1215
|
|
|
1077
1216
|
fig_kwargs :
|
|
1078
1217
|
Other matplotlib parameters for :func:`matplotlib.pyplot.figure` to
|
|
@@ -1084,7 +1223,7 @@ def figure( title : str = None, *,
|
|
|
1084
1223
|
figure: :class:`cdxcore.dynaplot.DynaFig`
|
|
1085
1224
|
A dynamic figure.
|
|
1086
1225
|
"""
|
|
1087
|
-
|
|
1226
|
+
fig = DynaFig( title=title,
|
|
1088
1227
|
row_size=row_size,
|
|
1089
1228
|
col_size=col_size,
|
|
1090
1229
|
fig_size=fig_size,
|
|
@@ -1093,6 +1232,7 @@ def figure( title : str = None, *,
|
|
|
1093
1232
|
draw_mode=draw_mode,
|
|
1094
1233
|
**fig_kwargs
|
|
1095
1234
|
)
|
|
1235
|
+
return fig
|
|
1096
1236
|
|
|
1097
1237
|
# ----------------------------------------------------------------------------------
|
|
1098
1238
|
# Utility class for animated content
|
|
@@ -1122,7 +1262,10 @@ class FigStore( object ):
|
|
|
1122
1262
|
fig.close()
|
|
1123
1263
|
|
|
1124
1264
|
As in the example above, the most convenient way to create a ``FigStore`` object is
|
|
1125
|
-
to call
|
|
1265
|
+
to call :meth:`cdxcore.dynaplot.DynaFig.store` on your figure.
|
|
1266
|
+
|
|
1267
|
+
The above pattern is not speed or memory optimal. It is more efficient to modify the `artist <https://matplotlib.org/stable/tutorials/artists.html>`__
|
|
1268
|
+
directly. While true, for most applications a somewhat rude cancel+replace is simpler. ``FigStore`` was introduced to facilitiate that.
|
|
1126
1269
|
"""
|
|
1127
1270
|
|
|
1128
1271
|
def __init__(self):
|
|
@@ -1132,7 +1275,7 @@ class FigStore( object ):
|
|
|
1132
1275
|
def add(self, element : Artist):
|
|
1133
1276
|
"""
|
|
1134
1277
|
Add an element to the store.
|
|
1135
|
-
The same operation is available using
|
|
1278
|
+
The same operation is available using ``+=``.
|
|
1136
1279
|
|
|
1137
1280
|
Parameters
|
|
1138
1281
|
----------
|
|
@@ -1142,8 +1285,8 @@ class FigStore( object ):
|
|
|
1142
1285
|
|
|
1143
1286
|
Returns
|
|
1144
1287
|
-------
|
|
1145
|
-
|
|
1146
|
-
This way compound statements ``a.add(x).add(y).add(z)`` work.
|
|
1288
|
+
self : ``Figstore``
|
|
1289
|
+
Returns ``self``. This way compound statements ``a.add(x).add(y).add(z)`` work.
|
|
1147
1290
|
"""
|
|
1148
1291
|
if element is None:
|
|
1149
1292
|
return self
|
|
@@ -1154,7 +1297,7 @@ class FigStore( object ):
|
|
|
1154
1297
|
self._elements.append( element )
|
|
1155
1298
|
return self
|
|
1156
1299
|
if not isinstance(element,Collection):
|
|
1157
|
-
raise ValueError("Cannot add element of type '{type(element).__name__}' as it is not derived from matplotlib.artist.Artist, nor is it a Collection")
|
|
1300
|
+
raise ValueError(f"Cannot add element of type '{type(element).__name__}' as it is not derived from matplotlib.artist.Artist, nor is it a Collection")
|
|
1158
1301
|
for l in element:
|
|
1159
1302
|
self += l
|
|
1160
1303
|
return self
|
|
@@ -1182,12 +1325,12 @@ class FigStore( object ):
|
|
|
1182
1325
|
rem( e.deferred_result )
|
|
1183
1326
|
return
|
|
1184
1327
|
if not e is None:
|
|
1185
|
-
raise RuntimeError("Cannot remove() element of type '{type(e).__name__}' as it is not derived from matplotlib.artist.Artist, nor is it a Collection")
|
|
1328
|
+
raise RuntimeError(f"Cannot remove() element of type '{type(e).__name__}' as it is not derived from matplotlib.artist.Artist, nor is it a Collection")
|
|
1186
1329
|
|
|
1187
1330
|
while len(self._elements) > 0:
|
|
1188
1331
|
rem( self._elements.pop(0) )
|
|
1189
1332
|
self._elements = []
|
|
1190
|
-
gc.collect()
|
|
1333
|
+
#gc.collect()
|
|
1191
1334
|
|
|
1192
1335
|
clear = remove
|
|
1193
1336
|
|
|
@@ -1251,8 +1394,8 @@ def color(i : int, table : str ="css4"):
|
|
|
1251
1394
|
i : int
|
|
1252
1395
|
Integer number. Colors will be rotated.
|
|
1253
1396
|
|
|
1254
|
-
table : str,
|
|
1255
|
-
Which color table from
|
|
1397
|
+
table : str, default ``css4``
|
|
1398
|
+
Which `color table from matplotlib <https://matplotlib.org/stable/users/explain/colors/index.html>`__ to use: `"css4"`, `"base"`, `"tableau"` or `"xkcd"`.
|
|
1256
1399
|
Default is ``"css4"``.
|
|
1257
1400
|
|
|
1258
1401
|
Returns
|
|
@@ -1307,6 +1450,6 @@ def colors_tableau():
|
|
|
1307
1450
|
return colors("tableau")
|
|
1308
1451
|
|
|
1309
1452
|
def colors_xkcd():
|
|
1310
|
-
""" Iterator for `xkcd <https://xkcd.com/color/rgb/>`__
|
|
1453
|
+
""" Iterator for `xkcd <https://xkcd.com/color/rgb/>`__ matplotlib colors """
|
|
1311
1454
|
return colors("xkcd")
|
|
1312
1455
|
|