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.
@@ -0,0 +1,107 @@
1
+ '''
2
+ Check inputs to MsRaster plot() or its GUI
3
+ '''
4
+
5
+ from vidavis.plot.ms_plot._ms_plot_constants import VIS_AXIS_OPTIONS, AGGREGATOR_OPTIONS
6
+
7
+ def check_inputs(inputs):
8
+ ''' Check plot input types, and axis (plot, agg, iter) input values. '''
9
+ _set_baseline_antenna_axis(inputs)
10
+ _check_axis_inputs(inputs)
11
+ _check_agg_inputs(inputs)
12
+ _check_color_inputs(inputs)
13
+ _check_other_inputs(inputs)
14
+
15
+ def _set_baseline_antenna_axis(inputs):
16
+ ''' Set baseline axis to dimension in data_dims '''
17
+ if 'data_dims' not in inputs:
18
+ return
19
+
20
+ data_dims = inputs['data_dims']
21
+ baseline_dim = 'antenna_name' if 'antenna_name' in data_dims else 'baseline'
22
+
23
+ # Convert baseline axis to existing baseline dimension
24
+ baseline_dims = ['baseline', 'antenna_name']
25
+ for axis in ['x_axis', 'y_axis', 'iter_axis', 'agg_axis']:
26
+ if inputs[axis] in baseline_dims:
27
+ inputs[axis] = baseline_dim
28
+
29
+ def _check_axis_inputs(inputs):
30
+ ''' Check x_axis, y_axis, vis_axis, and iter_axis inputs. '''
31
+ x_axis = inputs['x_axis']
32
+ y_axis = inputs['y_axis']
33
+ if x_axis == y_axis:
34
+ raise ValueError(f"Invalid parameter values: x_axis {x_axis} cannot be same as y_axis {y_axis}.")
35
+
36
+ iter_axis = inputs['iter_axis']
37
+ if iter_axis and iter_axis in (x_axis, y_axis):
38
+ raise ValueError(f"Invalid parameter value: iter_axis {iter_axis} cannot be x_axis ({x_axis}) or y_axis ({y_axis}).")
39
+
40
+ data_dims = inputs['data_dims'] if 'data_dims' in inputs else None
41
+ if data_dims:
42
+ if x_axis not in data_dims or y_axis not in data_dims:
43
+ raise ValueError(f"Invalid parameter value: x and y axis must be a data dimension in {data_dims}.")
44
+ if iter_axis and iter_axis not in data_dims:
45
+ raise ValueError(f"Invalid parameter value: iter_axis {iter_axis} must be a data dimension in {data_dims}.")
46
+
47
+ vis_axis = inputs['vis_axis']
48
+ if vis_axis not in VIS_AXIS_OPTIONS:
49
+ raise ValueError(f"Invalid parameter value: vis_axis {vis_axis} must be one of {VIS_AXIS_OPTIONS}")
50
+
51
+ def _check_agg_inputs(inputs):
52
+ ''' Check aggregator and agg_axis. Set agg_axis if not set. '''
53
+ aggregator = inputs['aggregator']
54
+ agg_axis = inputs['agg_axis']
55
+
56
+ x_axis = inputs['x_axis']
57
+ y_axis = inputs['y_axis']
58
+ data_dims = inputs['data_dims'] if 'data_dims' in inputs else None
59
+
60
+ if aggregator and aggregator not in AGGREGATOR_OPTIONS:
61
+ raise ValueError(f"Invalid parameter value: aggregator {aggregator} must be None or one of {AGGREGATOR_OPTIONS}.")
62
+
63
+ if agg_axis:
64
+ if not isinstance(agg_axis, str) and not isinstance(agg_axis, list):
65
+ raise TypeError(f"Invalid parameter type: agg_axis {agg_axis} must be str or list.")
66
+
67
+ # Make agg_axis a list
68
+ if isinstance(agg_axis, str):
69
+ agg_axis = [agg_axis]
70
+
71
+ for axis in agg_axis:
72
+ if axis in (x_axis, y_axis):
73
+ raise ValueError(f"Invalid parameter value: agg_axis {agg_axis} cannot be x_axis ({x_axis}) or y_axis ({y_axis}).")
74
+ if data_dims and axis not in data_dims:
75
+ raise ValueError(f"Invalid parameter value: agg_axis {axis} must be a data dimension in {data_dims}.")
76
+ elif aggregator and data_dims:
77
+ # Set agg_axis to non-plotted dim axes
78
+ agg_axis = data_dims.copy()
79
+ agg_axis.remove(x_axis)
80
+ agg_axis.remove(y_axis)
81
+ if 'iter_axis' in inputs and inputs['iter_axis']:
82
+ agg_axis.remove(inputs['iter_axis'])
83
+ inputs['agg_axis'] = agg_axis
84
+
85
+ def _check_color_inputs(inputs):
86
+ if inputs['color_mode']:
87
+ color_mode = inputs['color_mode'].lower()
88
+ valid_color_modes = ['auto', 'manual']
89
+ if color_mode not in valid_color_modes:
90
+ raise ValueError(f"Invalid parameter value: color_mode {color_mode} must be None or one of {valid_color_modes}.")
91
+ inputs['color_mode'] = color_mode
92
+
93
+ if inputs['color_range']:
94
+ if not (isinstance(inputs['color_range'], tuple) and len(inputs['color_range']) == 2):
95
+ raise ValueError("Invalid parameter type: color_range must be None or a tuple of (min, max).")
96
+
97
+ def _check_other_inputs(inputs):
98
+ if inputs['iter_range']:
99
+ if not (isinstance(inputs['iter_range'], tuple) and len(inputs['iter_range']) == 2):
100
+ raise ValueError("Invalid parameter type: iter_range must be None or a tuple of (start, end).")
101
+
102
+ if inputs['subplots']:
103
+ if not (isinstance(inputs['subplots'], tuple) and len(inputs['subplots']) == 2):
104
+ raise ValueError("Invalid parameter type: subplots must be None or a tuple of (rows, columns).")
105
+
106
+ if inputs['title'] and not isinstance(inputs['title'], str):
107
+ raise TypeError("Invalid parameter type: title must be None or a string.")
@@ -10,7 +10,94 @@ import panel as pn
10
10
 
