vidavis 0.0.12__py3-none-any.whl → 0.0.14__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.
@@ -8,17 +8,16 @@ from bokeh.models.formatters import NumeralTickFormatter
8
8
  import holoviews as hv
9
9
  import numpy as np
10
10
  from pandas import to_datetime
11
- import panel as pn
12
11
 
13
12
  from vidavis.bokeh._palette import available_palettes
14
13
  from vidavis.data.measurement_set.processing_set._ps_coords import set_index_coordinates
15
- from vidavis.plot.ms_plot._time_ticks import get_time_formatter
16
- from vidavis.plot.ms_plot._locate_points import locate_point, locate_box
17
14
  from vidavis.plot.ms_plot._ms_plot import MsPlot
18
15
  from vidavis.plot.ms_plot._ms_plot_constants import VIS_AXIS_OPTIONS, SPECTRUM_AXIS_OPTIONS, PS_SELECTION_OPTIONS, MS_SELECTION_OPTIONS
19
- from vidavis.plot.ms_plot._raster_plot_gui import create_raster_gui
20
- from vidavis.plot.ms_plot._raster_plot_inputs import check_inputs
16
+ from vidavis.plot.ms_plot._plot_inputs import inputs_changed
21
17
  from vidavis.plot.ms_plot._raster_plot import RasterPlot
18
+ from vidavis.plot.ms_plot._raster_plot_gui import create_raster_gui
19
+ from vidavis.plot.ms_plot._raster_plot_inputs import RasterPlotInputs
20
+ from vidavis.plot.ms_plot._time_ticks import get_time_formatter
22
21
 
23
22
  class MsRaster(MsPlot):
24
23
  '''
@@ -44,21 +43,15 @@ class MsRaster(MsPlot):
44
43
  def __init__(self, ms=None, log_level="info", log_to_file=True, show_gui=False):
45
44
  super().__init__(ms, log_level, log_to_file, show_gui, "MsRaster")
46
45
  self._raster_plot = RasterPlot()
47
- self._plot_data = None
46
+ self._plot_inputs = RasterPlotInputs()
47
+ self._plot_inputs.set_input('ms', self._ms_info['ms'])
48
48
 
49
49
  # Calculations for color limits
50
50
  self._spw_stats = {}
51
51
  self._spw_color_limits = {}
52
52
 
53
53
  if show_gui:
54
- # For checking if plot inputs or cursor position changed and new plot or cursor location info is needed.
55
- self._last_plot_inputs = None
56
- self._last_style_inputs = None
57
- self._last_cursor = None
58
- self._last_box = None
59
-
60
- # Return plot for gui DynamicMap:
61
- # Empty plot when ms not set or plot fails
54
+ # Return plot for gui DynamicMap: empty plot when ms not set or plot fails
62
55
  self._empty_plot = self._create_empty_plot()
63
56
 
64
57
  # Set default style and plot inputs to use when launching gui
@@ -66,7 +59,7 @@ class MsRaster(MsPlot):
66
59
  self.plot()
67
60
  self._launch_gui()
68
61
 
69
- # Set filename TextInput to input ms, which triggers default plot
62
+ # Set filename TextInput widget to input ms, which triggers default plot
70
63
  if 'ms' in self._ms_info and self._ms_info['ms']:
71
64
  self._set_filename([self._ms_info['ms']]) # function expects list from FileBrowser widget
72
65
 
@@ -97,10 +90,8 @@ class MsRaster(MsPlot):
97
90
  string_exact_match (bool): whether to require exact matches for string and string list columns (default True) or partial matches (False).
98
91
  query (str): a Pandas query string to apply additional filtering.
99
92
  **kwargs (dict): keyword arguments representing summary column names and values.
100
- See summary(data_group) and data_groups() for selection options. Use keyword 'data_group_name' for data group selection.
101
- Summary column names include 'name', 'intents', 'shape', 'polarization', 'scan_name', 'spw_name',
102
- 'field_name', 'source_name', 'field_coords', 'start_frequency', 'end_frequency'.
103
- The selection is applied to the data within the MeasurementSets where applicable (polarization, scan_name, field_name)
93
+ See data_groups() and summary(data_group) and for selection keyword options. Use keyword 'data_group_name' for data group selection.
94
+ The selection is also applied to the data within the MeasurementSets where keyword is a coordinate (polarization, scan_name, field_name) and
104
95
  string_exact_match=True, else the entire MS is selected.
105
96
  Selections are cumulative until clear_selection() is called.
106
97
  Raises exception with message if selection fails.
@@ -108,12 +99,10 @@ class MsRaster(MsPlot):
108
99
  For explanation and examples, see:
109
100
  https://xradio.readthedocs.io/en/latest/measurement_set/schema_and_api/measurement_set_api.html#xradio.measurement_set.ProcessingSetXdt.query
110
101
  '''
111
- if self._data and self._data.is_valid():
102
+ if self._ms_data and self._ms_data.is_valid():
112
103
  try:
113
- self._plot_inputs['selection'] |= kwargs
114
- if 'data_group_name' in kwargs:
115
- self._plot_inputs['data_group'] = kwargs['data_group_name']
116
- self._data.select_ps(query=query, string_exact_match=string_exact_match, **kwargs)
104
+ self._plot_inputs.set_selection(kwargs)
105
+ self._ms_data.select_ps(query=query, string_exact_match=string_exact_match, **kwargs)
117
106
  except KeyError as ke:
118
107
  error = "ProcessingSet selection yielded empty ProcessingSet."
119
108
  if not self._show_gui:
@@ -137,12 +126,10 @@ class MsRaster(MsPlot):
137
126
  For explanation of parameters and examples, see:
