class ScanPlotWidget(PlotWidget): """ Extend the PlotWidget Class with more functionality used for qudi scan images. Supported features: - draggable/static crosshair with optional range and size constraints. - zoom feature by rubberband selection - rubberband area selection This class depends on the ScanViewBox class defined further below. This class can be promoted in the Qt designer. """ sigMouseAreaSelected = QtCore.Signal( QtCore.QRectF) # mapped rectangle mouse cursor selection sigCrosshairPosChanged = QtCore.Signal(QtCore.QPointF) sigCrosshairDraggedPosChanged = QtCore.Signal(QtCore.QPointF) def __init__(self, *args, **kwargs): kwargs['viewBox'] = ScanViewBox() # Use custom pg.ViewBox subclass super().__init__(*args, **kwargs) self.getViewBox().sigMouseAreaSelected.connect( self.sigMouseAreaSelected) self._min_crosshair_factor = 0.02 self._crosshair_size = (0, 0) self._crosshair_range = None self.getViewBox().sigRangeChanged.connect( self._constraint_crosshair_size) self.crosshair = ROI((0, 0), (0, 0), pen={ 'color': '#00ff00', 'width': 1 }) self.hline = InfiniteLine(pos=0, angle=0, movable=True, pen={ 'color': '#00ff00', 'width': 1 }, hoverPen={ 'color': '#ffff00', 'width': 1 }) self.vline = InfiniteLine(pos=0, angle=90, movable=True, pen={ 'color': '#00ff00', 'width': 1 }, hoverPen={ 'color': '#ffff00', 'width': 1 }) self.vline.sigDragged.connect(self._update_pos_from_line) self.hline.sigDragged.connect(self._update_pos_from_line) self.crosshair.sigRegionChanged.connect(self._update_pos_from_roi) self.sigCrosshairDraggedPosChanged.connect(self.sigCrosshairPosChanged) @property def crosshair_enabled(self): items = self.items() return (self.vline in items) and (self.hline in items) and (self.crosshair in items) @property def crosshair_movable(self): return bool(self.crosshair.translatable) @property def crosshair_position(self): pos = self.vline.pos() pos[1] = self.hline.pos()[1] return tuple(pos) @property def crosshair_size(self): return tuple(self._crosshair_size) @property def crosshair_min_size_factor(self): return float(self._min_crosshair_factor) @property def crosshair_range(self): if self._crosshair_range is None: return None return tuple(self._crosshair_range) @property def selection_enabled(self): return bool(self.getViewBox().rectangle_selection) @property def zoom_by_selection_enabled(self): return bool(self.getViewBox().zoom_by_selection) def toggle_selection(self, enable): """ De-/Activate the rectangular rubber band selection tool. If active you can select a rectangular region within the ViewBox by dragging the mouse with the left button. Each selection rectangle in real-world data coordinates will be emitted by sigMouseAreaSelected. By using activate_zoom_by_selection you can optionally de-/activate zooming in on the selection. @param bool enable: Toggle selection on (True) or off (False) """ return self.getViewBox().toggle_selection(enable) def toggle_zoom_by_selection(self, enable): """ De-/Activate automatic zooming into a selection. See also: toggle_selection @param bool enable: Toggle zoom upon selection on (True) or off (False) """ return self.getViewBox().toggle_zoom_by_selection(enable) def _update_pos_from_line(self, obj): """ Called each time the position of the InfiniteLines has been changed by a user drag. Causes the crosshair rectangle to follow the lines. """ if obj not in (self.hline, self.vline): return pos = self.vline.pos() pos[1] = self.hline.pos()[1] size = self.crosshair.size() self.crosshair.blockSignals(True) self.crosshair.setPos((pos[0] - size[0] / 2, pos[1] - size[1] / 2)) self.crosshair.blockSignals(False) self.sigCrosshairDraggedPosChanged.emit(QtCore.QPointF(pos[0], pos[1])) return def _update_pos_from_roi(self, obj): """ Called each time the position of the rectangular ROI has been changed by a user drag. Causes the InfiniteLines to follow the ROI. """ if obj is not self.crosshair: return pos = self.crosshair.pos() size = self.crosshair.size() pos[0] += size[0] / 2 pos[1] += size[1] / 2 self.vline.setPos(pos[0]) self.hline.setPos(pos[1]) self.sigCrosshairDraggedPosChanged.emit(QtCore.QPointF(pos[0], pos[1])) return def toggle_crosshair(self, enable, movable=True): """ Disable/Enable the crosshair within the PlotWidget. Optionally also toggle if it can be dragged by the user. @param bool enable: enable crosshair (True), disable crosshair (False) @param bool movable: enable user drag (True), disable user drag (False) """ if not isinstance(enable, bool): raise TypeError('Positional argument "enable" must be bool type.') if not isinstance(movable, bool): raise TypeError('Optional argument "movable" must be bool type.') self.toggle_crosshair_movable(movable) is_enabled = self.crosshair_enabled if enable and not is_enabled: self.addItem(self.vline) self.addItem(self.hline) self.addItem(self.crosshair) elif not enable and is_enabled: self.removeItem(self.vline) self.removeItem(self.hline) self.removeItem(self.crosshair) return def toggle_crosshair_movable(self, enable): """ Toggle if the crosshair can be dragged by the user. @param bool enable: enable (True), disable (False) """ self.crosshair.translatable = bool(enable) self.vline.setMovable(enable) self.hline.setMovable(enable) return def set_crosshair_pos(self, pos): """ Set the crosshair center to the given coordinates. @param QPointF|float[2] pos: (x,y) position of the crosshair """ try: pos = tuple(pos) except TypeError: pos = (pos.x(), pos.y()) size = self.crosshair.size() self.crosshair.blockSignals(True) self.vline.blockSignals(True) self.hline.blockSignals(True) self.crosshair.setPos(pos[0] - size[0] / 2, pos[1] - size[1] / 2) self.vline.setPos(pos[0]) self.hline.setPos(pos[1]) self.crosshair.blockSignals(False) self.vline.blockSignals(False) self.hline.blockSignals(False) self.sigCrosshairPosChanged.emit(QtCore.QPointF(*pos)) return def set_crosshair_size(self, size, force_default=True): """ Set the default size of the crosshair rectangle (x, y) and update the display. @param QSize|float[2] size: the (x,y) size of the crosshair rectangle @param bool force_default: Set default crosshair size and enforce minimal size (True). Enforce displayed crosshair size while keeping default size untouched (False). """ try: size = tuple(size) except TypeError: size = (size.width(), size.height()) if force_default: if size[0] <= 0 and size[1] <= 0: self._crosshair_size = (0, 0) else: self._crosshair_size = size # Check if actually displayed size needs to be adjusted due to minimal size size = self._get_corrected_crosshair_size(size) pos = self.vline.pos() pos[1] = self.hline.pos()[1] - size[1] / 2 pos[0] -= size[0] / 2 if self._crosshair_range: crange = self._crosshair_range self.crosshair.maxBounds = QtCore.QRectF( crange[0][0] - size[0] / 2, crange[1][0] - size[1] / 2, crange[0][1] - crange[0][0] + size[0], crange[1][1] - crange[1][0] + size[1]) self.crosshair.blockSignals(True) self.crosshair.setSize(size) self.crosshair.setPos(pos) self.crosshair.blockSignals(False) return def set_crosshair_min_size_factor(self, factor): """ Sets the minimum crosshair size factor. This will determine the minimum size of the smallest edge of the crosshair center rectangle. This minimum size is calculated by taking the smallest visible axis of the ViewBox and multiplying it with the scale factor set by this method. The crosshair rectangle will be then scaled accordingly if the set crosshair size is smaller than this minimal size. @param float factor: The scale factor to set. If <= 0 no minimal crosshair size enforced. """ if factor <= 0: self._min_crosshair_factor = 0 elif factor <= 1: self._min_crosshair_factor = float(factor) else: raise ValueError('Crosshair min size factor must be a value <= 1.') return def set_crosshair_range(self, new_range): """ Sets a range boundary for the crosshair position. @param float[2][2] new_range: two min-max range value tuples (for x and y axis). If None set unlimited ranges. """ if new_range is None: self.vline.setBounds([None, None]) self.hline.setBounds([None, None]) self.crosshair.maxBounds = None else: self.vline.setBounds(new_range[0]) self.hline.setBounds(new_range[1]) size = self.crosshair.size() pos = self.crosshair_position self.crosshair.maxBounds = QtCore.QRectF( new_range[0][0] - size[0] / 2, new_range[1][0] - size[1] / 2, new_range[0][1] - new_range[0][0] + size[0], new_range[1][1] - new_range[1][0] + size[1]) self.crosshair.setPos(pos[0] - size[0] / 2, pos[1] - size[1] / 2) self._crosshair_range = new_range return def set_crosshair_pen(self, pen): """ Sets the pyqtgraph compatible pen to be used for drawing the crosshair lines. @param pen: pyqtgraph compatible pen to use """ self.crosshair.setPen(pen) self.vline.setPen(pen) self.hline.setPen(pen) return def _constraint_crosshair_size(self): if self._min_crosshair_factor == 0: return if self._crosshair_size[0] == 0 or self._crosshair_size[1] == 0: return corr_size = self._get_corrected_crosshair_size(self._crosshair_size) if corr_size != tuple(self.crosshair.size()): self.set_crosshair_size(corr_size, force_default=False) return def _get_corrected_crosshair_size(self, size): try: size = tuple(size) except TypeError: size = (size.width(), size.height()) min_size = min(size) if min_size == 0: return size vb_size = self.getViewBox().viewRect().size() short_index = int(vb_size.width() > vb_size.height()) min_vb_size = vb_size.width() if short_index == 0 else vb_size.height() min_vb_size *= self._min_crosshair_factor if min_size < min_vb_size: scale_factor = min_vb_size / min_size size = (size[0] * scale_factor, size[1] * scale_factor) return size
class SpecvizDataViewer(DataViewer): """ """ LABEL = 'SpecViz viewer' _state_cls = SpecvizViewerState _options_cls = SpecvizViewerStateWidget _layer_style_widget_cls = SpecvizLayerStateWidget _data_artist_cls = SpecvizLayerArtist _subset_artist_cls = SpecvizLayerArtist _inherit_tools = False tools = [] subtools = {} def __init__(self, *args, layout=None, include_line=False, **kwargs): # Load specviz plugins Application.load_local_plugins() super(SpecvizDataViewer, self).__init__(*args, **kwargs) self.statusBar().hide() # Instantiate workspace widget self.current_workspace = Workspace() self.hub = Hub(self.current_workspace) # Store a reference to the cubeviz layout instance self._layout = layout # Add an initially empty plot window self.current_workspace.add_plot_window() self.setCentralWidget(self.current_workspace) self.options.gridLayout.addWidget(self.current_workspace.list_view) # When a new data item is added to the specviz model, create a new # glue data component and add it to the glue data list # self.current_workspace.model.data_added.connect(self.reverse_add_data) self.current_workspace.mdi_area.setViewMode(QMdiArea.SubWindowView) self.current_workspace.current_plot_window.setWindowFlags(Qt.FramelessWindowHint) self.current_workspace.current_plot_window.showMaximized() # Create and attach a movable vertical line indicating the current # slice position in the cube if include_line: self._slice_indicator = InfiniteLine(0, movable=True, pen={'color': 'g', 'width': 3}) self.current_workspace.current_plot_window.plot_widget.addItem( self._slice_indicator) def reverse_add_data(self, data_item): """ Adds data from specviz to glue. Parameters ---------- data_item : :class:`specviz.core.items.DataItem` The data item recently added to model. """ new_data = Data(label=data_item.name) new_data.coords = coordinates_from_header(data_item.spectrum.wcs) flux_component = Component(data_item.spectrum.flux, data_item.spectrum.flux.unit) new_data.add_component(flux_component, "Flux") disp_component = Component(data_item.spectrum.spectral_axis, data_item.spectrum.spectral_axis.unit) new_data.add_component(disp_component, "Dispersion") if data_item.spectrum.uncertainty is not None: uncert_component = Component(data_item.spectrum.uncertainty.array, data_item.spectrum.uncertainty.unit) new_data.add_component(uncert_component, "Uncertainty") self._session.data_collection.append(new_data) def add_data(self, data): """ Parameters ---------- data Returns ------- """ if not glue_data_has_spectral_axis(data): QMessageBox.critical(self, "Error", "Data is not a 1D spectrum", buttons=QMessageBox.Ok) return False return super(SpecvizDataViewer, self).add_data(data) def add_subset(self, subset): """ Parameters ---------- subset Returns ------- """ if not glue_data_has_spectral_axis(subset): QMessageBox.critical(self, "Error", "Subset is not a 1D spectrum", buttons=QMessageBox.Ok) return False return super(SpecvizDataViewer, self).add_subset(subset) def get_layer_artist(self, cls, layer=None, layer_state=None): """ Parameters ---------- cls layer layer_state Returns ------- """ return cls(self.current_workspace, self.state, layer=layer, layer_state=layer_state) def initialize_toolbar(self): """ """ # Merge the main tool bar and the plot tool bar to get back some # real estate self.current_workspace.addToolBar( self.current_workspace.current_plot_window.tool_bar) self.current_workspace.main_tool_bar.setIconSize(QSize(15, 15)) # Hide the first five actions in the default specviz tool bar for act in self.current_workspace.main_tool_bar.actions()[:6]: act.setVisible(False) # Hide the tabs of the mdiarea in specviz. self.current_workspace.mdi_area.setViewMode(QMdiArea.SubWindowView) self.current_workspace.current_plot_window.setWindowFlags(Qt.FramelessWindowHint) self.current_workspace.current_plot_window.showMaximized() if self._layout is not None: cube_ops = QAction(QIcon(":/icons/cube.svg"), "Cube Operations", self.current_workspace.main_tool_bar) self.current_workspace.main_tool_bar.addAction(cube_ops) self.current_workspace.main_tool_bar.addSeparator() button = self.current_workspace.main_tool_bar.widgetForAction(cube_ops) button.setPopupMode(QToolButton.InstantPopup) menu = QMenu(self.current_workspace.main_tool_bar) button.setMenu(menu) # Create operation actions menu.addSection("2D Operations") act = QAction("Simple Linemap", self) act.triggered.connect(self._create_simple_linemap) menu.addAction(act) act = QAction("Fitted Linemap", self) act.triggered.connect(self._create_fitted_linemap) menu.addAction(act) menu.addSection("3D Operations") act = QAction("Fit Spaxels", self) act.triggered.connect(self._fit_spaxels) menu.addAction(act) act = QAction("Spectral Smoothing", self) act.triggered.connect(self._spectral_smoothing) menu.addAction(act) def update_units(self, spectral_axis_unit=None, data_unit=None): """ Interface for data viewers to update the plotted axis units in specviz. Parameters ---------- spectral_axis_unit : str or :class:`~astropy.unit.Quantity` The spectral axis unit to convert to. data_unit : str or :class:`~astropy.unit.Quantity` The data axis unit to convert to. """ if spectral_axis_unit is not None: self.hub.plot_widget.spectral_axis_unit = spectral_axis_unit if data_unit is not None: self.hub.plot_widget.data_unit = data_unit def update_slice_indicator_position(self, pos): """ Updates the current position of the movable vertical slice indicator. Parameters ---------- pos : float The new position of the vertical line. """ if self._slice_indicator is not None: self._slice_indicator.blockSignals(True) self._slice_indicator.setPos(u.Quantity(pos).value) self._slice_indicator.blockSignals(False) def _create_simple_linemap(self): def threadable_function(data, tracker): out = np.empty(shape=data.shape[1:]) mask = self.hub.region_mask for x in range(data.shape[1]): for y in range(data.shape[2]): out[x, y] = np.sum(data[:, x, y][mask]) tracker() return out, data.meta.get('unit') spectral_operation = SpectralOperationHandler( data=self.layers[0].state.layer, function=threadable_function, operation_name="Simple Linemap", component_id=self.layers[0].state.attribute, layout=self._layout, ui_settings={ 'title': "Simple Linemap", 'group_box_title': "Choose the component to use for linemap " "generation", 'description': "Sums the values of the chosen component in the " "range of the current ROI in the spectral view " "for each spectrum in the data cube."}) spectral_operation.exec_() def _create_fitted_linemap(self): # Check to see if the model fitting plugin is loaded model_editor_plugin = self.current_workspace._plugin_bars.get("Model Editor") if model_editor_plugin is None: logging.error("Model editor plugin is not loaded.") return if (model_editor_plugin.model_tree_view.model() is None or model_editor_plugin.model_tree_view.model().evaluate() is None): QMessageBox.warning(self, "No evaluable model.", "There is currently no model or the created " "model is empty. Unable to perform fitted " "linemap operation.") return def threadable_function(data, tracker): out = np.empty(shape=data.shape[1:]) mask = self.hub.region_mask spectral_axis = self.hub.plot_item.spectral_axis model = model_editor_plugin.model_tree_view.model().evaluate() for x in range(data.shape[1]): for y in range(data.shape[2]): flux = data[:, x, y].value fitter = LevMarLSQFitter() fit_model = fitter(model, spectral_axis[mask], flux[mask]) new_data = fit_model(spectral_axis) out[x, y] = np.sum(new_data[mask]) tracker() return out, data.meta.get('unit') spectral_operation = SpectralOperationHandler( data=self.layers[0].state.layer, function=threadable_function, operation_name="Fitted Linemap", component_id=self.layers[0].state.attribute, layout=self._layout, ui_settings={ 'title': "Fitted Linemap", 'group_box_title': "Choose the component to use for linemap " "generation", 'description': "Fits the current model to the values of the " "chosen component in the range of the current " "ROI in the spectral view for each spectrum in " "the data cube."}) spectral_operation.exec_() def _fit_spaxels(self): # Check to see if the model fitting plugin is loaded model_editor_plugin = self.current_workspace._plugin_bars.get("Model Editor") if model_editor_plugin is None: logging.error("Model editor plugin is not loaded.") return if (model_editor_plugin.model_tree_view.model() is None or model_editor_plugin.model_tree_view.model().evaluate() is None): QMessageBox.warning(self, "No evaluable model.", "There is currently no model or the created " "model is empty. Unable to perform fitted " "linemap operation.") return def threadable_function(data, tracker): out = np.empty(shape=data.shape) mask = self.hub.region_mask spectral_axis = self.hub.plot_item.spectral_axis model = model_editor_plugin.model_tree_view.model().evaluate() for x in range(data.shape[1]): for y in range(data.shape[2]): flux = data[:, x, y].value fitter = LevMarLSQFitter() fit_model = fitter(model, spectral_axis[mask], flux[mask]) new_data = fit_model(spectral_axis) out[:, x, y] = np.sum(new_data[mask]) tracker() return out, data.meta.get('unit') spectral_operation = SpectralOperationHandler( data=self.layers[0].state.layer, function=threadable_function, operation_name="Fit Spaxels", component_id=self.layers[0].state.attribute, layout=self._layout, ui_settings={ 'title': "Fit Spaxel", 'group_box_title': "Choose the component to use for spaxel " "fitting", 'description': "Fits the current model to the values of the " "chosen component in the range of the current " "ROI in the spectral view for each spectrum in " "the data cube."}) spectral_operation.exec_() def _spectral_smoothing(self): def threadable_function(func, data, tracker, **kwargs): out = np.empty(shape=data.shape) for x in range(data.shape[1]): for y in range(data.shape[2]): out[:, x, y] = func(data[:, x, y], data.spectral_axis) tracker() return out, data.meta.get('unit') stack = FunctionalOperation.operations()[::-1] if len(stack) == 0: QMessageBox.warning(self, "No smoothing in history.", "To apply a smoothing operation to the entire " "cube, first do a local smoothing operation " "(Operations > Smoothing). Once done, the " "operation can then be performed over the " "entire cube.") return spectral_operation = SpectralOperationHandler( data=self.layers[0].state.layer, func_proxy=threadable_function, stack=stack, component_id=self.layers[0].state.attribute, layout=self._layout, ui_settings={ 'title': "Spectral Smoothing", 'group_box_title': "Choose the component to smooth.", 'description': "Performs a previous smoothing operation over " "the selected component for the entire cube."}) spectral_operation.exec_()
class SpecvizDataViewer(DataViewer): """ """ LABEL = 'SpecViz viewer' _state_cls = SpecvizViewerState _options_cls = SpecvizViewerStateWidget _layer_style_widget_cls = SpecvizLayerStateWidget _data_artist_cls = SpecvizLayerArtist _subset_artist_cls = SpecvizLayerArtist _inherit_tools = False tools = [] subtools = {} def __init__(self, *args, layout=None, include_line=False, **kwargs): # Load specviz plugins Application.load_local_plugins() super(SpecvizDataViewer, self).__init__(*args, **kwargs) self.statusBar().hide() # Instantiate workspace widget self.current_workspace = Workspace() self.hub = Hub(self.current_workspace) # Store a reference to the cubeviz layout instance self._layout = layout # Add an initially empty plot window self.current_workspace.add_plot_window() self.setCentralWidget(self.current_workspace) self.options.gridLayout.addWidget(self.current_workspace.list_view) # When a new data item is added to the specviz model, create a new # glue data component and add it to the glue data list # self.current_workspace.model.data_added.connect(self.reverse_add_data) self.current_workspace.mdi_area.setViewMode(QMdiArea.SubWindowView) self.current_workspace.current_plot_window.setWindowFlags(Qt.FramelessWindowHint) self.current_workspace.current_plot_window.showMaximized() # Create and attach a movable vertical line indicating the current # slice position in the cube if include_line: self._slice_indicator = InfiniteLine(0, movable=True, pen={'color': 'g', 'width': 3}) self.current_workspace.current_plot_window.plot_widget.addItem( self._slice_indicator) def reverse_add_data(self, data_item): """ Adds data from specviz to glue. Parameters ---------- data_item : :class:`specviz.core.items.DataItem` The data item recently added to model. """ new_data = Data(label=data_item.name) new_data.coords = coordinates_from_header(data_item.spectrum.wcs) flux_component = Component(data_item.spectrum.flux, data_item.spectrum.flux.unit) new_data.add_component(flux_component, "Flux") disp_component = Component(data_item.spectrum.spectral_axis, data_item.spectrum.spectral_axis.unit) new_data.add_component(disp_component, "Dispersion") if data_item.spectrum.uncertainty is not None: uncert_component = Component(data_item.spectrum.uncertainty.array, data_item.spectrum.uncertainty.unit) new_data.add_component(uncert_component, "Uncertainty") self._session.data_collection.append(new_data) def add_data(self, data): """ Parameters ---------- data Returns ------- """ if not glue_data_has_spectral_axis(data): QMessageBox.critical(self, "Error", "Data is not a 1D spectrum", buttons=QMessageBox.Ok) return False return super(SpecvizDataViewer, self).add_data(data) def add_subset(self, subset): """ Parameters ---------- subset Returns ------- """ if not glue_data_has_spectral_axis(subset): QMessageBox.critical(self, "Error", "Subset is not a 1D spectrum", buttons=QMessageBox.Ok) return False return super(SpecvizDataViewer, self).add_subset(subset) def get_layer_artist(self, cls, layer=None, layer_state=None): """ Parameters ---------- cls layer layer_state Returns ------- """ return cls(self.current_workspace, self.state, layer=layer, layer_state=layer_state) def initialize_toolbar(self): """ """ # Merge the main tool bar and the plot tool bar to get back some # real estate self.current_workspace.addToolBar( self.current_workspace.current_plot_window.tool_bar) self.current_workspace.main_tool_bar.setIconSize(QSize(15, 15)) # Hide the first five actions in the default specviz tool bar for act in self.current_workspace.main_tool_bar.actions()[:6]: act.setVisible(False) # Hide the tabs of the mdiarea in specviz. self.current_workspace.mdi_area.setViewMode(QMdiArea.SubWindowView) self.current_workspace.current_plot_window.setWindowFlags(Qt.FramelessWindowHint) self.current_workspace.current_plot_window.showMaximized() if self._layout is not None: cube_ops = QAction(QIcon(":/icons/cube.svg"), "Cube Operations", self.current_workspace.main_tool_bar) self.current_workspace.main_tool_bar.addAction(cube_ops) self.current_workspace.main_tool_bar.addSeparator() button = self.current_workspace.main_tool_bar.widgetForAction(cube_ops) button.setPopupMode(QToolButton.InstantPopup) menu = QMenu(self.current_workspace.main_tool_bar) button.setMenu(menu) # Create operation actions menu.addSection("2D Operations") act = QAction("Simple Linemap", self) act.triggered.connect(lambda: simple_linemap(self)) menu.addAction(act) act = QAction("Fitted Linemap", self) act.triggered.connect(lambda: fitted_linemap(self)) menu.addAction(act) menu.addSection("3D Operations") act = QAction("Fit Spaxels", self) act.triggered.connect(lambda: fit_spaxels(self)) menu.addAction(act) act = QAction("Spectral Smoothing", self) act.triggered.connect(lambda: spectral_smoothing(self)) menu.addAction(act) def update_units(self, spectral_axis_unit=None, data_unit=None): """ Interface for data viewers to update the plotted axis units in specviz. Parameters ---------- spectral_axis_unit : str or :class:`~astropy.unit.Quantity` The spectral axis unit to convert to. data_unit : str or :class:`~astropy.unit.Quantity` The data axis unit to convert to. """ if spectral_axis_unit is not None: self.hub.plot_widget.spectral_axis_unit = spectral_axis_unit if data_unit is not None: self.hub.plot_widget.data_unit = data_unit def update_slice_indicator_position(self, pos): """ Updates the current position of the movable vertical slice indicator. Parameters ---------- pos : float The new position of the vertical line. """ if self._slice_indicator is not None: self._slice_indicator.blockSignals(True) self._slice_indicator.setPos(u.Quantity(pos).value) self._slice_indicator.blockSignals(False)
class ScanPlotWidget(PlotWidget): """ Extend the PlotWidget Class with more functionality used for qudi scan images. Supported features: - draggable/static crosshair with optional range and size constraints. - zoom feature by rubberband selection - rubberband area selection This class depends on the ScanViewBox class defined further below. This class can be promoted in the Qt designer. """ sigMouseAreaSelected = QtCore.Signal(QtCore.QRectF) # mapped rectangle mouse cursor selection sigCrosshairPosChanged = QtCore.Signal(QtCore.QPointF) sigCrosshairDraggedPosChanged = QtCore.Signal(QtCore.QPointF) def __init__(self, *args, **kwargs): kwargs['viewBox'] = ScanViewBox() # Use custom pg.ViewBox subclass super().__init__(*args, **kwargs) self.getViewBox().sigMouseAreaSelected.connect(self.sigMouseAreaSelected) self._min_crosshair_factor = 0.02 self._crosshair_size = (0, 0) self._crosshair_range = None self.getViewBox().sigRangeChanged.connect(self._constraint_crosshair_size) self.crosshair = ROI((0, 0), (0, 0), pen={'color': '#00ff00', 'width': 1}) self.hline = InfiniteLine(pos=0, angle=0, movable=True, pen={'color': '#00ff00', 'width': 1}, hoverPen={'color': '#ffff00', 'width': 1}) self.vline = InfiniteLine(pos=0, angle=90, movable=True, pen={'color': '#00ff00', 'width': 1}, hoverPen={'color': '#ffff00', 'width': 1}) self.vline.sigDragged.connect(self._update_pos_from_line) self.hline.sigDragged.connect(self._update_pos_from_line) self.crosshair.sigRegionChanged.connect(self._update_pos_from_roi) self.sigCrosshairDraggedPosChanged.connect(self.sigCrosshairPosChanged) @property def crosshair_enabled(self): items = self.items() return (self.vline in items) and (self.hline in items) and (self.crosshair in items) @property def crosshair_movable(self): return bool(self.crosshair.translatable) @property def crosshair_position(self): pos = self.vline.pos() pos[1] = self.hline.pos()[1] return tuple(pos) @property def crosshair_size(self): return tuple(self._crosshair_size) @property def crosshair_min_size_factor(self): return float(self._min_crosshair_factor) @property def crosshair_range(self): if self._crosshair_range is None: return None return tuple(self._crosshair_range) @property def selection_enabled(self): return bool(self.getViewBox().rectangle_selection) @property def zoom_by_selection_enabled(self): return bool(self.getViewBox().zoom_by_selection) def toggle_selection(self, enable): """ De-/Activate the rectangular rubber band selection tool. If active you can select a rectangular region within the ViewBox by dragging the mouse with the left button. Each selection rectangle in real-world data coordinates will be emitted by sigMouseAreaSelected. By using activate_zoom_by_selection you can optionally de-/activate zooming in on the selection. @param bool enable: Toggle selection on (True) or off (False) """ return self.getViewBox().toggle_selection(enable) def toggle_zoom_by_selection(self, enable): """ De-/Activate automatic zooming into a selection. See also: toggle_selection @param bool enable: Toggle zoom upon selection on (True) or off (False) """ return self.getViewBox().toggle_zoom_by_selection(enable) def _update_pos_from_line(self, obj): """ Called each time the position of the InfiniteLines has been changed by a user drag. Causes the crosshair rectangle to follow the lines. """ if obj not in (self.hline, self.vline): return pos = self.vline.pos() pos[1] = self.hline.pos()[1] size = self.crosshair.size() self.crosshair.blockSignals(True) self.crosshair.setPos((pos[0] - size[0] / 2, pos[1] - size[1] / 2)) self.crosshair.blockSignals(False) self.sigCrosshairDraggedPosChanged.emit(QtCore.QPointF(pos[0], pos[1])) return def _update_pos_from_roi(self, obj): """ Called each time the position of the rectangular ROI has been changed by a user drag. Causes the InfiniteLines to follow the ROI. """ if obj is not self.crosshair: return pos = self.crosshair.pos() size = self.crosshair.size() pos[0] += size[0] / 2 pos[1] += size[1] / 2 self.vline.setPos(pos[0]) self.hline.setPos(pos[1]) self.sigCrosshairDraggedPosChanged.emit(QtCore.QPointF(pos[0], pos[1])) return def toggle_crosshair(self, enable, movable=True): """ Disable/Enable the crosshair within the PlotWidget. Optionally also toggle if it can be dragged by the user. @param bool enable: enable crosshair (True), disable crosshair (False) @param bool movable: enable user drag (True), disable user drag (False) """ if not isinstance(enable, bool): raise TypeError('Positional argument "enable" must be bool type.') if not isinstance(movable, bool): raise TypeError('Optional argument "movable" must be bool type.') self.toggle_crosshair_movable(movable) is_enabled = self.crosshair_enabled if enable and not is_enabled: self.addItem(self.vline) self.addItem(self.hline) self.addItem(self.crosshair) elif not enable and is_enabled: self.removeItem(self.vline) self.removeItem(self.hline) self.removeItem(self.crosshair) return def toggle_crosshair_movable(self, enable): """ Toggle if the crosshair can be dragged by the user. @param bool enable: enable (True), disable (False) """ self.crosshair.translatable = bool(enable) self.vline.setMovable(enable) self.hline.setMovable(enable) return def set_crosshair_pos(self, pos): """ Set the crosshair center to the given coordinates. @param QPointF|float[2] pos: (x,y) position of the crosshair """ try: pos = tuple(pos) except TypeError: pos = (pos.x(), pos.y()) size = self.crosshair.size() self.crosshair.blockSignals(True) self.vline.blockSignals(True) self.hline.blockSignals(True) self.crosshair.setPos(pos[0] - size[0] / 2, pos[1] - size[1] / 2) self.vline.setPos(pos[0]) self.hline.setPos(pos[1]) self.crosshair.blockSignals(False) self.vline.blockSignals(False) self.hline.blockSignals(False) self.sigCrosshairPosChanged.emit(QtCore.QPointF(*pos)) return def set_crosshair_size(self, size, force_default=True): """ Set the default size of the crosshair rectangle (x, y) and update the display. @param QSize|float[2] size: the (x,y) size of the crosshair rectangle @param bool force_default: Set default crosshair size and enforce minimal size (True). Enforce displayed crosshair size while keeping default size untouched (False). """ try: size = tuple(size) except TypeError: size = (size.width(), size.height()) if force_default: if size[0] <= 0 and size[1] <= 0: self._crosshair_size = (0, 0) else: self._crosshair_size = size # Check if actually displayed size needs to be adjusted due to minimal size size = self._get_corrected_crosshair_size(size) pos = self.vline.pos() pos[1] = self.hline.pos()[1] - size[1] / 2 pos[0] -= size[0] / 2 if self._crosshair_range: crange = self._crosshair_range self.crosshair.maxBounds = QtCore.QRectF(crange[0][0] - size[0] / 2, crange[1][0] - size[1] / 2, crange[0][1] - crange[0][0] + size[0], crange[1][1] - crange[1][0] + size[1]) self.crosshair.blockSignals(True) self.crosshair.setSize(size) self.crosshair.setPos(pos) self.crosshair.blockSignals(False) return def set_crosshair_min_size_factor(self, factor): """ Sets the minimum crosshair size factor. This will determine the minimum size of the smallest edge of the crosshair center rectangle. This minimum size is calculated by taking the smallest visible axis of the ViewBox and multiplying it with the scale factor set by this method. The crosshair rectangle will be then scaled accordingly if the set crosshair size is smaller than this minimal size. @param float factor: The scale factor to set. If <= 0 no minimal crosshair size enforced. """ if factor <= 0: self._min_crosshair_factor = 0 elif factor <= 1: self._min_crosshair_factor = float(factor) else: raise ValueError('Crosshair min size factor must be a value <= 1.') return def set_crosshair_range(self, new_range): """ Sets a range boundary for the crosshair position. @param float[2][2] new_range: two min-max range value tuples (for x and y axis). If None set unlimited ranges. """ if new_range is None: self.vline.setBounds([None, None]) self.hline.setBounds([None, None]) self.crosshair.maxBounds = None else: self.vline.setBounds(new_range[0]) self.hline.setBounds(new_range[1]) size = self.crosshair.size() pos = self.crosshair_position self.crosshair.maxBounds = QtCore.QRectF(new_range[0][0] - size[0] / 2, new_range[1][0] - size[1] / 2, new_range[0][1] - new_range[0][0] + size[0], new_range[1][1] - new_range[1][0] + size[1]) self.crosshair.setPos(pos[0] - size[0] / 2, pos[1] - size[1] / 2) self._crosshair_range = new_range return def set_crosshair_pen(self, pen): """ Sets the pyqtgraph compatible pen to be used for drawing the crosshair lines. @param pen: pyqtgraph compatible pen to use """ self.crosshair.setPen(pen) self.vline.setPen(pen) self.hline.setPen(pen) return def _constraint_crosshair_size(self): if self._min_crosshair_factor == 0: return if self._crosshair_size[0] == 0 or self._crosshair_size[1] == 0: return corr_size = self._get_corrected_crosshair_size(self._crosshair_size) if corr_size != tuple(self.crosshair.size()): self.set_crosshair_size(corr_size, force_default=False) return def _get_corrected_crosshair_size(self, size): try: size = tuple(size) except TypeError: size = (size.width(), size.height()) min_size = min(size) if min_size == 0: return size vb_size = self.getViewBox().viewRect().size() short_index = int(vb_size.width() > vb_size.height()) min_vb_size = vb_size.width() if short_index == 0 else vb_size.height() min_vb_size *= self._min_crosshair_factor if min_size < min_vb_size: scale_factor = min_vb_size / min_size size = (size[0] * scale_factor, size[1] * scale_factor) return size