11
11
  from vidavis.plot.ms_plot._ms_plot_constants import TIME_FORMAT
12
12
 
13
- def locate_point(xds, position, vis_axis):
13
+ def cursor_changed(cursor, last_cursor):
14
+ ''' Check whether cursor position changed '''
15
+ x, y = cursor
16
+ if not x and not y:
17
+ return False # not cursor callback
18
+ if last_cursor and last_cursor == (x, y):
19
+ return False # same cursor
20
+ return True # new cursor or cursor changed
21
+
22
+ def points_changed(data, last_points):
23
+ ''' Check whether point positions changed '''
24
+ # No data = {'x': [], 'y': []}
25
+ if not data or (len(data['x']) == 0 and len(data['y']) == 0):
26
+ return False # not points callback
27
+ if last_points and last_points == data:
28
+ return False # same points
29
+ return True # new points, points changed, or points deleted
30
+
31
+ def box_changed(bounds, last_box):
32
+ ''' Check whether box position changed '''
33
+ # No bounds = None
34
+ if not bounds:
35
+ return False # no data, not box select callback
36
+ if last_box and last_box == bounds:
37
+ return False # same box
38
+ return True # new box, box changed, or box deleted
39
+
40
+ def update_cursor_location(cursor, plot_axes, xds, cursor_locate_box):
41
+ ''' Show data values for cursor x,y position in cursor location box (pn.WidgetBox) '''
42
+ # Convert plot values to selection values to select plot data
43
+ cursor_locate_box.clear()
44
+
45
+ x, y = cursor
46
+ x_axis, y_axis, vis_axis = plot_axes
47
+ cursor_position = {x_axis: x, y_axis: y}
48
+ cursor_location = _locate_point(xds, cursor_position, vis_axis)
49
+
50
+ location_column = pn.Column(pn.widgets.StaticText(name="Cursor Location"))
51
+ # Add row of columns to column layout
52
+ location_row = _layout_point_location(cursor_location)
53
+ location_column.append(location_row)
54
+ # Add location column to widget box
55
+ cursor_locate_box.append(location_column)
56
+
57
+ def update_points_location(data, plot_axes, xds, points_tab_column, logger):
58
+ ''' Show data values for points in point_draw in tab and log '''
59
+ points_tab_column.clear()
60
+ if data:
61
+ x_axis, y_axis, vis_axis = plot_axes
62
+ logger.info("Locate selected points:")
63
+
64
+ for point in list(zip(data['x'], data['y'])):
65
+ # Locate point
66
+ point_position = {x_axis: point[0], y_axis: point[1]}
67
+ point_location = _locate_point(xds, point_position, vis_axis)
68
+ # Format location and add to points locate column
69
+ location_layout = _layout_point_location(point_location)
70
+ points_tab_column.append(location_layout)
71
+ points_tab_column.append(pn.layout.Divider())
72
+
73
+ # Format and add to log
74
+ location_list = [f"{static_text.name}={static_text.value}" for static_text in point_location]
75
+ logger.info(", ".join(location_list))
76
+
77
+ def update_box_location(bounds, plot_axes, xds, box_tab_column, logger):
78
+ ''' Show data values for points in box_select in tab and log '''
79
+ box_tab_column.clear()
80
+ if bounds:
81
+ x_axis, y_axis, vis_axis = plot_axes
82
+ box_bounds = {x_axis: (bounds[0], bounds[2]), y_axis: (bounds[1], bounds[3])}
83
+ npoints, point_locations = _locate_box(xds, box_bounds, vis_axis)
84
+
85
+ message = f"Locate {npoints} points"
86
+ message += " (only first 100 shown):" if npoints > 100 else ":"
87
+ logger.info(message)
88
+ box_tab_column.append(pn.pane.Str(message))
89
+
90
+ for point in point_locations:
91
+ # Format and add to box locate column
92
+ location_layout = _layout_point_location(point)
93
+ box_tab_column.append(location_layout)
94
+ box_tab_column.append(pn.layout.Divider())
95
+
96
+ # Format and add to log
97
+ location_list = [f"{static_text.name}={static_text.value}" for static_text in point]
98
+ logger.info(", ".join(location_list))
99
+
100
+ def _locate_point(xds, position, vis_axis):
14
101
  '''
15
102
  Get cursor location as values of coordinates and data vars.
16
103
  xds (Xarray Dataset): data for plot
@@ -22,15 +109,18 @@ def locate_point(xds, position, vis_axis):
22
109
  static_text_list = []
23
110
  values, units = _get_point_location(xds, position, vis_axis)
24
111
 
25
- # Rename baseline coordinate names to not confuse user for selection
26
- baseline_names = {'baseline': 'baseline_index', 'baseline_name': 'baseline', 'antenna_name': 'antenna_index', 'antenna': 'antenna_name'}
112
+ # List indexed coordinate int value with with str value
113
+ index_coords = {'baseline': 'baseline_name', 'antenna_name': 'antenna', 'polarization': 'polarization_name'}
27
114
  for name, value in values.items():
28
- name = baseline_names[name] if name in baseline_names else name
115
+ if name in index_coords.values():
116
+ continue
117
+ if name in index_coords and isinstance(value, int):
118
+ value = f"{values[index_coords[name]]} ({value})" # append name to index
29
119
  static_text = _get_location_text(name, value, units)
30
120
  static_text_list.append(static_text)
31
121
  return static_text_list
32
122
 
33
- def locate_box(xds, bounds, vis_axis):
123
+ def _locate_box(xds, bounds, vis_axis):
34
124
  '''
