cubevis 0.5.13__py3-none-any.whl → 0.5.15__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 cubevis might be problematic. Click here for more details.

@@ -1,815 +0,0 @@
1
- '''
2
- Implementation of the ``MsRaster`` application for measurement set raster plotting and editing
3
- '''
4
-
5
- import time
6
-
7
- from bokeh.models.formatters import NumeralTickFormatter
8
- import holoviews as hv
9
- import numpy as np
10
- import panel as pn
11
- from pandas import to_datetime
12
-
13
- from cubevis.bokeh.format import get_time_formatter
14
- from cubevis.bokeh.state._palette import available_palettes
15
- from cubevis.plot.ms_plot._ms_plot import MsPlot
16
- from cubevis.plot.ms_plot._ms_plot_constants import VIS_AXIS_OPTIONS, SPECTRUM_AXIS_OPTIONS
17
- from cubevis.plot.ms_plot._ms_plot_selectors import (file_selector, title_selector, style_selector, axis_selector,
18
- aggregation_selector, iteration_selector, selection_selector, plot_starter)
19
- from cubevis.plot.ms_plot._raster_plot_inputs import check_inputs
20
- from cubevis.plot.ms_plot._raster_plot import RasterPlot
21
-
22
- class MsRaster(MsPlot):
23
- '''
24
- Plot MeasurementSet data as raster plot.
25
-
26
- Args:
27
- ms (str): path to MSv2 (.ms) or MSv4 (.zarr) file. Required when show_gui=False.
28
- log_level (str): logging threshold. Options include 'debug', 'info', 'warning', 'error', 'critical'. Default 'info'.
29
- show_gui (bool): whether to launch the interactive GUI in a browser tab. Default False.
30
-
31
- Example:
32
- from cubevis.plots import MsRaster
33
- msr = MsRaster(ms='myvis.ms')
34
- msr.summary()
35
- msr.set_style_params(unflagged_cmap='Plasma', flagged_cmap='Greys', show_colorbar=True)
36
- msr.plot(x_axis='frequency', y_axis='time', vis_axis='amp', data_group='base')
37
- msr.show()
38
- msr.save() # saves as {ms name}_raster.png
39
- '''
40
-
41
- def __init__(self, ms=None, log_level="info", show_gui=False):
42
- super().__init__(ms, log_level, show_gui, "MsRaster")
43
- self._raster_plot = RasterPlot()
44
-
45
- # Calculations for color limits
46
- self._spw_stats = {}
47
- self._spw_color_limits = {}
48
-
49
- if show_gui:
50
- # GUI based on panel widgets
51
- self._gui_layout = None
52
-
53
- # Check if plot inputs changed and new plot is needed.
54
- # GUI can change subparams which do not change plot
55
- self._last_plot_inputs = None
56
- self._last_style_inputs = None
57
- # Last plot when no new plot created (plot inputs same) or is iter Layout plot (opened in tab)
58
- self._last_gui_plot = None
59
-
60
- # Return plot for gui DynamicMap:
61
- # Empty plot when ms not set or plot fails
62
- self._empty_plot = self._create_empty_plot()
63
-
64
- # Set default style and plot inputs to use when launching gui
65
- self.set_style_params()
66
- self.plot()
67
- self._launch_gui()
68
-
69
- # Set filename TextInput to input ms to trigger plot
70
- if 'ms' in self._ms_info and self._ms_info['ms']:
71
- self._set_filename([self._ms_info['ms']]) # function expects list
72
-
73
- def colormaps(self):
74
- ''' List available colormap (Bokeh palettes). '''
75
- return available_palettes()
76
-
77
- def set_style_params(self, unflagged_cmap='Viridis', flagged_cmap='Reds', show_colorbar=True, show_flagged_colorbar=True):
78
- '''
79
- Set styling parameters for the plot, such as colormaps and whether to show colorbar.
80
- Placeholder for future styling such as fonts.
81
-
82
- Args:
83
- unflagged_cmap (str): colormap to use for unflagged data.
84
- flagged_cmap (str): colormap to use for flagged data.
85
- show_colorbar (bool): Whether to show colorbar with plot. Default True.
86
- '''
87
- cmaps = self.colormaps()
88
- if unflagged_cmap not in cmaps:
89
- raise ValueError(f"{unflagged_cmap} not in colormaps list: {cmaps}")
90
- if flagged_cmap not in cmaps:
91
- raise ValueError(f"{flagged_cmap} not in colormaps list: {cmaps}")
92
- self._raster_plot.set_style_params(unflagged_cmap, flagged_cmap, show_colorbar, show_flagged_colorbar)
93
-
94
- # pylint: disable=too-many-arguments, too-many-positional-arguments, too-many-locals, unused-argument
95
- def plot(self, x_axis='baseline', y_axis='time', vis_axis='amp', selection=None, aggregator=None, agg_axis=None,
96
- iter_axis=None, iter_range=None, subplots=None, color_mode=None, color_range=None, title=None, clear_plots=True):
97
- '''
98
- Create a raster plot of vis_axis data in the data_group after applying selection.
99
- Plot axes include data dimensions (time, baseline/antenna, frequency, polarization).
100
- Dimensions not set as plot axes can be selected, else the first value will be used, unless aggregated.
101
-
102
- Args:
103
- x_axis (str): Plot x-axis. Default 'baseline' ('antenna_name' for spectrum data).
104
- y_axis (str): Plot y-axis. Default 'time'.
105
- vis_axis (str): Complex visibility component to plot (amp, phase, real, imag). Default 'amp'.
106
- Call data_groups() to see options.
107
- selection (dict): selected data to plot. Options include:
108
- ProcessingSet selection: by summary column names. Call summary() to see options.
109
- 'query': for pandas query of summary() columns.
110
- Default: select first spw (by id).
111
- MeasurementSet selection:
112
- 'data_group': name for correlated data, flags, weights, and uvw. Default value 'base'.
113
- Use data_groups() to get data group names.
114
- Dimensions:
115
- Visibility dimensions: 'baseline' 'time', 'frequency', 'polarization'
116
- Spectrum dimensions: 'antenna_name', 'time', 'frequency', 'polarization'
117
- Default value is index 0 (after user selection) for non-axis dimensions unless aggregated.
118
- Use antennas() to get antenna names. Select 'baseline' as "<name1> & <name2>".
119
- Use summary() to list frequencies and polarizations.
120
- TODO: how to select time?
121
- aggregator (str): reduction for rasterization. Default None.
122
- Options include 'max', 'mean', 'min', 'std', 'sum', 'var'.
123
- agg_axis (str, list): which dimension to apply aggregator across. Default None.
124
- Options include one or more dimensions.
125
- If agg_axis is None and aggregator is set, aggregates over all non-axis dimensions.
126
- If one agg_axis is selected, the non-agg dimension will be selected.
127
- iter_axis (str): dimension over which to iterate values (using iter_range).
128
- iter_range (tuple): (start, end) inclusive index values for iteration plots.
129
- Default (0, 0) (first iteration only). Use (0, -1) for all iterations.
130
- If subplots is a grid, the range is limited by the grid size.
131
- If subplots is a single plot, all iteration plots in the range can be saved using export_range in save().
132
- subplots (None, tuple): set a grid of (rows, columns). None = (1, 1) for single plot.
133
- Use with iter_axis and iter_range, or clear_plots=False.
134
- If used in multiple calls, the last subplots tuple will be used to determine grid to show or save.
135
- color_mode (None, str): Whether to limit range of colorbar. Default None (no limit).
136
- Options include None (use data limits), 'auto' (calculate limits for amplitude), and 'manual' (use range in color_range).
137
- 'auto' is equivalent to None if vis_axis is not 'amp'.
138
- When subplots is set, the 'auto' or 'manual' range will be used for all plots.
139
- color_range (tuple): (min, max) of colorbar to use if color_mode is 'manual'.
140
- title (str): Plot title, default None (no title)
141
- Set title='ms' to generate title from ms name and iter_axis value, if any.
142
- clear_plots (bool): whether to clear list of plots. Default True.
143
-
144
- If not show_gui and plotting is successful, use show() or save() to view/save the plot only.
145
- '''
146
- inputs = locals() # collect arguments into dict (not unused as pylint complains!)
147
-
148
- start = time.time()
149
-
150
- # Clear for new plot
151
- self._reset_plot(clear_plots)
152
-
153
- # Get data dimensions if valid MS is set to check input axes
154
- if 'data_dims' in self._ms_info:
155
- data_dims = self._ms_info['data_dims']
156
- if 'baseline_id' in data_dims:
157
- data_dims.remove('baseline_id')
158
- data_dims.append('baseline')
159
- inputs['data_dims'] = data_dims
160
-
161
- # Validate input arguments; data dims needed to check input and rename baseline dimension
162
- check_inputs(inputs)
163
- self._plot_inputs = inputs
164
-
165
- # Copy user selection dict; selection will be modified for plot
166
- if inputs['selection']:
167
- self._plot_inputs['selection'] = inputs['selection'].copy()
168
-
169
- if not self._show_gui:
170
- # Cannot plot if no MS
171
- if not self._data or not self._data.is_valid():
172
- raise RuntimeError("Cannot plot MS: input MS path is invalid or missing.")
173
-
174
- # Create raster plot and add to plot list
175
- try:
176
- if self._plot_inputs['iter_axis']:
177
- self._do_iter_plot(self._plot_inputs)
178
- else:
179
- plot = self._do_plot(self._plot_inputs)
180
- self._plots.append(plot)
181
- except RuntimeError as e:
182
- error = f"Plot failed: {str(e)}"
183
- super()._notify(error, "error", 0)
184
-
185
- self._logger.debug("Plot elapsed time: %.2fs.", time.time() - start)
186
- # pylint: enable=too-many-arguments, too-many-positional-arguments, too-many-locals, unused-argument
187
-
188
- def save(self, filename='', fmt='auto', width=900, height=600):
189
- '''
190
- Save plot to file.
191
-
192
- Args:
193
- filename (str): Name of file to save. Default '': the plot will be saved as {ms}_raster.{ext}.
194
- If fmt is not set for extension, plot will be saved as .png.
195
- fmt (str): Format of file to save ('png', 'svg', 'html', or 'gif').
196
- Default 'auto': inferred from filename extension.
197
- width (int): width of exported plot.
198
- height (int): height of exported plot.
199
-
200
- If iteration plots were created:
201
- If subplots is a grid, the layout plot will be saved to a single file.
202
- If subplots is a single plot, iteration plots will be saved individually,
203
- with a plot index appended to the filename: {filename}_{index}.{ext}.
204
- '''
205
- if not filename:
206
- filename = f"{self._ms_info['basename']}_raster.png"
207
- super().save(filename, fmt, width, height)
208
-
209
- def _do_plot(self, plot_inputs):
210
- ''' Create plot using plot inputs '''
211
- if not self._plot_init:
212
- self._init_plot(plot_inputs)
213
-
214
- # Select vis_axis data to plot and update selection; returns xarray Dataset
215
- raster_data = self._data.get_raster_data(plot_inputs)
216
-
217
- # Add params needed for plot: auto color range and ms name
218
- self._set_auto_color_range(plot_inputs) # set calculated limits if auto mode
219
- ms_name = self._ms_info['basename'] # for title
220
- self._raster_plot.set_plot_params(raster_data, plot_inputs, ms_name)
221
-
222
- # Make plot. Add data min/max if GUI is shown to update color limits range.
223
- return self._raster_plot.raster_plot(raster_data, self._logger, self._show_gui)
224
-
225
- def _do_iter_plot(self, plot_inputs):
226
- ''' Create one plot per iteration value in iter_range which fits into subplots '''
227
- # Default (0, 0) (first iteration only). Use (0, -1) for all iterations.
228
- # If subplots is a grid, end iteration index is limited by the grid size.
229
- # If subplots is a single plot, all iteration plots in the range can be saved using export_range in save().
230
- iter_axis = plot_inputs['iter_axis']
231
- iter_range = plot_inputs['iter_range']
232
- subplots = plot_inputs['subplots']
233
-
234
- iter_range = (0, 0) if iter_range is None else iter_range
235
- start_idx, end_idx = iter_range
236
-
237
- # Init plot before getting iter values
238
- self._init_plot(plot_inputs)
239
-
240
- iter_values = self._data.get_dimension_values(iter_axis)
241
- n_iter = len(iter_values)
242
-
243
- if start_idx >= n_iter:
244
- raise IndexError(f"iter_range start {start_idx} is greater than number of iterations {n_iter}")
245
- end_idx = n_iter if (end_idx == -1 or end_idx >= n_iter) else end_idx + 1
246
- num_iter_plots = end_idx - start_idx
247
-
248
- # Plot the minimum of iter range or subplots number of plots
249
- num_subplots = np.prod(subplots) if subplots else 1
250
- num_iter_plots = min(num_iter_plots, num_subplots) if num_subplots > 1 else num_iter_plots
251
- end_idx = start_idx + num_iter_plots
252
-
253
- for i in range(start_idx, end_idx):
254
- # Select iteration value and make plot
255
- value = iter_values[i]
256
- self._logger.info("Plot %s iteration index %s value %s", iter_axis, i, value)
257
- plot_inputs['selection'][iter_axis] = value
258
- try:
259
- plot = self._do_plot(plot_inputs)
260
- self._plots.append(plot)
261
- except RuntimeError as e:
262
- self._logger.info("Iteration plot for value %s failed: %s", str(value), str(e))
263
- continue
264
-
265
- def _init_plot(self, plot_inputs):
266
- ''' Apply automatic selection '''
267
- # Apply user + data_group selection, then select first spw
268
- # Set data group and name of its correlated data
269
- self._set_data_group(plot_inputs)
270
-
271
- # Do selection and add spw
272
- self._data.select_data(plot_inputs['selection'])
273
- self._select_first_spw(plot_inputs)
274
-
275
- # Clear automatic or iter selection of unplotted dimensions
276
- if 'dim_selection' in self._plot_inputs:
277
- del self._plot_inputs['dim_selection']
278
-
279
- self._plot_init = True
280
-
281
- # Print data info for spw selection
282
- self._logger.info("Plotting %s msv4 datasets.", self._data.get_num_ms())
283
- self._logger.info("Maximum dimensions for selected spw: %s", self._data.get_max_data_dims())
284
-
285
- def _select_first_spw(self, plot_inputs):
286
- ''' Determine first spw if not in user selection '''
287
- if 'spw_name' not in plot_inputs['selection']:
288
- first_spw = self._data.get_first_spw()
289
- self._data.select_data({'spw_name': first_spw})
290
- plot_inputs['selection']['spw_name'] = first_spw
291
-
292
- def _set_auto_color_range(self, plot_inputs):
293
- ''' Calculate stats for color limits for non-gui amplitude plots. '''
294
- color_mode = plot_inputs['color_mode']
295
- color_limits = None
296
-
297
- if color_mode == 'auto':
298
- if plot_inputs['vis_axis']=='amp' and not plot_inputs['aggregator']:
299
- # For amplitude, limit colorbar range using stored per-spw ms stats
300
- spw_name = plot_inputs['selection']['spw_name']
301
- if spw_name in self._spw_color_limits:
302
- color_limits = self._spw_color_limits[spw_name]
303
- else:
304
- # Select spw name and data group only
305
- spw_data_selection = {'spw_name': spw_name, 'data_group_name': plot_inputs['selection']['data_group_name']}
306
- color_limits = self._calc_amp_color_limits(spw_data_selection)
307
- self._spw_color_limits[spw_name] = color_limits
308
- plot_inputs['auto_color_range'] = color_limits
309
-
310
- if color_limits:
311
- self._logger.info("Setting amplitude color range: (%.4f, %.4f).", color_limits[0], color_limits[1])
312
- elif color_mode is None:
313
- self._logger.info("Autoscale color range")
314
- else:
315
- self._logger.info("Using manual color range: %s", plot_inputs['color_range'])
316
-
317
- def _calc_amp_color_limits(self, selection):
318
- # Calculate colorbar limits from amplitude stats for unflagged data in selected spw
319
- self._logger.info("Calculating stats for colorbar limits.")
320
- start = time.time()
321
-
322
- ms_stats = self._data.get_vis_stats(selection, 'amp')
323
- self._spw_stats['spw_name'] = ms_stats
324
- if not ms_stats:
325
- return None # autoscale
326
-
327
- min_val, max_val, mean, std = ms_stats
328
-
329
- data_min = min(0.0, min_val)
330
- clip_min = max(data_min, mean - (3.0 * std))
331
- data_max = max(0.0, max_val)
332
- clip_max = min(data_max, mean + (3.0 * std))
333
-
334
- if clip_min == 0.0 and clip_max == 0.0:
335
- color_limits = None # flagged data only
336
- else:
337
- color_limits = (clip_min, clip_max)
338
- self._logger.debug("Stats elapsed time: %.2fs.", time.time() - start)
339
- return color_limits
340
-
341
- def _reset_plot(self, clear_plots=True):
342
- ''' Reset any plot settings for a new plot '''
343
- # Clear plot list
344
- if clear_plots:
345
- super().clear_plots()
346
-
347
- # Reset selection in data and dim selection in plot inputs
348
- super().clear_selection()
349
-
350
- # Reset params set for last plot
351
- self._raster_plot.reset_plot_params()
352
-
353
- self._plot_init = False
354
-
355
- def _set_data_group(self, plot_inputs):
356
- ''' Add base data_group to plot inputs selection if not in user selection '''
357
- if 'selection' not in plot_inputs or not plot_inputs['selection']:
358
- plot_inputs['selection'] = {}
359
-
360
- data_group = 'base'
361
- if 'data_group' in plot_inputs['selection']:
362
- data_group = plot_inputs['selection'].pop('data_group')
363
- plot_inputs['selection']['data_group_name'] = data_group
364
-
365
- if self._data and self._data.is_valid():
366
- plot_inputs['correlated_data'] = self._data.get_correlated_data(data_group)
367
-
368
- ### -----------------------------------------------------------------------
369
- ### Interactive GUI
370
- ### -----------------------------------------------------------------------
371
- def _launch_gui(self):
372
- ''' Use Holoviz Panel to create a dashboard for plot inputs. '''
373
- # Select MS
374
- file_selectors = file_selector('Path to MeasurementSet (ms or zarr) for plot', '~' , self._set_filename)
375
-
376
- # Select style - colormaps, colorbar, color limits
377
- style_selectors = style_selector(self._set_style_params, self._set_color_range)
378
-
379
- # Set title
380
- title_input = title_selector(self._set_title)
381
-
382
- # Select x, y, and vis axis
383
- x_axis = self._plot_inputs['x_axis']
384
- y_axis = self._plot_inputs['y_axis']
385
- data_dims = self._ms_info['data_dims'] if 'data_dims' in self._ms_info else None
386
- axis_selectors = axis_selector(x_axis, y_axis, data_dims, True, self._set_axes)
387
-
388
- # Select from ProcessingSet and MeasurementSet
389
- selection_selectors = selection_selector(self._set_ps_selection)
390
-
391
- # Generic axis options, updated when ms is set
392
- axis_options = data_dims if data_dims else []
393
-
394
- # Select aggregator and axes to aggregate
395
- agg_selectors = aggregation_selector(axis_options, self._set_aggregation)
396
-
397
- # Select iter_axis and iter value or range
398
- iter_selectors = iteration_selector(axis_options, self._set_iter_values, self._set_iteration)
399
-
400
- # Put user input widgets in accordion with only one card active at a time
401
- selectors = pn.Accordion(
402
- ("Select file", file_selectors), # [0]
403
- ("Plot style", style_selectors), # [1]
404
- ("Plot axes", axis_selectors), # [2]
405
- ("Selection", selection_selectors), # [3]
406
- ("Aggregation", agg_selectors), # [4]
407
- ("Iteration", iter_selectors), # [5]
408
- ("Plot title", title_input), # [6]
409
- )
410
- selectors.toggle = True
411
-
412
- # Plot button and spinner while plotting
413
- init_plot = plot_starter(self._update_plot_spinner)
414
-
415
- # Connect plot to filename and selector widgets
416
- dmap = hv.DynamicMap(
417
- pn.bind(
418
- self._update_plot,
419
- ms=file_selectors[0][0],
420
- do_plot=init_plot[0],
421
- ),
422
- )
423
-
424
- # Layout plot and input widgets in a row
425
- self._gui_layout = pn.Row(
426
- pn.Column( # [0]
427
- dmap,
428
- sizing_mode='stretch_width',
429
- ),
430
- pn.Spacer(width=10), # [1]
431
- pn.Column( # [2]
432
- pn.Spacer(height=25), # [0]
433
- selectors, # [1]
434
- init_plot, # [2]
435
- width_policy='min',
436
- width=400,
437
- sizing_mode='stretch_height',
438
- ),
439
- sizing_mode='stretch_height',
440
- )
441
-
442
- # Show gui
443
- # print("gui layout:", self._gui_layout) # for debugging
444
- self._gui_layout.show(title=self._app_name, threaded=True)
445
-
446
- ###
447
- ### Main callback to create plot
448
- ###
449
- def _update_plot(self, ms, do_plot):
450
- ''' Create plot with inputs from GUI. Must return plot, even if empty plot, for DynamicMap. '''
451
- if self._toast:
452
- self._toast.destroy()
453
-
454
- self._get_selector("selectors").active = []
455
- if not ms:
456
- # Launched GUI with no MS
457
- return self._empty_plot
458
-
459
- # If not first plot, user has to click Plot button (do_plot=True).
460
- first_plot = not self._last_gui_plot
461
- if not do_plot and not first_plot:
462
- # Not ready to update plot yet, return last plot.
463
- return self._last_gui_plot
464
-
465
- if (self._set_ms(ms) or first_plot) and self._data and self._data.is_valid():
466
- # New MS set and is valid
467
- self._plot_inputs['selection'] = None
468
- self._update_gui_axis_options()
469
-
470
- # Add ms path to detect change and make new plot
471
- self._plot_inputs['ms'] = ms
472
-
473
- # Do new plot or resend last plot
474
- self._reset_plot()
475
- gui_plot = None
476
-
477
- style_inputs = self._raster_plot.get_plot_params()['style']
478
- if self._inputs_changed(style_inputs):
479
- # First plot or changed plot
480
- try:
481
- # Check inputs from GUI then plot
482
- self._plot_inputs['data_dims'] = self._ms_info['data_dims']
483
- check_inputs(self._plot_inputs)
484
- gui_plot = self._do_gui_plot()
485
- except (ValueError, TypeError) as e:
486
- # Clear plot, inputs invalid
487
- self._notify(str(e), 'error', 0)
488
- gui_plot = self._empty_plot
489
- except RuntimeError as e:
490
- # Clear plot, plot failed
491
- self._notify(str(e), 'error', 0)
492
- gui_plot = self._empty_plot
493
- else:
494
- # Subparam values changed but not applied to plot
495
- gui_plot = self._last_gui_plot
496
-
497
- # Save inputs to see if changed
498
- self._last_plot_inputs = self._plot_inputs.copy()
499
- self._last_style_inputs = style_inputs.copy()
500
- self._last_plot_inputs['selection'] = self._plot_inputs['selection'].copy()
501
- if first_plot and not do_plot:
502
- self._plot_inputs['selection'].clear()
503
-
504
- # Save plot if no new plot
505
- self._last_gui_plot = gui_plot
506
-
507
- # Change plot button and stop spinner
508
- self._update_plot_status(False)
509
- self._update_plot_spinner(False)
510
-
511
- return gui_plot
512
-
513
- def _inputs_changed(self, style_inputs):
514
- ''' Check if inputs changed and need new plot '''
515
- if not self._last_plot_inputs:
516
- return True
517
-
518
- # Cannot use plot_inputs == self._plot_inputs until selection in GUI.
519
- # Check inputs one by one
520
- for key in self._plot_inputs:
521
- if not self._values_equal(self._plot_inputs[key], self._last_plot_inputs[key]):
522
- return True
523
-
524
- for key in style_inputs:
525
- if not self._values_equal(style_inputs[key], self._last_style_inputs[key]):
526
- return True
527
- return False
528
-
529
- def _values_equal(self, val1, val2):
530
- if val1 and val2: # both set
531
- return val1 == val2
532
-
533
- if not val1 and not val2: # both None
534
- return True
535
-
536
- return False # one set and other is None
537
-
538
- ###
539
- ### Create plot for DynamicMap
540
- ###
541
- def _do_gui_plot(self):
542
- ''' Create plot based on gui plot inputs '''
543
- if self._data and self._data.is_valid():
544
- try:
545
- if self._plot_inputs['iter_axis']:
546
- # Make iter plot (possibly with subplots layout)
547
- self._do_iter_plot(self._plot_inputs)
548
- subplots = self._plot_inputs['subplots']
549
- layout_plot, is_layout = super()._layout_plots(subplots)
550
-
551
- if is_layout:
552
- # Cannot show Layout in DynamicMap, show in new tab
553
- super().show()
554
- self._logger.info("Plot update complete")
555
- return self._last_gui_plot
556
- # Overlay raster plot for DynamicMap
557
- self._logger.info("Plot update complete")
558
- return layout_plot
559
-
560
- # Make single Overlay raster plot for DynamicMap
561
- plot = self._do_plot(self._plot_inputs)
562
- plot_params = self._raster_plot.get_plot_params()
563
-
564
- # Update color limits in gui with data range
565
- self._update_color_range(plot_params)
566
-
567
- # Update colorbar labels and limits
568
- plot.QuadMesh.I = self._set_plot_colorbar(plot.QuadMesh.I, plot_params, "flagged")
569
- plot.QuadMesh.II = self._set_plot_colorbar(plot.QuadMesh.II, plot_params, "unflagged")
570
-
571
- self._logger.info("Plot update complete")
572
- return plot.opts(
573
- hv.opts.QuadMesh(tools=['hover'])
574
- )
575
- except RuntimeError as e:
576
- error = f"Plot failed: {str(e)}"
577
- super()._notify(error, "error", 0)
578
-
579
- # Make single Overlay raster plot for DynamicMap
580
- return self._empty_plot
581
-
582
- def _create_empty_plot(self):
583
- ''' Create empty Overlay plot for DynamicMap with default color params and hover enabled '''
584
- plot_params = self._raster_plot.get_plot_params()
585
- plot = hv.Overlay(
586
- hv.QuadMesh([]).opts(
587
- colorbar=False,
588
- cmap=plot_params['style']['flagged_cmap'],
589
- responsive=True,
590
- ) * hv.QuadMesh([]).opts(
591
- colorbar=plot_params['style']['show_colorbar'],
592
- cmap=plot_params['style']['unflagged_cmap'],
593
- responsive=True,
594
- )
595
- )
596
- return plot.opts(
597
- hv.opts.QuadMesh(tools=['hover'])
598
- )
599
-
600
- def _set_plot_colorbar(self, plot, plot_params, plot_type):
601
- ''' Update plot colorbar labels and limits
602
- plot_type is "unflagged" or "flagged"
603
- '''
604
- # Update colorbar labels if shown, else hide
605
- colorbar_key = plot_type + '_colorbar'
606
- if plot_params['plot'][colorbar_key]:
607
- c_label = plot_params['plot']['axis_labels']['c']['label']
608
- cbar_title = "Flagged " + c_label if plot_type == "flagged" else c_label
609
-
610
- plot = plot.opts(
611
- colorbar=True,
612
- backend_opts={
613
- "colorbar.title": cbar_title,
614
- }
615
- )
616
- else:
617
- plot = plot.opts(
618
- colorbar=False,
619
- )
620
-
621
- # Update plot color limits
622
- color_limits = plot_params['plot']['color_limits']
623
- if color_limits:
624
- plot = plot.opts(
625
- clim=color_limits,
626
- )
627
-
628
- return plot
629
-
630
- def _update_color_range(self, plot_params):
631
- ''' Set the start/end range on the colorbar to min/max of plot data '''
632
- if self._gui_layout and 'data' in plot_params and 'data_range' in plot_params['data']:
633
- # Update range slider start and end to data min and max
634
- data_range = plot_params['data']['data_range']
635
- style_selectors = self._get_selector('style')
636
- range_slider = style_selectors[3][1]
637
- range_slider.start = data_range[0]
638
- range_slider.end = data_range[1]
639
-
640
- ###
641
- ### Update widget options based on MS
642
- ###
643
- def _get_selector(self, name):
644
- ''' Return selector group for name, for setting options '''
645
- selectors = self._gui_layout[2][1]
646
- selectors_index = {'file': 0, 'style': 1, 'axes': 2, 'selection': 3, 'agg': 4, 'iter': 5, 'title': 6}
647
- if name == "selectors":
648
- return selectors
649
- return selectors[selectors_index[name]]
650
-
651
- def _update_gui_axis_options(self):
652
- ''' Set gui options from ms data '''
653
- if 'data_dims' in self._ms_info:
654
- data_dims = self._ms_info['data_dims']
655
-
656
- # Update options for x_axis and y_axis selectors
657
- axis_selectors = self._get_selector('axes')
658
- axis_selectors.objects[0][0].options = data_dims
659
- axis_selectors.objects[0][1].options = data_dims
660
-
661
- # Update options for vis_axis selector
662
- if self._data.get_correlated_data('base') == 'SPECTRUM':
663
- axis_selectors.objects[1].options = SPECTRUM_AXIS_OPTIONS
664
- else:
665
- axis_selectors.objects[1].options = VIS_AXIS_OPTIONS
666
-
667
- # Update options for agg axes selector
668
- agg_selectors = self._get_selector('agg')
669
- agg_selectors[1].options = data_dims
670
-
671
- # Update options for iteration axis selector
672
- iter_selectors = self._get_selector('iter')
673
- iter_axis_selector = iter_selectors[0][0]
674
- iter_axis_selector.options = ['None']
675
- iter_axis_selector.options.extend(data_dims)
676
-
677
- ###
678
- ### Callbacks for widgets which update other widgets
679
- ###
680
- def _set_filename(self, filename):
681
- ''' Set filename in text box from file selector value (list) '''
682
- if filename and self._gui_layout:
683
- file_selectors = self._get_selector('file')
684
-
685
- # Collapse FileSelector card
686
- file_selectors[1].collapsed = True
687
-
688
- # Change plot button color to indicate change unless ms path not set previously
689
- filename_input = file_selectors[0][0]
690
- if filename_input.value:
691
- self._update_plot_status(True)
692
-
693
- # Set filename from last file in file selector (triggers _update_plot())
694
- #filename_input.width = len(filename[-1])
695
- filename_input.value = filename[-1]
696
-
697
-
698
- def _set_iter_values(self, iter_axis):
699
- ''' Set up player with values when iter_axis is selected '''
700
- iter_axis = None if iter_axis == 'None' else iter_axis
701
- if iter_axis and self._gui_layout:
702
- iter_values = self._data.get_dimension_values(iter_axis)
703
- if iter_values:
704
-
705
- iter_selectors = self._get_selector('iter')
706
-
707
- # Update value selector with values and select first value
708
- iter_value_player = iter_selectors[1][0]
709
- if iter_axis == 'time':
710
- if isinstance(iter_values[0], float):
711
- iter_values = self._get_datetime_values(iter_values)
712
- iter_value_player.format = get_time_formatter()
713
- elif iter_axis == 'frequency':
714
- iter_value_player.format = NumeralTickFormatter(format='0,0.0000000')
715
-
716
- iter_value_player.options = iter_values
717
- iter_value_player.value = iter_values[0]
718
- iter_value_player.show_value = True
719
-
720
- # Update range inputs end values and select first
721
- iter_range_inputs = iter_selectors[1][1]
722
- last_iter_index = len(iter_values) - 1
723
- # range start
724
- iter_range_inputs[0][0].end = last_iter_index
725
- iter_range_inputs[0][0].value = 0
726
- # range end
727
- iter_range_inputs[0][1].end = last_iter_index
728
- iter_range_inputs[0][1].value = 0
729
-
730
- def _get_datetime_values(self, float_times):
731
- ''' Return list of float time values as list of datetime values for gui options '''
732
- time_attrs = self._data.get_dimension_attrs('time')
733
- datetime_values = []
734
- try:
735
- datetime_values = to_datetime(float_times, unit=time_attrs['units'], origin=time_attrs['format'])
736
- except TypeError:
737
- datetime_values = to_datetime(float_times, unit=time_attrs['units'][0], origin=time_attrs['format'])
738
- return list(datetime_values)
739
-
740
- def _update_plot_spinner(self, plot_clicked):
741
- ''' Callback to start spinner when Plot button clicked. '''
742
- if self._gui_layout:
743
- # Start spinner
744
- spinner = self._gui_layout[2][2][1]
745
- spinner.value = plot_clicked
746
-
747
- def _update_plot_status(self, inputs_changed):
748
- ''' Change button color when inputs change. '''
749
- if self._gui_layout:
750
- # Set button color
751
- button = self._gui_layout[2][2][0]
752
- button.button_style = 'solid' if inputs_changed else 'outline'
753
-
754
- ###
755
- ### Callbacks for widgets which update plot inputs
756
- ###
757
- def _set_title(self, title):
758
- ''' Set title from gui text input '''
759
- self._plot_inputs['title'] = title
760
- self._update_plot_status(True) # Change plot button to solid
761
-
762
- def _set_style_params(self, unflagged_cmap, flagged_cmap, show_colorbar, show_flagged_colorbar):
763
- self.set_style_params(unflagged_cmap, flagged_cmap, show_colorbar, show_flagged_colorbar)
764
- self._update_plot_status(True) # Change plot button to solid
765
-
766
- def _set_color_range(self, color_mode, color_range):
767
- ''' Set style params from gui '''
768
- color_mode = color_mode.split()[0]
769
- color_mode = None if color_mode == 'No' else color_mode
770
- self._plot_inputs['color_mode'] = color_mode
771
- self._plot_inputs['color_range'] = color_range
772
- self._update_plot_status(True) # Change plot button to solid
773
-
774
- def _set_axes(self, x_axis, y_axis, vis_axis):
775
- ''' Set plot axis params from gui '''
776
- self._plot_inputs['x_axis'] = x_axis
777
- self._plot_inputs['y_axis'] = y_axis
778
- self._plot_inputs['vis_axis'] = vis_axis
779
- self._update_plot_status(True) # Change plot button to solid
780
-
781
- def _set_aggregation(self, aggregator, agg_axes):
782
- ''' Set aggregation params from gui '''
783
- aggregator = None if aggregator== 'None' else aggregator
784
- self._plot_inputs['aggregator'] = aggregator
785
- self._plot_inputs['agg_axis'] = agg_axes # ignored if aggregator not set
786
- self._update_plot_status(True) # Change plot button to solid
787
-
788
- # pylint: disable=too-many-arguments, too-many-positional-arguments
789
- def _set_iteration(self, iter_axis, iter_value_type, iter_value, iter_start, iter_end, subplot_rows, subplot_columns):
790
- ''' Set iteration params from gui '''
791
- iter_axis = None if iter_axis == 'None' else iter_axis
792
- self._plot_inputs['iter_axis'] = iter_axis
793
- self._plot_inputs['subplots'] = (subplot_rows, subplot_columns)
794
-
795
- if iter_axis:
796
- if iter_value_type == 'By Value':
797
- # Use index of iter_value for tuple
798
- if self._data and self._data.is_valid():
799
- iter_values = self._data.get_dimension_values(iter_axis)
800
- iter_index = iter_values.index(iter_value)
801
- self._plot_inputs['iter_range'] = (iter_index, iter_index)
802
- else:
803
- # 'By Range': use range start and end values for tuple
804
- self._plot_inputs['iter_range'] = (iter_start, iter_end)
805
- else:
806
- self._plot_inputs['iter_range'] = None
807
- self._update_plot_status(True) # Change plot button to solid
808
- # pylint: enable=too-many-arguments, too-many-positional-arguments
809
-
810
- def _set_ps_selection(self, query):
811
- ''' Select ProcessingSet from gui using summary columns '''
812
- if 'selection' not in self._plot_inputs or self._plot_inputs['selection'] is None:
813
- self._plot_inputs['selection'] = {}
814
- self._plot_inputs['selection']['query'] = query
815
- self._update_plot_status(True) # Change plot button to solid