138
127
  https://xradio.readthedocs.io/en/latest/measurement_set/schema_and_api/measurement_set_api.html#xradio.measurement_set.MeasurementSetXdt.sel
139
128
  '''
140
- if self._data and self._data.is_valid():
129
+ if self._ms_data and self._ms_data.is_valid():
141
130
  try:
142
- self._plot_inputs['selection'] |= indexers_kwargs
143
- self._data.select_ms(indexers=indexers, method=method, tolerance=tolerance, drop=drop, **indexers_kwargs)
144
- if 'data_group_name' in indexers_kwargs:
145
- self._plot_inputs['data_group'] |= indexers_kwargs['data_group_name']
131
+ self._plot_inputs.set_selection(indexers_kwargs)
132
+ self._ms_data.select_ms(indexers=indexers, method=method, tolerance=tolerance, drop=drop, **indexers_kwargs)
146
133
  except KeyError as ke:
147
134
  error = str(ke).strip("\'")
148
135
  if not self._show_gui:
@@ -156,8 +143,8 @@ class MsRaster(MsPlot):
156
143
  iter_axis=None, iter_range=None, subplots=None, color_mode=None, color_range=None, title=None, clear_plots=True):
157
144
  '''
158
145
  Create a raster plot of vis_axis data.
159
- Plot axes include data dimensions (time, baseline/antenna, frequency, polarization).
160
- The first spectral window (by id) and dimensions not set as plot axes (first value) will be automatically selected if no user selection has been made, unless aggregated.
146
+ Plot axes include data dimensions (time, baseline/antenna_name, frequency, polarization).
147
+ The first spectral window by time and dimensions not set as plot axes will be automatically selected by first value if no user selection has been made, unless aggregated.
161
148
 
162
149
  Args:
163
150
  x_axis (str): Plot x-axis. Default 'baseline' ('antenna_name' for spectrum data).
@@ -188,9 +175,7 @@ class MsRaster(MsPlot):
188
175
 
189
176
  If plot is successful, use show() or save() to view/save the plot.