35
125
  Get location of each point in box bounds as values of coordinate and data vars.
36
126
  xds (Xarray Dataset): data for plot
@@ -47,7 +137,7 @@ def locate_box(xds, bounds, vis_axis):
47
137
  selection = {}
48
138
  for coord, val in bounds.items():
49
139
  # Round index values to int for selection
50
- selection[coord] = slice(_round_index_value(coord, val[0]), _round_index_value(coord, val[1]))
140
+ selection[coord] = slice(_get_selection_value(coord, val[0]), _get_selection_value(coord, val[1]))
51
141
  sel_xds = xds.sel(indexers=None, method=None, tolerance=None, drop=False, **selection)
52
142
 
53
143
  x_coord, y_coord = bounds.keys()
@@ -57,7 +147,7 @@ def locate_box(xds, bounds, vis_axis):
57
147
  for y in sel_xds[y_coord].values:
58
148
  for x in sel_xds[x_coord].values:
59
149
  position = {x_coord: x, y_coord: y}
60
- points.append(locate_point(sel_xds, position, vis_axis))
150
+ points.append(_locate_point(sel_xds, position, vis_axis))
61
151
  counter += 1
62
152
  if counter == 100:
63
153
  break
@@ -81,9 +171,9 @@ def _get_point_location(xds, position, vis_axis):
81
171
 
