Coverage for python/bellhop/plotutils.py: 44%
345 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-23 13:34 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-09-23 13:34 +0000
1##############################################################################
2#
3# Copyright (c) 2025-, Will Robertson
4# Copyright (c) 2018-2025, Mandar Chitre
5#
6# This file was originally part of arlpy, released under Simplified BSD License.
7# It has been relicensed in this repository to be compatible with the Bellhop licence (GPL).
8#
9##############################################################################
11"""Easy-to-use plotting utilities based on `Bokeh <http://bokeh.pydata.org>`_."""
13import numpy as _np
14import os as _os
15import warnings as _warnings
16from tempfile import mkstemp as _mkstemp
17import bokeh.plotting as _bplt
18import bokeh.models as _bmodels
19import bokeh.resources as _bres
20import bokeh.io as _bio
21import scipy.signal as _sig
23light_palette = ['mediumblue', 'crimson', 'forestgreen', 'gold', 'darkmagenta', 'olive', 'palevioletred', 'yellowgreen',
24 'deepskyblue', 'dimgray', 'indianred', 'mediumaquamarine', 'orange', 'saddlebrown', 'teal', 'mediumorchid']
25dark_palette = ['lightskyblue', 'red', 'limegreen', 'salmon', 'magenta', 'forestgreen', 'silver', 'teal']
27_figure = None
28_figures = None
29_hold = False
30_figsize = (600, 400)
31_color = 0
32_notebook = False
33_disable_js = False
34_using_js = False
35_interactive = True
36_static_images = False
37_colors = light_palette
39try:
40 from IPython import get_ipython
41except ImportError:
42 get_ipython = None
44_notebook = False
45if get_ipython is not None:
46 shell = get_ipython().__class__.__name__
47 if "ZMQInteractiveShell" in shell: # IPython Jupyter shell
48 _bplt.output_notebook(resources=_bres.INLINE, hide_banner=True)
49 _notebook = True
51def _new_figure(title, width, height, xlabel, ylabel, xlim, ylim, xtype, ytype, interactive):
52 global _color
53 if width is None:
54 width = _figsize[0]
55 if height is None:
56 height = _figsize[1]
57 _color = 0
58 tools = []
59 if interactive is None:
60 interactive = _interactive
61 if interactive:
62 tools = 'pan,box_zoom,wheel_zoom,reset,save'
63 args = dict(title=title, width=width, height=height, x_range=xlim, y_range=ylim, x_axis_label=xlabel, y_axis_label=ylabel, x_axis_type=xtype, y_axis_type=ytype, tools=tools)
64 f = _bplt.figure(**{k:v for (k,v) in args.items() if v is not None})
65 f.toolbar.logo = None
66 return f
68def _process_canvas(figures):
69 global _using_js
70 if _disable_js:
71 return
72 if _using_js and len(figures) == 0:
73 return
74 disable = []
75 i = 0
76 for f in figures:
77 i += 1
78 if f is not None and f.tools == []:
79 disable.append(i)
80 else:
81 pass
82 if not _using_js and len(disable) == 0:
83 return
84 _using_js = True
85 js = 'var disable = '+str(disable)
86 js += """
87 var clist = document.getElementsByClassName('bk-canvas');
88 var j = 0;
89 for (var i = 0; i < clist.length; i++) {
90 if (clist[i].id == '') {
91 j++;
92 clist[i].id = 'bkc-'+String(i)+'-'+String(+new Date());
93 if (disable.indexOf(j) >= 0) {
94 var png = clist[i].toDataURL()
95 var img = document.createElement('img')
96 img.src = png
97 clist[i].parentNode.replaceChild(img, clist[i])
98 }
99 }
100 }
101 """
102 import IPython.display as _ipyd
103 _ipyd.display(_ipyd.Javascript(js))
105def _show_static_images(f):
106 fh, fname = _mkstemp(suffix='.png')
107 _os.close(fh)
108 with _warnings.catch_warnings(): # to avoid displaying deprecation warning
109 _warnings.simplefilter('ignore') # from bokeh 0.12.16
110 _bio.export_png(f, fname)
111 import IPython.display as _ipyd
112 _ipyd.display(_ipyd.Image(filename=fname, embed=True))
113 _os.unlink(fname)
115def _show(f):
116 if _figures is None:
117 if _static_images:
118 _show_static_images(f)
119 else:
120 _process_canvas([])
121 _bplt.show(f)
122 _process_canvas([f])
123 else:
124 _figures[-1].append(f)
126def _hold_enable(enable):
127 global _hold, _figure
128 ohold = _hold
129 _hold = enable
130 if not _hold and _figure is not None:
131 _show(_figure)
132 _figure = None
133 return ohold
135def theme(name):
136 """Set color theme.
138 :param name: name of theme
140 >>> import arlpy.plot
141 >>> arlpy.plot.theme('dark')
142 """
143 if name == 'dark':
144 name = 'dark_minimal'
145 set_colors(dark_palette)
146 elif name == 'light':
147 name = 'light_minimal'
148 set_colors(light_palette)
149 _bio.curdoc().theme = name
151def figsize(x, y):
152 """Set the default figure size in pixels.
154 :param x: figure width
155 :param y: figure height
156 """
157 global _figsize
158 _figsize = (x, y)
160def interactive(b):
161 """Set default interactivity for plots.
163 :param b: True to enable interactivity, False to disable it
164 """
165 global _interactive
166 _interactive = b
168def enable_javascript(b):
169 """Enable/disable Javascript.
171 :param b: True to use Javacript, False to avoid use of Javascript
173 Jupyterlab does not support Javascript output. To avoid error messages,
174 Javascript can be disabled using this call. This removes an optimization
175 to replace non-interactive plots with static images, but other than that
176 does not affect functionality.
177 """
178 global _disable_js
179 _disable_js = not b
181def use_static_images(b=True):
182 """Use static images instead of dynamic HTML/Javascript in Jupyter notebook.
184 :param b: True to use static images, False to use HTML/Javascript
186 Static images are useful when the notebook is to be exported as a markdown,
187 LaTeX or PDF document, since dynamic HTML/Javascript is not rendered in these
188 formats. When static images are used, all interactive functionality is disabled.
190 To use static images, you must have the following packages installed:
191 selenium, pillow, phantomjs.
192 """
193 global _static_images, _interactive
194 if not b:
195 _static_images = False
196 return
197 if not _notebook:
198 _warnings.warn('Not running in a Jupyter notebook, static png support disabled')
199 return
200 _interactive = False
201 _static_images = True
203def hold(enable=True):
204 """Combine multiple plots into one.
206 :param enable: True to hold plot, False to release hold
207 :returns: old state of hold if enable is True
209 >>> import arlpy.plot
210 >>> oh = arlpy.plot.hold()
211 >>> arlpy.plot.plot([0,10], [0,10], color='blue', legend='A')
212 >>> arlpy.plot.plot([10,0], [0,10], marker='o', color='green', legend='B')
213 >>> arlpy.plot.hold(oh)
214 """
215 rv = _hold_enable(enable)
216 return rv if enable else None
218class figure:
219 """Create a new figure, and optionally automatically display it.
221 :param title: figure title
222 :param xlabel: x-axis label
223 :param ylabel: y-axis label
224 :param xlim: x-axis limits (min, max)
225 :param ylim: y-axis limits (min, max)
226 :param xtype: x-axis type ('auto', 'linear', 'log', etc)
227 :param ytype: y-axis type ('auto', 'linear', 'log', etc)
228 :param width: figure width in pixels
229 :param height: figure height in pixels
230 :param interactive: enable interactive tools (pan, zoom, etc) for plot
232 This function can be used in standalone mode to create a figure:
234 >>> import arlpy.plot
235 >>> arlpy.plot.figure(title='Demo 1', width=500)
236 >>> arlpy.plot.plot([0,10], [0,10])
238 Or it can be used as a context manager to create, hold and display a figure:
240 >>> import arlpy.plot
241 >>> with arlpy.plot.figure(title='Demo 2', width=500):
242 >>> arlpy.plot.plot([0,10], [0,10], color='blue', legend='A')
243 >>> arlpy.plot.plot([10,0], [0,10], marker='o', color='green', legend='B')
245 It can even be used as a context manager to work with Bokeh functions directly:
247 >>> import arlpy.plot
248 >>> with arlpy.plot.figure(title='Demo 3', width=500) as f:
249 >>> f.line([0,10], [0,10], line_color='blue')
250 >>> f.square([3,7], [4,5], line_color='green', fill_color='yellow', size=10)
251 """
253 def __init__(self, title=None, xlabel=None, ylabel=None, xlim=None, ylim=None, xtype='auto', ytype='auto', width=None, height=None, interactive=None):
254 global _figure
255 _figure = _new_figure(title, width, height, xlabel, ylabel, xlim, ylim, xtype, ytype, interactive)
257 def __enter__(self):
258 global _hold
259 _hold = True
260 return _figure
262 def __exit__(self, *args):
263 global _hold, _figure
264 _hold = False
265 _show(_figure)
266 _figure = None
268class many_figures:
269 """Create a grid of many figures.
271 :param figsize: default size of figure in grid as (width, height)
273 >>> import arlpy.plot
274 >>> with arlpy.plot.many_figures(figsize=(300,200)):
275 >>> arlpy.plot.plot([0,10], [0,10])
276 >>> arlpy.plot.plot([0,10], [0,10])
277 >>> arlpy.plot.next_row()
278 >>> arlpy.plot.next_column()
279 >>> arlpy.plot.plot([0,10], [0,10])
280 """
282 def __init__(self, figsize=None):
283 self.figsize = figsize
285 def __enter__(self):
286 global _figures, _figsize
287 _figures = [[]]
288 self.ofigsize = _figsize
289 if self.figsize is not None:
290 _figsize = self.figsize
292 def __exit__(self, *args):
293 global _figures, _figsize
294 if len(_figures) > 1 or len(_figures[0]) > 0:
295 f = _bplt.gridplot(_figures, merge_tools=False)
296 if _static_images:
297 _show_static_images(f)
298 else:
299 _process_canvas([])
300 _bplt.show(f)
301 _process_canvas([item for sublist in _figures for item in sublist])
302 _figures = None
303 _figsize = self.ofigsize
305def next_row():
306 """Move to the next row in a grid of many figures."""
307 global _figures
308 if _figures is not None:
309 _figures.append([])
311def next_column():
312 """Move to the next column in a grid of many figures."""
313 global _figures
314 if _figures is not None:
315 _figures[-1].append(None)
317def gcf():
318 """Get the current figure.
320 :returns: handle to the current figure
321 """
322 return _figure
324def plot(x, y=None, fs=None, maxpts=10000, pooling=None, color=None, style='solid', thickness=1, marker=None, filled=False, size=6, mskip=0, title=None, xlabel=None, ylabel=None, xlim=None, ylim=None, xtype='auto', ytype='auto', width=None, height=None, legend=None, hold=False, interactive=None):
325 """Plot a line graph or time series.
327 :param x: x data or time series data (if y is None)
328 :param y: y data or None (if time series)
329 :param fs: sampling rate for time series data
330 :param maxpts: maximum number of points to plot (downsampled if more points provided)
331 :param pooling: pooling for downsampling (None, 'max', 'min', 'mean', 'median')
332 :param color: line color (see `Bokeh colors <https://bokeh.pydata.org/en/latest/docs/reference/colors.html>`_)
333 :param style: line style ('solid', 'dashed', 'dotted', 'dotdash', 'dashdot', None)
334 :param thickness: line width in pixels
335 :param marker: point markers ('.', 'o', 's', '*', 'x', '+', 'd', '^')
336 :param filled: filled markers or outlined ones
337 :param size: marker size
338 :param mskip: number of points to skip marking (to avoid too many markers)
339 :param title: figure title
340 :param xlabel: x-axis label
341 :param ylabel: y-axis label
342 :param xlim: x-axis limits (min, max)
343 :param ylim: y-axis limits (min, max)
344 :param xtype: x-axis type ('auto', 'linear', 'log', etc)
345 :param ytype: y-axis type ('auto', 'linear', 'log', etc)
346 :param width: figure width in pixels
347 :param height: figure height in pixels
348 :param legend: legend text
349 :param interactive: enable interactive tools (pan, zoom, etc) for plot
350 :param hold: if set to True, output is not plotted immediately, but combined with the next plot
352 >>> import arlpy.plot
353 >>> import numpy as np
354 >>> arlpy.plot.plot([0,10], [1,-1], color='blue', marker='o', filled=True, legend='A', hold=True)
355 >>> arlpy.plot.plot(np.random.normal(size=1000), fs=100, color='green', legend='B')
356 """
357 global _figure, _color
358 x = _np.asarray(x, dtype=_np.float64)
359 if y is None:
360 y = x
361 x = _np.arange(x.size)
362 if fs is not None:
363 x = x/fs
364 if xlabel is None:
365 xlabel = 'Time (s)'
366 if xlim is None:
367 xlim = (x[0], x[-1])
368 else:
369 y = _np.asarray(y, dtype=_np.float64)
371 if x.ndim == 0: # 0-dimensional array (scalar)
372 x = _np.array([x])
373 if y.ndim == 0: # 0-dimensional array (scalar)
374 y = _np.array([y])
376 if _figure is None:
377 _figure = _new_figure(title, width, height, xlabel, ylabel, xlim, ylim, xtype, ytype, interactive)
378 if color is None:
379 color = _colors[_color % len(_colors)]
380 _color += 1
381 if x.size > maxpts:
382 n = int(_np.ceil(x.size/maxpts))
383 x = x[::n]
384 desc = 'Downsampled by '+str(n)
385 if pooling is None:
386 y = y[::n]
387 elif pooling == 'max':
388 desc += ', '+pooling+' pooled'
389 y = _np.amax(_np.reshape(y[:n*(y.size//n)], (-1, n)), axis=1)
390 elif pooling == 'min':
391 desc += ', '+pooling+' pooled'
392 y = _np.amin(_np.reshape(y[:n*(y.size//n)], (-1, n)), axis=1)
393 elif pooling == 'mean':
394 desc += ', '+pooling+' pooled'
395 y = _np.mean(_np.reshape(y[:n*(y.size//n)], (-1, n)), axis=1)
396 elif pooling == 'median':
397 desc += ', '+pooling+' pooled'
398 y = _np.mean(_np.reshape(y[:n*(y.size//n)], (-1, n)), axis=1)
399 else:
400 _warnings.warn('Unknown pooling: '+pooling)
401 y = y[::n]
402 if len(x) > len(y):
403 x = x[:len(y)]
404 _figure.add_layout(_bmodels.Label(x=5, y=5, x_units='screen', y_units='screen', text=desc, text_font_size="8pt", text_alpha=0.5))
405 if style is not None:
406 if legend is None:
407 _figure.line(x, y, line_color=color, line_dash=style, line_width=thickness)
408 else:
409 _figure.line(x, y, line_color=color, line_dash=style, line_width=thickness, legend_label=legend)
410 if marker is not None:
411 scatter(x[::(mskip+1)], y[::(mskip+1)], marker=marker, filled=filled, size=size, color=color, legend=legend, hold=True)
412 if not hold and not _hold:
413 _show(_figure)
414 _figure = None
416def scatter(x, y, marker='.', filled=False, size=6, color=None, title=None, xlabel=None, ylabel=None, xlim=None, ylim=None, xtype='auto', ytype='auto', width=None, height=None, legend=None, hold=False, interactive=None):
417 """Plot a scatter plot.
419 :param x: x data
420 :param y: y data
421 :param color: marker color (see `Bokeh colors`_)
422 :param marker: point markers ('.', 'o', 's', '*', 'x', '+', 'd', '^')
423 :param filled: filled markers or outlined ones
424 :param size: marker size
425 :param title: figure title
426 :param xlabel: x-axis label
427 :param ylabel: y-axis label
428 :param xlim: x-axis limits (min, max)
429 :param ylim: y-axis limits (min, max)
430 :param xtype: x-axis type ('auto', 'linear', 'log', etc)
431 :param ytype: y-axis type ('auto', 'linear', 'log', etc)
432 :param width: figure width in pixels
433 :param height: figure height in pixels
434 :param legend: legend text
435 :param interactive: enable interactive tools (pan, zoom, etc) for plot
436 :param hold: if set to True, output is not plotted immediately, but combined with the next plot
438 >>> import arlpy.plot
439 >>> import numpy as np
440 >>> arlpy.plot.scatter(np.random.normal(size=100), np.random.normal(size=100), color='blue', marker='o')
441 """
442 global _figure, _color
443 if _figure is None:
444 _figure = _new_figure(title, width, height, xlabel, ylabel, xlim, ylim, xtype, ytype, interactive)
445 x = _np.asarray(x, dtype=_np.float64)
446 y = _np.asarray(y, dtype=_np.float64)
447 if color is None:
448 color = _colors[_color % len(_colors)]
449 _color += 1
450 kwargs = {'size': size, 'line_color': color}
451 if filled:
452 kwargs['fill_color'] = color
453 if legend is not None:
454 kwargs['legend_label'] = legend
455 if marker == '.':
456 kwargs['size'] = kwargs['size']/2
457 kwargs['fill_color'] = color
458 _figure.scatter(x, y, **kwargs)
459 elif marker == 'o':
460 _figure.scatter(x, y, **kwargs)
461 elif marker == 's':
462 _figure.square(x, y, **kwargs)
463 elif marker == '*':
464 _figure.scatter(x, y, marker="*", **kwargs)
465 elif marker == 'x':
466 _figure.x(x, y, **kwargs)
467 elif marker == '+':
468 _figure.cross(x, y, **kwargs)
469 elif marker == 'd':
470 _figure.diamond(x, y, **kwargs)
471 elif marker == '^':
472 _figure.triangle(x, y, **kwargs)
473 elif marker is not None:
474 _warnings.warn('Bad marker type: '+marker)
475 if not hold and not _hold:
476 _show(_figure)
477 _figure = None
479def image(img, x=None, y=None, colormap='Plasma256', clim=None, clabel=None, title=None, xlabel=None, ylabel=None, xlim=None, ylim=None, xtype='auto', ytype='auto', width=None, height=None, hold=False, interactive=None):
480 """Plot a heatmap of 2D scalar data.
482 :param img: 2D image data
483 :param x: x-axis range for image data (min, max)
484 :param y: y-axis range for image data (min, max)
485 :param colormap: named color palette or Bokeh ColorMapper (see `Bokeh palettes <https://bokeh.pydata.org/en/latest/docs/reference/palettes.html>`_)
486 :param clim: color axis limits (min, max)
487 :param clabel: color axis label
488 :param title: figure title
489 :param xlabel: x-axis label
490 :param ylabel: y-axis label
491 :param xlim: x-axis limits (min, max)
492 :param ylim: y-axis limits (min, max)
493 :param xtype: x-axis type ('auto', 'linear', 'log', etc)
494 :param ytype: y-axis type ('auto', 'linear', 'log', etc)
495 :param width: figure width in pixels
496 :param height: figure height in pixels
497 :param interactive: enable interactive tools (pan, zoom, etc) for plot
498 :param hold: if set to True, output is not plotted immediately, but combined with the next plot
500 >>> import arlpy.plot
501 >>> import numpy as np
502 >>> arlpy.plot.image(np.random.normal(size=(100,100)), colormap='Inferno256')
503 """
504 global _figure
505 if x is None:
506 x = (0, img.shape[1]-1)
507 if y is None:
508 y = (0, img.shape[0]-1)
509 if xlim is None:
510 xlim = x
511 if ylim is None:
512 ylim = y
513 if _figure is None:
514 _figure = _new_figure(title, width, height, xlabel, ylabel, xlim, ylim, xtype, ytype, interactive)
515 if clim is None:
516 clim = [_np.amin(img), _np.amax(img)]
517 if not isinstance(colormap, _bmodels.ColorMapper):
518 colormap = _bmodels.LinearColorMapper(palette=colormap, low=clim[0], high=clim[1])
519 _figure.image([img], x=x[0], y=y[0], dw=x[-1]-x[0], dh=y[-1]-y[0], color_mapper=colormap)
520 cbar = _bmodels.ColorBar(color_mapper=colormap, location=(0,0), title=clabel)
521 _figure.add_layout(cbar, 'right')
522 if not hold and not _hold:
523 _show(_figure)
524 _figure = None
526def vlines(x, color='gray', style='dashed', thickness=1, hold=False):
527 """Draw vertical lines on a plot.
529 :param x: x location of lines
530 :param color: line color (see `Bokeh colors`_)
531 :param style: line style ('solid', 'dashed', 'dotted', 'dotdash', 'dashdot')
532 :param thickness: line width in pixels
533 :param hold: if set to True, output is not plotted immediately, but combined with the next plot
535 >>> import arlpy.plot
536 >>> arlpy.plot.plot([0, 20], [0, 10], hold=True)
537 >>> arlpy.plot.vlines([7, 12])
538 """
539 global _figure
540 if _figure is None:
541 return
542 x = _np.asarray(x, dtype=_np.float64)
543 for j in range(x.size):
544 _figure.add_layout(_bmodels.Span(location=x[j], dimension='height', line_color=color, line_dash=style, line_width=thickness))
545 if not hold and not _hold:
546 _show(_figure)
547 _figure = None
549def hlines(y, color='gray', style='dashed', thickness=1, hold=False):
550 """Draw horizontal lines on a plot.
552 :param y: y location of lines
553 :param color: line color (see `Bokeh colors`_)
554 :param style: line style ('solid', 'dashed', 'dotted', 'dotdash', 'dashdot')
555 :param thickness: line width in pixels
556 :param hold: if set to True, output is not plotted immediately, but combined with the next plot
558 >>> import arlpy.plot
559 >>> arlpy.plot.plot([0, 20], [0, 10], hold=True)
560 >>> arlpy.plot.hlines(3, color='red', style='dotted')
561 """
562 global _figure
563 if _figure is None:
564 return
565 y = _np.asarray(y, dtype=_np.float64)
566 for j in range(y.size):
567 _figure.add_layout(_bmodels.Span(location=y[j], dimension='width', line_color=color, line_dash=style, line_width=thickness))
568 if not hold and not _hold:
569 _show(_figure)
570 _figure = None
572def text(x, y, s, color='gray', size='8pt', hold=False):
573 """Add text annotation to a plot.
575 :param x: x location of left of text
576 :param y: y location of bottom of text
577 :param s: text to add
578 :param color: text color (see `Bokeh colors`_)
579 :param size: text size (e.g. '12pt', '3em')
580 :param hold: if set to True, output is not plotted immediately, but combined with the next plot
582 >>> import arlpy.plot
583 >>> arlpy.plot.plot([0, 20], [0, 10], hold=True)
584 >>> arlpy.plot.text(7, 3, 'demo', color='orange')
585 """
586 global _figure
587 if _figure is None:
588 return
589 _figure.add_layout(_bmodels.Label(x=x, y=y, text=s, text_font_size=size, text_color=color))
590 if not hold and not _hold:
591 _show(_figure)
592 _figure = None
594def box(left=None, right=None, top=None, bottom=None, color='yellow', alpha=0.1, hold=False):
595 """Add a highlight box to a plot.
597 :param left: x location of left of box
598 :param right: x location of right of box
599 :param top: y location of top of box
600 :param bottom: y location of bottom of box
601 :param color: text color (see `Bokeh colors`_)
602 :param alpha: transparency (0-1)
603 :param hold: if set to True, output is not plotted immediately, but combined with the next plot
605 >>> import arlpy.plot
606 >>> arlpy.plot.plot([0, 20], [0, 10], hold=True)
607 >>> arlpy.plot.box(left=5, right=10, top=8)
608 """
609 global _figure
610 if _figure is None:
611 return
612 _figure.add_layout(_bmodels.BoxAnnotation(left=left, right=right, top=top, bottom=bottom, fill_color=color, fill_alpha=alpha))
613 if not hold and not _hold:
614 _show(_figure)
615 _figure = None
617def color(n):
618 """Get a numbered color to cycle over a set of colors.
620 >>> import arlpy.plot
621 >>> arlpy.plot.color(0)
622 'blue'
623 >>> arlpy.plot.color(1)
624 'red'
625 >>> arlpy.plot.plot([0, 20], [0, 10], color=arlpy.plot.color(3))
626 """
627 return _colors[n % len(_colors)]
629def set_colors(c):
630 """Provide a list of named colors to cycle over.
632 >>> import arlpy.plot
633 >>> arlpy.plot.set_colors(['red', 'blue', 'green', 'black'])
634 >>> arlpy.plot.color(2)
635 'green'
636 """
637 global _colors
638 _colors = c
640def specgram(x, fs=2, nfft=None, noverlap=None, colormap='Plasma256', clim=None, clabel='dB', title=None, xlabel='Time (s)', ylabel='Frequency (Hz)', xlim=None, ylim=None, width=None, height=None, hold=False, interactive=None):
641 """Plot spectrogram of a given time series signal.
643 :param x: time series signal
644 :param fs: sampling rate
645 :param nfft: FFT size (see `scipy.signal.spectrogram <https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.spectrogram.html>`_)
646 :param noverlap: overlap size (see `scipy.signal.spectrogram`_)
647 :param colormap: named color palette or Bokeh ColorMapper (see `Bokeh palettes`_)
648 :param clim: color axis limits (min, max), or dynamic range with respect to maximum
649 :param clabel: color axis label
650 :param title: figure title
651 :param xlabel: x-axis label
652 :param ylabel: y-axis label
653 :param xlim: x-axis limits (min, max)
654 :param ylim: y-axis limits (min, max)
655 :param width: figure width in pixels
656 :param height: figure height in pixels
657 :param interactive: enable interactive tools (pan, zoom, etc) for plot
658 :param hold: if set to True, output is not plotted immediately, but combined with the next plot
660 >>> import arlpy.plot
661 >>> import numpy as np
662 >>> arlpy.plot.specgram(np.random.normal(size=(10000)), fs=10000, clim=30)
663 """
664 f, t, Sxx = _sig.spectrogram(x, fs=fs, nperseg=nfft, noverlap=noverlap)
665 Sxx = 10*_np.log10(Sxx+_np.finfo(float).eps)
666 if isinstance(clim, float) or isinstance(clim, int):
667 clim = (_np.max(Sxx)-clim, _np.max(Sxx))
668 image(Sxx, x=(t[0], t[-1]), y=(f[0], f[-1]), title=title, colormap=colormap, clim=clim, clabel=clabel, xlabel=xlabel, ylabel=ylabel, xlim=xlim, ylim=ylim, width=width, height=height, hold=hold, interactive=interactive)
670def psd(x, fs=2, nfft=512, noverlap=None, window='hann', color=None, style='solid', thickness=1, marker=None, filled=False, size=6, title=None, xlabel='Frequency (Hz)', ylabel='Power spectral density (dB/Hz)', xlim=None, ylim=None, width=None, height=None, legend=None, hold=False, interactive=None):
671 """Plot power spectral density of a given time series signal.
673 :param x: time series signal
674 :param fs: sampling rate
675 :param nfft: segment size (see `scipy.signal.welch <https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.welch.html>`_)
676 :param noverlap: overlap size (see `scipy.signal.welch`_)
677 :param window: window to use (see `scipy.signal.welch`_)
678 :param color: line color (see `Bokeh colors`_)
679 :param style: line style ('solid', 'dashed', 'dotted', 'dotdash', 'dashdot')
680 :param thickness: line width in pixels
681 :param marker: point markers ('.', 'o', 's', '*', 'x', '+', 'd', '^')
682 :param filled: filled markers or outlined ones
683 :param size: marker size
684 :param title: figure title
685 :param xlabel: x-axis label
686 :param ylabel: y-axis label
687 :param xlim: x-axis limits (min, max)
688 :param ylim: y-axis limits (min, max)
689 :param width: figure width in pixels
690 :param height: figure height in pixels
691 :param legend: legend text
692 :param interactive: enable interactive tools (pan, zoom, etc) for plot
693 :param hold: if set to True, output is not plotted immediately, but combined with the next plot
695 >>> import arlpy.plot
696 >>> import numpy as np
697 >>> arlpy.plot.psd(np.random.normal(size=(10000)), fs=10000)
698 """
699 f, Pxx = _sig.welch(x, fs=fs, nperseg=nfft, noverlap=noverlap, window=window)
700 Pxx = 10*_np.log10(Pxx+_np.finfo(float).eps)
701 if xlim is None:
702 xlim = (0, fs/2)
703 if ylim is None:
704 ylim = (_np.max(Pxx)-50, _np.max(Pxx)+10)
705 plot(f, Pxx, color=color, style=style, thickness=thickness, marker=marker, filled=filled, size=size, title=title, xlabel=xlabel, ylabel=ylabel, xlim=xlim, ylim=ylim, maxpts=len(f), width=width, height=height, hold=hold, legend=legend, interactive=interactive)
707def iqplot(data, marker='.', color=None, labels=None, filled=False, size=None, title=None, xlabel=None, ylabel=None, xlim=[-2, 2], ylim=[-2, 2], width=None, height=None, hold=False, interactive=None):
708 """Plot signal points.
710 :param data: complex baseband signal points
711 :param marker: point markers ('.', 'o', 's', '*', 'x', '+', 'd', '^')
712 :param color: marker/text color (see `Bokeh colors`_)
713 :param labels: label for each signal point, or True to auto-generate labels
714 :param filled: filled markers or outlined ones
715 :param size: marker/text size (e.g. 5, '8pt')
716 :param title: figure title
717 :param xlabel: x-axis label
718 :param ylabel: y-axis label
719 :param xlim: x-axis limits (min, max)
720 :param ylim: y-axis limits (min, max)
721 :param width: figure width in pixels
722 :param height: figure height in pixels
723 :param interactive: enable interactive tools (pan, zoom, etc) for plot
724 :param hold: if set to True, output is not plotted immediately, but combined with the next plot
726 >>> import arlpy
727 >>> import arlpy.plot
728 >>> arlpy.plot.iqplot(arlpy.comms.psk(8))
729 >>> arlpy.plot.iqplot(arlpy.comms.qam(16), color='red', marker='x')
730 >>> arlpy.plot.iqplot(arlpy.comms.psk(4), labels=['00', '01', '11', '10'])
731 """
732 data = _np.asarray(data, dtype=_np.complex128)
733 if not _hold:
734 figure(title=title, xlabel=xlabel, ylabel=ylabel, xlim=xlim, ylim=ylim, width=width, height=height, interactive=interactive)
735 if labels is None:
736 if size is None:
737 size = 5
738 scatter(data.real, data.imag, marker=marker, filled=filled, color=color, size=size, hold=hold)
739 else:
740 if labels:
741 labels = range(len(data))
742 if color is None:
743 color = 'black'
744 plot([0], [0], hold=True)
745 for i in range(len(data)):
746 text(data[i].real, data[i].imag, str(labels[i]), color=color, size=size, hold=True if i < len(data)-1 else hold)
748def freqz(b, a=1, fs=2.0, worN=None, whole=False, degrees=True, style='solid', thickness=1, title=None, xlabel='Frequency (Hz)', xlim=None, ylim=None, width=None, height=None, hold=False, interactive=None):
749 """Plot frequency response of a filter.
751 This is a convenience function to plot frequency response, and internally uses
752 :func:`scipy.signal.freqz` to estimate the response. For further details, see the
753 documentation for :func:`scipy.signal.freqz`.
755 :param b: numerator of a linear filter
756 :param a: denominator of a linear filter
757 :param fs: sampling rate in Hz (optional, normalized frequency if not specified)
758 :param worN: see :func:`scipy.signal.freqz`
759 :param whole: see :func:`scipy.signal.freqz`
760 :param degrees: True to display phase in degrees, False for radians
761 :param style: line style ('solid', 'dashed', 'dotted', 'dotdash', 'dashdot')
762 :param thickness: line width in pixels
763 :param title: figure title
764 :param xlabel: x-axis label
765 :param ylabel1: y-axis label for magnitude
766 :param ylabel2: y-axis label for phase
767 :param xlim: x-axis limits (min, max)
768 :param ylim: y-axis limits (min, max)
769 :param width: figure width in pixels
770 :param height: figure height in pixels
771 :param interactive: enable interactive tools (pan, zoom, etc) for plot
772 :param hold: if set to True, output is not plotted immediately, but combined with the next plot
774 >>> import arlpy
775 >>> arlpy.plot.freqz([1,1,1,1,1], fs=120000);
776 """
777 w, h = _sig.freqz(b, a, worN, whole)
778 Hxx = 20*_np.log10(abs(h)+_np.finfo(float).eps)
779 f = w*fs/(2*_np.pi)
780 if xlim is None:
781 xlim = (0, fs/2)
782 if ylim is None:
783 ylim = (_np.max(Hxx)-50, _np.max(Hxx)+10)
784 figure(title=title, xlabel=xlabel, ylabel='Amplitude (dB)', xlim=xlim, ylim=ylim, width=width, height=height, interactive=interactive)
785 _hold_enable(True)
786 plot(f, Hxx, color=color(0), style=style, thickness=thickness, legend='Magnitude')
787 fig = gcf()
788 units = 180/_np.pi if degrees else 1
789 fig.extra_y_ranges = {'phase': _bmodels.Range1d(start=-_np.pi*units, end=_np.pi*units)}
790 fig.add_layout(_bmodels.LinearAxis(y_range_name='phase', axis_label='Phase (degrees)' if degrees else 'Phase (radians)'), 'right')
791 phase = _np.angle(h)*units
792 fig.line(f, phase, line_color=color(1), line_dash=style, line_width=thickness, legend_label='Phase', y_range_name='phase')
793 _hold_enable(hold)