190
177
  '''
191
- inputs = locals() # collect arguments into dict (not unused as pylint complains!)
192
- if self._plot_inputs['selection']:
193
- self._logger.info("Create raster plot with selection: %s", self._plot_inputs['selection'])
178
+ inputs = locals() # collect arguments into dict
194
179
 
195
180
  start = time.time()
196
181
 
@@ -199,23 +184,28 @@ class MsRaster(MsPlot):
199
184
 
200
185
  # Get data dimensions if valid MS is set to check input axes
201
186
  if 'data_dims' in self._ms_info:
202
- inputs['data_dims'] = self._ms_info['data_dims'] if 'data_dims' in self._ms_info else None
187
+ data_dims = self._ms_info['data_dims'] if 'data_dims' in self._ms_info else None
188
+ inputs['data_dims'] = data_dims
203
189
 
204
- # Validate input arguments then set
205
- check_inputs(inputs)
206
- self._set_plot_inputs(inputs)
190
+ self._plot_inputs.set_inputs(inputs)
191
+ selection = self._plot_inputs.get_input('selection')
192
+ if selection:
193
+ self._logger.info("Create raster plot with selection: %s", selection)
194
+
195
+ # If previous plot was shown, unlink from data streams
196
+ super().unlink_plot_streams()
207
197
 
208
198
  if not self._show_gui:
209
199
  # Cannot plot if no MS
210
- if not self._data or not self._data.is_valid():
200
+ if not self._ms_data or not self._ms_data.is_valid():
211
201
  raise RuntimeError("Cannot plot MS: input MS path is invalid or missing.")
212
202
 
213
203
  # Create raster plot and add to plot list
214
204
  try:
215
- if self._plot_inputs['iter_axis']:
216
- self._do_iter_plot(self._plot_inputs)
205
+ if self._plot_inputs.get_input('iter_axis'):
206
+ self._do_iter_plot()
217
207
  else:
218
- plot = self._do_plot(self._plot_inputs)
208
+ plot = self._do_plot()
219
209
  self._plots.append(plot)
220
210
  except RuntimeError as e:
221
211
  error = f"Plot failed: {str(e)}"
@@ -248,137 +238,155 @@ class MsRaster(MsPlot):
248
238
  filename = f"{self._ms_info['basename']}_raster.png"
249
239
  super().save(filename, fmt, width, height)
250
240
 
251
- def _set_plot_inputs(self, plot_inputs):
252
- ''' Set inputs from latest plot() call '''
253
- for key, val in plot_inputs.items():
254
- self._plot_inputs[key] = val
255
-
256
- def _do_plot(self, plot_inputs):
241
+ def _do_plot(self, is_gui_plot=False):
257
242
  ''' Create plot using plot inputs '''
258
243
  if not self._plot_init:
259
- self._init_plot(plot_inputs)
244
+ self._init_plot()
260
245
 
261
246
  # Select vis_axis data to plot and update selection; returns xarray Dataset
262
- raster_data = self._data.get_raster_data(plot_inputs)
263
- self._plot_data = raster_data
247
+ raster_data = self._ms_data.get_raster_data(self._plot_inputs.get_inputs())
248
+
249
+ # Save plot data for plot location callbacks unless layout (location not supported)
250
+ if not self._plot_inputs.is_layout():
251
+ x_axis = self._plot_inputs.get_input('x_axis')
252
+ y_axis = self._plot_inputs.get_input('y_axis')
253
+ if is_gui_plot:
254
+ self._gui_plot_data = set_index_coordinates(raster_data, (x_axis, y_axis))
255
+ else:
256
+ self._plot_data = set_index_coordinates(raster_data, (x_axis, y_axis))
264
257
 
265
258
  # Add params needed for plot: auto color range and ms name
266
- self._set_auto_color_range(plot_inputs) # set calculated limits if auto mode
259
+ self._set_auto_color_range() # set calculated limits if auto mode
267
260
  ms_name = self._ms_info['basename'] # for title
261
+ plot_inputs = self._plot_inputs.get_inputs()
268
262
  self._raster_plot.set_plot_params(raster_data, plot_inputs, ms_name)
269
263
 
270
264
  # Show plot inputs in log
271
265
  super()._set_plot_params(plot_inputs | self._raster_plot.get_plot_params()['style'])
272
- self._logger.info("MsRaster plot parameters: %s", ", ".join(self._plot_params))
266
+ plot_params = [f"{key}={value}" for key, value in self._plot_params.items()]
267
+ self._logger.info("MsRaster plot parameters: %s", ", ".join(plot_params))
273
268
 
274
269
  # Make plot. Add data min/max if GUI is shown to update color limits range.
275
270
  return self._raster_plot.raster_plot(raster_data, self._logger, self._show_gui)
276
271
 
277
- def _do_iter_plot(self, plot_inputs):
272
+ def _do_iter_plot(self):
278
273
  ''' Create one plot per iteration value in iter_range which fits into subplots '''
279
274
  # Default (0, 0) (first iteration only). Use (0, -1) for all iterations.
280
275
  # If subplots is a grid, end iteration index is limited by the grid size.
281
276
  # If subplots is a single plot, all iteration plots in the range can be saved using export_range in save().
282
- iter_axis = plot_inputs['iter_axis']
283
- iter_range = plot_inputs['iter_range']
284
- subplots = plot_inputs['subplots']
285
-
286
- iter_range = (0, 0) if iter_range is None else iter_range
287
- start_idx, end_idx = iter_range
277
+ iter_axis = self._plot_inputs.get_input('iter_axis')
278
+ iter_range = self._plot_inputs.get_input('iter_range')
279
+ subplots = self._plot_inputs.get_input('subplots')
288
280
 
289
281
  # Init plot before getting iter values
290
- self._init_plot(plot_inputs)
291
-
292
- iter_values = self._data.get_dimension_values(iter_axis)
282
+ self._init_plot()
283
+ iter_values = self._ms_data.get_dimension_values(iter_axis)
293
284
  n_iter = len(iter_values)
294
285
 
286
+ iter_range = (0, 0) if iter_range is None else iter_range
287
+ start_idx, end_idx = iter_range
288
+ auto_range = end_idx == -1
289
+
295
290
  if start_idx >= n_iter:
296
291
  raise IndexError(f"iter_range start {start_idx} is greater than number of iterations {n_iter}")
297
292
  end_idx = n_iter if (end_idx == -1 or end_idx >= n_iter) else end_idx + 1
298
293
  num_iter_plots = end_idx - start_idx
299
294
 
300
- # Plot the minimum of iter range or subplots number of plots
295
+ # Plot the minimum of iter range or subplots number of plots.
296
+ # If subplots is single plot, plot all for save()
301
297
  num_subplots = np.prod(subplots) if subplots else 1
302
298
  num_iter_plots = min(num_iter_plots, num_subplots) if num_subplots > 1 else num_iter_plots
303
299
  end_idx = start_idx + num_iter_plots
304
300
 
305
- # Set each iter value in ms_selection
306
- if 'ms_selection' not in plot_inputs:
307
- plot_inputs['ms_selection'] = {}
301
+ if auto_range:
302
+ # For listing plot inputs
303
+ start_idx = start_idx.item() if isinstance(start_idx, np.int64) else start_idx
304
+ end_idx = end_idx.item() if isinstance(end_idx, np.int64) else end_idx
305
+ self._plot_inputs.set_input('auto_iter_range', (start_idx, end_idx - 1))
308
306
 
309
307
  for i in range(start_idx, end_idx):
310
308
  # Select iteration value and make plot
311
309
  value = iter_values[i]
312
310
  self._logger.info("Plot %s iteration index %s value %s", iter_axis, i, value)
313
- plot_inputs['selection'][iter_axis] = value
311
+ self._plot_inputs.set_selection({iter_axis: value})
314
312
  try:
315
- plot = self._do_plot(plot_inputs)
313
+ plot = self._do_plot()
316
314
  self._plots.append(plot)
317
315
  except RuntimeError as e:
318
316
  self._logger.info("Iteration plot for value %s failed: %s", str(value), str(e))
319
317
  continue
320
318
 
321
- def _init_plot(self, plot_inputs):
319
+ def _init_plot(self):
322
320
  ''' Apply automatic selection '''
323
321
  # Remove previous auto selections
324
322
  for key in ['dim_selection', 'auto_spw']:
325
- try:
326
- del self._plot_inputs[key]
327
- except KeyError:
328
- pass
323
+ self._plot_inputs.remove_input(key)
329
324
 
330
325
  # Automatically select data group and spw name if not user-selected
331
326
  auto_selection = {}
332
- if 'data_group' not in plot_inputs:
327
+ if not self._plot_inputs.get_input('data_group'):
333
328
  auto_selection['data_group_name'] = 'base'
334
- plot_inputs['data_group'] = 'base'
335
- if 'selection' not in plot_inputs or 'spw_name' not in plot_inputs['selection']:
336
- first_spw = self._data.get_first_spw(plot_inputs['data_group'])
329
+ self._plot_inputs.set_input('data_group', 'base')
330
+
331
+ data_group = self._plot_inputs.get_input('data_group')
332
+ spw_selection = self._plot_inputs.get_selection('spw_name')
333
+ if not spw_selection:
334
+ first_spw = self._ms_data.get_first_spw(data_group)
337
335
  auto_selection['spw_name'] = first_spw
338
- plot_inputs['auto_spw'] = first_spw # do not add to user selection
336
+ self._plot_inputs.set_input('auto_spw', first_spw) # keep separate from user selection
339
337
 
340
338
  if auto_selection:
341
339
  # Do selection and save to plot inputs
342
340
  self._logger.info("Automatic selection of data group and/or spw: %s", auto_selection)
343
- self._data.select_ps(query=None, string_exact_match=True, **auto_selection)
341
+ self._ms_data.select_ps(query=None, string_exact_match=True, **auto_selection)
344
342
 
345
343
  # Print data info for spw selection
346
- self._logger.info("Plotting %s msv4 datasets.", self._data.get_num_ms())
347
- self._logger.info("Maximum dimensions for selected spw: %s", self._data.get_max_data_dims())
344
+ self._logger.info("Plotting %s msv4 datasets.", self._ms_data.get_num_ms())
345
+ self._logger.info("Maximum dimensions for selected spw: %s", self._ms_data.get_max_data_dims())
348
346
  self._plot_init = True
349
347
 
350
- def _set_auto_color_range(self, plot_inputs):
348
+ def _set_auto_color_range(self):
351
349
  ''' Calculate stats for color limits for non-gui amplitude plots. '''
352
- color_mode = plot_inputs['color_mode']
353
- color_limits = None
350
+ color_mode = self._plot_inputs.get_input('color_mode')
351
+ auto_color_limits = None
354
352
 
355
353
  if color_mode == 'auto':
356
- if plot_inputs['vis_axis']=='amp' and not plot_inputs['aggregator']:
354
+ if self._plot_inputs.get_input('vis_axis') == 'amp' and not self._plot_inputs.get_input('aggregator'):
357
355
  # For amplitude, limit colorbar range using stored per-spw ms stats
358
- spw_name = self._plot_inputs['selection']['spw_name'] if ('selection' in plot_inputs and 'spw_name' in plot_inputs['selection']) else self._plot_inputs['auto_spw']
356
+ spw_name = self._plot_inputs.get_selection('spw_name')
357
+ if not spw_name:
358
+ spw_name = self._plot_inputs.get_input('auto_spw')
359
+
359
360
  if spw_name in self._spw_color_limits:
360
- color_limits = self._spw_color_limits[spw_name]
361
+ auto_color_limits = self._spw_color_limits[spw_name]
361
362
  else:
362
363
  # Select spw name and data group only, no dimensions
363
- data_group = self._plot_inputs['data_group']
364
+ data_group = self._plot_inputs.get_input('data_group')
364
365
  spw_data_selection = {'spw_name': spw_name, 'data_group_name': data_group}
365
- color_limits = self._calc_amp_color_limits(spw_data_selection)
366
- self._spw_color_limits[spw_name] = color_limits
367
- plot_inputs['auto_color_range'] = color_limits
368
-
369
- if color_limits:
370
- self._logger.info("Setting amplitude color range: (%.4f, %.4f).", color_limits[0], color_limits[1])
366
+ auto_color_limits = self._calc_amp_color_limits(spw_data_selection)
367
+
368
+ if auto_color_limits:
369
+ # Convert to float for listing plot inputs
370
+ start, end = auto_color_limits
371
+ start = start.item() if isinstance(start, np.float64) else start
372
+ end = end.item() if isinstance(end, np.float64) else end
373
+ auto_color_limits = (start, end)
374
+ self._spw_color_limits[spw_name] = auto_color_limits
375
+ self._plot_inputs.set_input('auto_color_range', auto_color_limits)
376
+
377
+ if auto_color_limits:
378
+ self._logger.info("Setting amplitude color range: (%.4f, %.4f).", auto_color_limits[0], auto_color_limits[1])
371
379
  elif color_mode is None:
372
380
  self._logger.info("Autoscale color range")
373
381
  else:
374
- self._logger.info("Using manual color range: %s", plot_inputs['color_range'])
382
+ self._logger.info("Using manual color range: %s", self._plot_inputs.get_input('color_range'))
375
383
 
376
384
  def _calc_amp_color_limits(self, selection):
377
385
  # Calculate colorbar limits from amplitude stats for unflagged data in selected spw
378
386
  self._logger.info("Calculating stats for colorbar limits.")
379
387
  start = time.time()
380
388
 
381
- ms_stats = self._data.get_vis_stats(selection, 'amp')
389
+ ms_stats = self._ms_data.get_vis_stats(selection, 'amp')
382
390
  self._spw_stats['spw_name'] = ms_stats
383
391
  if not ms_stats:
384
392
  return None # autoscale
@@ -403,7 +411,7 @@ class MsRaster(MsPlot):
403
411
  if clear_plots:
404
412
  super().clear_plots()
405
413
 
406
- # Clear params set for last plot
414
+ # Clear params for last plot
407
415
  self._raster_plot.reset_plot_params()
408
416
 
409
417
  # Unitialize plot to redo auto selections if needed
@@ -429,8 +437,8 @@ class MsRaster(MsPlot):
429
437
  'update_plot': self._update_plot,
430
438
  }
431
439
  data_dims = self._ms_info['data_dims'] if 'data_dims' in self._ms_info else None
432
- x_axis = self._plot_inputs['x_axis']
433
- y_axis = self._plot_inputs['y_axis']
440
+ x_axis = self._plot_inputs.get_input('x_axis')
441
+ y_axis = self._plot_inputs.get_input('y_axis')
434
442
  self._gui_layout = create_raster_gui(callbacks, data_dims, x_axis, y_axis)
435
443
  self._gui_layout.show(title=self._app_name, threaded=True)
436
444
 
@@ -438,13 +446,14 @@ class MsRaster(MsPlot):
438
446
  ### Main callback to create plot if inputs changed
439
447
  ###
440
448
  # pylint: disable=too-many-arguments, too-many-positional-arguments
441
- def _update_plot(self, ms, do_plot, x, y, bounds):
449
+ def _update_plot(self, ms, do_plot, x, y, data, bounds):
442
450
  ''' Create plot with inputs from GUI, or update cursor/box location.