82
172
  if xds:
83
173
  try:
84
- # Round index coordinates to int for selection
85
174
  for coord, value in position.items():
86
- position[coord] = _round_index_value(coord, value)
175
+ # Round index coordinates to int and convert time to datetime if float for selection
176
+ position[coord] = _get_selection_value(coord, value)
87
177
 
88
178
  sel_xds = xds.sel(indexers=None, method='nearest', tolerance=None, drop=False, **position)
89
179
  for coord in sel_xds.coords:
@@ -112,9 +202,15 @@ def _get_point_location(xds, position, vis_axis):
112
202
  values[vis_axis.upper()] = values.pop('VISIBILITY')
113
203
  return values, units
114
204
 
115
- def _round_index_value(coord, value):
116
- ''' Round index coordinates to int for selecction '''
117
- return round(value) if coord in ['baseline', 'antenna_name', 'polarization'] else value
205
+ def _get_selection_value(coord, value):
206
+ ''' Convert index coordinates to int and float time coordinate to datetime '''
207
+ if coord in ['baseline', 'antenna_name', 'polarization']:
208
+ # Round index coordinates to int for selecction
209
+ value = round(value)
210
+ elif coord == 'time' and isinstance(value, float):
211
+ # Bokeh datetime values are floating-point numbers: milliseconds since the Unix epoch
212
+ value = to_datetime(value, unit='ms', origin='unix')
213
+ return value
118
214
 
119
215
  def _get_xda_val_unit(xda):
120
216
  ''' Return value and unit of xda (selected so only one value) '''
@@ -151,3 +247,21 @@ def _get_location_text(name, value, units):
151
247
  units.pop(name) # no unit for datetime string
152
248
  unit = units[name] if name in units else ""
153
249
  return pn.widgets.StaticText(name=name, value=f"{value} {unit}")
250
+
251
+ def _layout_point_location(text_list):
252
+ ''' Layout list of StaticText in row of columns containing 3 rows '''
253
+ location_row = pn.Row()
254
+ location_col = pn.Column()
255
+
256
+ for static_text in text_list:
257
+ # 3 entries per column; append to row and start new column
258
+ if len(location_col.objects) == 3:
259
+ location_row.append(location_col)
260
+ location_col = pn.Column()
261
+
262
+ static_text.margin = (0, 10) # default (5, 10)
263
+ location_col.append(static_text)
264
+
265
+ # Add last column
266
+ location_row.append(location_col)
267
+ return location_row
@@ -6,7 +6,7 @@ import os
6
6
  import time
7
7
 
8
8
  from bokeh.io import export_png, export_svg
9
- from bokeh.plotting import save, show
9
+ from bokeh.plotting import save
10
10
  import hvplot
11
11
  import holoviews as hv
12
12
  import numpy as np
@@ -20,6 +20,7 @@ except ImportError:
20
20
  _HAVE_TOOLVIPER = False
21
21
 
22
22
  from vidavis.data.measurement_set._ms_data import MsData
