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/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
+