class FigureWidget(QWidget): def __init__( self, title=f"Figure", yRange=[-2, 6], \ xdata=[], ydata=[], \ color=20 \ ): super().__init__() self.title = f'<h3> {title} </h3>' self.color = color self.x_data = np.array(xdata) self.y_data = np.array(ydata) self.y_min, self.y_max = yRange[0] , yRange[1] self.initFigure() def initFigure(self): # Configure Layout self.layout_model = QGridLayout() self.setLayout( self.layout_model ) # Figure title is 1x3 (row x col) excel cell self.TITLE = QLabel(text=self.title) self.TITLE.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.TITLE.setAlignment(QtCoreQtClass.AlignCenter) self.layout_model.addWidget(self.TITLE, 0, 0, 1, 3) self.layout_model.setRowStretch(0, 1) #Figure size is 4x3 (row x col) excel cell self.Figure = PlotWidget() self.Figure.setYRange(self.y_min, self.y_max) self.graph = PlotCurveItem(pen = self.color ) #plot initial data self.graph.setData(self.x_data, self.y_data) self.Figure.addItem(self.graph) # add to main figure widget & resize the grid layout self.layout_model.addWidget(self.Figure, 1, 0, 4, 3) for n in range(4): self.layout_model.setRowStretch(n+1, 7) def PlotData(self, x_data, y_data): self.x_data = x_data self.y_data = y_data self.graph.setData(self.x_data, self.y_data)
class ProcessImage(QWidget): def __init__(self, parent=None, place='LI-Energy', prefix=_VACA_PREFIX): super().__init__(parent) self._place = place or 'LI-Energy' self._prefix = prefix self._select_experimental_setup() self.cen_x = None self.cen_y = None self.sigma_x = None self.sigma_y = None self.bg_ready = False self.bg = None self.nbg = 0 self._setupUi() def _select_experimental_setup(self): pref = self._prefix if self._place.lower().startswith('li-ene'): prof = pref + ('-' if pref else '') + 'LA-BI:PRF4' self.conv_coefx = PV(prof + ':X:Gauss:Coef') self.conv_coefy = PV(prof + ':Y:Gauss:Coef') self.image_channel = prof + ':RAW:ArrayData' self.width_channel = prof + ':ROI:MaxSizeX_RBV' self.trig_name = 'LI-Fam:TI-Scrn' elif self._place.lower().startswith('li-emit'): prof = pref + ('-' if pref else '') + 'LA-BI:PRF5' self.conv_coefx = PV(prof + ':X:Gauss:Coef') self.conv_coefy = PV(prof + ':Y:Gauss:Coef') self.image_channel = prof + ':RAW:ArrayData' self.width_channel = prof + ':ROI:MaxSizeX_RBV' self.trig_name = 'LI-Fam:TI-Scrn' elif self._place.lower().startswith('tb-emit'): prof = _PVName('TB-02:DI-ScrnCam-2').substitute(prefix=pref) self.conv_coefx = PV(prof.substitute(propty='ImgScaleFactorX-RB')) self.conv_coefy = PV(prof.substitute(propty='ImgScaleFactorY-RB')) prof = _PVName('TB-02:DI-Scrn-2').substitute(prefix=pref) self.image_channel = prof.substitute(propty='ImgData-Mon') self.width_channel = prof.substitute(propty='ImgROIWidth-RB') self.trig_name = 'TB-Fam:TI-Scrn' else: raise Exception('Wrong value for "place".') def _setupUi(self): vl = QVBoxLayout(self) self.image_view = ImageView( self.process_image, parent=self, image_channel=self.image_channel, width_channel=self.width_channel) self.image_view.maxRedrawRate = 5 self.image_view.readingOrder = self.image_view.Clike self.plt_roi = PlotCurveItem([0, 0, 400, 400, 0], [0, 400, 400, 0, 0]) pen = mkPen() pen.setColor(QColor('red')) pen.setWidth(1) self.plt_roi.setPen(pen) self.image_view.addItem(self.plt_roi) self.plt_fit_x = PlotCurveItem([0, 0], [0, 400]) self.plt_fit_y = PlotCurveItem([0, 0], [0, 400]) self.plt_his_x = PlotCurveItem([0, 0], [0, 400]) self.plt_his_y = PlotCurveItem([0, 0], [0, 400]) pen = mkPen() pen.setColor(QColor('yellow')) self.plt_his_x.setPen(pen) self.plt_his_y.setPen(pen) self.image_view.addItem(self.plt_fit_x) self.image_view.addItem(self.plt_fit_y) self.image_view.addItem(self.plt_his_x) self.image_view.addItem(self.plt_his_y) vl.addWidget(self.image_view) gb_trig = QGroupBox('Trigger', self) vl.addWidget(gb_trig) gb_trig.setLayout(QVBoxLayout()) gb_trig.layout().addWidget(HLTriggerSimple( gb_trig, device=self.trig_name, prefix=self._prefix)) gb_pos = QGroupBox('Position [mm]', self) vl.addWidget(gb_pos) hl = QHBoxLayout(gb_pos) fl = QFormLayout() hl.addLayout(fl) self.cbox_method = QComboBox(gb_pos) self.cbox_method.addItem('Gauss Fit') self.cbox_method.addItem('Moments') fl.addRow(QLabel('Method', gb_pos), self.cbox_method) self.spbox_roi_size_x = QSpinBoxPlus(gb_pos) self.spbox_roi_size_y = QSpinBoxPlus(gb_pos) self.spbox_roi_center_x = QSpinBoxPlus(gb_pos) self.spbox_roi_center_y = QSpinBoxPlus(gb_pos) self.spbox_roi_size_x.setKeyboardTracking(False) self.spbox_roi_size_y.setKeyboardTracking(False) self.spbox_roi_center_x.setKeyboardTracking(False) self.spbox_roi_center_y.setKeyboardTracking(False) self.spbox_roi_size_x.setMaximum(2448) self.spbox_roi_size_y.setMaximum(2050) self.spbox_roi_center_x.setMaximum(2448) self.spbox_roi_center_y.setMaximum(2050) self.spbox_roi_size_x.setValue(300) self.spbox_roi_size_y.setValue(400) self.spbox_roi_center_x.setValue(500) self.spbox_roi_center_y.setValue(500) fl.addRow(QLabel('ROI Size X', gb_pos), self.spbox_roi_size_x) fl.addRow(QLabel('ROI Size Y', gb_pos), self.spbox_roi_size_y) self.cbbox_auto_center = QCheckBox('Automatic Centering', gb_pos) self.cbbox_auto_center.clicked.connect(self.cbbox_auto_center_clicked) self.cbbox_auto_center.setChecked(True) fl.addRow(self.cbbox_auto_center) fl.addRow(QLabel('ROI Center X', gb_pos), self.spbox_roi_center_x) fl.addRow(QLabel('ROI Center Y', gb_pos), self.spbox_roi_center_y) self.spbox_img_max = QSpinBoxPlus(gb_pos) self.spbox_img_max.setKeyboardTracking(False) self.spbox_img_max.setMinimum(0) self.spbox_img_max.setMaximum(2448) self.spbox_img_max.setValue(0) fl.addRow(QLabel('Max. Pixel Val.', gb_pos), self.spbox_img_max) self.cbbox_acq_bg = QCheckBox('Acquire Background', gb_pos) self.cbbox_acq_bg.clicked.connect(self.cbbox_acq_bg_checked) fl.addRow(self.cbbox_acq_bg) self.pb_reset_bg = QPushButton('Reset BG', gb_pos) self.pb_reset_bg.clicked.connect(self.pb_reset_bg_clicked) fl.addRow(self.pb_reset_bg) fl = QFormLayout() hl.addLayout(fl) self.lb_xave = QLabel('0', gb_pos) self.lb_yave = QLabel('0', gb_pos) self.lb_xstd = QLabel('0', gb_pos) self.lb_ystd = QLabel('0', gb_pos) fl.addRow(QLabel('Average Position', gb_pos)) fl.addRow(QLabel('x = ', gb_pos), self.lb_xave) fl.addRow(QLabel('y = ', gb_pos), self.lb_yave) fl.addRow(QLabel('Beam Size', gb_pos)) fl.addRow(QLabel('x = ', gb_pos), self.lb_xstd) fl.addRow(QLabel('y = ', gb_pos), self.lb_ystd) hl.setSpacing(12) hl.setStretch(0, 1) hl.setStretch(1, 1) def cbbox_auto_center_clicked(self, clicked): self.spbox_roi_center_x.setEnabled(not clicked) self.spbox_roi_center_y.setEnabled(not clicked) def pb_reset_bg_clicked(self, clicked=False): self.bg_ready = False self.bg = None self.nbg = 0 def cbbox_acq_bg_checked(self, check): if check: self.pb_reset_bg_clicked() else: if self.bg is not None: self.bg /= self.nbg self.bg_ready = True def calc_roi(self, image): proj_x = image.sum(axis=0) proj_y = image.sum(axis=1) axis_x = np.arange(image.shape[1]) axis_y = np.arange(image.shape[0]) if self.cbbox_auto_center.isChecked(): cen_x, _ = _calc_moments(axis_x, proj_x) cen_y, _ = _calc_moments(axis_y, proj_y) else: cen_x = self.spbox_roi_center_x.value() cen_y = self.spbox_roi_center_y.value() roi_size_x = self.spbox_roi_size_x.value() roi_size_y = self.spbox_roi_size_y.value() strt_x, end_x = np.array([-1, 1])*roi_size_x + int(cen_x) strt_y, end_y = np.array([-1, 1])*roi_size_y + int(cen_y) strt_x = max(strt_x, 0) strt_y = max(strt_y, 0) end_x = min(end_x, image.shape[1]) end_y = min(end_y, image.shape[0]) self.plt_roi.setData( np.array([strt_x, strt_x, end_x, end_x, strt_x]), np.array([strt_y, end_y, end_y, strt_y, strt_y])) image = image[strt_y:end_y, strt_x:end_x] proj_x = image.sum(axis=0) proj_y = image.sum(axis=1) axis_x = axis_x[strt_x:end_x] axis_y = axis_y[strt_y:end_y] return proj_x, proj_y, axis_x, axis_y def process_image(self, image, wid): if wid <= 0: return image try: image = image.reshape((-1, wid)) except (TypeError, ValueError, AttributeError): return image if self.cbbox_acq_bg.isChecked(): if self.bg is None: self.bg = np.array(image, dtype=float) else: self.bg += np.array(image, dtype=float) self.nbg += 1 return image if self.bg_ready: image -= np.array(self.bg, dtype=image.dtype) b = np.where(image < 0) image[b] = 0 maxi = self.spbox_img_max.value() if maxi > 0: b = np.where(image > maxi) self.image_view.colorMapMax = maxi image[b] = maxi proj_x, proj_y, axis_x, axis_y = self.calc_roi(image) x_max = max(proj_x) y_max = max(proj_y) if self.cbox_method.currentIndex(): cen_x, std_x = _calc_moments(axis_x, proj_x) cen_y, std_y = _calc_moments(axis_y, proj_y) amp_x = x_max amp_y = y_max off_x = 0 off_y = 0 else: amp_x, cen_x, std_x, off_x = _fit_gaussian(axis_x, proj_x) amp_y, cen_y, std_y, off_y = _fit_gaussian(axis_y, proj_y) std_x = abs(std_x) std_y = abs(std_y) yd = _gaussian(axis_x, amp_x, cen_x, std_x, off_x)/x_max*400 self.plt_fit_x.setData(axis_x, yd + axis_y[0]) self.plt_his_x.setData(axis_x, proj_x/x_max*400 + axis_y[0]) yd = _gaussian(axis_y, amp_y, cen_y, std_y, off_y)/y_max*400 self.plt_fit_y.setData(yd + axis_x[0], axis_y) self.plt_his_y.setData(proj_y/y_max*400 + axis_x[0], axis_y) offset_x = image.shape[1]/2 offset_y = image.shape[0]/2 self.lb_xave.setText('{0:4d}'.format(int(cen_x or 0))) self.lb_yave.setText('{0:4d}'.format(int(cen_y or 0))) self.lb_xstd.setText('{0:4d}'.format(int(std_x or 0))) self.lb_ystd.setText('{0:4d}'.format(int(std_y or 0))) coefx = self.conv_coefx.value coefy = self.conv_coefy.value if coefx is None or coefy is None: return cen_x -= offset_x cen_y -= offset_y self.cen_x = cen_x * coefx*1e-3 # transform to meter self.cen_y = cen_y * coefy*1e-3 self.sigma_x = std_x * coefx*1e-3 self.sigma_y = std_y * coefy*1e-3 return image def get_params(self): return self.cen_x, self.sigma_x, self.cen_y, self.sigma_y
class SiriusSpectrogramView(GraphicsLayoutWidget, PyDMWidget, PyDMColorMap, ReadingOrder): """ A SpectrogramView with support for Channels and more from PyDM. If there is no :attr:`channelWidth` it is possible to define the width of the image with the :attr:`width` property. The :attr:`normalizeData` property defines if the colors of the images are relative to the :attr:`colorMapMin` and :attr:`colorMapMax` property or to the minimum and maximum values of the image. Use the :attr:`newImageSignal` to hook up to a signal that is emitted when a new image is rendered in the widget. Parameters ---------- parent : QWidget The parent widget for the Label image_channel : str, optional The channel to be used by the widget for the image data. xaxis_channel : str, optional The channel to be used by the widget to receive the image width (if ReadingOrder == Clike), and to set the xaxis values yaxis_channel : str, optional The channel to be used by the widget to receive the image width (if ReadingOrder == Fortranlike), and to set the yaxis values background : QColor, optional QColor to set the background color of the GraphicsView """ Q_ENUMS(PyDMColorMap) Q_ENUMS(ReadingOrder) color_maps = cmaps def __init__(self, parent=None, image_channel=None, xaxis_channel=None, yaxis_channel=None, roioffsetx_channel=None, roioffsety_channel=None, roiwidth_channel=None, roiheight_channel=None, title='', background='w', image_width=0, image_height=0): """Initialize widget.""" GraphicsLayoutWidget.__init__(self, parent) PyDMWidget.__init__(self) self.thread = None self._imagechannel = None self._xaxischannel = None self._yaxischannel = None self._roioffsetxchannel = None self._roioffsetychannel = None self._roiwidthchannel = None self._roiheightchannel = None self._channels = 7 * [ None, ] self.image_waveform = np.zeros(0) self._image_width = image_width if not xaxis_channel else 0 self._image_height = image_height if not yaxis_channel else 0 self._roi_offsetx = 0 self._roi_offsety = 0 self._roi_width = 0 self._roi_height = 0 self._normalize_data = False self._auto_downsample = True self._last_yaxis_data = None self._last_xaxis_data = None self._auto_colorbar_lims = True self.format_tooltip = '{0:.4g}, {1:.4g}' # ViewBox and imageItem. self._view = ViewBox() self._image_item = ImageItem() self._view.addItem(self._image_item) # ROI self.ROICurve = PlotCurveItem([0, 0, 0, 0, 0], [0, 0, 0, 0, 0]) self.ROIColor = QColor('red') pen = mkPen() pen.setColor(QColor('transparent')) pen.setWidth(1) self.ROICurve.setPen(pen) self._view.addItem(self.ROICurve) # Axis. self.xaxis = AxisItem('bottom') self.xaxis.setPen(QColor(0, 0, 0)) if not xaxis_channel: self.xaxis.setVisible(False) self.yaxis = AxisItem('left') self.yaxis.setPen(QColor(0, 0, 0)) if not yaxis_channel: self.yaxis.setVisible(False) # Colorbar legend. self.colorbar = _GradientLegend() # Title. start_row = 0 if title: self.title = LabelItem(text=title, color='#000000') self.addItem(self.title, 0, 0, 1, 3) start_row = 1 # Set layout. self.addItem(self._view, start_row, 1) self.addItem(self.yaxis, start_row, 0) self.addItem(self.colorbar, start_row, 2) self.addItem(self.xaxis, start_row + 1, 1) self.setBackground(background) self.ci.layout.setColumnSpacing(0, 0) self.ci.layout.setRowSpacing(start_row, 0) # Set color map limits. self.cm_min = 0.0 self.cm_max = 255.0 # Set default reading order of numpy array data to Clike. self._reading_order = ReadingOrder.Clike # Make a right-click menu for changing the color map. self.cm_group = QActionGroup(self) self.cmap_for_action = {} for cm in self.color_maps: action = self.cm_group.addAction(cmap_names[cm]) action.setCheckable(True) self.cmap_for_action[action] = cm # Set the default colormap. self._cm_colors = None self.colorMap = PyDMColorMap.Inferno # Setup the redraw timer. self.needs_redraw = False self.redraw_timer = QTimer(self) self.redraw_timer.timeout.connect(self.redrawImage) self._redraw_rate = 30 self.maxRedrawRate = self._redraw_rate self.newImageSignal = self._image_item.sigImageChanged # Set Channels. self.imageChannel = image_channel self.xAxisChannel = xaxis_channel self.yAxisChannel = yaxis_channel self.ROIOffsetXChannel = roioffsetx_channel self.ROIOffsetYChannel = roioffsety_channel self.ROIWidthChannel = roiwidth_channel self.ROIHeightChannel = roiheight_channel # --- Context menu --- def widget_ctx_menu(self): """ Fetch the Widget specific context menu. It will be populated with additional tools by `assemble_tools_menu`. Returns ------- QMenu or None If the return of this method is None a new QMenu will be created by `assemble_tools_menu`. """ self.menu = ViewBoxMenu(self._view) cm_menu = self.menu.addMenu("Color Map") for act in self.cmap_for_action.keys(): cm_menu.addAction(act) cm_menu.triggered.connect(self._changeColorMap) return self.menu # --- Colormap methods --- def _changeColorMap(self, action): """ Method invoked by the colormap Action Menu. Changes the current colormap used to render the image. Parameters ---------- action : QAction """ self.colorMap = self.cmap_for_action[action] @Property(float) def colorMapMin(self): """ Minimum value for the colormap. Returns ------- float """ return self.cm_min @colorMapMin.setter @Slot(float) def colorMapMin(self, new_min): """ Set the minimum value for the colormap. Parameters ---------- new_min : float """ if self.cm_min != new_min: self.cm_min = new_min if self.cm_min > self.cm_max: self.cm_max = self.cm_min @Property(float) def colorMapMax(self): """ Maximum value for the colormap. Returns ------- float """ return self.cm_max @colorMapMax.setter @Slot(float) def colorMapMax(self, new_max): """ Set the maximum value for the colormap. Parameters ---------- new_max : float """ if self.cm_max != new_max: self.cm_max = new_max if self.cm_max < self.cm_min: self.cm_min = self.cm_max def setColorMapLimits(self, mn, mx): """ Set the limit values for the colormap. Parameters ---------- mn : int The lower limit mx : int The upper limit """ if mn >= mx: return self.cm_max = mx self.cm_min = mn @Property(PyDMColorMap) def colorMap(self): """ Return the color map used by the SpectrogramView. Returns ------- PyDMColorMap """ return self._colormap @colorMap.setter def colorMap(self, new_cmap): """ Set the color map used by the SpectrogramView. Parameters ------- new_cmap : PyDMColorMap """ self._colormap = new_cmap self._cm_colors = self.color_maps[new_cmap] self.setColorMap() for action in self.cm_group.actions(): if self.cmap_for_action[action] == self._colormap: action.setChecked(True) else: action.setChecked(False) def setColorMap(self, cmap=None): """ Update the image colormap. Parameters ---------- cmap : ColorMap """ if not cmap: if not self._cm_colors.any(): return # Take default values pos = np.linspace(0.0, 1.0, num=len(self._cm_colors)) cmap = ColorMap(pos, self._cm_colors) self._view.setBackgroundColor(cmap.map(0)) lut = cmap.getLookupTable(0.0, 1.0, alpha=False) self.colorbar.setIntColorScale(colors=lut) self._image_item.setLookupTable(lut) # --- Connection Slots --- @Slot(bool) def image_connection_state_changed(self, conn): """ Callback invoked when the Image Channel connection state is changed. Parameters ---------- conn : bool The new connection state. """ if conn: self.redraw_timer.start() else: self.redraw_timer.stop() @Slot(bool) def yaxis_connection_state_changed(self, connected): """ Callback invoked when the TimeAxis Channel connection state is changed. Parameters ---------- conn : bool The new connection state. """ self._timeaxis_connected = connected @Slot(bool) def roioffsetx_connection_state_changed(self, conn): """ Run when the ROIOffsetX Channel connection state changes. Parameters ---------- conn : bool The new connection state. """ if not conn: self._roi_offsetx = 0 @Slot(bool) def roioffsety_connection_state_changed(self, conn): """ Run when the ROIOffsetY Channel connection state changes. Parameters ---------- conn : bool The new connection state. """ if not conn: self._roi_offsety = 0 @Slot(bool) def roiwidth_connection_state_changed(self, conn): """ Run when the ROIWidth Channel connection state changes. Parameters ---------- conn : bool The new connection state. """ if not conn: self._roi_width = 0 @Slot(bool) def roiheight_connection_state_changed(self, conn): """ Run when the ROIHeight Channel connection state changes. Parameters ---------- conn : bool The new connection state. """ if not conn: self._roi_height = 0 # --- Value Slots --- @Slot(np.ndarray) def image_value_changed(self, new_image): """ Callback invoked when the Image Channel value is changed. We try to do as little as possible in this method, because it gets called every time the image channel updates, which might be extremely often. Basically just store the data, and set a flag requesting that the image be redrawn. Parameters ---------- new_image : np.ndarray The new image data. This can be a flat 1D array, or a 2D array. """ if new_image is None or new_image.size == 0: return logging.debug("SpectrogramView Received New Image: Needs Redraw->True") self.image_waveform = new_image self.needs_redraw = True if not self._image_height and self._image_width: self._image_height = new_image.size / self._image_width elif not self._image_width and self._image_height: self._image_width = new_image.size / self._image_height @Slot(np.ndarray) @Slot(float) def xaxis_value_changed(self, new_array): """ Callback invoked when the Image Width Channel value is changed. Parameters ---------- new_array : np.ndarray The new x axis array """ if new_array is None: return if isinstance(new_array, float): new_array = np.array([ new_array, ]) self._last_xaxis_data = new_array if self._reading_order == self.Clike: self._image_width = new_array.size else: self._image_height = new_array.size self.needs_redraw = True @Slot(np.ndarray) @Slot(float) def yaxis_value_changed(self, new_array): """ Callback invoked when the TimeAxis Channel value is changed. Parameters ---------- new_array : np.array The new y axis array """ if new_array is None: return if isinstance(new_array, float): new_array = np.array([ new_array, ]) self._last_yaxis_data = new_array if self._reading_order == self.Fortranlike: self._image_width = new_array.size else: self._image_height = new_array.size self.needs_redraw = True @Slot(int) def roioffsetx_value_changed(self, new_offset): """ Run when the ROIOffsetX Channel value changes. Parameters ---------- new_offsetx : int The new image ROI horizontal offset """ if new_offset is None: return self._roi_offsetx = new_offset self.redrawROI() @Slot(int) def roioffsety_value_changed(self, new_offset): """ Run when the ROIOffsetY Channel value changes. Parameters ---------- new_offsety : int The new image ROI vertical offset """ if new_offset is None: return self._roi_offsety = new_offset self.redrawROI() @Slot(int) def roiwidth_value_changed(self, new_width): """ Run when the ROIWidth Channel value changes. Parameters ---------- new_width : int The new image ROI width """ if new_width is None: return self._roi_width = int(new_width) self.redrawROI() @Slot(int) def roiheight_value_changed(self, new_height): """ Run when the ROIHeight Channel value changes. Parameters ---------- new_height : int The new image ROI height """ if new_height is None: return self._roi_height = int(new_height) self.redrawROI() # --- Image update methods --- def process_image(self, image): """ Boilerplate method. To be used by applications in order to add calculations and also modify the image before it is displayed at the widget. .. warning:: This code runs in a separated QThread so it **MUST** not try to write to QWidgets. Parameters ---------- image : np.ndarray The Image Data as a 2D numpy array Returns ------- np.ndarray The Image Data as a 2D numpy array after processing. """ return image def redrawImage(self): """ Set the image data into the ImageItem, if needed. If necessary, reshape the image to 2D first. """ if self.thread is not None and not self.thread.isFinished(): logger.warning( "Image processing has taken longer than the refresh rate.") return self.thread = SpectrogramUpdateThread(self) self.thread.updateSignal.connect(self._updateDisplay) logging.debug("SpectrogramView RedrawImage Thread Launched") self.thread.start() @Slot(list) def _updateDisplay(self, data): logging.debug("SpectrogramView Update Display with new image") # Update axis if self._last_xaxis_data is not None: szx = self._last_xaxis_data.size xMin = self._last_xaxis_data.min() xMax = self._last_xaxis_data.max() else: szx = self.imageWidth if self.readingOrder == self.Clike \ else self.imageHeight xMin = 0 xMax = szx if self._last_yaxis_data is not None: szy = self._last_yaxis_data.size yMin = self._last_yaxis_data.min() yMax = self._last_yaxis_data.max() else: szy = self.imageHeight if self.readingOrder == self.Clike \ else self.imageWidth yMin = 0 yMax = szy self.xaxis.setRange(xMin, xMax) self.yaxis.setRange(yMin, yMax) self._view.setLimits(xMin=0, xMax=szx, yMin=0, yMax=szy, minXRange=szx, maxXRange=szx, minYRange=szy, maxYRange=szy) # Update image if self.autoSetColorbarLims: self.colorbar.setLimits(data) mini, maxi = data[0], data[1] img = data[2] self._image_item.setLevels([mini, maxi]) self._image_item.setImage(img, autoLevels=False, autoDownsample=self.autoDownsample) # ROI update methods def redrawROI(self): startx = self._roi_offsetx endx = self._roi_offsetx + self._roi_width starty = self._roi_offsety endy = self._roi_offsety + self._roi_height self.ROICurve.setData([startx, startx, endx, endx, startx], [starty, endy, endy, starty, starty]) def showROI(self, show): """Set ROI visibility.""" pen = mkPen() if show: pen.setColor(self.ROIColor) else: pen.setColor(QColor('transparent')) self.ROICurve.setPen(pen) # --- Properties --- @Property(bool) def autoDownsample(self): """ Return if we should or not apply the autoDownsample option. Return ------ bool """ return self._auto_downsample @autoDownsample.setter def autoDownsample(self, new_value): """ Whether we should or not apply the autoDownsample option. Parameters ---------- new_value: bool """ if new_value != self._auto_downsample: self._auto_downsample = new_value @Property(bool) def autoSetColorbarLims(self): """ Return if we should or not auto set colorbar limits. Return ------ bool """ return self._auto_colorbar_lims @autoSetColorbarLims.setter def autoSetColorbarLims(self, new_value): """ Whether we should or not auto set colorbar limits. Parameters ---------- new_value: bool """ if new_value != self._auto_colorbar_lims: self._auto_colorbar_lims = new_value @Property(int) def imageWidth(self): """ Return the width of the image. Return ------ int """ return self._image_width @imageWidth.setter def imageWidth(self, new_width): """ Set the width of the image. Can be overridden by :attr:`xAxisChannel` and :attr:`yAxisChannel`. Parameters ---------- new_width: int """ boo = self._image_width != int(new_width) boo &= not self._xaxischannel boo &= not self._yaxischannel if boo: self._image_width = int(new_width) @Property(int) def imageHeight(self): """ Return the height of the image. Return ------ int """ return self._image_height @Property(int) def ROIOffsetX(self): """ Return the ROI offset in X axis in pixels. Return ------ int """ return self._roi_offsetx @ROIOffsetX.setter def ROIOffsetX(self, new_offset): """ Set the ROI offset in X axis in pixels. Can be overridden by :attr:`ROIOffsetXChannel`. Parameters ---------- new_offset: int """ if new_offset is None: return boo = self._roi_offsetx != int(new_offset) boo &= not self._roioffsetxchannel if boo: self._roi_offsetx = int(new_offset) self.redrawROI() @Property(int) def ROIOffsetY(self): """ Return the ROI offset in Y axis in pixels. Return ------ int """ return self._roi_offsety @ROIOffsetY.setter def ROIOffsetY(self, new_offset): """ Set the ROI offset in Y axis in pixels. Can be overridden by :attr:`ROIOffsetYChannel`. Parameters ---------- new_offset: int """ if new_offset is None: return boo = self._roi_offsety != int(new_offset) boo &= not self._roioffsetychannel if boo: self._roi_offsety = int(new_offset) self.redrawROI() @Property(int) def ROIWidth(self): """ Return the ROI width in pixels. Return ------ int """ return self._roi_width @ROIWidth.setter def ROIWidth(self, new_width): """ Set the ROI width in pixels. Can be overridden by :attr:`ROIWidthChannel`. Parameters ---------- new_width: int """ if new_width is None: return boo = self._roi_width != int(new_width) boo &= not self._roiwidthchannel if boo: self._roi_width = int(new_width) self.redrawROI() @Property(int) def ROIHeight(self): """ Return the ROI height in pixels. Return ------ int """ return self._roi_height @ROIHeight.setter def ROIHeight(self, new_height): """ Set the ROI height in pixels. Can be overridden by :attr:`ROIHeightChannel`. Parameters ---------- new_height: int """ if new_height is None: return boo = self._roi_height != int(new_height) boo &= not self._roiheightchannel if boo: self._roi_height = int(new_height) self.redrawROI() @Property(bool) def normalizeData(self): """ Return True if the colors are relative to data maximum and minimum. Returns ------- bool """ return self._normalize_data @normalizeData.setter @Slot(bool) def normalizeData(self, new_norm): """ Define if the colors are relative to minimum and maximum of the data. Parameters ---------- new_norm: bool """ if self._normalize_data != new_norm: self._normalize_data = new_norm @Property(ReadingOrder) def readingOrder(self): """ Return the reading order of the :attr:`imageChannel` array. Returns ------- ReadingOrder """ return self._reading_order @readingOrder.setter def readingOrder(self, order): """ Set reading order of the :attr:`imageChannel` array. Parameters ---------- order: ReadingOrder """ if self._reading_order != order: self._reading_order = order if order == self.Clike: if self._last_xaxis_data is not None: self._image_width = self._last_xaxis_data.size if self._last_yaxis_data is not None: self._image_height = self._last_yaxis_data.size elif order == self.Fortranlike: if self._last_yaxis_data is not None: self._image_width = self._last_yaxis_data.size if self._last_xaxis_data is not None: self._image_height = self._last_xaxis_data.size @Property(int) def maxRedrawRate(self): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Returns ------- int """ return self._redraw_rate @maxRedrawRate.setter def maxRedrawRate(self, redraw_rate): """ The maximum rate (in Hz) at which the plot will be redrawn. The plot will not be redrawn if there is not new data to draw. Parameters ------- redraw_rate : int """ self._redraw_rate = redraw_rate self.redraw_timer.setInterval(int((1.0 / self._redraw_rate) * 1000)) # --- Events rederivations --- def keyPressEvent(self, ev): """Handle keypress events.""" return def mouseMoveEvent(self, ev): if not self._image_item.width() or not self._image_item.height(): super().mouseMoveEvent(ev) return pos = ev.pos() posaux = self._image_item.mapFromDevice(ev.pos()) if posaux.x() < 0 or posaux.x() >= self._image_item.width() or \ posaux.y() < 0 or posaux.y() >= self._image_item.height(): super().mouseMoveEvent(ev) return pos_scene = self._view.mapSceneToView(pos) x = round(pos_scene.x()) y = round(pos_scene.y()) if self.xAxisChannel and self._last_xaxis_data is not None: maxx = len(self._last_xaxis_data) - 1 x = x if x < maxx else maxx valx = self._last_xaxis_data[x] else: valx = x if self.yAxisChannel and self._last_yaxis_data is not None: maxy = len(self._last_yaxis_data) - 1 y = y if y < maxy else maxy valy = self._last_yaxis_data[y] else: valy = y txt = self.format_tooltip.format(valx, valy) QToolTip.showText(self.mapToGlobal(pos), txt, self, self.geometry(), 5000) super().mouseMoveEvent(ev) # --- Channels --- @Property(str) def imageChannel(self): """ The channel address in use for the image data . Returns ------- str Channel address """ if self._imagechannel: return str(self._imagechannel.address) else: return '' @imageChannel.setter def imageChannel(self, value): """ The channel address in use for the image data . Parameters ---------- value : str Channel address """ if self._imagechannel != value: # Disconnect old channel if self._imagechannel: self._imagechannel.disconnect() # Create and connect new channel self._imagechannel = PyDMChannel( address=value, connection_slot=self.image_connection_state_changed, value_slot=self.image_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[0] = self._imagechannel self._imagechannel.connect() @Property(str) def xAxisChannel(self): """ The channel address in use for the x-axis of image. Returns ------- str Channel address """ if self._xaxischannel: return str(self._xaxischannel.address) else: return '' @xAxisChannel.setter def xAxisChannel(self, value): """ The channel address in use for the x-axis of image. Parameters ---------- value : str Channel address """ if self._xaxischannel != value: # Disconnect old channel if self._xaxischannel: self._xaxischannel.disconnect() # Create and connect new channel self._xaxischannel = PyDMChannel( address=value, connection_slot=self.connectionStateChanged, value_slot=self.xaxis_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[1] = self._xaxischannel self._xaxischannel.connect() @Property(str) def yAxisChannel(self): """ The channel address in use for the time axis. Returns ------- str Channel address """ if self._yaxischannel: return str(self._yaxischannel.address) else: return '' @yAxisChannel.setter def yAxisChannel(self, value): """ The channel address in use for the time axis. Parameters ---------- value : str Channel address """ if self._yaxischannel != value: # Disconnect old channel if self._yaxischannel: self._yaxischannel.disconnect() # Create and connect new channel self._yaxischannel = PyDMChannel( address=value, connection_slot=self.yaxis_connection_state_changed, value_slot=self.yaxis_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[2] = self._yaxischannel self._yaxischannel.connect() @Property(str) def ROIOffsetXChannel(self): """ Return the channel address in use for the image ROI horizontal offset. Returns ------- str Channel address """ if self._roioffsetxchannel: return str(self._roioffsetxchannel.address) else: return '' @ROIOffsetXChannel.setter def ROIOffsetXChannel(self, value): """ Return the channel address in use for the image ROI horizontal offset. Parameters ---------- value : str Channel address """ if self._roioffsetxchannel != value: # Disconnect old channel if self._roioffsetxchannel: self._roioffsetxchannel.disconnect() # Create and connect new channel self._roioffsetxchannel = PyDMChannel( address=value, connection_slot=self.roioffsetx_connection_state_changed, value_slot=self.roioffsetx_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[3] = self._roioffsetxchannel self._roioffsetxchannel.connect() @Property(str) def ROIOffsetYChannel(self): """ Return the channel address in use for the image ROI vertical offset. Returns ------- str Channel address """ if self._roioffsetychannel: return str(self._roioffsetychannel.address) else: return '' @ROIOffsetYChannel.setter def ROIOffsetYChannel(self, value): """ Return the channel address in use for the image ROI vertical offset. Parameters ---------- value : str Channel address """ if self._roioffsetychannel != value: # Disconnect old channel if self._roioffsetychannel: self._roioffsetychannel.disconnect() # Create and connect new channel self._roioffsetychannel = PyDMChannel( address=value, connection_slot=self.roioffsety_connection_state_changed, value_slot=self.roioffsety_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[4] = self._roioffsetychannel self._roioffsetychannel.connect() @Property(str) def ROIWidthChannel(self): """ Return the channel address in use for the image ROI width. Returns ------- str Channel address """ if self._roiwidthchannel: return str(self._roiwidthchannel.address) else: return '' @ROIWidthChannel.setter def ROIWidthChannel(self, value): """ Return the channel address in use for the image ROI width. Parameters ---------- value : str Channel address """ if self._roiwidthchannel != value: # Disconnect old channel if self._roiwidthchannel: self._roiwidthchannel.disconnect() # Create and connect new channel self._roiwidthchannel = PyDMChannel( address=value, connection_slot=self.roiwidth_connection_state_changed, value_slot=self.roiwidth_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[5] = self._roiwidthchannel self._roiwidthchannel.connect() @Property(str) def ROIHeightChannel(self): """ Return the channel address in use for the image ROI height. Returns ------- str Channel address """ if self._roiheightchannel: return str(self._roiheightchannel.address) else: return '' @ROIHeightChannel.setter def ROIHeightChannel(self, value): """ Return the channel address in use for the image ROI height. Parameters ---------- value : str Channel address """ if self._roiheightchannel != value: # Disconnect old channel if self._roiheightchannel: self._roiheightchannel.disconnect() # Create and connect new channel self._roiheightchannel = PyDMChannel( address=value, connection_slot=self.roiheight_connection_state_changed, value_slot=self.roiheight_value_changed, severity_slot=self.alarmSeverityChanged) self._channels[6] = self._roiheightchannel self._roiheightchannel.connect() def channels(self): """ Return the channels being used for this Widget. Returns ------- channels : list List of PyDMChannel objects """ return self._channels def channels_for_tools(self): """Return channels for tools.""" return [self._imagechannel]
class CurveItem: """Represents a curve to be plotted in a diagram.""" SignalAppearance = namedtuple('SignalAppearance', ['pen_color', 'pen_width', 'pen_style']) colors = [ SignalAppearance(QtGui.QColor(255, 255, 0), 1, QtCore.Qt.SolidLine), SignalAppearance(QtGui.QColor(255, 0, 0), 1, QtCore.Qt.SolidLine), SignalAppearance(QtGui.QColor(0, 255, 0), 1, QtCore.Qt.SolidLine), SignalAppearance(QtGui.QColor(255, 255, 255), 1, QtCore.Qt.SolidLine), SignalAppearance(QtGui.QColor(51, 153, 255), 1, QtCore.Qt.SolidLine), SignalAppearance(QtGui.QColor(0, 255, 255), 1, QtCore.Qt.SolidLine), SignalAppearance(QtGui.QColor(255, 0, 255), 1, QtCore.Qt.SolidLine), SignalAppearance(QtGui.QColor(204, 153, 102), 1, QtCore.Qt.SolidLine), SignalAppearance(QtGui.QColor(0, 0, 255), 1, QtCore.Qt.SolidLine), SignalAppearance(QtGui.QColor(0, 255, 0), 1, QtCore.Qt.SolidLine), SignalAppearance(QtGui.QColor(255, 204, 0), 1, QtCore.Qt.SolidLine), SignalAppearance(QtGui.QColor(153, 255, 153), 2, QtCore.Qt.DotLine), SignalAppearance(QtGui.QColor(255, 170, 0), 2, QtCore.Qt.DashLine), SignalAppearance(QtGui.QColor(255, 0, 0), 2, QtCore.Qt.DashLine), SignalAppearance(QtGui.QColor(0, 255, 255), 1, QtCore.Qt.DotLine), SignalAppearance(QtGui.QColor(255, 170, 255), 1, QtCore.Qt.DashLine), SignalAppearance(QtGui.QColor(127, 255, 127), 1, QtCore.Qt.DashLine), SignalAppearance(QtGui.QColor(255, 255, 127), 1, QtCore.Qt.DashLine), SignalAppearance(QtGui.QColor(255, 0, 0), 2, QtCore.Qt.DotLine), SignalAppearance(QtGui.QColor(255, 0, 0), 1, QtCore.Qt.DashLine), SignalAppearance(QtGui.QColor(0, 255, 0), 2, QtCore.Qt.DotLine), SignalAppearance(QtGui.QColor(255, 255, 255), 2, QtCore.Qt.SolidLine), SignalAppearance(QtGui.QColor(51, 153, 255), 1, QtCore.Qt.DashLine), SignalAppearance(QtGui.QColor(255, 0, 255), 1, QtCore.Qt.DashLine), SignalAppearance(QtGui.QColor(255, 153, 204), 1, QtCore.Qt.DashLine), SignalAppearance(QtGui.QColor(204, 153, 102), 1, QtCore.Qt.DashLine), SignalAppearance(QtGui.QColor(255, 204, 0), 1, QtCore.Qt.DashLine), SignalAppearance(QtGui.QColor(255, 0, 255), 1, QtCore.Qt.DashLine), SignalAppearance(QtGui.QColor(255, 153, 204), 1, QtCore.Qt.DashLine), SignalAppearance(QtGui.QColor(204, 153, 102), 1, QtCore.Qt.DashLine), SignalAppearance(QtGui.QColor(255, 204, 0), 1, QtCore.Qt.DashLine) ] def __init__(self, subscription_id, driver_addr, sig_name, y_axis, color_idx): """ Initializes an instance of class CurveItem. driver_addr - IcePAP driver address. sig_name - Signal name. y_axis - Y axis to plot against. """ self.subscription_id = subscription_id self.driver_addr = driver_addr self.signal_name = sig_name self.y_axis = y_axis self.array_time = [] self.array_val = [] self.val_min = 0 self.val_max = 0 col_item = self.colors[color_idx] self.color = col_item.pen_color self.pen = {'color': col_item.pen_color, 'width': col_item.pen_width, 'style': col_item.pen_style} self.curve = None self.lock = RLock() self.signature = '' self.update_signature() def update_signature(self): """Sets the new value of the signature string.""" self.signature = '{}:{}:{}'.format(self.driver_addr, self.signal_name, self.y_axis) def create_curve(self): """Creates a new plot item.""" with self.lock: self.curve = PlotCurveItem(x=self.array_time, y=self.array_val, pen=self.pen) return self.curve def update_curve(self, time_min, time_max): """Updates the curve with recent collected data.""" with self.lock: idx_min = self.get_time_index(time_min) idx_max = self.get_time_index(time_max) self.curve.setData(x=self.array_time[idx_min:idx_max], y=self.array_val[idx_min:idx_max]) def in_range(self, t): """ Check to see if time is within range of collected data. t - Time value. Return: True if time is within range of collected data. Otherwise False. """ with self.lock: if self.array_time and \ self.array_time[0] < t < self.array_time[-1]: return True return False def start_time(self): """ Get time for first data sample. Return: Time of the first collected data sample. -1 if none. """ with self.lock: if self.array_time: return self.array_time[0] return -1 def collect(self, new_data): """Store new collected data.""" with self.lock: if not self.array_val: self.val_min = self.val_max = new_data[0][1] for t, v in new_data: self.array_time.append(t) self.array_val.append(v) if v > self.val_max: self.val_max = v elif v < self.val_min: self.val_min = v def get_y(self, time_val): """ Retrieve the signal value corresponding to the provided time value. t_val - Time value. Return: Signal value corresponding to an adjacent sample in time. """ with self.lock: idx = self.get_time_index(time_val) return self.array_val[idx] def clear(self): self.array_time[:] = [] self.array_val[:] = [] def get_time_index(self, time_val): """ Retrieve the sample index corresponding to the provided time value. t_val - Time value. Return: Index of a sample adjacent to the provided time value. """ with self.lock: if not self.array_time: return -1 if len(self.array_time) == 1: return 0 time_min = self.array_time[0] time_max = self.array_time[-1] if time_val < time_min: return 0 elif time_val > time_max: return len(self.array_time) delta_t = time_max - time_min t = time_val - time_min idx = int((t / delta_t) * len(self.array_time)) while self.array_time[idx] > time_val: idx -= 1 while self.array_time[idx] < time_val: idx += 1 return idx
class SiriusProcessImage(QWidget): def __init__(self, parent=None, device='', convertion_set=True, orientation='V'): super().__init__(parent) self._dev = SiriusPVName(device) self._conv_set = convertion_set self._ori = orientation self._setupUi() def _setupUi(self): self.image_view = PyDMImageView( parent=self, image_channel=self._dev+':Image-RB', width_channel=self._dev+':Width-RB') self.image_view.setObjectName('image') self.image_view.setStyleSheet( '#image{min-width:20em; min-height:20em;}') self.image_view.maxRedrawRate = 5 self.image_view.colorMap = self.image_view.Jet self.image_view.readingOrder = self.image_view.Clike self._roixproj = SiriusConnectionSignal(self._dev+':ROIProjX-Mon') self._roiyproj = SiriusConnectionSignal(self._dev+':ROIProjY-Mon') self._roixfit = SiriusConnectionSignal(self._dev+':ROIGaussFitX-Mon') self._roiyfit = SiriusConnectionSignal(self._dev+':ROIGaussFitY-Mon') self._roixaxis = SiriusConnectionSignal(self._dev+':ROIAxisX-Mon') self._roiyaxis = SiriusConnectionSignal(self._dev+':ROIAxisY-Mon') self._roistartx = SiriusConnectionSignal(self._dev+':ROIStartX-Mon') self._roistarty = SiriusConnectionSignal(self._dev+':ROIStartY-Mon') self._roiendx = SiriusConnectionSignal(self._dev+':ROIEndX-Mon') self._roiendy = SiriusConnectionSignal(self._dev+':ROIEndY-Mon') self._roixproj.new_value_signal[np.ndarray].connect(self._update_roi) self.plt_roi = PlotCurveItem([0, 0, 500, 500, 0], [0, 500, 500, 0, 0]) pen = mkPen() pen.setColor(QColor('red')) pen.setWidth(1) self.plt_roi.setPen(pen) self.image_view.addItem(self.plt_roi) self.plt_fit_x = PlotCurveItem([0, 0], [0, 400]) self.plt_fit_y = PlotCurveItem([0, 0], [0, 400]) self.plt_his_x = PlotCurveItem([0, 0], [0, 400]) self.plt_his_y = PlotCurveItem([0, 0], [0, 400]) pen = mkPen() pen.setColor(QColor('yellow')) self.plt_his_x.setPen(pen) self.plt_his_y.setPen(pen) self.image_view.addItem(self.plt_fit_x) self.image_view.addItem(self.plt_fit_y) self.image_view.addItem(self.plt_his_x) self.image_view.addItem(self.plt_his_y) gb_conf = self._get_config_widget(self) gb_posi = self._get_position_widget(self) gb_size = self._get_size_widget(self) gl = QGridLayout(self) gl.setContentsMargins(0, 0, 0, 0) if self._ori == 'V': gl.addWidget(self.image_view, 0, 0, 1, 2) gl.addWidget(gb_posi, 1, 0) gl.addWidget(gb_size, 1, 1) gl.addWidget(gb_conf, 2, 0, 1, 2) else: gl.addWidget(self.image_view, 0, 0, 1, 2) gl.addWidget(gb_conf, 0, 2, 2, 1) gl.addWidget(gb_posi, 1, 0) gl.addWidget(gb_size, 1, 1) gl.setColumnStretch(0, 5) gl.setColumnStretch(1, 5) gl.setColumnStretch(2, 1) def _get_config_widget(self, parent): gb_pos = QGroupBox('Image Processing ', parent) meth_sp = PyDMEnumComboBox( gb_pos, init_channel=self._dev+':CalcMethod-Sel') meth_lb = SiriusLabel(gb_pos, init_channel=self._dev+':CalcMethod-Sts') meth_ld = QLabel('Method', gb_pos) nrpt_ld = QLabel('Num. Pts.', gb_pos) nrpt_sp = SiriusSpinbox( gb_pos, init_channel=self._dev+':NrAverages-SP') nrpt_sp.showStepExponent = False rdb = PyDMLabel(gb_pos, init_channel=self._dev+':NrAverages-RB') rdb.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) slsh = QLabel('/', gb_pos, alignment=Qt.AlignCenter) slsh.setStyleSheet('min-width:0.7em; max-width:0.7em;') cnt = PyDMLabel(gb_pos, init_channel=self._dev+':BufferSize-Mon') cnt.setAlignment(Qt.AlignRight | Qt.AlignVCenter) cnt.setToolTip('Current Buffer Size') pbt = PyDMPushButton( gb_pos, init_channel=self._dev+':ResetBuffer-Cmd', pressValue=1) pbt.setToolTip('Reset Buffer') pbt.setIcon(qta.icon('mdi.delete-empty')) pbt.setObjectName('rst') pbt.setStyleSheet( '#rst{min-width:25px; max-width:25px; icon-size:20px;}') nrpt_wd = QWidget(gb_pos) hbl = QHBoxLayout(nrpt_wd) hbl.addWidget(pbt) hbl.addStretch() hbl.addWidget(cnt) hbl.addWidget(slsh) hbl.addWidget(rdb) rsx_sp = SiriusSpinbox(gb_pos, init_channel=self._dev+':ROISizeX-SP') rsx_sp.showStepExponent = False rsx_lb = SiriusLabel(gb_pos, init_channel=self._dev+':ROISizeX-RB') rsx_ld = QLabel('ROI Size X', gb_pos) rsy_sp = SiriusSpinbox(gb_pos, init_channel=self._dev+':ROISizeY-SP') rsy_sp.showStepExponent = False rsy_lb = SiriusLabel(gb_pos, init_channel=self._dev+':ROISizeY-RB') rsy_ld = QLabel('ROI Size Y', gb_pos) ra_bt = PyDMStateButton( gb_pos, init_channel=self._dev+':ROIAutoCenter-Sel') ra_lb = SiriusLabel( gb_pos, init_channel=self._dev+':ROIAutoCenter-Sts') ra_ld = QLabel('Auto Center:', gb_pos) rcx_sp = SiriusSpinbox(gb_pos, init_channel=self._dev+':ROICenterX-SP') rcx_sp.showStepExponent = False rcx_lb = SiriusLabel(gb_pos, init_channel=self._dev+':ROICenterX-RB') rcx_ld = QLabel('ROI Center X', gb_pos) rcy_sp = SiriusSpinbox(gb_pos, init_channel=self._dev+':ROICenterY-SP') rcy_sp.showStepExponent = False rcy_lb = SiriusLabel(gb_pos, init_channel=self._dev+':ROICenterY-RB') rcy_ld = QLabel('ROI Center Y', gb_pos) sts_bt = QPushButton(qta.icon('fa5s.ellipsis-h'), '', gb_pos) sts_bt.setToolTip('Open Detailed Configs') sts_bt.setObjectName('sts') sts_bt.setStyleSheet( '#sts{min-width:25px; max-width:25px; icon-size:20px;}') Window = create_window_from_widget( _DetailedWidget, title='Image Processing Detailed Configs') connect_window( sts_bt, Window, gb_pos, device=self._dev, convertion_set=self._conv_set) hlay = QHBoxLayout() hlay.addWidget(sts_bt, alignment=Qt.AlignRight) lay = QGridLayout(gb_pos) if self._ori == 'V': lay.addWidget(meth_ld, 0, 0, 2, 1, alignment=Qt.AlignLeft) lay.addWidget(meth_sp, 0, 1) lay.addWidget(meth_lb, 1, 1) lay.addWidget(nrpt_ld, 2, 0, alignment=Qt.AlignLeft) lay.addWidget(nrpt_sp, 2, 1) lay.addWidget(nrpt_wd, 3, 0, 1, 2) lay.addWidget(rsx_ld, 4+0, 0, 2, 1, alignment=Qt.AlignLeft) lay.addWidget(rsx_sp, 4+0, 1) lay.addWidget(rsx_lb, 4+1, 1) lay.addWidget(rsy_ld, 6+0, 0, 2, 1, alignment=Qt.AlignLeft) lay.addWidget(rsy_sp, 6+0, 1) lay.addWidget(rsy_lb, 6+1, 1) lay.addWidget(sts_bt, 0, 0+4, alignment=Qt.AlignRight) lay.addWidget(ra_ld, 2+0, 0+3, 2, 1, alignment=Qt.AlignLeft) lay.addWidget(ra_bt, 2+0, 1+3) lay.addWidget(ra_lb, 2+1, 1+3, alignment=Qt.AlignLeft) lay.addWidget(rcx_ld, 4+0, 0+3, 2, 1, alignment=Qt.AlignLeft) lay.addWidget(rcx_sp, 4+0, 1+3) lay.addWidget(rcx_lb, 4+1, 1+3) lay.addWidget(rcy_ld, 6+0, 0+3, 2, 1, alignment=Qt.AlignLeft) lay.addWidget(rcy_sp, 6+0, 1+3) lay.addWidget(rcy_lb, 6+1, 1+3) else: lay.addWidget(meth_ld, 0, 0, 2, 1, alignment=Qt.AlignLeft) lay.addWidget(meth_sp, 0, 1) lay.addWidget(meth_lb, 1, 1) lay.addWidget(nrpt_ld, 2, 0, alignment=Qt.AlignLeft) lay.addWidget(nrpt_sp, 2, 1) lay.addWidget(nrpt_wd, 3, 0, 1, 2) lay.addWidget(rsx_ld, 4+0, 0, 2, 1, alignment=Qt.AlignLeft) lay.addWidget(rsx_sp, 4+0, 1) lay.addWidget(rsx_lb, 4+1, 1) lay.addWidget(rsy_ld, 6+0, 0, 2, 1, alignment=Qt.AlignLeft) lay.addWidget(rsy_sp, 6+0, 1) lay.addWidget(rsy_lb, 6+1, 1) lay.addWidget(ra_ld, 8+0, 0, 2, 1, alignment=Qt.AlignLeft) lay.addWidget(ra_bt, 8+0, 1) lay.addWidget(ra_lb, 8+1, 1, alignment=Qt.AlignLeft) lay.addWidget(rcx_ld, 10+0, 0, 2, 1, alignment=Qt.AlignLeft) lay.addWidget(rcx_sp, 10+0, 1) lay.addWidget(rcx_lb, 10+1, 1) lay.addWidget(rcy_ld, 12+0, 0, 2, 1, alignment=Qt.AlignLeft) lay.addWidget(rcy_sp, 12+0, 1) lay.addWidget(rcy_lb, 12+1, 1) lay.addWidget(sts_bt, 14, 0, alignment=Qt.AlignLeft) lay.setRowStretch(15, 5) return gb_pos def _get_position_widget(self, parent): gb_posi = QGroupBox('Position [px / mm]', parent) fl_posi = QFormLayout(gb_posi) wid = QWidget(gb_posi) wid.setLayout(QHBoxLayout()) xave = SiriusLabel(wid, init_channel=self._dev+':BeamCenterX-Mon') xavemm = SiriusLabel(wid, init_channel=self._dev+':BeamCentermmX-Mon') xave.setAlignment(Qt.AlignVCenter | Qt.AlignRight) xavemm.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) sep = QLabel('/', wid) sep.setStyleSheet('max-width:0.7em;') wid.layout().addWidget(xave) wid.layout().addWidget(sep) wid.layout().addWidget(xavemm) fl_posi.addRow(QLabel( 'X =', gb_posi, alignment=Qt.AlignBottom), wid) wid = QWidget(gb_posi) wid.setLayout(QHBoxLayout()) yave = SiriusLabel(wid, init_channel=self._dev+':BeamCenterY-Mon') yavemm = SiriusLabel(wid, init_channel=self._dev+':BeamCentermmY-Mon') yave.setAlignment(Qt.AlignVCenter | Qt.AlignRight) yavemm.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) sep = QLabel('/', wid) sep.setStyleSheet('max-width:0.7em;') wid.layout().addWidget(yave) wid.layout().addWidget(sep) wid.layout().addWidget(yavemm) fl_posi.addRow(QLabel( 'Y =', gb_posi, alignment=Qt.AlignBottom), wid) return gb_posi def _get_size_widget(self, parent): gb_size = QGroupBox('Size [px / mm]', parent) fl_size = QFormLayout(gb_size) wid = QWidget(gb_size) wid.setLayout(QHBoxLayout()) xave = SiriusLabel(wid, init_channel=self._dev+':BeamSizeX-Mon') xavemm = SiriusLabel(wid, init_channel=self._dev+':BeamSizemmX-Mon') xave.setAlignment(Qt.AlignVCenter | Qt.AlignRight) xavemm.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) sep = QLabel('/', wid) sep.setStyleSheet('max-width:0.7em;') wid.layout().addWidget(xave) wid.layout().addWidget(sep) wid.layout().addWidget(xavemm) fl_size.addRow(QLabel( 'X =', gb_size, alignment=Qt.AlignBottom), wid) wid = QWidget(gb_size) wid.setLayout(QHBoxLayout()) yave = SiriusLabel(wid, init_channel=self._dev+':BeamSizeY-Mon') yavemm = SiriusLabel(wid, init_channel=self._dev+':BeamSizemmY-Mon') yave.setAlignment(Qt.AlignVCenter | Qt.AlignRight) yavemm.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) sep = QLabel('/', wid) sep.setStyleSheet('max-width:0.7em;') wid.layout().addWidget(yave) wid.layout().addWidget(sep) wid.layout().addWidget(yavemm) fl_size.addRow(QLabel( 'Y =', gb_size, alignment=Qt.AlignBottom), wid) return gb_size def _update_roi(self): xaxis = self._roixaxis.getvalue() yaxis = self._roiyaxis.getvalue() xproj = self._roixproj.getvalue() yproj = self._roiyproj.getvalue() xfit = self._roixfit.getvalue() yfit = self._roiyfit.getvalue() notnone = xaxis is not None notnone &= yaxis is not None notnone &= xproj is not None notnone &= yproj is not None notnone &= xfit is not None notnone &= yfit is not None if notnone: samesize = xaxis.size == xproj.size samesize &= xaxis.size == xfit.size samesize &= yaxis.size == yproj.size samesize &= yaxis.size == yfit.size if samesize: xproj = xproj/np.max(xproj) * 400 + yaxis[0] yproj = yproj/np.max(yproj) * 400 + xaxis[0] xfit = xfit/np.max(xfit) * 400 + yaxis[0] yfit = yfit/np.max(yfit) * 400 + xaxis[0] self.plt_his_x.setData(xaxis, xproj) self.plt_his_y.setData(yproj, yaxis) self.plt_fit_x.setData(xaxis, xfit) self.plt_fit_y.setData(yfit, yaxis) srtx = self._roistartx.getvalue() srty = self._roistarty.getvalue() endx = self._roiendx.getvalue() endy = self._roiendy.getvalue() if set([None, ]) - set([srtx, srty, endx, endy]): self.plt_roi.setData( [srtx, srtx, endx, endx, srtx], [srty, endy, endy, srty, srty])
class HistogramItem(GraphicsWidget): """ This is a graphicsWidget which provides controls for adjusting the display of an image. Includes: - Image histogram - Movable region over histogram to select black/white levels Parameters ---------- image : ImageItem or None If *image* is provided, then the control will be automatically linked to the image and changes to the control will be immediately reflected in the image's appearance. fillHistogram : bool By default, the histogram is rendered with a fill. For performance, set *fillHistogram* = False. """ sigLevelsChanged = pyqtSignal(object) sigLevelChangeFinished = pyqtSignal(object) def __init__(self, image=None, fillHistogram=True, bounds: tuple = None): GraphicsWidget.__init__(self) self.imageItem = lambda: None # fake a dead weakref self.layout = QGraphicsGridLayout() self.setLayout(self.layout) self.layout.setContentsMargins(1, 1, 1, 1) self.layout.setSpacing(0) self.vb = ViewBox(parent=self) # self.vb.setMaximumHeight(152) # self.vb.setMinimumWidth(45) self.vb.setMouseEnabled(x=True, y=False) self.region = LinearRegionItem([0, 1], 'vertical', swapMode='block', bounds=bounds) self.region.setZValue(1000) self.vb.addItem(self.region) self.region.lines[0].addMarker('<|', 0.5) self.region.lines[1].addMarker('|>', 0.5) self.region.sigRegionChanged.connect(self.regionChanging) self.region.sigRegionChangeFinished.connect(self.regionChanged) self.axis = AxisItem('bottom', linkView=self.vb, maxTickLength=-10, parent=self) self.layout.addItem(self.axis, 1, 0) self.layout.addItem(self.vb, 0, 0) self.range = None self.vb.sigRangeChanged.connect(self.viewRangeChanged) self.plot = PlotCurveItem(pen=(200, 200, 200, 100)) # self.plot.rotate(90) self.vb.addItem(self.plot) self.fillHistogram(fillHistogram) self._showRegions() self.autoHistogramRange() if image is not None: self.setImageItem(image) def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)): if fill: self.plot.setFillLevel(level) self.plot.setBrush(color) else: self.plot.setFillLevel(None) def paint(self, p, *args): rgn = self.getLevels() self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0])) self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1])) def setHistogramRange(self, mn, mx, padding=0.1): """Set the Y range on the histogram plot. This disables auto-scaling.""" self.vb.enableAutoRange(self.vb.XAxis, False) self.vb.setYRange(mn, mx, padding) def autoHistogramRange(self): """Enable auto-scaling on the histogram plot.""" self.vb.enableAutoRange(self.vb.XYAxes) def setImageItem(self, img): """Set an ImageItem to have its levels and LUT automatically controlled by this HistogramLUTItem. """ self.imageItem = weakref.ref(img) img.sigImageChanged.connect(self.imageChanged) self.regionChanged() self.imageChanged(autoLevel=True) def viewRangeChanged(self): self.update() def regionChanged(self): if self.imageItem() is not None: self.imageItem().setLevels(self.getLevels()) self.sigLevelChangeFinished.emit(self) def regionChanging(self): if self.imageItem() is not None: self.imageItem().setLevels(self.getLevels()) self.sigLevelsChanged.emit(self) self.update() def imageChanged(self, autoLevel=False): if self.imageItem() is None: return self.plot.setVisible(True) # plot one histogram for all image data h = self.imageItem().getHistogram() if h[0] is None: return self.plot.setData(*h) if autoLevel: mn = h[0][0] mx = h[0][-1] self.region.setRegion([mn, mx]) else: mn, mx = self.imageItem().levels self.region.setRegion([mn, mx]) def getLevels(self): """ Return the min and max levels. """ return self.region.getRegion() def setLevels(self, min=None, max=None): """ Set the min/max (bright and dark) levels. """ assert None not in (min, max) self.region.setRegion((min, max)) def _showRegions(self): self.region.setVisible(True) def saveState(self): return { 'levels': self.getLevels(), } def restoreState(self, state): self.setLevels(*state['levels'])
class Plot(PlotWidget): """ Sublcass of PyQtGraph.PlotWidget to display cycling data. Parameters ---------- parent : QMainWindow Main window. """ currentPointChanged = Signal(dict) """ **signal** currentPointChanged(dict `values`) Emitted when a point in the plot is hovered over. The dict provides the date, speed, distance, calories and time data for the chosen point. """ pointSelected = Signal(object) """ **signal** pointSelected(datetime `currentPoint`) Emitted when the plot is double clicked, with the date from the current point. """ def __init__(self, parent, style="dark"): self._ySeries = None self.plotItem = None self.style = PlotStyle(style) self.setStyle(style) self.dateAxis = CustomDateAxisItem() self.plotItem = CustomPlotItem(viewBox=CustomViewBox(), axisItems={ 'bottom': self.dateAxis, 'left': CustomAxisItem('left') }) super().__init__(plotItem=self.plotItem) self.dateAxis.axisDoubleClicked.connect(self.setPlotRange) self.hgltPnt = None self.parent = parent self.plottable = ['speed', 'distance', 'time', 'calories'] self._initRightAxis() # axis labels self.plotItem.setLabel('bottom', text='Date') # show grid on left and bottom axes self.plotItem.getAxis('left').setGrid(255) self.plotItem.getAxis('bottom').setGrid(255) # cross hairs self.vLine = InfiniteLine(angle=90, movable=False) self.hLine = InfiniteLine(angle=0, movable=False) self.plotItem.addItem(self.vLine, ignoreBounds=True) self.plotItem.addItem(self.hLine, ignoreBounds=True) self.plotItem.scene().sigMouseMoved.connect(self.mouseMoved) self.plotItem.scene().sigMouseClicked.connect(self.plotClicked) # update second view box self.updateViews() self.plotItem.vb.sigResized.connect(self.updateViews) self.currentPoint = {} # all points that are/were PBs can be highlighted self._showPBs = False self._regenerateCachedPBs = {key: False for key in self.plottable} self.hgltPBs = {key: [] for key in self.plottable} self.viewMonths = None self.setYSeries('speed') self.plotTotalDistance() @property def data(self): return self.parent.data def _initRightAxis(self): self.vb2 = CustomViewBox() self.plotItem.showAxis('right') self.plotItem.scene().addItem(self.vb2) self.plotItem.getAxis('right').linkToView(self.vb2) self.vb2.setXLink(self.plotItem) @property def viewBoxes(self): """ Return list of all viexBoxes in the plot. """ vbx = [self.plotItem.vb] if hasattr(self, 'vb2'): vbx.append(self.vb2) return vbx @Slot() def updateViews(self): ## Copied from PyQtGraph MultiplePlotAxes.py example ## # view has resized; update auxiliary views to match self.vb2.setGeometry(self.plotItem.vb.sceneBoundingRect()) # need to re-update linked axes since this was called # incorrectly while views had different shapes. # (probably this should be handled in ViewBox.resizeEvent) self.vb2.linkedViewChanged(self.plotItem.vb, self.vb2.XAxis) def getState(self): state = {} state['ySeries'] = self.ySeries for n, vb in enumerate(self.viewBoxes): key = f"vb{n}State" state[key] = vb.getState() if self.currentPoint: state['currentPoint'] = self.currentPoint['index'] return state def setState(self, state): self.ySeries = state['ySeries'] for n, vb in enumerate(self.viewBoxes): key = f"vb{n}State" vb.setState(state[key]) if len(self.viewBoxes) > 1: self.viewBoxes[1].setXLink(self.plotItem) if 'currentPoint' in state: self.setCurrentPoint(state['currentPoint']) def setStyle(self, style): self.style.name = style dct = { 'foreground': self.style.foreground, 'background': self.style.background } setConfigOptions(**dct) if self.plotItem is not None: self.plotItem.setButtonPixmaps() if self.ySeries is not None: self.updatePlots() @Slot() def viewAll(self): # enableAutoRange on both viewBoxes for vb in self.viewBoxes: vb.enableAutoRange() if self._showPBs: self._highlightPBs(self._showPBs) @Slot() def resetMonthRange(self): if self.viewMonths is not None: self.setXAxisRange(self.viewMonths) if self._showPBs: self._highlightPBs(self._showPBs) @Slot(float, float) def setPlotRange(self, x0, x1): """ Set range of both view boxes to cover the points between the two given timestamps. """ # apply to both the current scatter and background plot if not hasattr(self, 'dataItem') or not hasattr( self, 'backgroundItem'): return None xRange0, xRange1 = self.viewBoxes[0].xRange data = [(self.dataItem.scatter.data['x'], self.dataItem.scatter.data['y'], self.viewBoxes[0]), (self.backgroundItem.xData, self.backgroundItem.yData, self.viewBoxes[1])] for xPoints, yData, viewBox in data: # find x-coords of points in the given month mask = np.in1d( np.where(xPoints >= x0)[0], np.where(xPoints <= x1)[0]) if np.any(mask): # select the corresponding y data idx = np.where(xPoints >= x0)[0][mask] yPoints = yData[idx] # get min and max y0 = np.min(yPoints) y1 = np.max(yPoints) # set min and max for x and y in the viewBox viewBox.setRange(xRange=(x0, x1), yRange=(y0, y1)) @Slot(object, bool) def setXAxisRange(self, months, fromRecentSession=True): """ Scale the plot to show the most recent `months` months. If `fromRecentSession` is True (default), the month range is calculated relative to the most recent session in the `CycleData` object. Otherwise, it is calculated from the current date. These two options are equivalent if there are sessions from the current month in the `CycleData` object. """ self.viewMonths = months if fromRecentSession: ts1 = self.data.dateTimestamps[-1] else: now = datetime.now() ts1 = now.timestamp() if months is not None and now.month != self.data.datetimes[ -1].month: months -= now.month - self.data.datetimes[-1].month if months is None: ts0 = self.data.dateTimestamps[0] else: days = self.viewMonths * 365 / 12 # number of days to go back td = timedelta(days=days) ts0 = (datetime.fromtimestamp(ts1) - td).timestamp() self.setPlotRange(ts0, ts1) @Slot() def updatePlots(self): self.plotSeries(self.ySeries, mode='set') self.plotTotalDistance(mode='set') self.resetMonthRange() self._regenerateCachedPBs = { key: True for key in self._regenerateCachedPBs } @property def ySeries(self): return self._ySeries @ySeries.setter def ySeries(self, key): self._ySeries = key self.plotSeries(self.ySeries) def setYSeries(self, key): self.ySeries = key def plotSeries(self, key, mode='new'): """ Plot given series on y1 axis. """ label = self.data.quickNames[key] if key == 'time': series = self.data.timeHours self.plotItem.getAxis('left').tickFormatter = floatToHourMinSec else: series = self.data[label] self.plotItem.getAxis('left').tickFormatter = None styleDict = self.style[key] style = self._makeScatterStyle(**styleDict) if mode == 'new': self.dataItem = self.plotItem.scatterPlot(self.data.dateTimestamps, series, **style) elif mode == 'set': self.dataItem.setData(self.data.dateTimestamps, series, **style) self.plotItem.setLabel('left', text=label, color=styleDict['colour']) if self.viewBoxes[0].xRange is not None: self.setPlotRange(*self.viewBoxes[0].xRange) if self._showPBs: self._highlightPBs(self._showPBs) def plotTotalDistance(self, mode='new'): """ Plot monthly total distance. """ colour = self.style['odometer']['colour'] style = self._makeFillStyle(colour) dts, odo = self.data.getMonthlyOdometer() dts = [dt.timestamp() for dt in dts] if mode == 'new': self.backgroundItem = PlotCurveItem(dts, odo, **style) self.vb2.addItem(self.backgroundItem) elif mode == 'set': self.backgroundItem.setData(dts, odo, **style) self.plotItem.getAxis('right').setLabel('Total monthly distance', color=colour) @Slot(str) def switchSeries(self, key): if key in self.plottable and key in self.data.quickNames.keys(): self.plotItem.removeItem(self.dataItem) self.plotSeries(key) @Slot(object) def plotClicked(self, event): """ If the plot is clicked, emit `pointSelected` signal with `currentPoint` datetime. """ # get x and y bounds yMin = 0 # no top axis yMax = self.plotItem.getAxis('bottom').scenePos().y() # left axis position is 1,1 (don't know why), so use the bottom axis x here xMin = self.plotItem.getAxis('bottom').scenePos().x() xMax = self.plotItem.getAxis('right').scenePos().x() pos = event.scenePos() if xMin <= pos.x() <= xMax and yMin <= pos.y( ) <= yMax: # event.double() and idx = self.currentPoint['index'] date = self.data.datetimes[idx] self.pointSelected.emit(date) @staticmethod def _makeScatterStyle(colour, symbol): """ Make style for series with no line but with symbols. """ pen = mkPen(colour) brush = mkBrush(colour) d = {'symbol': symbol, 'symbolPen': pen, 'symbolBrush': brush} return d @staticmethod def _makeFillStyle(colour): """ Make style for PlotCurveItem with fill underneath. """ pen = mkPen(colour) brush = mkBrush(colour) d = {'pen': pen, 'brush': brush, 'fillLevel': 0} return d @property def currentPoint(self): return self._point @currentPoint.setter def currentPoint(self, value): self._point = value def setCurrentPoint(self, idx): """ Set the `currentPoint` dict from index `idx` in the `data` DataFrame and emit `currentPointChanged` signal. """ self.currentPoint['index'] = idx self.currentPoint['date'] = self.data.datetimes[idx].strftime( "%a %d %b %Y") self.currentPoint['speed'] = self.data.avgSpeed[idx] self.currentPoint['distance'] = self.data.distance[idx] self.currentPoint['calories'] = self.data.calories[idx] self.currentPoint['time'] = self.data.time[idx] self.currentPointChanged.emit(self.currentPoint) @Slot(object) def setCurrentPointFromDate(self, date): """ Find point at the given date and highlight it. """ dt = datetime(date.year, date.month, date.day) idx = self.data.datetimes.index(dt) pt = self.dataItem.scatter.points()[idx] self.ensurePointVisible(pt) self._highlightPoint(pt) self.setCurrentPoint(idx) def ensurePointVisible(self, pt): ts = pt.pos().x() x0, x1 = self.viewBoxes[0].xRange if ts < x0: x0 = ts - 1e6 elif ts > x1: x1 = ts + 1e6 self.setPlotRange(x0, x1) @Slot(object) def _highlightPoint(self, point=None): """ Change pen and brush of given point (and reset any previously highlighted points). """ if point is None: if self.hgltPnt is not None: point = self.hgltPnt else: return None try: # if other points are already highlighted, remove highlighting self.hgltPnt.resetPen() self.hgltPnt.resetBrush() except (AttributeError, RuntimeError): pass colour = self.style['highlightPoint']['colour'] pen = mkPen(colour) brush = mkBrush(colour) self.hgltPnt = point self.hgltPnt.setPen(pen) self.hgltPnt.setBrush(brush) @Slot(bool) def _highlightPBs(self, show): """ Highlight points that are, or were, PBs. """ if show: self._showPBs = True if self._regenerateCachedPBs[self.ySeries] or len( self.hgltPBs[self.ySeries]) == 0: self.hgltPBs[self.ySeries] = self._getPBs() self._regenerateCachedPBs[self.ySeries] = False for idx in self.hgltPBs[self.ySeries]: pt = self.dataItem.scatter.points()[idx] colour = self.style['highlightPoint']['colour'] pen = mkPen(colour) brush = mkBrush(colour) pt.setPen(pen) pt.setBrush(brush) else: self._showPBs = False for idx in self.hgltPBs[self.ySeries]: pt = self.dataItem.scatter.points()[idx] pt.resetPen() pt.resetBrush() def _getPBs(self): """ Return array of points that represent(ed) a PB in the current series. """ # get number of top sessions and current y series col = self.data.quickNames[self.ySeries] num = self.parent.settings.value("pb/numSessions", cast=int) series = self.data[col] minBest = min(series[:num]) # minimum value to beat to be a PB idx = list(range(num)) # first num values will be PBs for n in range(num, len(series)): if series[n] >= minBest: idx.append(n) minBest = series[n] return idx @Slot(object) def mouseMoved(self, pos): if self.plotItem.sceneBoundingRect().contains(pos): mousePoint = self.plotItem.vb.mapSceneToView(pos) idx = int(mousePoint.x()) if idx > min(self.data.dateTimestamps) and idx < max( self.data.dateTimestamps): self._makePointDict(idx) pts = self.scatterPointsAtX(mousePoint, self.dataItem.scatter) if len(pts) != 0: # could be multiple points at same x, so get closest point to mouse by y value yVals = np.array([pt.pos().y() for pt in pts]) idx = (np.abs(yVals - mousePoint.y())).argmin() self._highlightPoint(pts[idx]) self.vLine.setPos(mousePoint.x()) self.hLine.setPos(mousePoint.y()) def _makePointDict(self, ts): # given timestamp in seconds, find nearest date and speed idx = (np.abs(self.data.dateTimestamps - ts)).argmin() self.setCurrentPoint(idx) @staticmethod def scatterPointsAtX(pos, scatter): """ Return a list of points on `scatter` under the x-coordinate of the given position `pos`, ignoring the y-coord. """ # Tried to subclass ScatterPlotItem and add this method there, but it # messed up the DateAxis x = pos.x() pw = scatter.pixelWidth() pts = [] for s in scatter.points(): sp = s.pos() ss = s.size() sx = sp.x() s2x = ss * 0.5 if scatter.opts['pxMode']: s2x *= pw if x > sx - s2x and x < sx + s2x: pts.append(s) return pts[::-1]