23
+ from vidavis.plot.ms_plot._locate_points import cursor_changed, points_changed, box_changed, update_cursor_location, update_points_location, update_box_location
23
24
  from vidavis.toolbox import AppContext, get_logger
24
25
 
25
26
  class MsPlot:
@@ -45,28 +46,42 @@ class MsPlot:
45
46
  # Set up temp dir for output html files
46
47
  self._app_context = AppContext(app_name)
47
48
 
48
- if show_gui:
49
- # Enable "toast" notifications
50
- pn.config.notifications = True
51
- self._toast = None # for destroy() with new plot or new notification
52
-
53
49
  # Initialize plot inputs and params
54
- self._plot_inputs = {'selection': {}}
55
- self._plot_params = None
50
+ self._plot_inputs = None # object to manage plot inputs
56
51
 
57
52
  # Initialize plots
58
53
  self._plot_init = False
59
- self._plots_locked = False
60
54
  self._plots = []
55
+ self._last_plot = None
56
+ self._plot_params = [] # for plot inputs tab
61
57
 
62
- # Initialize gui
63
58
  if show_gui:
59
+ # Enable "toast" notifications
60
+ pn.config.notifications = True
61
+ self._toast = None # for destroy() with new plot or new notification
62
+
63
+ # Initialize gui panel for callbacks
64
+ self._gui_plot_data = None
65
+ self._gui_selection = {}
64
66
  self._gui_layout = None
65
67
  self._first_gui_plot = True
66
- self._last_gui_plot = None
68
+
69
+ # For _update_plot callback: check if inputs changed
70
+ self._last_plot_inputs = None
71
+ self._last_style_inputs = None
72
+
73
+ # For locate callback: check if points changed
74
+ self._plot_axes = None
75
+ self._last_cursor = None
76
+ self._last_points = None
77
+ self._last_box = None
78
+
79
+ # Initialize non-gui show() panel for callbacks
80
+ self._show_layout = None
81
+ self._plot_data = None
67
82
 
68
83
  # Set data (if ms)
69
- self._data = None
84
+ self._ms_data = None
70
85
  self._ms_info = {}
71
86
  self._set_ms(ms)
72
87
  # pylint: enable=too-many-arguments, too-many-positional-arguments
@@ -83,15 +98,15 @@ class MsPlot:
83
98
  'field_name', 'source_name', 'field_coords', 'start_frequency', 'end_frequency'
84
99
  Returns: list of unique values when single column is requested, else None
85
100
  '''
86
- if self._data:
87
- self._data.summary(data_group, columns)
101
+ if self._ms_data:
102
+ self._ms_data.summary(data_group, columns)
88
103
  else:
89
104
  self._logger.error("Error: MS path has not been set")
90
105
 
91
106
  def data_groups(self):
92
107
  ''' Returns set of data groups from all ProcessingSet ms_xds. '''
93
- if self._data:
94
- return self._data.data_groups()
108
+ if self._ms_data:
109
+ return self._ms_data.data_groups()
95
110
  self._logger.error("Error: MS path has not been set")
96
111
  return None
97
112
 
@@ -100,8 +115,8 @@ class MsPlot:
100
115
  Dimension options include 'time', 'baseline' (for visibility data), 'antenna' (for spectrum data), 'antenna1',
101
116
  'antenna2', 'frequency', 'polarization'.
102
117
  '''
103
- if self._data:
104
- return self._data.get_dimension_values(dimension)
118
+ if self._ms_data:
119
+ return self._ms_data.get_dimension_values(dimension)
105
120
  self._logger.error("Error: MS path has not been set")
106
121
  return None
107
122
 
@@ -109,8 +124,8 @@ class MsPlot:
109
124
  ''' Plot antenna positions.
110
125
  label_antennas (bool): label positions with antenna names.
111
126
  '''
112
- if self._data:
113
- self._data.plot_antennas(label_antennas)
127
+ if self._ms_data:
128
+ self._ms_data.plot_antennas(label_antennas)
114
129
  else:
