def __init__(self, **kwargs): super(MDIChildPlot, self).__init__(**kwargs) self.setAttribute(Qt.WA_DeleteOnClose) self.is_untitled: bool = True self.is_modified: bool = False self.cur_file: str = '' self.curves = [] self.plotItem2 = ViewBox() self.plotItem.showAxis('right') self.plotItem.scene().addItem(self.plotItem2) self.plotItem.getAxis('right').linkToView(self.plotItem2) self.plotItem2.setXLink(self.plotItem) self.plotItem.showButtons() self.plotItem.showGrid(x=True, y=True) # Handle view resizing def update_views(): # view has resized; update auxiliary views to match self.plotItem2.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.plotItem2.linkedViewChanged(self.plotItem.vb, self.plotItem2.XAxis) update_views() self.plotItem.vb.sigResized.connect(update_views) # add items to the context menu self.plotItem.vb.menu.addSeparator() self.delete_curve_menu: QMenu = QMenu('Delete') self.copy_curve_menu: QMenu = QMenu('Copy') self.paste_curve_menu: QMenu = QMenu('Paste') menus = {self.delete_curve_menu: [('Last Curve…', self.delete_last_curve), ('Curves No.…', self.delete_curves), ('All Curves…', self.delete_all_curves)], self.copy_curve_menu: [('Last Curve…', self.copy_last_curve), ('Curves No.…', self.copy_curves), ('All Curves…', self.copy_all_curves)], } for parent_menu, actions in menus.items(): for title, callback in actions: new_action: QAction = QAction(title, parent_menu) parent_menu.addAction(new_action) new_action.triggered.connect(callback) self.plotItem.vb.menu.addMenu(self.delete_curve_menu) self.plotItem.vb.menu.addMenu(self.copy_curve_menu) self.plotItem.vb.menu.addMenu(self.paste_curve_menu) # hide buggy menu items for undesired_menu_item_index in (5, 2, 1): self.plotItem.subMenus.pop(undesired_menu_item_index) self.plotItem.subMenus[1].actions()[0].defaultWidget().children()[1].hide()
def __init__(self, *args, show_tooltip=False, **kws): super().__init__(*args, **kws) self._filled_with_arch_data = dict() self._show_tooltip = show_tooltip self.vb2 = ViewBox() self.plotItem.scene().addItem(self.vb2) self.vb2.setXLink(self.plotItem) self.plotItem.getAxis('right').linkToView(self.vb2) self._updateViews() self.plotItem.vb.sigResized.connect(self._updateViews) self.carch = None # show auto adjust button self.plotItem.showButtons() # use pan mouse mode (3-button) self.plotItem.getViewBox().setMouseMode(ViewBox.PanMode) # connect sigMouseMoved self.plotItem.scene().sigMouseMoved.connect(self._handle_mouse_moved) # add new actions to menu rst_act = QAction("Clear buffers") rst_act.triggered.connect(self._resetBuffers) tsp_act = QAction("Change time span") tsp_act.triggered.connect(self._changeTimeSpan) self.plotItem.scene().contextMenu.extend([rst_act, tsp_act])
def add_source_id_label(view_box: pyqtgraph.ViewBox, context: Context) -> pyqtgraph.TextItem: """Add a translucent TextItem pinned to the bottom left of the view box displaying the context source id string. """ text_item = pyqtgraph.TextItem(text="", anchor=(0, 1), color=(255, 255, 255), fill=(0, 0, 0)) text_item.setZValue(1000) text_item.setOpacity(0.3) view_box.addItem(text_item, ignoreBounds=True) def update_text(*args): text_item.setText(" " + context.get_source_id() + " ") context.source_id_changed.connect(update_text) update_text() def update_text_pos(*args): ((x, _), (y, _)) = view_box.viewRange() text_item.setPos(x, y) view_box.sigRangeChanged.connect(update_text_pos) update_text_pos() return text_item
def __init__( self, # chart: 'ChartPlotWidget', # noqa view: pg.ViewBox, anchor_at: str = ('top', 'right'), justify_text: str = 'left', font_size: Optional[int] = None, ) -> None: font_size = font_size or _font_small.px_size super().__init__(justify=justify_text, size=f'{str(font_size)}px') # anchor to viewbox self.setParentItem(view) self.vb = view view.scene().addItem(self) v, h = anchor_at index = (self._corner_anchors[h], self._corner_anchors[v]) margins = self._corner_margins[(v, h)] ydim = margins[1] if inspect.isfunction(margins[1]): margins = margins[0], ydim(font_size) self.anchor(itemPos=index, parentPos=index, offset=margins)
def add_image(self, image, label): w = QFrame() w.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) w.setLineWidth(2) w.setFixedSize(QSize(500, 500)) w.setLayout(QVBoxLayout()) gv = ScrollableGraphicsLayoutWidget() vb = ViewBox(lockAspect=True) ii = ActivatableImageItem(image=image) ii.sigActivated.connect(self.set_current_imageitem) self.hist_widget.item.setImageItem(ii) self.current_image_item = ii self.image_items.append(ii) self.views.append(vb) vb.sigRangeChangedManually.connect(self.propagate_axes) vb.addItem(ii) gv.addItem(vb) self.set_current_imageitem(ii) w.layout().addWidget(gv) l = QLabel(label) # l.setStyleSheet("color: white;") w.layout().addWidget(l) self.flow_layout.addWidget(w) self.last_vb = vb
def __init__(self, diff, parent=None): super(SideView, self).__init__(parent=parent) self.parent = parent self.brain = parent.brain self.diff = diff self.view1 = ViewBox() self.setCentralItem(self.view1) # Making Images out of data self.brain.section = (self.brain.section + self.diff) % 3 self.i = int(self.brain.shape[self.brain.section] / 2) data_slice = self.brain.get_data_slice(self.i) self.brain_img1 = ImageItem( data_slice, autoDownsample=False, compositionMode=QtGui.QPainter.CompositionMode_SourceOver) self.brain.section = (self.brain.section - self.diff) % 3 self.view1.addItem(self.brain_img1) self.view1.setAspectLocked(True) self.view1.setFixedHeight(250) self.view1.setFixedWidth(250) self.setMinimumHeight(250) self.vLine = InfiniteLine(angle=90, movable=False) self.hLine = InfiniteLine(angle=0, movable=False) self.vLine.setVisible(False) self.hLine.setVisible(False) self.view1.addItem(self.vLine, ignoreBounds=True) self.view1.addItem(self.hLine, ignoreBounds=True)
def __init__(self, parent, image_item: ChannelImageItem): ViewBox.__init__(self, parent, border=mkPen("d", width=1), lockAspect=True, name=image_item.channel.metal, invertY=True) self.addItem(image_item, ignoreBounds=False)
def __init__(self, *args, **kwargs): BaseConfigurableClass.__init__(self) ViewBox.__init__(self, *args, **kwargs) self.registerConfigProperty(self.getCurves, self.setCurves, 'Y2Curves') self.registerConfigProperty(self._getState, self.setState, 'viewState') self._isAttached = False self.plotItem = None self._curvesModelNames = []
def __init__(self, *args, **kwargs): self._isAttached = False self.plotItem = None name = kwargs.pop("name", "Y2 ViewBox") BaseConfigurableClass.__init__(self) ViewBox.__init__(self, *args, name=name, **kwargs) self.registerConfigProperty(self._getCurvesNames, self._addCurvesByName, "Y2Curves") self.registerConfigProperty(self._getState, self.setState, "viewState")
def removeItem(self, item): """Reimplemented from :class:`pyqtgraph.ViewBox`""" ViewBox.removeItem(self, item) # when last curve is removed from self (axis Y2), we must remove the # axis from scene and hide the axis. if len(self.addedItems) < 1: self.plotItem.scene().removeItem(self) self.plotItem.hideAxis('right') self._curvesModelNames.remove(item.getFullModelNames())
def addPlotAera(self): plotname = 'Plot' + str(len(self.lPlotWindows) + 1) axis = self.TimeAxisItem(orientation='bottom') vb = ViewBox() newdataPlot = PlotWidget(self, viewBox=vb, axisItems={'bottom': axis}, name = plotname) self.dataPlotLayout.addWidget(newdataPlot) self.configPlotArea(newdataPlot) newdataPlot.plotItem.scene().sigMouseClicked.connect(self.mouseClick) newdataPlot.plotItem.scene().sigMouseMoved.connect(self.mouseMove) ## drag and drop # newdataPlot.dragEnterEvent = self.dragEnterEvent # newdataPlot.plotItem.setAcceptDrops(True) # newdataPlot.plotItem.dropEvent = self.dropEvent # set the default plot range newdataPlot.setXRange(self.minTimestamp,self.maxTimestamp,padding=20) newdataPlot.setYRange(-10, 10, padding=20) newdataPlot.plotItem.getAxis('left').setWidth(w=30) newdataPlot.plotItem.hideButtons() newdataPlot.installEventFilter(self) newdataPlot.plotItem.showGrid(True, True, 0.5) vb.scaleBy(y=None) # make it the current selection plot area self.currSelctPlotWgt.setBackground('default') self.currSelctPlotWgt = newdataPlot # set the current selection to plot1 self.currSelctPlotWgt.setBackground(0.95) # link x axis to view box of the first data plot viewBox = self.dataPlot.plotItem.vb # reference to viewbox of the plot 1 axis.linkToView(viewBox) #axis.linkToView(vb) # Link plot 1 X axia to the view box lastplotItem = self.dataPlotLayout.itemAt(self.dataPlotLayout.count()-2).widget() lastplotItem.getViewBox().setXLink(newdataPlot) #lastplotItem.getViewBox().autoRange() txtY_value = TextItem("", fill=(0, 0, 255, 80), anchor=(0, 1), color='w') txtY_value.setParentItem(newdataPlot.plotItem.getViewBox()) self.autoRangeAllWins() self.lPlotWindows.append(plotname)
def addItem(self, item, ignoreBounds=False): """Reimplemented from :class:`pyqtgraph.ViewBox`""" ViewBox.addItem(self, item, ignoreBounds=ignoreBounds) if len(self.addedItems) == 1: # when the first curve is added to self (axis Y2), we must # add Y2 to main scene(), show the axis and link X axis to self. self.plotItem.showAxis('right') self.plotItem.scene().addItem(self) self.plotItem.getAxis('right').linkToView(self) self.setXLink(self.plotItem) if (len(self.addedItems) > 0 and item.getFullModelNames() not in self._curvesModelNames): self._curvesModelNames.append(item.getFullModelNames())
def __init__(self, parent: QWidget): GraphicsView.__init__(self, parent) layout = GraphicsLayout() self.setCentralItem(layout) self._image_item = ImageItem() self.viewbox = ViewBox(layout, lockAspect=True, invertY=True) self.viewbox.addItem(self._image_item) layout.addItem(self.viewbox) self.scale = ScaleBar(size=10, suffix='μm') self.scale.setParentItem(self.viewbox) self.scale.anchor((1, 1), (1, 1), offset=(-20, -20)) self.scale.hide() self._show_mask = False self.blend_mode = QPainter.CompositionMode_Screen self.items: List[ChannelImageItem] = None
def addItem(self, item, ignoreBounds=False): """Reimplemented from :class:`pyqtgraph.ViewBox`""" # first add it to plotItem and then move it from main viewbox to y2 if self.plotItem is not None: if item not in self.plotItem.listDataItems(): self.plotItem.addItem(item) if item in self.plotItem.getViewBox().addedItems: self.plotItem.getViewBox().removeItem(item) ViewBox.addItem(self, item, ignoreBounds=ignoreBounds) if self.plotItem is not None: self.plotItem.showAxis("right", show=bool(self.addedItems)) # set the item log mode to match this view: if hasattr(item, "setLogMode"): item.setLogMode( self.plotItem.getAxis("bottom").logMode, self.plotItem.getAxis("right").logMode, )
def __init__(self, name: str = "MIMiniImageView"): super().__init__() self.name = name.title() self.im = ImageItem() self.vb = ViewBox(invertY=True, lockAspect=True, name=name) self.vb.addItem(self.im) self.hist = HistogramLUTItem(self.im) graveyard.append(self.vb) # Sub-layout prevents resizing issues when details text changes image_layout = self.addLayout(colspan=2) image_layout.addItem(self.vb) image_layout.addItem(self.hist) self.hist.setFixedWidth(100) # HistogramLUTItem used pixel sizes self.nextRow() self.details = self.addLabel("", colspan=2) self.im.hoverEvent = lambda ev: self.mouse_over(ev) self.axis_siblings: "WeakSet[MIMiniImageView]" = WeakSet() self.histogram_siblings: "WeakSet[MIMiniImageView]" = WeakSet()
def __init__(self, *args, **kwargs): # Initialize the superclass ViewBox.__init__(self, *args, **kwargs) # Create a scale box self.scaleBox = DraggableScaleBox() self.scaleBox.hide() self.addItem(self.scaleBox, ignoreBounds=True) # And disable the one from PyQtGraph self.removeItem(self.rbScaleBox) # The mouse mode is not used, since we override what left clicking does anyway removeMenuItems = ('Mouse Mode', ) for menuItem in removeMenuItems: actions = self.menu.actions() for action in actions: if action.text() == menuItem: self.menu.removeAction(action) # Extra menu actions self.makeTracesDifferentAction = QtGui.QAction("Make All Traces Different", self.menu) self.makeTracesDifferentAction.triggered.connect(self.makeTracesDifferent)
def __init__(self): super().__init__() viewbox = ViewBox() self.graph = GridItem() viewbox.setAspectLocked() viewbox.addItem(self.graph) self.setBackground('w') self.setCentralItem(viewbox)
def __init__(self): super().__init__() # settings self.setBackground("#fff") self.setFrameStyle(QFrame.StyledPanel|QFrame.Sunken) self.setAntialiasing(True) # create custom view box view_box = ViewBox() view_box.setMouseEnabled(False, False) view_box.setLimits(xMin=0, yMin=0, minXRange=10, minYRange=100) view_box.setRange(xRange=(0, 400), yRange=(0, 5000)) view_box.enableAutoRange() # create natural axis items self.x_axis = NaturalAxis("bottom") self.x_axis.setLabel(QApplication.translate("NaturalPlotView", "Fence length"), "m") self.y_axis = NaturalAxis("left") self.y_axis.setLabel(QApplication.translate("NaturalPlotView", "Number of plants")) # create fence information text self.fenceItem = TextItem(border=pyqtgraph.mkPen(width=2, color="#555"), fill=pyqtgraph.mkBrush((255, 255, 255, 200))) # create tube information text self.tubeItem = TextItem(border=pyqtgraph.mkPen(width=2, color="#555"), fill=pyqtgraph.mkBrush((255, 255, 255, 200)), anchor=(1,1)) # create plot item with custom view box and natural axis items self.plotItem = PlotItem(viewBox=view_box, axisItems={"bottom" : self.x_axis, "left" : self.y_axis}, enableMenu=False) self.plotItem.setContentsMargins(5, 5, 12, 5) self.plotItem.hideButtons() self.plotItem.hide() self.setCentralWidget(self.plotItem) # connect actions view_box.sigResized.connect(self.updateTubeLegendPosition) # translate the plot item self.retranslateUi()
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 addItem(self, item, ignoreBounds=False): """Reimplemented from :class:`pyqtgraph.ViewBox`""" ViewBox.addItem(self, item, ignoreBounds=ignoreBounds) if len(self.addedItems) == 1: # when the first curve is added to self (axis Y2), we must # add Y2 to main scene(), show the axis and link X axis to self. self.plotItem.showAxis("right") self.plotItem.scene().addItem(self) self.plotItem.getAxis("right").linkToView(self) self.setXLink(self.plotItem) # set the item log mode to match this view: if hasattr(item, "setLogMode"): item.setLogMode( self.plotItem.getAxis("bottom").logMode, self.plotItem.getAxis("right").logMode, ) if hasattr(item, "getFullModelNames") and ( len(self.addedItems) > 0 and item.getFullModelNames() not in self._curvesModelNames ): self._curvesModelNames.append(item.getFullModelNames())
class FanDiagram(QtGui.QWidget): """ Produces a fan diagram alphas/gammas should be the matrices saved from the FanCompiler, of form: arb | niralpha1 | niralpha2 | niralpha3 | niralpha4 | ... 1st sb | 1sb alpha | 1sb alpha | . 2nd sb | 2sb alpha | 2sb alpha | . 3rd sb | 3sb alpha | 3sb alpha | . . . . Assumes both alphas/gammas are the same shape Alternatively, pass nirAlpha and SBs as 1st/2nd args (as 1D arrays) to have it create the fan without any data Or, if alphaData and gammaData are strings, assumes they're paths to data files to plot :param alphas: :param gammas: :param kwargs: :return: """ def __init__(self, alphaData, gammaData=None, view=None): super(FanDiagram, self).__init__() if gammaData is None and isinstance(alphaData, FanCompiler): alphaData, gammaData = alphaData.build(withErrors=False)[:2] self.layout = QtWidgets.QHBoxLayout() self.histAlpha = HistogramLUTWidget(self) self.centralView = GraphicsView() self.histGamma = HistogramLUTWidget(self) self.histAlpha.setMinimumWidth(150) self.histGamma.setMinimumWidth(150) self.layout.addWidget(self.histGamma) self.layout.addWidget(self.centralView) self.layout.addWidget(self.histAlpha) self.layout.setContentsMargins(0,0,0,0) self.layout.setSpacing(0) self.setLayout(self.layout) if view is None: self.view = ViewBox() else: self.view = view self.centralView.setCentralItem(self.view) self.view.setAspectLocked(True) self.view.invertY(True) if isinstance(alphaData, str) and isinstance(gammaData, str): alphaData = np.genfromtxt(alphaData, delimiter=',') gammaData = np.genfromtxt(gammaData, delimiter=',') if alphaData.ndim == gammaData.ndim == 1: # Assume you just want it to be created, and will later populate it nirAlphas = alphaData sbs = gammaData alphaData = np.ones((sbs.shape[0] + 1, nirAlphas.shape[0] + 1)) * -1 alphaData[1:, 0] = sbs alphaData[0, 1:] = nirAlphas gammas = np.ones((sbs.shape[0] + 1, nirAlphas.shape[0] + 1)) * -1 gammas[1:, 0] = sbs gammas[0, 1:] = nirAlphas sbs = alphaData[1:, 0] maxSB = sbs.max() nirAlphas = alphaData[0, 1:] self.alphaItem = PolarImageItem(r=sbs, theta=nirAlphas) self.alphaItem.setImage(alphaData[1:,1:]) # nirAlphas+180 is what causes the gamma angles to appear on the left side of the # fan. This seemed easier than doing some sort of coordinate inversion/flipping # on the plot itself. self.gammaItem = PolarImageItem(sbs, nirAlphas+180, gammaData[1:,1:]) self.view.addItem(self.alphaItem) self.view.addItem(self.gammaItem) self.histAlpha.setImageItem(self.alphaItem) self.histGamma.setImageItem(self.gammaItem) # manually set the default state to the black-gold-white-green-black. Not sure # if it's necessary to have this be a free parameter vs being hardcoded self.histAlpha.gradient.restoreState({ "mode": "rgb", "ticks": [ (0, (0, 0, 0, 255)), (.25, (128, 128, 0, 255)), (.5, (255, 255, 255, 255)), (.75, (0, 128, 0, 255)), (1, (0, 0, 0, 255)) ] }) # Set the default spacings for the alpha color axis. Again, not sure if it's # better to leave the 18pt font hard-coded or not, but I am self.histAlpha.axis.setTickFont(QtGui.QFont("Arial", 18)) self.histAlpha.axis.setTickSpacing(30, 15) self.histAlpha.axis.setLabel("α (°)", **{'font-family': 'Times', "font-size": "18pt"}) # As with alpha, hard code the initial color space for gamma (blue-white-red) # and the font spacings and stuff self.histGamma.gradient.restoreState({ "mode": "rgb", "ticks": [ (0, (255, 0, 0, 255)), (.5, (255, 255, 255, 255)), (1, (0, 0, 255, 255)) ] }) self.histGamma.axis.setTickFont(QtGui.QFont("Arial", 18)) self.histGamma.axis.setTickSpacing(15, 15) self.histGamma.axis.setLabel("γ (°)", **{'font-family': 'Times', "font-size": "18pt"}) self.histAlpha.item.setLevels(-90, 90) self.histGamma.item.setLevels(-45, 45) self.histAlpha.autoHistogramRange() self.histGamma.autoHistogramRange() # Make it the right dimensions, making sure that the width is appropriate. # This makes it easier to automate plotting/saving fans and making sure # their dimensions are consistent. g = self.geometry() # I found these by eye, there's not very much important about them g.setWidth(773) g.setHeight(480) # Manually center it on the screen, since geometry isn't well defined at this point # before events are processed g.moveCenter(QtWidgets.QApplication.desktop().screenGeometry().center()) self.setGeometry(g) # Add in the radial axes for it self.axes = { "radial": PolarAxis("radial"), "azimuthal": PolarAxis("azimuthal") } # Lighten the radial font to make it distinct from the other p = self.axes["radial"].pen() p.setColor(mkColor("#666666")) self.axes["radial"].setPen(p) for a in self.axes.values(): # Make sure the axes sit on top of all other items a.setZValue(10000) # make sure that they scale appropriately, instead of just floating on top a.linkToView(self.view) # Ignore bounds prevents the window from resizing to try and fit in # the axes items self.addItem(a, ignoreBounds=True) # manually set the positions and string values for alpha angles. [-90, 90] work # well. The other half needs the +-180 to make sure the gamma angles have the # correctly labeled with respect to alpha_nir self.axes["azimuthal"].setTicks( [ [(ii, str(ii)) for ii in np.arange(-90, 91, 30)] + # alpha side (Q1+Q4) [(ii, str(ii + 180)) for ii in np.arange(-180, -91, 30)] + #Q3 [(ii, str(ii - 180)) for ii in np.arange(120, 151, 30)], #Q1 ] ) # add a title (without text) self.titleItem = TextItem() self.titleItem.setAnchor(Point(0.5, 1)) # anchor on bottom-center # Again, not sure if it's necessary to have the font color/size being # a free parameter self.titleItem.setColor("k") self.titleItem.setFont(QtGui.QFont("Arial", 15)) # Ignore bounds so that the view won't try to account for it (which # causes a conflict because the title is placed with respect to the # view region) self.view.addItem(self.titleItem, ignoreBounds=True) self.show() # Arbitrary forcing updates to try and track down why some things don't # update correctly QtWidgets.QApplication.processEvents() self.view.updateViewRange(True, True) def setAlphaImage(self, img): self.alphaItem.setImage(img) def setGammaImage(self, img): self.gammaItem.setImage(img) def setImages(self, alpha, gamma): self.setAlphaImage(alpha) self.setGammaImage(gamma) def export(self, fname, hideHistograms=True, pngScale = 4): """ Save fan diagrams to file, with the full image, and color bars on the alpha/gamma values :param fname: the fname to save as hideHistograms - (True) Prevent rendering the histograms, often ncier for figures/presentations If fname.endswith(".svg"), it outputs as an SVG. Howver, it's not the cleanest thing (the files are quite large/unoptimized, and I can't think of an easy way to correct that). Also, when the svg is converted to pdf via Inkscape, things get f****d up for some reasons (axes get thicker, fonts get borked, pixels get messed up). So, it kinda works, but there's stuff seriously wrong. One thing to make things cleaner is to use this site: https://jakearchibald.github.io/svgomg/ which optimizies the svg and makes it a lot easier to work with :return: """ # defaults = { # "hideHistograms": False # } # # defaults.update(kwargs) doSvg = fname.endswith(".svg") if hideHistograms: # Hide the histogram data (and shrink the plot) # to avoid confusing people self.histAlpha.plot.hide() self.histAlpha.vb.setMaximumWidth(20) self.histGamma.plot.hide() self.histGamma.vb.setMaximumWidth(20) QtWidgets.QApplication.processEvents() self.histGamma.axis.setRange(-46.75, 46.75) self.histAlpha.axis.setRange(-94, 94) width, height = self.width(), self.height() if doSvg: from PyQt5 import QtSvg outputImage = QtSvg.QSvgGenerator() outputImage.setFileName(fname) outputImage.setSize(QtCore.QSize(int(width), int(height))) # I'm not sure why it has to be this, but the axis on the histogrm # were fuckingup without it outputImage.setResolution(96) else: outputImage = QtGui.QImage(width * pngScale, height * pngScale, QtGui.QImage.Format_ARGB32) # outputImage.setDotsPerMeterX(650 * 100 / 2.54) # outputImage.setDotsPerMeterY(650 * 100 / 2.54) # this gives a moderatly high quality image outputImage.setDevicePixelRatio(pngScale) outputImage.fill(QtGui.QColor("white")) outputPainter = QtGui.QPainter(outputImage) self.render(outputPainter) if not doSvg: ret = outputImage.save(fname) outputPainter.end() def addItem(self, item, ignoreBounds=False): self.view.addItem(item, ignoreBounds) def setViewRadius(self, r): # Set the view range of the fan diagram such that radius r is visible self.view.setRange(QtCore.QRect(-r, -r, 2*r, 2*r), padding=0) def hideHistogramAxes(self, hideTicks=True): # Hide the histogram region item and plots and all that for # less cluttered plots. Definitely useful if export is called with # hideHistograms=True, where the regions are useless. # Hide the linear regions self.histGamma.region.hide() self.histAlpha.region.hide() # Keep a reference to the old paint methods so you can reverse it if desired # This stops the painting of the bars which go from the linear region to the # gradient editor self.histGamma.item.oldPaint = self.histGamma.item.paint self.histAlpha.item.oldPaint = self.histAlpha.item.paint # Overwriting the functions to return None causes all the other rendering # things to abort self.histGamma.item.paint = lambda *x: None self.histAlpha.item.paint = lambda *x: None if hideTicks: # Hide the ticks which can be used for changing the stops/colors of # the gradients, which are rather ugly # Note: Since this only hides ticks which are present, I don't think [ii.hide() for ii in self.histAlpha.item.gradient.ticks.keys()] [ii.hide() for ii in self.histGamma.item.gradient.ticks.keys()] QtWidgets.QApplication.processEvents() # Hard coded numbers which make it look like the axes values line up with # the gradient item, which is more in-line with how color bars are interpreted self.histGamma.axis.setRange(-46.75, 46.75) self.histAlpha.axis.setRange(-94, 94) def showHistogramAxes(self, showTicks=True): try: self.histGamma.item.paint = self.histGamma.item.oldPaint self.histAlpha.item.paint = self.histAlpha.item.oldPaint del self.histAlpha.item.oldPaint del self.histGamma.item.oldPaint except AttributeError: # You didn't hide them first (or at least not here return self.histGamma.region.show() self.histAlpha.region.show() if showTicks: [ii.show() for ii in self.histAlpha.item.gradient.ticks.keys()] [ii.show() for ii in self.histGamma.item.gradient.ticks.keys()] @staticmethod def fromTMatrix(tMatrix, angle = 45, sbs=None): """ Create a fan diagram from T matrices directly. The angle needs to be specified so the T matrix can be converted to a J matrix. The angle is relative to what's specified in the qwp.extractMatrices.makeU function. if you pass a string, it assumes it's a file name from a saved one. It'll load that and plot it. If you also pass values to sbs, it'll make sure only the passed values are plotted. Otherwise, it'll plot all the sbs in the file If you pass a tMatrix as returned from the fitting routines, you also need to pass the sbs directly in this case, since the tMatrices don't include them. :param tMatrix: :param angle: :param sbs: :return: """ if isinstance(tMatrix, str): # a file is passed if sbs is not None: # Pass an array of sbs with a string, and this'll parse # out the sidebands which aren't included in the passed array wantsbs = sbs else: wantsbs = None tMatrix, sbs = loadT(tMatrix) # Handle if only a select number of sidebands is specified if wantsbs is not None: try: # Find the indices of the desired sidebands within the array of # sidebands actually loaded wantIdx = [sbs.tolist().index(ii) for ii in wantsbs] # Cull it to only those specified sbs = sbs[wantIdx] # tMatrix is multidimensional (tMatrix.ndim>2), so ellipses cut # out the other axes tMatrix = tMatrix[..., wantIdx] # Ensure that you got everything you want. Could happen if sidebands # are requested (passed to the function) and not found assert np.all(wantsbs == sbs) except ValueError as e: raise IndexError("Invalid sideband requested ({} is not in loaded)".format( e.args[0].split(' ')[0] )) except AssertionError: raise IndexError("Invalid sideband requested") jMatrix = makeJfromT(tMatrix, angle) if sbs is None: raise RuntimeWarning("Desired sidebands to plot should be specified as kwarg sbs") sbs = np.arange(8, 38, 2) alpha, gamma = jonesToFans(sbs = sbs, J=jMatrix) return FanDiagram(alpha, gamma) def setTitle(self, title="", adjustBounds=True): """ Sets the title of the fan diagram, positioning the text right above the center of the fan :param title: :param adjustBounds: :return: """ self.titleItem.setText(title) # Move the title so the bottom is at the top of the outer axis self.titleItem.setPos(0, self.axes["azimuthal"].fullBoundingRect.top()) QtWidgets.QApplication.processEvents() # Double up because of some weird f*****g issue with Qt not appropriately # updating things when requested self.titleItem.setPos(0, self.axes["azimuthal"].fullBoundingRect.top()) QtWidgets.QApplication.processEvents() # print(self.titleItem.mapRectToView(self.titleItem.boundingRect())) if adjustBounds: # Readjust the viewbox to frame the fan better # Find the top, based on the coordinates of the top of the title top = self.titleItem.mapRectToView(self.titleItem.boundingRect()).top() # Bottom is defiend by the bottom of the axes (includes the text) # Note: this assumes the bottom = self.axes["azimuthal"].fullBoundingRect.bottom() # print("bottom", bottom) w = abs(top-bottom) # print("new rect", QtCore.QRectF(-w/2, top, w, w)) self.view.setRange(QtCore.QRectF(-w/2, top, w, w), padding=0) self.view.setRange(QtCore.QRectF(-w/2, top, w, w), padding=0) # self.view.update() def setMaxRadius(self, radius=40): # Set the maximum value for both of the axes to the value specified. # The 1e-6 is to prevent it from producing an "r=0" label and stuff self.axes["azimuthal"]._bounds["radial"] = [1e-6, radius] self.axes["radial"]._bounds["radial"] = [1e-6, radius] # Need to invalidate the cache for the axes, forcing it to redraw and update # the bounding rect and stuff self.axes["azimuthal"].picture = None self.axes["radial"].picture = None
def __init__(self, alphaData, gammaData=None, view=None): super(FanDiagram, self).__init__() if gammaData is None and isinstance(alphaData, FanCompiler): alphaData, gammaData = alphaData.build(withErrors=False)[:2] self.layout = QtWidgets.QHBoxLayout() self.histAlpha = HistogramLUTWidget(self) self.centralView = GraphicsView() self.histGamma = HistogramLUTWidget(self) self.histAlpha.setMinimumWidth(150) self.histGamma.setMinimumWidth(150) self.layout.addWidget(self.histGamma) self.layout.addWidget(self.centralView) self.layout.addWidget(self.histAlpha) self.layout.setContentsMargins(0,0,0,0) self.layout.setSpacing(0) self.setLayout(self.layout) if view is None: self.view = ViewBox() else: self.view = view self.centralView.setCentralItem(self.view) self.view.setAspectLocked(True) self.view.invertY(True) if isinstance(alphaData, str) and isinstance(gammaData, str): alphaData = np.genfromtxt(alphaData, delimiter=',') gammaData = np.genfromtxt(gammaData, delimiter=',') if alphaData.ndim == gammaData.ndim == 1: # Assume you just want it to be created, and will later populate it nirAlphas = alphaData sbs = gammaData alphaData = np.ones((sbs.shape[0] + 1, nirAlphas.shape[0] + 1)) * -1 alphaData[1:, 0] = sbs alphaData[0, 1:] = nirAlphas gammas = np.ones((sbs.shape[0] + 1, nirAlphas.shape[0] + 1)) * -1 gammas[1:, 0] = sbs gammas[0, 1:] = nirAlphas sbs = alphaData[1:, 0] maxSB = sbs.max() nirAlphas = alphaData[0, 1:] self.alphaItem = PolarImageItem(r=sbs, theta=nirAlphas) self.alphaItem.setImage(alphaData[1:,1:]) # nirAlphas+180 is what causes the gamma angles to appear on the left side of the # fan. This seemed easier than doing some sort of coordinate inversion/flipping # on the plot itself. self.gammaItem = PolarImageItem(sbs, nirAlphas+180, gammaData[1:,1:]) self.view.addItem(self.alphaItem) self.view.addItem(self.gammaItem) self.histAlpha.setImageItem(self.alphaItem) self.histGamma.setImageItem(self.gammaItem) # manually set the default state to the black-gold-white-green-black. Not sure # if it's necessary to have this be a free parameter vs being hardcoded self.histAlpha.gradient.restoreState({ "mode": "rgb", "ticks": [ (0, (0, 0, 0, 255)), (.25, (128, 128, 0, 255)), (.5, (255, 255, 255, 255)), (.75, (0, 128, 0, 255)), (1, (0, 0, 0, 255)) ] }) # Set the default spacings for the alpha color axis. Again, not sure if it's # better to leave the 18pt font hard-coded or not, but I am self.histAlpha.axis.setTickFont(QtGui.QFont("Arial", 18)) self.histAlpha.axis.setTickSpacing(30, 15) self.histAlpha.axis.setLabel("α (°)", **{'font-family': 'Times', "font-size": "18pt"}) # As with alpha, hard code the initial color space for gamma (blue-white-red) # and the font spacings and stuff self.histGamma.gradient.restoreState({ "mode": "rgb", "ticks": [ (0, (255, 0, 0, 255)), (.5, (255, 255, 255, 255)), (1, (0, 0, 255, 255)) ] }) self.histGamma.axis.setTickFont(QtGui.QFont("Arial", 18)) self.histGamma.axis.setTickSpacing(15, 15) self.histGamma.axis.setLabel("γ (°)", **{'font-family': 'Times', "font-size": "18pt"}) self.histAlpha.item.setLevels(-90, 90) self.histGamma.item.setLevels(-45, 45) self.histAlpha.autoHistogramRange() self.histGamma.autoHistogramRange() # Make it the right dimensions, making sure that the width is appropriate. # This makes it easier to automate plotting/saving fans and making sure # their dimensions are consistent. g = self.geometry() # I found these by eye, there's not very much important about them g.setWidth(773) g.setHeight(480) # Manually center it on the screen, since geometry isn't well defined at this point # before events are processed g.moveCenter(QtWidgets.QApplication.desktop().screenGeometry().center()) self.setGeometry(g) # Add in the radial axes for it self.axes = { "radial": PolarAxis("radial"), "azimuthal": PolarAxis("azimuthal") } # Lighten the radial font to make it distinct from the other p = self.axes["radial"].pen() p.setColor(mkColor("#666666")) self.axes["radial"].setPen(p) for a in self.axes.values(): # Make sure the axes sit on top of all other items a.setZValue(10000) # make sure that they scale appropriately, instead of just floating on top a.linkToView(self.view) # Ignore bounds prevents the window from resizing to try and fit in # the axes items self.addItem(a, ignoreBounds=True) # manually set the positions and string values for alpha angles. [-90, 90] work # well. The other half needs the +-180 to make sure the gamma angles have the # correctly labeled with respect to alpha_nir self.axes["azimuthal"].setTicks( [ [(ii, str(ii)) for ii in np.arange(-90, 91, 30)] + # alpha side (Q1+Q4) [(ii, str(ii + 180)) for ii in np.arange(-180, -91, 30)] + #Q3 [(ii, str(ii - 180)) for ii in np.arange(120, 151, 30)], #Q1 ] ) # add a title (without text) self.titleItem = TextItem() self.titleItem.setAnchor(Point(0.5, 1)) # anchor on bottom-center # Again, not sure if it's necessary to have the font color/size being # a free parameter self.titleItem.setColor("k") self.titleItem.setFont(QtGui.QFont("Arial", 15)) # Ignore bounds so that the view won't try to account for it (which # causes a conflict because the title is placed with respect to the # view region) self.view.addItem(self.titleItem, ignoreBounds=True) self.show() # Arbitrary forcing updates to try and track down why some things don't # update correctly QtWidgets.QApplication.processEvents() self.view.updateViewRange(True, True)
from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg app = QtGui.QApplication([]) window = QMainWindow() holder = QWidget() holder_layout = QGridLayout() holder.setLayout(holder_layout) window.setCentralWidget(holder) # # Grid section # grid_widget = pg.GraphicsLayoutWidget() grid_viewBox1 = ViewBox(enableMenu=True) grid_viewBox1.setXRange(0, 5, padding=0) grid_viewBox1.setYRange(0, 2.5, padding=0) grid_subplot1 = PlotItem(viewBox=grid_viewBox1) grid_subplot1.showGrid(True, True, 0.2) grid_viewBox1.setParent(grid_subplot1) grid_widget.addItem(grid_subplot1, col=0, row=0) grid_viewBox2 = ViewBox(enableMenu=True) grid_viewBox2.setXRange(0, 5, padding=0) grid_viewBox2.setYRange(0, 2.5, padding=0) grid_subplot2 = PlotItem(viewBox=grid_viewBox2) grid_subplot2.showGrid(True, True, 0.2) grid_viewBox2.setParent(grid_subplot2) grid_widget.addItem(grid_subplot2, col=1, row=0)
class SideView(GraphicsView): """SideView class This class contains a series of functions allowing the user to simultaneously view the 3D volume from all orientations. Args: diff (int): Flag indicating the different brain view orientations. parent (class): Base or parent class """ def __init__(self, diff, parent=None): super(SideView, self).__init__(parent=parent) self.parent = parent self.brain = parent.brain self.diff = diff self.view1 = ViewBox() self.setCentralItem(self.view1) # Making Images out of data self.brain.section = (self.brain.section + self.diff) % 3 self.i = int(self.brain.shape[self.brain.section] / 2) data_slice = self.brain.get_data_slice(self.i) self.brain_img1 = ImageItem( data_slice, autoDownsample=False, compositionMode=QtGui.QPainter.CompositionMode_SourceOver) self.brain.section = (self.brain.section - self.diff) % 3 self.view1.addItem(self.brain_img1) self.view1.setAspectLocked(True) self.view1.setFixedHeight(250) self.view1.setFixedWidth(250) self.setMinimumHeight(250) self.vLine = InfiniteLine(angle=90, movable=False) self.hLine = InfiniteLine(angle=0, movable=False) self.vLine.setVisible(False) self.hLine.setVisible(False) self.view1.addItem(self.vLine, ignoreBounds=True) self.view1.addItem(self.hLine, ignoreBounds=True) def refresh_image(self): """Refresh Image This function refreshes the displayed volume orientation image. """ self.brain.section = (self.brain.section + self.diff) % 3 data_slice = self.brain.get_data_slice(self.i) self.brain_img1.setImage(data_slice) self.brain.section = (self.brain.section - self.diff) % 3 def refresh_all_images(self): """Refresh all images This function refreshes both the side view images when triggered. """ self.parent.main_widget._update_section_helper() self.parent.win1.refresh_image() self.parent.win1.view1.menu.actions()[0].trigger() self.parent.win2.refresh_image() self.parent.win2.view1.menu.actions()[0].trigger() def mouseDoubleClickEvent(self, event): """Click trigger Tracks a mouse double click event which triggers the refreshing of all the views Args event (event): Mouse double click events for the widget. """ super(SideView, self).mouseDoubleClickEvent(event) self.brain.section = (self.brain.section + self.diff) % 3 self.refresh_all_images() def set_i(self, position, out_of_box=False): """Set position This function sets the cursor position for each window. Args: position (tuple): Tuple containing the required coordinates. out_of_box (bool): Flag indicating if event possition is outisde of the considered volume. """ section = (self.brain.section + self.diff) % 3 if out_of_box: self.i = int(self.brain.shape[section] / 2) self.vLine.setVisible(False) self.hLine.setVisible(False) else: i = position[section] self.i = np.clip(i, 0, self.brain.shape[section] - 1) self.vLine.setVisible(True) self.hLine.setVisible(True) self.brain.section = (self.brain.section + self.diff) % 3 x, y = self.brain.voxel_as_position(position[0], position[1], position[2]) self.brain.section = (self.brain.section - self.diff) % 3 self.vLine.setPos(x) self.hLine.setPos(y)
class MIMiniImageView(GraphicsLayout, BadDataOverlay): def __init__(self, name: str = "MIMiniImageView"): super().__init__() self.name = name.title() self.im = ImageItem() self.vb = ViewBox(invertY=True, lockAspect=True, name=name) self.vb.addItem(self.im) self.hist = HistogramLUTItem(self.im) graveyard.append(self.vb) # Sub-layout prevents resizing issues when details text changes image_layout = self.addLayout(colspan=2) image_layout.addItem(self.vb) image_layout.addItem(self.hist) self.hist.setFixedWidth(100) # HistogramLUTItem used pixel sizes self.nextRow() self.details = self.addLabel("", colspan=2) self.im.hoverEvent = lambda ev: self.mouse_over(ev) self.axis_siblings: "WeakSet[MIMiniImageView]" = WeakSet() self.histogram_siblings: "WeakSet[MIMiniImageView]" = WeakSet() @property def image_item(self) -> ImageItem: return self.im @property def viewbox(self) -> ViewBox: return self.vb def clear(self): self.im.clear() def setImage(self, *args, **kwargs): self.im.setImage(*args, **kwargs) self.check_for_bad_data() @staticmethod def set_siblings(sibling_views: List["MIMiniImageView"], axis=False, hist=False): for view1 in sibling_views: for view2 in sibling_views: if view2 is not view1: if axis: view1.add_axis_sibling(view2) if hist: view1.add_hist_sibling(view2) def add_axis_sibling(self, sibling: "MIMiniImageView"): self.axis_siblings.add(sibling) def add_hist_sibling(self, sibling: "MIMiniImageView"): self.histogram_siblings.add(sibling) def get_parts(self) -> Tuple[ImageItem, ViewBox, HistogramLUTItem]: return self.im, self.vb, self.hist def mouse_over(self, ev): # Ignore events triggered by leaving window or right clicking if ev.exit: return pos = CloseEnoughPoint(ev.pos()) self.show_value(pos) for img_view in self.axis_siblings: img_view.show_value(pos) def show_value(self, pos): image = self.im.image if image is not None and pos.y < image.shape[ 0] and pos.x < image.shape[1]: pixel_value = image[pos.y, pos.x] value_string = ("%.6f" % pixel_value)[:8] self.details.setText(f"{self.name}: {value_string}") def link_sibling_axis(self): # Linking multiple viewboxes with locked aspect ratios causes # odd resizing behaviour. Use workaround from # https://github.com/pyqtgraph/pyqtgraph/issues/1348 self.vb.setAspectLocked(True) for view1, view2 in pairwise(chain([self], self.axis_siblings)): view2.vb.linkView(ViewBox.XAxis, view1.vb) view2.vb.linkView(ViewBox.YAxis, view1.vb) view2.vb.setAspectLocked(False) def unlink_sibling_axis(self): for img_view in chain([self], self.axis_siblings): img_view.vb.linkView(ViewBox.XAxis, None) img_view.vb.linkView(ViewBox.YAxis, None) img_view.vb.setAspectLocked(True) def link_sibling_histogram(self): for view1, view2 in pairwise(chain([self], self.histogram_siblings)): view1.hist.vb.linkView(ViewBox.YAxis, view2.hist.vb) for img_view in chain([self], self.histogram_siblings): img_view.hist.sigLevelChangeFinished.connect( img_view.update_sibling_histograms) def unlink_sibling_histogram(self): for img_view in chain([self], self.histogram_siblings): img_view.hist.vb.linkView(ViewBox.YAxis, None) try: img_view.hist.sigLevelChangeFinished.disconnect() except TypeError: # This is expected if there are slots currently connected pass def update_sibling_histograms(self): hist_range = self.hist.getLevels() for img_view in self.histogram_siblings: with BlockQtSignals(img_view.hist): img_view.hist.setLevels(*hist_range)
def image_in_vb(self, name=None): im = ImageItem() vb = ViewBox(invertY=True, lockAspect=True, name=name) vb.addItem(im) hist = HistogramLUTItem(im) return im, vb, hist
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
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 MDIChildPlot(PlotWidget): LINE_COLORS: List[str] = ['r', 'g', 'b', 'c', 'm', 'y', 'w'] AXES_NAMES: Dict[int, str] = {2: 'bottom', 3: 'left', 4: 'right'} child_number: int = 1 def __init__(self, **kwargs): super(MDIChildPlot, self).__init__(**kwargs) self.setAttribute(Qt.WA_DeleteOnClose) self.is_untitled: bool = True self.is_modified: bool = False self.cur_file: str = '' self.curves = [] self.plotItem2 = ViewBox() self.plotItem.showAxis('right') self.plotItem.scene().addItem(self.plotItem2) self.plotItem.getAxis('right').linkToView(self.plotItem2) self.plotItem2.setXLink(self.plotItem) self.plotItem.showButtons() self.plotItem.showGrid(x=True, y=True) # Handle view resizing def update_views(): # view has resized; update auxiliary views to match self.plotItem2.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.plotItem2.linkedViewChanged(self.plotItem.vb, self.plotItem2.XAxis) update_views() self.plotItem.vb.sigResized.connect(update_views) # add items to the context menu self.plotItem.vb.menu.addSeparator() self.delete_curve_menu: QMenu = QMenu('Delete') self.copy_curve_menu: QMenu = QMenu('Copy') self.paste_curve_menu: QMenu = QMenu('Paste') menus = {self.delete_curve_menu: [('Last Curve…', self.delete_last_curve), ('Curves No.…', self.delete_curves), ('All Curves…', self.delete_all_curves)], self.copy_curve_menu: [('Last Curve…', self.copy_last_curve), ('Curves No.…', self.copy_curves), ('All Curves…', self.copy_all_curves)], } for parent_menu, actions in menus.items(): for title, callback in actions: new_action: QAction = QAction(title, parent_menu) parent_menu.addAction(new_action) new_action.triggered.connect(callback) self.plotItem.vb.menu.addMenu(self.delete_curve_menu) self.plotItem.vb.menu.addMenu(self.copy_curve_menu) self.plotItem.vb.menu.addMenu(self.paste_curve_menu) # hide buggy menu items for undesired_menu_item_index in (5, 2, 1): self.plotItem.subMenus.pop(undesired_menu_item_index) self.plotItem.subMenus[1].actions()[0].defaultWidget().children()[1].hide() def new_file(self): self.is_untitled = True self.cur_file = f'Plot {MDIChildPlot.child_number:d}' MDIChildPlot.child_number += 1 self.setWindowTitle(self.cur_file + '[*]') self.setWindowModified(True) # self.sig.connect(self.document_was_modified) def load_irtecon_file(self, file_name: str): file = QFile(file_name) if not file.open(QFile.ReadOnly | QFile.Text): QMessageBox.warning(self, 'MDI', f'Cannot read file {file_name}:\n{file.errorString()}.') return False QApplication.setOverrideCursor(Qt.WaitCursor) in_str = QTextStream(file).readAll() file_data = IRTECONFile(in_str) self.plotItem.setTitle(file_data.sample_name) self.plotItem.addLegend() for index, curve in enumerate(file_data.curves): self.curves.append(self.plotItem.plot(curve.data[..., :2], name=curve.legend_key, pen=mkPen(self.LINE_COLORS[index % len(self.LINE_COLORS)]))) for ax in self.AXES_NAMES.values(): self.plotItem.hideAxis(ax) for ax in file_data.axes: if ax.axis in self.AXES_NAMES: self.plotItem.showAxis(self.AXES_NAMES[ax.axis]) self.plotItem.setLabel(self.AXES_NAMES[ax.axis], ax.name, ax.unit) QApplication.restoreOverrideCursor() self.set_current_file(file_name) # self.document().contentsChanged.connect(self.document_was_modified) return True def delete_last_curve(self): if not self.curves: return ret = QMessageBox.warning(self, 'MDI', 'Do you want to delete the last curve?', QMessageBox.Yes | QMessageBox.No) if ret == QMessageBox.Yes: self.plotItem.legend.removeItem(self.curves[-1]) self.curves[-1].clear() del self.curves[-1] self.is_modified = True self.setWindowTitle(self.user_friendly_current_file() + '[*]') self.setWindowModified(True) def delete_curves(self): def parse_range() -> List[int]: # https://stackoverflow.com/a/4248689/8554611 result = set() for part in ranges.split(','): x = part.split('-') result.update(list(range(int(x[0]), int(x[-1]) + 1))) return sorted(result) if not self.curves: return ranges, ok = QInputDialog.getText(self, 'Delete Curves', 'Curves No.:') if ok: for index in reversed(parse_range()): index -= 1 if index in range(len(self.curves)): self.plotItem.legend.removeItem(self.curves[index]) self.curves[index].clear() del self.curves[index] self.is_modified = True self.setWindowTitle(self.user_friendly_current_file() + '[*]') self.setWindowModified(True) def delete_all_curves(self): if not self.curves: return ret = QMessageBox.question(self, 'MDI', 'Do you want to delete all the curves?', QMessageBox.Yes | QMessageBox.No) if ret == QMessageBox.Yes: while self.curves: self.plotItem.legend.removeItem(self.curves[-1]) self.curves[-1].clear() del self.curves[-1] self.is_modified = True self.setWindowTitle(self.user_friendly_current_file() + '[*]') self.setWindowModified(True) def copy_last_curve(self): if not self.curves: return raise NotImplementedError def copy_curves(self, ranges: str): def parse_range() -> List[int]: # https://stackoverflow.com/a/4248689/8554611 result = set() for part in ranges.split(','): x = part.split('-') result.update(list(range(int(x[0]), int(x[-1]) + 1))) return sorted(result) if not self.curves: return parse_range() raise NotImplementedError def copy_all_curves(self): if not self.curves: return raise NotImplementedError def save(self): if self.is_untitled: return self.save_as() else: return self.save_file(self.cur_file) def save_as(self): file_name, _ = QFileDialog.getSaveFileName(self, 'Save As', self.cur_file) if not file_name: return False return self.save_file(file_name) def save_file(self, file_name: str): file = QFile(file_name) if not file.open(QFile.WriteOnly | QFile.Text): QMessageBox.warning(self, 'MDI', f'Cannot write file {file_name}:\n{file.errorString()}.') return False out_str = QTextStream(file) QApplication.setOverrideCursor(Qt.WaitCursor) out_str << self.toPlainText() QApplication.restoreOverrideCursor() self.set_current_file(file_name) return True def user_friendly_current_file(self): def stripped_name(full_file_name): return QFileInfo(full_file_name).fileName() return stripped_name(self.cur_file) def current_file(self): return self.cur_file def close_event(self, event): if self.maybe_save(): event.accept() else: event.ignore() def maybe_save(self): if self.document().isModified(): ret = QMessageBox.warning(self, 'MDI', f'"{self.user_friendly_current_file()}" has been modified.\n' 'Do you want to save your changes?', QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel) if ret == QMessageBox.Save: return self.save() elif ret == QMessageBox.Cancel: return False return True def set_current_file(self, file_name): self.cur_file = QFileInfo(file_name).canonicalFilePath() self.is_untitled = False self.is_modified = False self.setWindowModified(False) self.setWindowTitle(self.user_friendly_current_file())
def image_in_vb(name=None) -> Tuple[ImageItem, ViewBox, HistogramLUTItem]: im = ImageItem() vb = ViewBox(invertY=True, lockAspect=True, name=name) vb.addItem(im) hist = HistogramLUTItem(im) return im, vb, hist
def removeItem(self, item): """Reimplemented from :class:`pyqtgraph.ViewBox`""" ViewBox.removeItem(self, item) if self.plotItem is not None: self.plotItem.showAxis("right", show=bool(self.addedItems))