443
451
  Callbacks:
444
452
  ms (first plot) - return plot with default inputs
445
453
  do_plot (Plot button clicked) - return plot with inputs from GUI
446
- x, y (cursor position) - update cursor location box
447
- bounds (box select) - update location of points in box in tab and log
454
+ x, y (PointerXY) - update cursor location box
455
+ data (PointDraw from point_draw tool) - update location of drawn points in tab and log
456
+ bounds (Bounds from box_select tool) - update location of points in box in tab and log
448
457
  This function *must* return plot, even if empty plot or last plot, for DynamicMap.
449
458
  '''
450
459
  # Remove toast notification and collapse selection accordion
@@ -458,40 +467,36 @@ class MsRaster(MsPlot):
458
467
 
459
468
  # User changed inputs without clicking Plot button, or callback for cursor/box.
460
469
  if not do_plot and not self._first_gui_plot:
461
- if self._cursor_changed(x, y):
462
- # new cursor position - update cursor location box
463
- self._update_cursor_location(x, y)
464
- self._last_cursor = (x, y)
465
-
466
- if self._box_changed(bounds):
467
- # new box_select position - update location for points in box
468
- self._update_box_location(bounds)
469
- self._last_box = bounds
470
-
470
+ # Locate callbacks
471
+ super()._locate_cursor(x, y, self._gui_plot_data, self._gui_layout[0])
472
+ super()._locate_points(data, self._gui_plot_data, self._gui_layout[0])
473
+ super()._locate_box(bounds, self._gui_plot_data, self._gui_layout[0])
471
474
  # Not ready to update plot yet, return last plot.
472
- return self._last_gui_plot
475
+ return self._last_plot
473
476
 
474
477
  # First plot for input ms, or user clicked Plot button for new inputs
475
- if (self._set_ms(ms) or self._first_gui_plot) and self._data and self._data.is_valid():
478
+ if (self._set_ms(ms) or self._first_gui_plot) and self._ms_data and self._ms_data.is_valid():
476
479
  # New MS set and is valid
477
480
  self._update_gui_axis_options()
478
481
 
479
482
  # Add ms path to detect change and make new plot
480
- self._plot_inputs['ms'] = ms
483
+ self._plot_inputs.set_input('ms', ms)
481
484
 
482
485
  # Do new plot or resend last plot
483
486
  self._reset_plot()
484
487
  self.clear_selection()
485
488
  gui_plot = None
486
489
 
490
+ # Make plot if first plot or changed plot
487
491
  style_inputs = self._raster_plot.get_plot_params()['style']
488
- if self._inputs_changed(style_inputs):
489
- # First plot or changed plot
492
+ plot_inputs = self._plot_inputs.get_inputs()
493
+ if inputs_changed(plot_inputs, self._last_plot_inputs) or inputs_changed(style_inputs, self._last_style_inputs):
490
494
  try:
491
495
  # Check inputs from GUI then plot
492
- self._plot_inputs['data_dims'] = self._ms_info['data_dims']
493
- check_inputs(self._plot_inputs)
494
- if 'ps_selection' in self._plot_inputs or 'ms_selection' in self._plot_inputs:
496
+ data_dims = self._ms_info['data_dims']
497
+ self._plot_inputs.set_input('data_dims', data_dims)
498
+ self._plot_inputs.check_inputs()
499
+ if 'ps_selection' in self._gui_selection or 'ms_selection' in self._gui_selection:
495
500
  self._do_gui_selection()
496
501
  gui_plot = self._do_gui_plot()
497
502
  except (ValueError, TypeError, KeyError, RuntimeError) as e:
@@ -500,101 +505,57 @@ class MsRaster(MsPlot):
500
505
  gui_plot = self._empty_plot
501
506
  else:
502
507
  # Subparam values changed but not applied to plot
503
- gui_plot = self._last_gui_plot
508
+ gui_plot = self._last_plot
504
509
 
505
510
  # Update plot inputs for gui
506
- self._set_plot_params(self._plot_inputs | style_inputs)
511
+ self._set_plot_params(plot_inputs | style_inputs)
507
512
 
508
513
  # Save inputs to see if changed next time
509
- self._last_plot_inputs = self._plot_inputs.copy()
514
+ self._last_plot_inputs = plot_inputs.copy()
510
515
  self._last_style_inputs = style_inputs.copy()
511
- self._last_plot_inputs['ps_selection'] = self._plot_inputs['ps_selection'].copy()
512
- self._last_plot_inputs['ms_selection'] = self._plot_inputs['ms_selection'].copy()
513
- if self._first_gui_plot and not do_plot:
514
- self._plot_inputs['ps_selection'].clear()
515
- self._plot_inputs['ms_selection'].clear()
516
516
 
517
517
  # Save plot for return value if plot update not requested
518
- self._last_gui_plot = gui_plot
518
+ self._last_plot = gui_plot
519
519
  self._first_gui_plot = False
520
520
 
521
- # Add plot inputs, change plot button to outline, and stop spinner
522
- self._update_plot_inputs()
521
+ # Add plot inputs to GUI, change plot button to outline, and stop spinner
522
+ self._show_plot_inputs()
523
523
  self._update_plot_status(False)
524
524
  self._update_plot_spinner(False)
525
525
 
526
526
  return gui_plot
527
527
  # pylint: enable=too-many-arguments, too-many-positional-arguments
528
528
 
529
- def _cursor_changed(self, x, y):
530
- ''' Check whether cursor position changed '''
531
- if not x and not y:
532
- return False # not cursor callback
533
- if self._last_cursor and self._last_cursor == (x, y):
534
- return False # same cursor
535
- return True # new cursor or cursor changed
536
-
537
- def _box_changed(self, bounds):
538
- ''' Check whether box position changed '''
539
- if not bounds:
540
- return False # no data, not box select callback
541
- if self._last_box and self._last_box == bounds:
542
- return False # same box
543
- return True # new box or box changed
544
-
545
- def _inputs_changed(self, style_inputs):
546
- ''' Check if inputs changed and need new plot '''
547
- if not self._last_plot_inputs:
548
- return True
549
-
550
- for key, val in self._plot_inputs.items():
551
- if not self._values_equal(val, self._last_plot_inputs[key]):
552
- return True
553
-
554
- for key, val in style_inputs.items():
555
- if not self._values_equal(val, self._last_style_inputs[key]):
556
- return True
557
- return False
558
-
559
- def _values_equal(self, val1, val2):
560
- ''' Test if values are set and equal, or not set (cannot compare value with None) '''
561
- if val1 is not None and val2 is not None: # both set
562
- return val1 == val2
563
- if val1 is None and val2 is None: # both None
564
- return True
565
- return False # one set and other is None
566
-
567
529
  def _do_gui_selection(self):
568
530
  ''' Apply selections selected in GUI '''
569
- if self._plot_inputs['ps_selection']:
570
- self.select_ps(**self._plot_inputs['ps_selection'])
571
- if self._plot_inputs['ms_selection']:
572
- self.select_ms(**self._plot_inputs['ms_selection'])
531
+ if self._gui_selection['ps_selection']:
532
+ self.select_ps(**self._gui_selection['ps_selection'])
533
+ if self._gui_selection['ms_selection']:
534
+ self.select_ms(**self._gui_selection['ms_selection'])
573
535
 
574
536
  ###
575
537
  ### Create plot for DynamicMap
576
538
  ###
577
539
  def _do_gui_plot(self):
578
540
  ''' Create plot based on gui plot inputs '''
579
- if self._data and self._data.is_valid():
541
+ if self._ms_data and self._ms_data.is_valid():
580
542
  try:
581
- if self._plot_inputs['iter_axis']:
543
+ if self._plot_inputs.get_input('iter_axis'):
582
544
  # Make iter plot (possibly with subplots layout)
583
- self._do_iter_plot(self._plot_inputs)
584
- subplots = self._plot_inputs['subplots']
545
+ self._do_iter_plot()
546
+ subplots = self._plot_inputs.get_input('subplots')
585
547
  layout_plot, is_layout = super()._layout_plots(subplots)
586
-
587
548
  if is_layout:
588
549
  # Cannot show Layout in DynamicMap, show in new tab
589
550
  super().show()
590
551
  self._logger.info("Plot update complete")
591
- return self._last_gui_plot
552
+ return self._last_plot
592
553
  # Overlay raster plot for DynamicMap
593
554
  self._logger.info("Plot update complete")
594
555
  return layout_plot
595
556
 
596
557
  # Make single Overlay raster plot for DynamicMap
597
- plot = self._do_plot(self._plot_inputs)
558
+ plot = self._do_plot(True)
598
559
  plot_params = self._raster_plot.get_plot_params()
599
560
 
600
561
  # Update color limits in gui with data range
@@ -606,7 +567,11 @@ class MsRaster(MsPlot):
606
567
 
607
568
  self._logger.info("Plot update complete")
608
569
  return plot.opts(
609
- hv.opts.QuadMesh(tools=['hover', 'box_select'])
570
+ hv.opts.QuadMesh(
571
+ tools=['hover', 'box_select'],
572
+ selection_fill_alpha=0.2, # dim selected areas of plot
573
+ nonselection_fill_alpha=1.0, # do not dim unselected areas of plot
574
+ )
610
575
  )
611
576
  except RuntimeError as e:
612
577
  error = f"Plot failed: {str(e)}"
@@ -631,7 +596,11 @@ class MsRaster(MsPlot):
631
596
  )
632
597
  )
633
598
  return plot.opts(
634
- hv.opts.QuadMesh(tools=['hover', 'box_select'])
599
+ hv.opts.QuadMesh(
600
+ tools=['hover', 'box_select'],
601
+ selection_fill_alpha=0.5, # dim selected areas of plot
602
+ nonselection_fill_alpha=1.0, # do not dim unselected areas of plot
603
+ )
635
604
  )
636
605
 
637
606
  def _set_plot_colorbar(self, plot, plot_params, plot_type):
@@ -712,7 +681,7 @@ class MsRaster(MsPlot):
712
681
  # Update options for vis_axis selector
713
682
  vis_axis_selector = axis_selectors.objects[2]
714
683
  vis_axis_value = vis_axis_selector.value
715
- if self._data.get_correlated_data('base') == 'SPECTRUM':
684
+ if self._ms_data.get_correlated_data('base') == 'SPECTRUM':
716
685
  vis_axis_selector.options = SPECTRUM_AXIS_OPTIONS
717
686
  else:
718
687
  vis_axis_selector.options = VIS_AXIS_OPTIONS
@@ -738,8 +707,8 @@ class MsRaster(MsPlot):
738
707
 
739
708
  def _update_ps_selection_options(self, ps_selectors):
740
709
  ''' Set ProcessingSet gui options from ms summary '''
741
- if self._data and self._data.is_valid():
742
- summary = self._data.get_summary()
710
+ if self._ms_data and self._ms_data.is_valid():
711
+ summary = self._ms_data.get_summary()
743
712
  if summary is None:
744
713
  return
745
714
 
@@ -756,7 +725,7 @@ class MsRaster(MsPlot):
756
725
 
757
726
  def _update_ms_selection_options(self, ms_selectors):
758
727
  ''' Set MeasurementSet gui options from ms data '''
759
- if self._data and self._data.is_valid():
728
+ if self._ms_data and self._ms_data.is_valid():
760
729
  for selector in ms_selectors:
761
730
  selection_key = MS_SELECTION_OPTIONS[selector.name] if selector.name in MS_SELECTION_OPTIONS else None
762
731
  if selection_key:
@@ -789,7 +758,7 @@ class MsRaster(MsPlot):
789
758
  ''' Set up player with values when iter_axis is selected '''
790
759
  iter_axis = None if iter_axis == 'None' else iter_axis
791
760
  if iter_axis and self._gui_layout:
792
- iter_values = self._data.get_dimension_values(iter_axis)
761
+ iter_values = self._ms_data.get_dimension_values(iter_axis)
793
762
  if iter_values:
794
763
 
795
764
  iter_selectors = self._get_selector('iter')
@@ -819,7 +788,7 @@ class MsRaster(MsPlot):
819
788
 
820
789
  def _get_datetime_values(self, float_times):
821
790
  ''' Return list of float time values as list of datetime values for gui options '''
822
- time_attrs = self._data.get_dimension_attrs('time')
791
+ time_attrs = self._ms_data.get_dimension_attrs('time')
823
792
  datetime_values = []
824
793
  try:
825
794
  datetime_values = to_datetime(float_times, unit=time_attrs['units'], origin=time_attrs['format'])
@@ -834,92 +803,24 @@ class MsRaster(MsPlot):
834
803
  spinner = self._gui_layout[2][2][1]
835
804
  spinner.value = plot_clicked
836
805
 
837
- def _update_plot_status(self, inputs_changed):
838
- ''' Change button color when inputs change. '''
806
+ def _update_plot_status(self, plot_changed):
807
+ ''' Change button color when plot inputs change. '''
839
808
  if self._gui_layout:
840
809
  # Set button color
841
810
  button = self._gui_layout[2][2][0]
842
- button.button_style = 'solid' if inputs_changed else 'outline'
843
-
844
- def _update_plot_inputs(self):
845
- ''' Show inputs for raster plot in GUI '''
846
- if self._plot_params:
847
- inputs_column = self._gui_layout[0][1]
848
- inputs_column.clear()
849
- for param in self._plot_params:
850
- str_pane = pn.pane.Str(param)
851
- str_pane.margin = (0, 10)
852
- inputs_column.append(str_pane)
853
-
854
- def _update_cursor_location(self, x, y):
855
- ''' Show data values for cursor x,y position in cursor location box below plot '''
856
- # Convert plot values to selection values to select plot data
857
- x_axis = self._plot_inputs['x_axis']
858
- y_axis = self._plot_inputs['y_axis']
859
- plot_data = set_index_coordinates(self._plot_data, (x_axis, y_axis))
860
- cursor_position = {x_axis: x, y_axis: y}
861
- cursor_location = locate_point(plot_data, cursor_position, self._plot_inputs['vis_axis'])
862
- self._update_cursor_box(cursor_location)
863
-
864
- def _update_cursor_box(self, cursor_location):
865
- ''' Update cursor location widget box with list of Panel StaticText widgets '''
866
- cursor_location_box = self._gui_layout[0][0][1] # row 0 tab 0 row 1 in column
867
- cursor_location_box.clear() # pn.WidgetBox
868
- location_layout = pn.Column(pn.widgets.StaticText(name="Cursor Location"))
869
- location_row = self._layout_point_location(cursor_location)
870
- # Add row of columns to column layout
871
- location_layout.append(location_row)
872
- # Add column layout to widget box
873
- cursor_location_box.append(location_layout)
874
-
875
- def _layout_point_location(self, text_list):
876
- ''' Layout list of StaticText in row of columns containing 3 rows '''
877
- location_row = pn.Row()
878
- location_col = pn.Column()
879
-
880
- for static_text in text_list:
881
- # 3 entries per column; append to row and start new column
882
- if len(location_col.objects) == 3:
883
- location_row.append(location_col)
884
- location_col = pn.Column()
885
-
886
- static_text.margin = (0, 10) # default (5, 10)
887
- location_col.append(static_text)
888
-
889
- # Add last column
890
- location_row.append(location_col)
891
- return location_row
892
-
893
- def _update_box_location(self, bounds):
894
- ''' Show data values for points in box in box location tab and log '''
895
- x_axis = self._plot_inputs['x_axis']
896
- y_axis = self._plot_inputs['y_axis']
897
- plot_data = set_index_coordinates(self._plot_data, (x_axis, y_axis))
898
- box_bounds = {x_axis: (bounds[0], bounds[2]), y_axis: (bounds[1], bounds[3])}
899
- npoints, point_locations = locate_box(plot_data, box_bounds, self._plot_inputs['vis_axis'])
900
-
901
- locate_column = self._gui_layout[0][2] # row 0 tab 2
902
- locate_column.clear()
903
- message = f"Locate {npoints} points"
904
- message += " (only first 100 shown):" if npoints > 100 else ":"
905
- self._logger.info(message)
906
-
907
- for point in point_locations:
908
- # Format and add to locate column
909
- location_layout = self._layout_point_location(point)
910
- locate_column.append(location_layout)
911
- locate_column.append(pn.layout.Divider())
912
-
913
- # Format and add to log
914
- location_list = [f"{static_text.name}={static_text.value}" for static_text in point]
915
- self._logger.info(", ".join(location_list))
811
+ button.button_style = 'solid' if plot_changed else 'outline'
812
+
813
+ def _show_plot_inputs(self):
814
+ ''' Show inputs for raster plot in column in GUI tab '''
815
+ inputs_column = self._gui_layout[0][1]
816
+ super()._fill_inputs_column(inputs_column)
916
817
 
917
818
  ###
918
819
  ### Callbacks for widgets which update plot inputs
919
820
  ###
920
821
  def _set_title(self, title):
921
822
  ''' Set title from gui text input '''
922
- self._plot_inputs['title'] = title
823
+ self._plot_inputs.set_input('title', title)
923
824
  self._update_plot_status(True) # Change plot button to solid
924
825
 
925
826
  def _set_style_params(self, unflagged_cmap, flagged_cmap, show_colorbar, show_flagged_colorbar):
@@ -928,67 +829,59 @@ class MsRaster(MsPlot):
928
829
 
929
830
  def _set_color_range(self, color_mode, color_range):
930
831
  ''' Set style params from gui '''
931
- color_mode = color_mode.split()[0]
932
- color_mode = None if color_mode == 'No' else color_mode
933
- self._plot_inputs['color_mode'] = color_mode
934
- self._plot_inputs['color_range'] = color_range
832
+ self._plot_inputs.set_color_inputs(color_mode, color_range)
935
833
  self._update_plot_status(True) # Change plot button to solid
936
834
 
937
835
  def _set_axes(self, x_axis, y_axis, vis_axis):
938
- ''' Set plot axis params from gui '''
939
- self._plot_inputs['x_axis'] = x_axis
940
- self._plot_inputs['y_axis'] = y_axis
941
- self._plot_inputs['vis_axis'] = vis_axis
836
+ ''' Set plot axis inputs from gui '''
837
+ self._plot_inputs.set_axis_inputs(x_axis, y_axis, vis_axis)
942
838
  self._update_plot_status(True) # Change plot button to solid
943
839
 
944
840
  def _set_aggregation(self, aggregator, agg_axes):
945
841
  ''' Set aggregation params from gui '''
946
- aggregator = None if aggregator== 'None' else aggregator
947
- self._plot_inputs['aggregator'] = aggregator
948
- self._plot_inputs['agg_axis'] = agg_axes # ignored if aggregator not set
842
+ self._plot_inputs.set_aggregation_inputs(aggregator, agg_axes)
949
843
  self._update_plot_status(True) # Change plot button to solid
950
844
 
951
845
  # pylint: disable=too-many-arguments, too-many-positional-arguments
952
846
  def _set_iteration(self, iter_axis, iter_value_type, iter_value, iter_start, iter_end, subplot_rows, subplot_columns):
953
847
  ''' Set iteration params from gui '''
954
848
  iter_axis = None if iter_axis == 'None' else iter_axis
955
- self._plot_inputs['iter_axis'] = iter_axis
956
- self._plot_inputs['subplots'] = (subplot_rows, subplot_columns)
957
-
849
+ iter_range = None
958
850
  if iter_axis:
851
+ # Set iter_range
959
852
  if iter_value_type == 'By Value':
960
853
  # Use index of iter_value for tuple
961
- if self._data and self._data.is_valid():
962
- iter_values = self._data.get_dimension_values(iter_axis)
854
+ if self._ms_data and self._ms_data.is_valid():
855
+ iter_values = self._ms_data.get_dimension_values(iter_axis)
963
856
  iter_index = iter_values.index(iter_value)
964
- self._plot_inputs['iter_range'] = (iter_index, iter_index)
857
+ iter_range = (iter_index, iter_index)
965
858
  else:
966
859
  # 'By Range': use range start and end values for tuple
967
- self._plot_inputs['iter_range'] = (iter_start, iter_end)
968
- else:
969
- self._plot_inputs['iter_range'] = None
860
+ iter_range = (iter_start, iter_end)
861
+
862
+ self._plot_inputs.set_iteration_inputs(iter_axis, iter_range, subplot_rows, subplot_columns)
970
863
  self._update_plot_status(True) # Change plot button to solid
971
864
  # pylint: enable=too-many-arguments, too-many-positional-arguments
972
865
 
973
866
  # pylint: disable=too-many-arguments, too-many-positional-arguments, unused-argument
974
867
  def _set_ps_selection(self, query, name, intents, scan_name, spw_name, field_name, source_name, line_name):
975
- ''' Select ProcessingSet from gui using summary columns '''
868
+ ''' Set ProcessingSet selection from gui using summary columns '''
976
869
  inputs = locals()
977
870
  ps_selection = {}
978
871
  for key, val in inputs.items():
979
872
  if key in PS_SELECTION_OPTIONS.values() and val:
980
873
  ps_selection[key] = val
981
- self._plot_inputs['ps_selection'] = ps_selection
874
+ self._gui_selection['ps_selection'] = ps_selection
982
875
  self._update_plot_status(True) # Change plot button to solid
983
876
 
984
877
  def _set_ms_selection(self, data_group, datetime, baseline, antenna1, antenna2, frequency, polarization):
985
- ''' Select MeasurementSet from gui using data group, dimensions, and coordinates '''
878
+ ''' Set MeasurementSet selection from gui using data group, dimensions, and coordinates '''
986
879
  inputs = locals()
987
880
  inputs['time'] = inputs.pop('datetime')
988
881
  ms_selection = {}
989
882
  for key, val in inputs.items():
990
883
  if key in MS_SELECTION_OPTIONS.values() and val:
991
884
  ms_selection[key] = val
992
- self._plot_inputs['ms_selection'] = ms_selection
885
+ self._gui_selection['ms_selection'] = ms_selection
993
886
  self._update_plot_status(True) # Change plot button to solid
994
887
  # pylint: enable=too-many-arguments, too-many-positional-arguments, unused-argument