115
130
  self._logger.error("Error: MS path has not been set")
116
131
 
@@ -119,23 +134,33 @@ class MsPlot:
119
134
  data_group (str): data group to use for field and source xds.
120
135
  label_fields (bool): label all fields on the plot if True, else label central field only
121
136
  '''
122
- if self._data:
123
- self._data.plot_phase_centers(data_group, label_fields)
137
+ if self._ms_data:
138
+ self._ms_data.plot_phase_centers(data_group, label_fields)
124
139
  else:
125
140
  self._logger.error("Error: MS path has not been set")
126
141
 
127
142
  def clear_plots(self):
128
143
  ''' Clear plot list '''
129
- while self._plots_locked:
130
- time.sleep(1)
131
144
  self._plots.clear()
145
+ self._plot_params.clear()
146
+ self._plot_axes = None
147
+
148
+ def unlink_plot_streams(self):
149
+ ''' Disconnect streams when plot data is going to be replaced '''
150
+ if self._show_layout and len(self._show_layout.objects) == 4:
151
+ # Remove dmap (streams with callback) from previous plot
152
+ self._show_layout[0][0] = self._last_plot.opts(tools=['hover'])
153
+ # Remove locate widgets
154
+ self._show_layout[0].pop(1) # cursor locate box
155
+ self._show_layout.pop(3) # box locate tab
156
+ self._show_layout.pop(2) # points locate tab
132
157
 
133
158
  def clear_selection(self):
134
159
  ''' Clear data selection and restore original ProcessingSet '''
135
- if self._data:
136
- self._data.clear_selection()
160
+ if self._ms_data:
161
+ self._ms_data.clear_selection()
137
162
 
138
- self._plot_inputs['selection'] = {}
163
+ self._plot_inputs.remove_input('selection')
139
164
 
140
165
  def show(self):
141
166
  '''
@@ -144,25 +169,62 @@ class MsPlot:
144
169
  if not self._plots:
145
170
  raise RuntimeError("No plots to show. Run plot() to create plot.")
146
171
 
147
- # Do not delete plot list until rendered
148
- self._plots_locked = True
149
-
150
172
  # Single plot or combine plots into layout using subplots (rows, columns)
151
- layout_plot = self._layout_plots(self._plot_inputs['subplots'])
152
-
153
- # Render plot as Bokeh Figure or GridPlot so can show() in script without tying up thread
154
- bokeh_fig = hv.render(layout_plot)
173
+ subplots = self._plot_inputs.get_input('subplots')
174
+ layout_plot = self._layout_plots(subplots)
155
175
 
156
- self._plots_locked = False
176
+ # Add plot inputs column tab
177
+ inputs_column = None
157
178
  if self._plot_params:
158
- # Show plot and plot inputs in tabs
159
- column = pn.Column()
160
- for param in self._plot_params:
161
- column.append(pn.pane.Str(param))
162
- tabs = pn.Tabs(('Plot', bokeh_fig), ('Plot Inputs', column))
163
- tabs.show(title=self._app_name, threaded=True)
179
+ inputs_column = pn.Column()
180
+ self._fill_inputs_column(inputs_column)
181
+
182
+ # Show plot and plot inputs in tabs
183
+ if self._plot_inputs.is_layout():
184
+ self._show_layout = pn.Tabs(('Plot', layout_plot))
185
+ if inputs_column:
186
+ self._show_layout.append(('Plot Inputs', inputs_column))
164
187
  else:
