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

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############################################################################## 

10 

11"""Easy-to-use plotting utilities based on `Bokeh <http://bokeh.pydata.org>`_.""" 

12 

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 

22 

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'] 

26 

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 

38 

39try: 

40 from IPython import get_ipython 

41except ImportError: 

42 get_ipython = None 

43 

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 

50 

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 

67 

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)) 

104 

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) 

114 

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) 

125 

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 

134 

135def theme(name): 

136 """Set color theme. 

137 

138 :param name: name of theme 

139 

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 

150 

151def figsize(x, y): 

152 """Set the default figure size in pixels. 

153 

154 :param x: figure width 

155 :param y: figure height 

156 """ 

157 global _figsize 

158 _figsize = (x, y) 

159 

160def interactive(b): 

161 """Set default interactivity for plots. 

162 

163 :param b: True to enable interactivity, False to disable it 

164 """ 

165 global _interactive 

166 _interactive = b 

167 

168def enable_javascript(b): 

169 """Enable/disable Javascript. 

170 

171 :param b: True to use Javacript, False to avoid use of Javascript 

172 

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 

180 

181def use_static_images(b=True): 

182 """Use static images instead of dynamic HTML/Javascript in Jupyter notebook. 

183 

184 :param b: True to use static images, False to use HTML/Javascript 

185 

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. 

189 

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 

202 

203def hold(enable=True): 

204 """Combine multiple plots into one. 

205 

206 :param enable: True to hold plot, False to release hold 

207 :returns: old state of hold if enable is True 

208 

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 

217 

218class figure: 

219 """Create a new figure, and optionally automatically display it. 

220 

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 

231 

232 This function can be used in standalone mode to create a figure: 

233 

234 >>> import arlpy.plot 

235 >>> arlpy.plot.figure(title='Demo 1', width=500) 

236 >>> arlpy.plot.plot([0,10], [0,10]) 

237 

238 Or it can be used as a context manager to create, hold and display a figure: 

239 

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') 

244 

245 It can even be used as a context manager to work with Bokeh functions directly: 

246 

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 """ 

252 

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) 

256 

257 def __enter__(self): 

258 global _hold 

259 _hold = True 

260 return _figure 

261 

262 def __exit__(self, *args): 

263 global _hold, _figure 

264 _hold = False 

265 _show(_figure) 

266 _figure = None 

267 

268class many_figures: 

269 """Create a grid of many figures. 

270 

271 :param figsize: default size of figure in grid as (width, height) 

272 

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 """ 

281 

282 def __init__(self, figsize=None): 

283 self.figsize = figsize 

284 

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 

291 

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 

304 

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([]) 

310 

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) 

316 

317def gcf(): 

318 """Get the current figure. 

319 

320 :returns: handle to the current figure 

321 """ 

322 return _figure 

323 

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. 

326 

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 

351 

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) 

370 

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]) 

375 

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 

415 

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. 

418 

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 

437 

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 

478 

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. 

481 

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 

499 

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 

525 

526def vlines(x, color='gray', style='dashed', thickness=1, hold=False): 

527 """Draw vertical lines on a plot. 

528 

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 

534 

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 

548 

549def hlines(y, color='gray', style='dashed', thickness=1, hold=False): 

550 """Draw horizontal lines on a plot. 

551 

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 

557 

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 

571 

572def text(x, y, s, color='gray', size='8pt', hold=False): 

573 """Add text annotation to a plot. 

574 

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 

581 

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 

593 

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. 

596 

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 

604 

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 

616 

617def color(n): 

618 """Get a numbered color to cycle over a set of colors. 

619 

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)] 

628 

629def set_colors(c): 

630 """Provide a list of named colors to cycle over. 

631 

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 

639 

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. 

642 

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 

659 

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) 

669 

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. 

672 

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 

694 

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) 

706 

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. 

709 

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 

725 

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) 

747 

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. 

750 

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`. 

754 

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 

773 

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)