165
- show(bokeh_fig)
188
+ plot = layout_plot.opts(
189
+ hv.opts.QuadMesh(
190
+ tools=['hover', 'box_select'],
191
+ selection_fill_alpha=0.2, # dim selected areas of plot
192
+ nonselection_fill_alpha=1.0, # do not dim unselected areas of plot
193
+ )
194
+ )
195
+ # Add DynamicMap for streams for single plot
196
+ points = hv.Points([]).opts(
197
+ size=5,
198
+ fill_color='white'
199
+ )
200
+ dmap = hv.DynamicMap(
201
+ self._locate,
202
+ streams=[
203
+ hv.streams.PointerXY(), # cursor location (x, y)
204
+ hv.streams.PointDraw(source=points), # fixed points location (data)
205
+ hv.streams.BoundsXY() # box location (bounds)
206
+ ]
207
+ )
208
+
209
+ # Create panel layout
210
+ self._show_layout = pn.Tabs(
211
+ ('Plot', pn.Column(
212
+ plot * dmap * points,
213
+ pn.WidgetBox(), # cursor info
214
+ )
215
+ ),
216
+ sizing_mode='stretch_width',
217
+ )
218
+ if inputs_column:
219
+ self._show_layout.append(('Plot Inputs', inputs_column))
220
+ self._show_layout.append(('Locate Selected Points', pn.Column()))
221
+ self._show_layout.append(('Locate Selected Box', pn.Column()))
222
+
223
+ # return value for locate callback
224
+ self._last_plot = plot
225
+
226
+ # Show panel layout
227
+ self._show_layout.show(title=self._app_name, threaded=True)
166
228
 
167
229
  def save(self, filename='ms_plot.png', fmt='auto', width=900, height=600):
168
230
  '''
@@ -182,11 +244,14 @@ class MsPlot:
182
244
 
183
245
  # Combine plots into layout using subplots (rows, columns) if not single plot.
184
246
  # Set fixed size for export.
185
- layout_plot = self._layout_plots(self._plot_inputs['subplots'], (width, height))
247
+ subplots = self._plot_inputs.get_input('subplots')
248
+ layout_plot = self._layout_plots(subplots, (width, height))
186
249
 
187
- if not isinstance(layout_plot, hv.Layout) and self._plot_inputs['iter_axis']:
250
+ iter_axis = self._plot_inputs.get_input('iter_axis')
251
+ if not isinstance(layout_plot, hv.Layout) and iter_axis:
188
252
  # Save iterated plots individually, with index appended to filename
189
- plot_idx = 0 if self._plot_inputs['iter_range'] is None else self._plot_inputs['iter_range'][0]
253
+ iter_range = self._plot_inputs.get_input('iter_range')
254
+ plot_idx = 0 if iter_range is None else iter_range[0]
190
255
  for plot in self._plots:
191
256
  exportname = f"{name}_{plot_idx}{ext}"
192
257
  self._save_plot(plot, exportname, fmt)
@@ -255,22 +320,22 @@ class MsPlot:
255
320
  Return whether ms changed (false if ms_path is None, not set yet), even if error. '''
256
321
  self._ms_info['ms'] = ms_path
257
322
  ms_error = ""
258
- if not ms_path or (self._data and self._data.is_ms_path(ms_path)):
323
+ if not ms_path or (self._ms_data and self._ms_data.is_ms_path(ms_path)):
259
324
  return False
260
325
 
261
326
  try:
262
327
  # Set new MS data
263
- self._data = MsData(ms_path, self._logger)
264
- data_path = self._data.get_path()
328
+ self._ms_data = MsData(ms_path, self._logger)
329
+ data_path = self._ms_data.get_path()
265
330
  self._ms_info['ms'] = data_path
266
331
  root, ext = os.path.splitext(os.path.basename(data_path))
267
332
  while ext != '':
268
333
  root, ext = os.path.splitext(root)
269
334
  self._ms_info['basename'] = root
270
- self._ms_info['data_dims'] = self._data.get_data_dimensions()
335
+ self._ms_info['data_dims'] = self._ms_data.get_data_dimensions()
271
336
  except RuntimeError as e:
272
337
  ms_error = str(e)
273
- self._data = None
338
+ self._ms_data = None
274
339
  if ms_error:
275
340
  self._notify(ms_error, 'error', 0)
276
341
  return True
@@ -309,4 +374,70 @@ class MsPlot:
309
374
  del plot_inputs[key]
310
375
  except KeyError:
311
376
  pass
312
- self._plot_params = sorted([f"{key}={value}" for key, value in plot_inputs.items()])
377
+ if not self._plot_params:
378
+ self._plot_params = plot_inputs
379
+ else:
380
+ for param, value in self._plot_params.items():
381
+ if plot_inputs[param] != value:
382
+ if isinstance(value, list):
383
+ # append new value to existing list if not repeat
384
+ if plot_inputs[param] != value[-1]:
385
+ value.append(plot_inputs[param])
386
+ else:
387
+ # make list to include new value
388
+ value = [value, plot_inputs[param]]
389
+ self._plot_params[param] = value
390
+
391
+ def _fill_inputs_column(self, inputs_tab_column):
392
+ ''' Format plot inputs and list in Panel column '''
393
+ if self._plot_params:
394
+ inputs_tab_column.clear()
395
+ plot_params = sorted([f"{key}={value}" for key, value in self._plot_params.items()])
396
+ for param in plot_params:
397
+ str_pane = pn.pane.Str(param)
398
+ str_pane.margin = (0, 10)
399
+ inputs_tab_column.append(str_pane)
400
+
401
+ def _get_plot_axes(self):
402
+ ''' Return x, y, vis axes '''
403
+ if not self._plot_axes:
404
+ x_axis = self._plot_inputs.get_input('x_axis')
405
+ y_axis = self._plot_inputs.get_input('y_axis')
406
+ vis_axis = self._plot_inputs.get_input('vis_axis')
407
+ self._plot_axes = (x_axis, y_axis, vis_axis)
408
+ return self._plot_axes
409
+
410
+ def _locate(self, x, y, data, bounds):
411
+ ''' Callback for all show plot streams '''
412
+ self._locate_cursor(x, y, self._plot_data, self._show_layout)
413
+ self._locate_points(data, self._plot_data, self._show_layout)
414
+ self._locate_box(bounds, self._plot_data, self._show_layout)
415
+ return self._last_plot
416
+
417
+ def _locate_cursor(self, x, y, plot_data, tabs):
418
+ ''' Show location from cursor position in cursor locate box '''
419
+ cursor = (x, y)
420
+ if cursor_changed(cursor, self._last_cursor):
421
+ # new cursor position - update cursor location box
422
+ plot_axes = self._get_plot_axes()
423
+ cursor_box = tabs[0][1]
424
+ update_cursor_location(cursor, plot_axes, plot_data, cursor_box)
425
+ self._last_cursor = cursor
426
+
427
+ def _locate_points(self, point_data, plot_data, tabs):
428
+ ''' Show points locations from point_draw tool '''
429
+ if points_changed(point_data, self._last_points):
430
+ # new points position - update selected points location tab
431
+ plot_axes = self._get_plot_axes()
432
+ points_tab = tabs[2]
433
+ update_points_location(point_data, plot_axes, plot_data, points_tab, self._logger)
434
+ self._last_points = point_data
435
+
436
+ def _locate_box(self, box_bounds, plot_data, tabs):
437
+ ''' Show points locations in box from box_select tool '''
438
+ if box_changed(box_bounds, self._last_box):
439
+ # new box_select position - update selected box location tab
440
+ plot_axes = self._get_plot_axes()
441
+ box_tab = tabs[3]
442
+ update_box_location(box_bounds, plot_axes, plot_data, box_tab, self._logger)
443
+ self._last_box = box_bounds
@@ -0,0 +1,19 @@
1
+ ''' Utilities for inputs to MeasurementSet plots '''
2
+
3
+ def inputs_changed(plot_inputs, last_plot_inputs):
4
+ ''' Check if inputs changed and need new plot '''
5
+ if not last_plot_inputs:
6
+ return True
7
+
8
+ for key, val in plot_inputs.items():
9
+ if not _values_equal(val, last_plot_inputs[key]):
10
+ return True
11
+ return False
12
+
13
+ def _values_equal(val1, val2):
14
+ ''' Test if values are set and equal, or not set (cannot compare value with None) '''
15
+ if val1 is not None and val2 is not None: # both set
16
+ return val1 == val2
17
+ if val1 is None and val2 is None: # both None
18
+ return True
19
+ return False # one set and other is None