class GUI(QtGui.QWidget): def __init__(self): super(GUI, self).__init__() self.setWindowTitle('Label Cells') self.cellNames = [ '1.p', '4.a', '1.pp', '4.aa', '1.ppa', '1.ppp', '4.aaa', '4.aap', 'b_1', 'b_4' ] self.initUI() #----------------------------------------------------------------------------------------------- # INITIALIZATION OF THE WINDOW - DEFINE AND PLACE ALL THE WIDGETS #----------------------------------------------------------------------------------------------- def initUI(self): # SET THE GEOMETRY mainWindow = QtGui.QVBoxLayout() mainWindow.setSpacing(15) fileBox = QtGui.QHBoxLayout() spaceBox1 = QtGui.QHBoxLayout() rawDataBox = QtGui.QHBoxLayout() mainWindow.addLayout(fileBox) mainWindow.addLayout(spaceBox1) mainWindow.addLayout(rawDataBox) Col1 = QtGui.QGridLayout() Col2 = QtGui.QHBoxLayout() Col3 = QtGui.QVBoxLayout() rawDataBox.addLayout(Col1) rawDataBox.addLayout(Col2) rawDataBox.addLayout(Col3) self.setLayout(mainWindow) # DEFINE ALL WIDGETS AND BUTTONS loadBtn = QtGui.QPushButton('Load DataSet') saveBtn = QtGui.QPushButton('Save data (F12)') tpLbl = QtGui.QLabel('Relative Tp:') slLbl = QtGui.QLabel('Slice:') fNameLbl = QtGui.QLabel('File name:') self.tp = QtGui.QSpinBox(self) self.tp.setValue(0) self.tp.setMaximum(100000) self.sl = QtGui.QSpinBox(self) self.sl.setValue(0) self.sl.setMaximum(100000) self.fName = QtGui.QLabel('') self._488nmBtn = QtGui.QRadioButton('488nm') self._561nmBtn = QtGui.QRadioButton('561nm') self.CoolLEDBtn = QtGui.QRadioButton('CoolLED') self.sld1 = QtGui.QSlider(QtCore.Qt.Vertical, self) self.sld1.setMaximum(2**16 - 1) self.sld1.setValue(0) self.sld2 = QtGui.QSlider(QtCore.Qt.Vertical, self) self.sld2.setMaximum(2**16) self.sld2.setValue(2**16 - 1) self.fig1 = Figure((8.0, 8.0), dpi=100) self.fig1.subplots_adjust(left=0., right=1., top=1., bottom=0.) self.ax1 = self.fig1.add_subplot(111) self.canvas1 = FigureCanvas(self.fig1) self.canvas1.setFocusPolicy(QtCore.Qt.ClickFocus) self.canvas1.setFocus() self.canvas1.setFixedSize(QtCore.QSize(600, 600)) self.canvas1.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.cellTbl = QtGui.QTableWidget() self.fig2 = Figure((4.0, 4.0), dpi=100) self.fig2.subplots_adjust(left=0., right=1., top=1., bottom=0.) self.ax2 = self.fig2.add_subplot(111) self.canvas2 = FigureCanvas(self.fig2) self.canvas2.setFixedSize(QtCore.QSize(300, 300)) self.canvas2.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) # self.cellNamesBox=QtGui.QMenu(self.canvas1) # actn = [] # for cname in self.cellNames: # actn.append( QtGui.QAction(cname, self.cellNamesBox) ) # self.cellNamesBox.addAction(actn[-1]) # actn[-1].triggered.connect( lambda item = cname : self.actionFunct(cname) ) self.cellNamesBox = QtGui.QMenu(self.canvas1) ### define all actions (can be done better!!!) actn1p = QtGui.QAction('1.p', self.cellNamesBox) self.cellNamesBox.addAction(actn1p) actn1p.triggered.connect(lambda item='1.p': self.actionFunct1p('1.p')) actn1pp = QtGui.QAction('1.pp', self.cellNamesBox) self.cellNamesBox.addAction(actn1pp) actn1pp.triggered.connect( lambda item='1.pp': self.actionFunct1pp('1.pp')) actn1ppp = QtGui.QAction('1.ppp', self.cellNamesBox) self.cellNamesBox.addAction(actn1ppp) actn1ppp.triggered.connect( lambda item='1.ppp': self.actionFunct1ppp('1.ppp')) actn1ppa = QtGui.QAction('1.ppa', self.cellNamesBox) self.cellNamesBox.addAction(actn1ppa) actn1ppa.triggered.connect( lambda item='1.ppa': self.actionFunct1ppa('1.ppa')) actn4a = QtGui.QAction('4.a', self.cellNamesBox) self.cellNamesBox.addAction(actn4a) actn4a.triggered.connect(lambda item='4.a': self.actionFunct4a('4.a')) actn4aa = QtGui.QAction('4.aa', self.cellNamesBox) self.cellNamesBox.addAction(actn4aa) actn4aa.triggered.connect( lambda item='4.aa': self.actionFunct4aa('4.aa')) actn4aaa = QtGui.QAction('4.aaa', self.cellNamesBox) self.cellNamesBox.addAction(actn4aaa) actn4aaa.triggered.connect( lambda item='4.aaa': self.actionFunct4aaa('4.aaa')) actn4aap = QtGui.QAction('4.aap', self.cellNamesBox) self.cellNamesBox.addAction(actn4aap) actn4aap.triggered.connect( lambda item='4.aap': self.actionFunct4aap('4.aap')) actnb1 = QtGui.QAction('b_1', self.cellNamesBox) self.cellNamesBox.addAction(actnb1) actnb1.triggered.connect(lambda item='b_1': self.actionFunctb1('b_1')) actnb4 = QtGui.QAction('b_4', self.cellNamesBox) self.cellNamesBox.addAction(actnb4) actnb4.triggered.connect(lambda item='b_4': self.actionFunctb4('b_4')) ### END OF THE UGLY PART :) self.cellNamesBox.installEventFilter(self) self.canvas1.installEventFilter(self) self.cellNamesBox.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.cellNamesBox.connect( self.cellNamesBox, QtCore.SIGNAL("customContextMenuRequested(QPoint)"), self.leftClicked) # PLACE ALL THE WIDGET ACCORDING TO THE GRIDS fileBox.addWidget(loadBtn) fileBox.addWidget(saveBtn) spaceBox1.addWidget(self.HLine()) Col1.addWidget(tpLbl, 0, 0) #, 1, 1, Qt.AlignTop) Col1.addWidget(self.tp, 0, 1) #, 1, 1, Qt.AlignTop) Col1.addWidget(slLbl, 1, 0) #, 1, 1, Qt.AlignTop) Col1.addWidget(self.sl, 1, 1) #, 1, 1, Qt.AlignTop) Col1.addWidget(fNameLbl, 2, 0) Col1.addWidget(self.fName, 2, 1) Col1.addWidget(self._488nmBtn, 3, 0) Col1.addWidget(self._561nmBtn, 4, 0) Col1.addWidget(self.CoolLEDBtn, 5, 0) Col2.addWidget(self.sld1) Col2.addWidget(self.sld2) Col2.addWidget(self.canvas1) # Col3.addWidget(self.cellTbl) # Col3.addWidget(self.cellNamesBox) Col3.addWidget(self.canvas2) self.setFocus() self.show() # BIND BUTTONS TO FUNCTIONS loadBtn.clicked.connect(self.selectWorm) saveBtn.clicked.connect(self.saveData) self.checkNames = True self.tp.valueChanged.connect(self.changeTp) self.sl.valueChanged.connect(self.updateCanvas1) self.sld1.valueChanged.connect(self.updateBC) self.sld2.valueChanged.connect(self.updateBC) self._488nmBtn.toggled.connect(self.radio488Clicked) self._561nmBtn.toggled.connect(self.radio561Clicked) self.CoolLEDBtn.toggled.connect(self.radioCoolLEDClicked) self.fig1.canvas.mpl_connect('scroll_event', self.wheelEvent) #----------------------------------------------------------------------------------------------- # FORMATTING THE WINDOW #----------------------------------------------------------------------------------------------- def HLine(self): toto = QtGui.QFrame() toto.setFrameShape(QtGui.QFrame.HLine) toto.setFrameShadow(QtGui.QFrame.Sunken) return toto #----------------------------------------------------------------------------------------------- # ACTION FUNCTIONS in THE POPUP MENU #----------------------------------------------------------------------------------------------- def changeName(self, item): idx = len(self.currentCells.cname) - 1 self.currentCells.ix[idx, 'cname'] = str(item) self.currentCells = self.currentCells.reset_index(drop=True) self.updateCanvas1() self.setFocus() def actionFunct1p(self, item): self.changeName(item) def actionFunct1pp(self, item): self.changeName(item) def actionFunct1ppp(self, item): self.changeName(item) def actionFunct1ppa(self, item): self.changeName(item) def actionFunct4a(self, item): self.changeName(item) def actionFunct4aa(self, item): self.changeName(item) def actionFunct4aaa(self, item): self.changeName(item) def actionFunct4aap(self, item): self.changeName(item) def actionFunctb1(self, item): self.changeName(item) def actionFunctb4(self, item): self.changeName(item) def eventFilter(self, widget, event): # print( 'eventFilter', widget, event) if widget == self.canvas1 and isinstance( event, QtGui.QMouseEvent) and event.buttons() & QtCore.Qt.LeftButton: self.leftClicked(event.pos()) return True if widget == self.canvas1 and isinstance( event, QtGui.QMouseEvent) and event.buttons() & QtCore.Qt.RightButton: self.rightClicked(event.pos()) return True return False def leftClicked(self, QPos): refpos = np.array([ int(QPos.x() / 600. * 512.), int(QPos.y() / 600. * 512.), self.sl.value() ]) # print(refpos) # create an empty cell in the currentCells df: the only entries are tidx, xyzpos and cname newcell = create_single_cell_pos(refpos.astype(np.uint16), self.tp.value()) self.currentCells = pd.concat([self.currentCells, newcell]) self.currentCells = self.currentCells.reset_index(drop=True) self.updateCanvas1() parentPosition = self.canvas1.mapToGlobal(QtCore.QPoint(0, 0)) # print('leftClicked', QPos, parentPosition) menuPosition = parentPosition + QPos self.cellNamesBox.move(menuPosition) self.cellNamesBox.show() self.setFocus() def rightClicked(self, QPos): refpos = np.array( [QPos.x() / 600. * 512., QPos.y() / 600. * 512., self.sl.value()]) # print(refpos) if len(self.currentCells) == 0: self.setFocus() return # remove a cell (the closest to the cursor at the moment of right-click) idx = closer_cell(refpos.astype(np.uint16), self.currentCells) self.currentCells = self.currentCells.drop([idx]) self.currentCells = self.currentCells.reset_index(drop=True) self.updateCanvas1() self.setFocus() #----------------------------------------------------------------------------------------------- # BUTTON FUNCTIONS #----------------------------------------------------------------------------------------------- def selectWorm(self): ### store the folders self.pathDial = QtGui.QFileDialog.getExistingDirectory( self, 'Select a folder', 'X:/Simone/160129_MCHERRY_HLH2GFP_onHB101') #'Y:\\Images') self.worm = self.pathDial.split("/")[-1].split('_')[0] self.path = os.path.dirname(self.pathDial) self.setWindowTitle('Label cells - ' + self.pathDial.split("/")[-2] + ' - ' + self.pathDial.split("/")[-1][:3]) ### give error message if there is no CoolLED movie in the selected folder flist = glob.glob(self.pathDial + '/*_movie.tif') if len( flist ) == 0: #not os.path.isfile( os.path.join( self.pathDial, '*_movie.tif' ) ): QtGui.QMessageBox.about( self, 'Warning!', 'There is no movie in this folder! Create a movie first!') return ### load parameters and times dataframes self.paramsDF = load_data_frame_pandas(self.path, self.worm + '_01params.pickle') self.timesDF = load_data_frame_pandas(self.path, self.worm + '_01times.pickle') self.gpDF = load_data_frame_pandas(self.path, self.worm + '_02gonadPos.pickle') # extract some info self.compression = self.paramsDF.compression self.hatchingtidx = int(self.paramsDF.tidxHatch) ### if the cellPos pickle file already exists, load it, otherwise create a blank one if os.path.isfile( os.path.join(self.path, self.worm + '_04cellPos.pickle')): self.cellPosDF = load_data_frame_pandas( self.path, self.worm + '_04cellPos.pickle') else: self.cellPosDF = create_cell_pos(self.timesDF, self.cellNames) # detect available channels self.channels = [] chns = ['CoolLED', '488nm', '561nm'] for c in chns: if os.path.isfile(os.path.join(self.pathDial, c + '_movie.tif')): self.channels.append(c) self.currentChannel = self.channels[0] ### detect size of the cropped images tp = np.min(self.gpDF.ix[pd.notnull(self.gpDF.X), 'tidx']) self.prevtp = tp - 1 tRow = self.timesDF.ix[self.timesDF.tidxRel == tp].squeeze() fileName = os.path.join(self.pathDial, tRow.fName + self.currentChannel + '.tif') firststack = load_stack(fileName) self.cropsize = firststack.shape[1] self.nslices = firststack.shape[0] ### load CoolLED movie if 'CoolLED' in self.channels: self.LEDmovie = load_stack( os.path.join(self.pathDial, 'CoolLED_movie.tif')) else: self.LEDmovie = load_stack( os.path.join(self.pathDial, self.currentChannel + '_movie.tif')) self.initializeCanvas1() self.initializeCanvas2() ### extract current cells already labeled self.currentCells = extract_current_cell_pos(self.cellPosDF, self.tp.value()) ### update the text of the fileName self.fName.setText(self.timesDF.ix[self.timesDF.tidxRel == tp, 'fName'].values[0]) ### set the timepoint to the hatching time self.tp.setMinimum(np.min(self.timesDF.tidxRel)) self.tp.setMaximum(np.max(self.timesDF.tidxRel)) ### set the max slice number self.sl.setMaximum(self.nslices - 1) self.tp.setValue(tp) if self.currentChannel == 'CoolLED': self.CoolLEDBtn.setChecked( True) # this uppdates the canvas1 once more elif self.currentChannel == '561nm': self._561nmBtn.setChecked( True) # this uppdates the canvas1 once more elif self.currentChannel == '488nm': self._488nmBtn.setChecked( True) # this uppdates the canvas1 once more # self.pathDial.show() self.setFocus() def loadNewStack(self): # print(self.fList['gfp'][self.tp.value()]) tRow = self.timesDF.ix[self.timesDF.tidxRel == self.tp.value()].squeeze() ### update the text of the fileName self.fName.setText( self.timesDF.ix[self.timesDF.tidxRel == self.tp.value(), 'fName'].values[0]) print('Loading... ', self.pathDial, tRow.fName) # calculate the max value of the previous stack try: prevmax = np.max([np.max(self.stacks[ch]) for ch in self.channels]) # if it's the first time a stack is to be loaded (so if there is no previous stack), set it to zero except: prevmax = 0 # load all the available stacks - this is the slowest part of the code!!! self.stacks = {} for ch in self.channels: fileName = os.path.join(self.pathDial, tRow.fName + ch + '.tif') if os.path.isfile(fileName): # print(MultiImage('X:\\Simone\\160129_MCHERRY_HLH2GFP_onHB101\\C02_analyzedImages\\Z003_488nm.tif')) # print(fileName, MultiImage( fileName )) # self.stacks[ch] = MultiImage( fileName ) self.stacks[ch] = load_stack(fileName) # if there are no files for the timepoint, create a blank image else: self.stacks[ch] = prevmax * np.ones( (self.nslices, self.cropsize, self.cropsize)) # if the BC bound are different, the BCsliderMinMax will automatically update canvas1. Otherwise, manually update it! self.setBCslidersMinMax() self.updateCanvas1() self.updateCanvas2() def changeTp(self): # if it's the second time you are checking the same tp, don't do anything if self.checkNames: cellFine = self.checkConsistencyCellNames() else: return # before changing timepoint, print labeled cells and check if they are OK print('cells labeled:\n ', self.currentCells) ### extract current cells already labeled self.newCells = extract_current_cell_pos(self.cellPosDF, self.tp.value()) if cellFine: # if everything fine, load the new stack self.currentCells = self.newCells self.loadNewStack() else: # otherwise, go back to prev tp self.checkNames = False self.tp.setValue(self.prevtp) self.checkNames = True self.prevtp = self.tp.value() def saveData(self): if self.checkConsistencyCellNames(): save_data_frame(self.cellPosDF, self.path, self.worm + '_04cellPos.pickle') else: QtGui.QMessageBox.about(self, 'Warning!', 'There is a mistake in the cell labels!') self.setFocus() def radio488Clicked(self, enabled): # print('radio 488 clicked') if enabled: if '488nm' in self.channels: self.currentChannel = '488nm' self.setFocus() self.updateCanvas1() else: if self.currentChannel == 'CoolLED': self.CoolLEDBtn.setChecked( True) # this uppdates the canvas1 once more elif self.currentChannel == '561nm': self._561nmBtn.setChecked( True) # this uppdates the canvas1 once more QtGui.QMessageBox.about(self, 'Warning', 'No 488nm channel!') def radio561Clicked(self, enabled): # print('radio 561 clicked') if enabled: if '561nm' in self.channels: self.currentChannel = '561nm' self.setFocus() self.updateCanvas1() else: if self.currentChannel == 'CoolLED': self.CoolLEDBtn.setChecked( True) # this uppdates the canvas1 once more elif self.currentChannel == '488nm': self._488nmBtn.setChecked( True) # this uppdates the canvas1 once more QtGui.QMessageBox.about(self, 'Warning', 'No 561nm channel!') def radioCoolLEDClicked(self, enabled): # print('radio LED clicked') if enabled: if 'CoolLED' in self.channels: self.currentChannel = 'CoolLED' self.setFocus() self.updateCanvas1() else: if self.currentChannel == '561nm': self._561nmBtn.setChecked( True) # this uppdates the canvas1 once more elif self.currentChannel == '488nm': self._488nmBtn.setChecked( True) # this uppdates the canvas1 once more QtGui.QMessageBox.about(self, 'Warning', 'No CoolLED channel!') def updateBC(self): # change brightness and contrast self.imgplot1.set_clim(self.sld1.value(), self.sld2.value()) self.canvas1.draw() #----------------------------------------------------------------------------------------------- # DEFAULT FUNCTION FOR KEY AND MOUSE PRESS ON WINDOW #----------------------------------------------------------------------------------------------- def keyPressEvent(self, event): # print(event.key()) # change timepoint if event.key() == QtCore.Qt.Key_Right: self.changeSpaceTime('time', +1) elif event.key() == QtCore.Qt.Key_Left: self.changeSpaceTime('time', -1) # change slice elif event.key() == QtCore.Qt.Key_Up: self.changeSpaceTime('space', +1) elif event.key() == QtCore.Qt.Key_Down: self.changeSpaceTime('space', -1) elif event.key() == QtCore.Qt.Key_PageDown: idx = self.channels.index(self.currentChannel) if self.channels[(idx + 1) % len(self.channels)] == 'CoolLED': self.CoolLEDBtn.setChecked(True) if self.channels[(idx + 1) % len(self.channels)] == '488nm': self._488nmBtn.setChecked(True) if self.channels[(idx + 1) % len(self.channels)] == '561nm': self._561nmBtn.setChecked(True) elif event.key() == QtCore.Qt.Key_PageUp: idx = self.channels.index(self.currentChannel) if self.channels[(idx - 1) % len(self.channels)] == 'CoolLED': self.CoolLEDBtn.setChecked(True) if self.channels[(idx - 1) % len(self.channels)] == '488nm': self._488nmBtn.setChecked(True) if self.channels[(idx - 1) % len(self.channels)] == '561nm': self._561nmBtn.setChecked(True) # key press on cropped image if self.canvas1.underMouse(): self.onKeyPressOnCanvas1(event) self.setFocus() def wheelEvent(self, event): if self.canvas1.underMouse(): step = event.step else: step = event.delta() / abs(event.delta()) self.sl.setValue(self.sl.value() + step) #----------------------------------------------------------------------------------------------- # ADDITIONAL FUNCTIONS FOR KEY AND MOUSE PRESS ON CANVASES #----------------------------------------------------------------------------------------------- def onKeyPressOnCanvas1(self, event): motherCells = [QtCore.Qt.Key_1, QtCore.Qt.Key_4, QtCore.Qt.Key_B] daughterCells = [QtCore.Qt.Key_A, QtCore.Qt.Key_P] # find the position of the cursor relative to the image in pixel imgshape = self.stacks[self.currentChannel][self.sl.value()].shape canshape = self.canvas1.size() cf = imgshape[0] / canshape.width() refpos = self.canvas1.mapFromGlobal(QtGui.QCursor.pos()) refpos = np.array([int(refpos.x() * cf), int(refpos.y() * cf)]) refpos = np.append(refpos, self.sl.value()) ### find the closest cell to the cursor idx = closer_cell(refpos.astype(np.uint16), self.currentCells) ### assign the name to the cell if any([event.key() == cn for cn in motherCells]): # if labeling bckg, add the 1 or 2 to the name if self.currentCells.ix[idx, 'cname'] == 'b_': self.currentCells.ix[idx, 'cname'] += QtGui.QKeySequence( event.key()).toString().lower() else: # if not, rename the cell from scratch if event.key() == QtCore.Qt.Key_B: self.currentCells.ix[idx, 'cname'] = QtGui.QKeySequence( event.key()).toString().lower() + '_' else: self.currentCells.ix[idx, 'cname'] = QtGui.QKeySequence( event.key()).toString().lower() + '.' # add the anterior/posterior to the cell name elif any([event.key() == cp for cp in daughterCells]): # don't do it if labeling background (bckg doesn't have a/p!) if self.currentCells.ix[idx, 'cname'] == 'b_': return else: self.currentCells.ix[idx, 'cname'] += QtGui.QKeySequence( event.key()).toString().lower() # remove the last entry of the name with backspace elif event.key() == QtCore.Qt.Key_Backspace: self.currentCells.ix[idx, 'cname'] = self.currentCells.ix[idx, 'cname'][:-1] if (event.key() != QtCore.Qt.Key_Left) and ( event.key() != QtCore.Qt.Key_Right) and ( event.key() != QtCore.Qt.Key_Up) and (event.key() != QtCore.Qt.Key_Down): self.updateCanvas1() self.setFocus() #----------------------------------------------------------------------------------------------- # UTILS #----------------------------------------------------------------------------------------------- def setBCslidersMinMax(self): self.sld1.setMaximum( np.max([np.max(self.stacks[ch]) for ch in self.channels])) self.sld1.setMinimum(0) self.sld2.setMaximum( np.max([np.max(self.stacks[ch]) for ch in self.channels])) self.sld2.setMinimum(0) def initializeCanvas1(self): # print('initializing canvas1') self.fig1.clf() self.fig1.subplots_adjust(left=0., right=1., top=1., bottom=0.) self.ax1 = self.fig1.add_subplot(111) self.canvas1.draw() # plot the first blank image with the right size self.ax1.cla() self.imgplot1 = self.ax1.imshow(np.zeros( (self.cropsize, self.cropsize)), cmap='gray') # remove the white borders self.ax1.autoscale(False) self.ax1.axis('Off') self.fig1.subplots_adjust(left=0., right=1., top=1., bottom=0.) # plot cell pos and name self.text1 = [] self.points1 = [] # redraw the canvas self.canvas1.draw() self.setFocus() def updateCanvas1(self): # print('updating canvas1') # plot the image self.imgplot1.set_data( self.stacks[self.currentChannel][self.sl.value()]) # change brightness and contrast self.imgplot1.set_clim(self.sld1.value(), self.sld2.value()) # clear cell text and points # print(self.text1,self.points1) for text in self.text1: text.remove() self.text1 = [] for points in self.points1: self.ax1.lines.remove(points) self.points1 = [] # draw cell text and point for idx, cell in self.currentCells.iterrows(): if cell.Z == self.sl.value(): self.text1.append( self.ax1.text(cell.X, cell.Y + 18, cell.cname, color='orange', fontsize=10, alpha=.7, rotation=0)) self.points1.append( self.ax1.plot(cell.X, cell.Y, 'o', color='orange', alpha=.7, mew=0, ms=6)[0]) # redraw the canvas self.canvas1.draw() self.setFocus() def initializeCanvas2(self): # print('initializing canvas2') # plot the image self.ax2.cla() self.imgplot2 = self.ax2.imshow(np.zeros((512, 512)), cmap='gray') self.imgplot2.set_clim(np.min(self.LEDmovie), np.max(self.LEDmovie)) # remove the white borders and plot outline and spline self.ax2.autoscale(False) self.ax2.axis('Off') self.fig2.subplots_adjust(left=0., right=1., top=1., bottom=0.) # print gonad position gonadPos = [np.nan, np.nan] self.points2, = self.ax2.plot(gonadPos[0], gonadPos[1], 'o', color='blue', ms=10, mew=0, alpha=.5, lw=0) # print time self.text2 = self.ax2.text(5, 25, '--.--', color='white') # redraw the canvas self.canvas2.draw() self.setFocus() def updateCanvas2(self): # print('updating canvas2') # plot the image self.imgplot2.set_data(self.LEDmovie[self.tp.value() + self.hatchingtidx]) # print gonad position gonadPos = extract_pos(self.gpDF.ix[ self.gpDF.tidx == self.tp.value()].squeeze()) / self.compression if len(gonadPos.shape) > 0: self.points2.set_xdata(gonadPos[0]) self.points2.set_ydata(gonadPos[1]) plt.draw() # print time # print time ### find ecdysis timepoint ecd = np.loadtxt(open(os.path.join(self.path, 'skin.txt'), 'rb')) # load ecdysis data index = np.where(ecd[:, 0] == float(self.worm[1:])) mintp = np.min([i for i in ecd[index, 1:6][0][0] if i >= 0]) lethtidx = ecd[index, 1:6][0][0] lethtidx = lethtidx[lethtidx >= 0] tpL2 = self.timesDF.ix[self.timesDF.tidxRel == (lethtidx[1] - mintp), 'timesRel'].values[0] # print(self.timesDF.ix[ self.timesDF.tidxRel == self.tp.value(), 'timesRel' ]) self.text2.set_text( '%.2f' % (self.timesDF.ix[self.timesDF.tidxRel == self.tp.value(), 'timesRel'].values[0] - tpL2)) # redraw the canvas self.canvas2.draw() self.setFocus() def checkConsistencyCellNames(self): ### check consistency of cell names tp = self.prevtp # if no cells are labeled (currentCells df is empty), remove all labeled cells in the cellPosDF and return if len(self.currentCells) == 0: newCellPosDF = update_cell_pos_DF(self.currentCells, self.cellPosDF, tp) correctCellNames = True if len(self.currentCells) > 0: correctCellNames = check_cell_names(self.currentCells, self.cellNames) # if cells are not properly labeled, give a Warning if not correctCellNames: QtGui.QMessageBox.about( self, 'Warning!', 'There is a mistake in the cell labels!') # else, update final cellPosDF and return OK else: newCellPosDF = update_cell_pos_DF(self.currentCells, self.cellPosDF, tp) self.cellPosDF = newCellPosDF return correctCellNames def changeSpaceTime(self, whatToChange, increment): if whatToChange == 'time': # if they are OK (and not going to negative times), change timepoint self.tp.setValue(self.tp.value() + increment) if whatToChange == 'space': self.sl.setValue(self.sl.value() + increment)
class XYView(View): AUTOFIT_MARGIN = 0.03 # 3% # See http://matplotlib.org/api/markers_api.html: CURVE_MARKERS = [ "o" ,# circle "*", # star "+", # plus "x", # x "s", # square "p", # pentagon "h", # hexagon1 "8", # octagon "D", # diamond "^", # triangle_up "<", # triangle_left ">", # triangle_right "1", # tri_down "2", # tri_up "3", # tri_left "4", # tri_right "v", # triangle_down "H", # hexagon2 "d", # thin diamond "", # NO MARKER ] _DEFAULT_LEGEND_STATE = False # for test purposes mainly - initial status of the legend def __init__(self, controller): View.__init__(self, controller) self._eventHandler = EventHandler() self._curveViews = {} # key: curve (model) ID, value: CurveView self._salomeViewID = None self._mplFigure = None self._mplAxes = None self._mplCanvas = None self._plotWidget = None self._sgPyQt = self._controller._sgPyQt self._toolbar = None self._mplNavigationActions = {} self._toobarMPL = None self._grid = None self._currCrv = None # current curve selected in the view self._legend = None self._legendLoc = "right" # "right" or "bottom" self._fitArea = False self._zoomPan = False self._dragOnDrop = False self._move = False self._patch = None self._xdata = None self._ydata = None self._defaultLineStyle = None self._last_point = None self._lastMarkerID = -1 self._blockLogSignal = False self._axisXSciNotation = False self._axisYSciNotation = False self._prevTitle = None def __repaintOK(self): """ To be called inside XYView each time a low-level expansive matplotlib methods is to be invoked. @return False if painting is currently locked, in which case it will also register the current XYView as needing a refresh when unlocked """ ret = self._controller._plotManager.isRepaintLocked() if ret: self._controller._plotManager.registerRepaint(self._model) return (not ret) def appendCurve(self, curveID): newC = CurveView(self._controller, self) newC.setModel(self._model._curves[curveID]) newC.setMPLAxes(self._mplAxes) newC.draw() newC.setMarker(self.getMarker(go_next=True)) self._curveViews[curveID] = newC def removeCurve(self, curveID): v = self._curveViews.pop(curveID) v.erase() if self._currCrv is not None and self._currCrv.getID() == curveID: self._currCrv = None def cleanBeforeClose(self): """ Clean some items to avoid accumulating stuff in memory """ self._mplFigure.clear() plt.close(self._mplFigure) self._plotWidget.clearAll() # For memory debugging only: import gc gc.collect() def repaint(self): if self.__repaintOK(): Logger.Debug("XYView::draw") self._mplCanvas.draw() def onXLabelChange(self): if self.__repaintOK(): self._mplAxes.set_xlabel(self._model._xlabel) self.repaint() def onYLabelChange(self): if self.__repaintOK(): self._mplAxes.set_ylabel(self._model._ylabel) self.repaint() def onTitleChange(self): if self.__repaintOK(): self._mplAxes.set_title(self._model._title) self.updateViewTitle() self.repaint() def onCurveTitleChange(self): # Updating the legend should suffice self.showHideLegend() def onClearAll(self): """ Just does an update with a reset of the marker cycle. """ if self.__repaintOK(): self._lastMarkerID = -1 self.update() def onPick(self, event): """ MPL callback when picking """ if event.mouseevent.button == 1: selected_id = -1 a = event.artist for crv_id, cv in self._curveViews.items(): if cv._mplLines[0] is a: selected_id = crv_id # Use the plotmanager so that other plot sets get their current reset: self._controller._plotManager.setCurrentCurve(selected_id) def createAndAddLocalAction(self, icon_file, short_name): return self._toolbar.addAction(self._sgPyQt.loadIcon("CURVEPLOT", icon_file), short_name) def createPlotWidget(self): self._mplFigure = Figure((8.0,5.0), dpi=100) self._mplCanvas = FigureCanvasQTAgg(self._mplFigure) self._mplCanvas.installEventFilter(self._eventHandler) self._mplCanvas.mpl_connect('pick_event', self.onPick) self._mplAxes = self._mplFigure.add_subplot(1, 1, 1) self._plotWidget = PlotWidget() self._toobarMPL = NavigationToolbar2QT(self._mplCanvas, None) for act in self._toobarMPL.actions(): actionName = str(act.text()).strip() self._mplNavigationActions[actionName] = act self._plotWidget.setCentralWidget(self._mplCanvas) self._toolbar = self._plotWidget.toolBar self.populateToolbar() self._popupMenu = QtGui.QMenu() self._popupMenu.addAction(self._actionLegend) # Connect evenement for the graphic scene self._mplCanvas.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self._mplCanvas.customContextMenuRequested.connect(self.onContextMenu) self._mplCanvas.mpl_connect('scroll_event', self.onScroll) self._mplCanvas.mpl_connect('button_press_event', self.onMousePress) def populateToolbar(self): # Action to dump view in a file a = self.createAndAddLocalAction("dump_view.png", trQ("DUMP_VIEW_TXT")) a.triggered.connect(self.dumpView) self._toolbar.addSeparator() # Actions to manipulate the scene a = self.createAndAddLocalAction("fit_all.png", trQ("FIT_ALL_TXT")) a.triggered.connect(self.autoFit) # Zoom and pan are mutually exclusive but can be both de-activated: self._zoomAction = self.createAndAddLocalAction("fit_area.png", trQ("FIT_AREA_TXT")) self._zoomAction.triggered.connect(self.zoomArea) self._zoomAction.setCheckable(True) self._panAction = self.createAndAddLocalAction("zoom_pan.png", trQ("ZOOM_PAN_TXT")) self._panAction.triggered.connect(self.pan) self._panAction.setCheckable(True) self._toolbar.addSeparator() # Actions to change the representation of curves self._curveActionGroup = QtGui.QActionGroup(self._plotWidget) self._pointsAction = self.createAndAddLocalAction("draw_points.png", trQ("DRAW_POINTS_TXT")) self._pointsAction.setCheckable(True) self._linesAction = self.createAndAddLocalAction("draw_lines.png", trQ("DRAW_LINES_TXT")) self._linesAction.setCheckable(True) self._curveActionGroup.addAction(self._pointsAction) self._curveActionGroup.addAction(self._linesAction) self._linesAction.setChecked(True) self._curveActionGroup.triggered.connect(self.changeModeCurve) self._curveActionGroup.setExclusive(True) self._toolbar.addSeparator() # Actions to draw horizontal curves as linear or logarithmic self._horActionGroup = QtGui.QActionGroup(self._plotWidget) self._horLinearAction = self.createAndAddLocalAction("hor_linear.png", trQ("HOR_LINEAR_TXT")) self._horLinearAction.setCheckable(True) self._horLogarithmicAction = self.createAndAddLocalAction("hor_logarithmic.png", trQ("HOR_LOGARITHMIC_TXT")) self._horLogarithmicAction.setCheckable(True) self._horActionGroup.addAction(self._horLinearAction) self._horActionGroup.addAction(self._horLogarithmicAction) self._horLinearAction.setChecked(True) self._horActionGroup.triggered.connect(self.onViewHorizontalMode) self._toolbar.addSeparator() # Actions to draw vertical curves as linear or logarithmic self._verActionGroup = QtGui.QActionGroup(self._plotWidget) self._verLinearAction = self.createAndAddLocalAction("ver_linear.png", trQ("VER_LINEAR_TXT")) self._verLinearAction.setCheckable(True) self._verLogarithmicAction = self.createAndAddLocalAction("ver_logarithmic.png", trQ("VER_LOGARITHMIC_TXT")) self._verLogarithmicAction.setCheckable(True) self._verActionGroup.addAction(self._verLinearAction) self._verActionGroup.addAction(self._verLogarithmicAction) self._verLinearAction.setChecked(True) self._verActionGroup.triggered.connect(self.onViewVerticalMode) self._verActionGroup.setExclusive(True) self._toolbar.addSeparator() # Action to show or hide the legend self._actionLegend = self.createAndAddLocalAction("legend.png", trQ("SHOW_LEGEND_TXT")) self._actionLegend.setCheckable(True) self._actionLegend.triggered.connect(self.showHideLegend) if self._DEFAULT_LEGEND_STATE: self._actionLegend.setChecked(True) self._toolbar.addSeparator() # Action to set the preferences a = self.createAndAddLocalAction("settings.png", trQ("SETTINGS_TXT")) a.triggered.connect(self.onSettings) pass def dumpView(self): # Choice of the view backup file filters = [] for form in ["IMAGES_FILES", "PDF_FILES", "POSTSCRIPT_FILES", "ENCAPSULATED_POSTSCRIPT_FILES"]: filters.append(trQ(form)) fileName = self._sgPyQt.getFileName(self._sgPyQt.getDesktop(), "", filters, trQ("DUMP_VIEW_FILE"), False ) if not fileName.isEmpty(): name = str(fileName) self._mplAxes.figure.savefig(name) pass def autoFit(self, check=True, repaint=True): if self.__repaintOK(): self._mplAxes.relim() xm, xM = self._mplAxes.xaxis.get_data_interval() ym, yM = self._mplAxes.yaxis.get_data_interval() i = yM-ym self._mplAxes.axis([xm, xM, ym-i*self.AUTOFIT_MARGIN, yM+i*self.AUTOFIT_MARGIN]) if repaint: self.repaint() def zoomArea(self): if self._panAction.isChecked() and self._zoomAction.isChecked(): self._panAction.setChecked(False) # Trigger underlying matplotlib action: self._mplNavigationActions["Zoom"].trigger() def pan(self): if self._panAction.isChecked() and self._zoomAction.isChecked(): self._zoomAction.setChecked(False) # Trigger underlying matplotlib action: self._mplNavigationActions["Pan"].trigger() def getMarker(self, go_next=False): if go_next: self._lastMarkerID = (self._lastMarkerID+1) % len(self.CURVE_MARKERS) return self.CURVE_MARKERS[self._lastMarkerID] def changeModeCurve(self, repaint=True): if not self.__repaintOK(): return action = self._curveActionGroup.checkedAction() if action is self._pointsAction : for crv_view in self._curveViews.values(): crv_view.setLineStyle("None") elif action is self._linesAction : for crv_view in self._curveViews.values(): crv_view.setLineStyle("-") else : raise NotImplementedError if repaint: self.repaint() def setXLog(self, log, repaint=True): if not self.__repaintOK(): return self._blockLogSignal = True if log: self._mplAxes.set_xscale('log') self._horLogarithmicAction.setChecked(True) else: self._mplAxes.set_xscale('linear') self._horLinearAction.setChecked(True) if repaint: self.autoFit() self.repaint() self._blockLogSignal = False def setYLog(self, log, repaint=True): if not self.__repaintOK(): return self._blockLogSignal = True if log: self._mplAxes.set_yscale('log') self._verLogarithmicAction.setChecked(True) else: self._mplAxes.set_yscale('linear') self._verLinearAction.setChecked(True) if repaint: self.autoFit() self.repaint() self._blockLogSignal = False def setXSciNotation(self, sciNotation, repaint=True): self._axisXSciNotation = sciNotation self.changeFormatAxis() if repaint: self.repaint() def setYSciNotation(self, sciNotation, repaint=True): self._axisYSciNotation = sciNotation self.changeFormatAxis() if repaint: self.repaint() def onViewHorizontalMode(self, checked=True, repaint=True): if self._blockLogSignal: return action = self._horActionGroup.checkedAction() if action is self._horLinearAction: self.setXLog(False, repaint) elif action is self._horLogarithmicAction: self.setXLog(True, repaint) else: raise NotImplementedError def onViewVerticalMode(self, checked=True, repaint=True): if self._blockLogSignal: return action = self._verActionGroup.checkedAction() if action is self._verLinearAction: self.setYLog(False, repaint) elif action is self._verLogarithmicAction: self.setYLog(True, repaint) else: raise NotImplementedError if repaint: self.repaint() def __adjustFigureMargins(self, withLegend): """ Adjust figure margins to make room for the legend """ if withLegend: leg = self._legend bbox = leg.get_window_extent() # In axes coordinates: bbox2 = bbox.transformed(leg.figure.transFigure.inverted()) if self._legendLoc == "right": self._mplFigure.subplots_adjust(right=1.0-(bbox2.width+0.02)) elif self._legendLoc == "bottom": self._mplFigure.subplots_adjust(bottom=bbox2.height+0.1) else: # Reset to default (rc) values self._mplFigure.subplots_adjust(bottom=0.1, right=0.9) def setLegendVisible(self, visible, repaint=True): if visible and not self._actionLegend.isChecked(): self._actionLegend.setChecked(True) self.showHideLegend(repaint=repaint) if not visible and self._actionLegend.isChecked(): self._actionLegend.setChecked(False) self.showHideLegend(repaint=repaint) def showHideLegend(self, actionChecked=None, repaint=True): if not self.__repaintOK(): # Show/hide legend is extremely costly return show = self._actionLegend.isChecked() nCurves = len(self._curveViews) if nCurves > 10: fontSize = 'x-small' else: fontSize = None if nCurves == 0: # Remove legend leg = self._mplAxes.legend() if leg is not None: leg.remove() if show and nCurves > 0: # Recreate legend from scratch if self._legend is not None: self._legend = None self._mplAxes._legend = None if self._legendLoc == "bottom": self._legend = self._mplAxes.legend(loc="upper left", bbox_to_anchor=(0.0, -0.05, 1.0, -0.05), borderaxespad=0.0, mode="expand", fancybox=True, shadow=True, ncol=3, prop={'size':fontSize, 'style': 'italic'}) elif self._legendLoc == "right": self._legend = self._mplAxes.legend(loc="upper left", bbox_to_anchor=(1.02,1.0), borderaxespad=0.0, ncol=1, fancybox=True, shadow=True, prop={'size':fontSize, 'style': 'italic'}) else: raise Exception("Invalid legend placement! Must be 'bottom' or 'right'") # Canvas must be drawn so we can adjust the figure placement: self._mplCanvas.draw() self.__adjustFigureMargins(withLegend=True) else: if self._legend is None: # Nothing to do return else: self._legend.set_visible(False) self._legend = None self._mplAxes._legend = None self._mplCanvas.draw() self.__adjustFigureMargins(withLegend=False) curr_crv = self._model._currentCurve if curr_crv is None: curr_title = None else: curr_title = curr_crv.getTitle() if self._legend is not None: for label in self._legend.get_texts() : text = label.get_text() if (text == curr_title): label.set_backgroundcolor('0.85') else : label.set_backgroundcolor('white') if repaint: self.repaint() def onSettings(self, trigger=False, dlg_test=None): dlg = dlg_test or PlotSettings() dlg.titleEdit.setText(self._mplAxes.get_title()) dlg.axisXTitleEdit.setText(self._mplAxes.get_xlabel()) dlg.axisYTitleEdit.setText(self._mplAxes.get_ylabel()) dlg.gridCheckBox.setChecked(self._mplAxes.xaxis._gridOnMajor) # could not find a relevant API to check this dlg.axisXSciCheckBox.setChecked(self._axisXSciNotation) dlg.axisYSciCheckBox.setChecked(self._axisYSciNotation) xmin, xmax = self._mplAxes.get_xlim() ymin, ymax = self._mplAxes.get_ylim() xminText = "%g" %xmin xmaxText = "%g" %xmax yminText = "%g" %ymin ymaxText = "%g" %ymax dlg.axisXMinEdit.setText(xminText) dlg.axisXMaxEdit.setText(xmaxText) dlg.axisYMinEdit.setText(yminText) dlg.axisYMaxEdit.setText(ymaxText) # List of markers dlg.markerCurve.clear() for marker in self.CURVE_MARKERS : dlg.markerCurve.addItem(marker) curr_crv = self._model.getCurrentCurve() if not curr_crv is None: dlg.colorCurve.setEnabled(True) dlg.markerCurve.setEnabled(True) name = curr_crv.getTitle() dlg.nameCurve.setText(name) view = self._curveViews[curr_crv.getID()] marker = view.getMarker() color = view.getColor() index = dlg.markerCurve.findText(marker) dlg.markerCurve.setCurrentIndex(index) rgb = colors.colorConverter.to_rgb(color) dlg.setRGB(rgb[0],rgb[1],rgb[2]) else : dlg.colorCurve.setEnabled(False) dlg.markerCurve.setEnabled(False) dlg.nameCurve.setText("") view = None if self._legend is None: dlg.showLegendCheckBox.setChecked(False) dlg.legendPositionComboBox.setEnabled(False) else : if self._legend.get_visible(): dlg.showLegendCheckBox.setChecked(True) dlg.legendPositionComboBox.setEnabled(True) if self._legendLoc == "bottom": dlg.legendPositionComboBox.setCurrentIndex(0) elif self._legendLoc == "right" : dlg.legendPositionComboBox.setCurrentIndex(1) else : dlg.showLegendCheckBox.setChecked(False) dlg.legendPositionComboBox.setEnabled(False) if dlg.exec_(): # Title self._model.setTitle(dlg.titleEdit.text()) # Axis self._model.setXLabel(dlg.axisXTitleEdit.text()) self._model.setYLabel(dlg.axisYTitleEdit.text()) # Grid if dlg.gridCheckBox.isChecked() : self._mplAxes.grid(True) else : self._mplAxes.grid(False) # Legend if dlg.showLegendCheckBox.isChecked(): self._actionLegend.setChecked(True) if dlg.legendPositionComboBox.currentIndex() == 0 : self._legendLoc = "bottom" elif dlg.legendPositionComboBox.currentIndex() == 1 : self._legendLoc = "right" else : self._actionLegend.setChecked(False) xminText = dlg.axisXMinEdit.text() xmaxText = dlg.axisXMaxEdit.text() yminText = dlg.axisYMinEdit.text() ymaxText = dlg.axisYMaxEdit.text() self._mplAxes.axis([float(xminText), float(xmaxText), float(yminText), float(ymaxText)] ) self._axisXSciNotation = dlg.axisXSciCheckBox.isChecked() self._axisYSciNotation = dlg.axisYSciCheckBox.isChecked() self.changeFormatAxis() # Color and marker of the curve if view: view.setColor(dlg.getRGB()) view.setMarker(self.CURVE_MARKERS[dlg.markerCurve.currentIndex()]) self.showHideLegend(repaint=True) self._mplCanvas.draw() pass def updateViewTitle(self): s = "" if self._model._title != "": s = " - %s" % self._model._title title = "CurvePlot (%d)%s" % (self._model.getID(), s) self._sgPyQt.setViewTitle(self._salomeViewID, title) def onCurrentPlotSetChange(self): """ Avoid a unnecessary call to update() when just switching current plot set! """ pass def onCurrentCurveChange(self): curr_crv2 = self._model.getCurrentCurve() if curr_crv2 != self._currCrv: if self._currCrv is not None: view = self._curveViews[self._currCrv.getID()] view.toggleHighlight(False) if not curr_crv2 is None: view = self._curveViews[curr_crv2.getID()] view.toggleHighlight(True) self._currCrv = curr_crv2 self.showHideLegend(repaint=False) # redo legend self.repaint() def changeFormatAxis(self) : if not self.__repaintOK(): return # don't try to switch to sci notation if we are not using the # matplotlib.ticker.ScalarFormatter (i.e. if in Log for ex.) if self._horLinearAction.isChecked(): if self._axisXSciNotation : self._mplAxes.ticklabel_format(style='sci',scilimits=(0,0), axis='x') else : self._mplAxes.ticklabel_format(style='plain',axis='x') if self._verLinearAction.isChecked(): if self._axisYSciNotation : self._mplAxes.ticklabel_format(style='sci',scilimits=(0,0), axis='y') else : self._mplAxes.ticklabel_format(style='plain',axis='y') def update(self): if self._salomeViewID is None: self.createPlotWidget() self._salomeViewID = self._sgPyQt.createView("CurvePlot", self._plotWidget) Logger.Debug("Creating SALOME view ID=%d" % self._salomeViewID) self._sgPyQt.setViewVisible(self._salomeViewID, True) self.updateViewTitle() # Check list of curve views: set_mod = set(self._model._curves.keys()) set_view = set(self._curveViews.keys()) # Deleted/Added curves: dels = set_view - set_mod added = set_mod - set_view for d in dels: self.removeCurve(d) if not len(self._curveViews): # Reset color cycle self._mplAxes.set_color_cycle(None) for a in added: self.appendCurve(a) # Axes labels and title self._mplAxes.set_xlabel(self._model._xlabel) self._mplAxes.set_ylabel(self._model._ylabel) self._mplAxes.set_title(self._model._title) self.onViewHorizontalMode(repaint=False) self.onViewVerticalMode(repaint=False) self.changeModeCurve(repaint=False) self.showHideLegend(repaint=False) # The canvas is repainted anyway (needed to get legend bounding box) self.changeFormatAxis() # Redo auto-fit self.autoFit(repaint=False) self.repaint() def onDataChange(self): # the rest is done in the CurveView: self.autoFit(repaint=True) def onMousePress(self, event): if event.button == 3 : if self._panAction.isChecked(): self._panAction.setChecked(False) if self._zoomAction.isChecked(): self._zoomAction.setChecked(False) def onContextMenu(self, position): pos = self._mplCanvas.mapToGlobal(QtCore.QPoint(position.x(),position.y())) self._popupMenu.exec_(pos) def onScroll(self, event): # Event location (x and y) xdata = event.xdata ydata = event.ydata cur_xlim = self._mplAxes.get_xlim() cur_ylim = self._mplAxes.get_ylim() base_scale = 2. if event.button == 'down': # deal with zoom in scale_factor = 1 / base_scale elif event.button == 'up': # deal with zoom out scale_factor = base_scale else: # deal with something that should never happen scale_factor = 1 new_width = (cur_xlim[1] - cur_xlim[0]) * scale_factor new_height = (cur_ylim[1] - cur_ylim[0]) * scale_factor relx = (cur_xlim[1] - xdata)/(cur_xlim[1] - cur_xlim[0]) rely = (cur_ylim[1] - ydata)/(cur_ylim[1] - cur_ylim[0]) self._mplAxes.set_xlim([xdata - new_width * (1-relx), xdata + new_width * (relx)]) self._mplAxes.set_ylim([ydata - new_height * (1-rely), ydata + new_height * (rely)]) self.repaint() pass def onPressEvent(self, event): if event.button == 3 : #self._mplCanvas.emit(QtCore.SIGNAL("button_release_event()")) canvasSize = event.canvas.geometry() point = event.canvas.mapToGlobal(QtCore.QPoint(event.x,canvasSize.height()-event.y)) self._popupMenu.exec_(point) else : print "Press event on the other button" #if event.button == 3 : # canvasSize = event.canvas.geometry() # point = event.canvas.mapToGlobal(QtCore.QPoint(event.x,canvasSize.height()-event.y)) # self._popupMenu.move(point) # self._popupMenu.show() def onMotionEvent(self, event): print "OnMotionEvent ",event.button #if event.button == 3 : # event.button = None # return True def onReleaseEvent(self, event): print "OnReleaseEvent ",event.button
class XYView(View): AUTOFIT_MARGIN = 0.03 # 3% # See http://matplotlib.org/api/markers_api.html: CURVE_MARKERS = [ "o", # circle "*", # star "+", # plus "x", # x "s", # square "p", # pentagon "h", # hexagon1 "8", # octagon "D", # diamond "^", # triangle_up "<", # triangle_left ">", # triangle_right "1", # tri_down "2", # tri_up "3", # tri_left "4", # tri_right "v", # triangle_down "H", # hexagon2 "d", # thin diamond "", # NO MARKER ] _DEFAULT_LEGEND_STATE = False # for test purposes mainly - initial status of the legend def __init__(self, controller): View.__init__(self, controller) self._eventHandler = EventHandler() self._curveViews = {} # key: curve (model) ID, value: CurveView self._salomeViewID = None self._mplFigure = None self._mplAxes = None self._mplCanvas = None self._plotWidget = None self._sgPyQt = self._controller._sgPyQt self._toolbar = None self._mplNavigationActions = {} self._toobarMPL = None self._grid = None self._currCrv = None # current curve selected in the view self._legend = None self._legendLoc = "right" # "right" or "bottom" self._fitArea = False self._zoomPan = False self._dragOnDrop = False self._move = False self._patch = None self._xdata = None self._ydata = None self._defaultLineStyle = None self._last_point = None self._lastMarkerID = -1 self._blockLogSignal = False self._axisXSciNotation = False self._axisYSciNotation = False self._prevTitle = None def __repaintOK(self): """ To be called inside XYView each time a low-level expansive matplotlib methods is to be invoked. @return False if painting is currently locked, in which case it will also register the current XYView as needing a refresh when unlocked """ ret = self._controller._plotManager.isRepaintLocked() if ret: self._controller._plotManager.registerRepaint(self._model) return (not ret) def appendCurve(self, curveID): newC = CurveView(self._controller, self) newC.setModel(self._model._curves[curveID]) newC.setMPLAxes(self._mplAxes) newC.draw() newC.setMarker(self.getMarker(go_next=True)) self._curveViews[curveID] = newC def removeCurve(self, curveID): v = self._curveViews.pop(curveID) v.erase() if self._currCrv is not None and self._currCrv.getID() == curveID: self._currCrv = None def cleanBeforeClose(self): """ Clean some items to avoid accumulating stuff in memory """ self._mplFigure.clear() plt.close(self._mplFigure) self._plotWidget.clearAll() # For memory debugging only: import gc gc.collect() def repaint(self): if self.__repaintOK(): Logger.Debug("XYView::draw") self._mplCanvas.draw() def onXLabelChange(self): if self.__repaintOK(): self._mplAxes.set_xlabel(self._model._xlabel) self.repaint() def onYLabelChange(self): if self.__repaintOK(): self._mplAxes.set_ylabel(self._model._ylabel) self.repaint() def onTitleChange(self): if self.__repaintOK(): self._mplAxes.set_title(self._model._title) self.updateViewTitle() self.repaint() def onCurveTitleChange(self): # Updating the legend should suffice self.showHideLegend() def onClearAll(self): """ Just does an update with a reset of the marker cycle. """ if self.__repaintOK(): self._lastMarkerID = -1 self.update() def onPick(self, event): """ MPL callback when picking """ if event.mouseevent.button == 1: selected_id = -1 a = event.artist for crv_id, cv in self._curveViews.items(): if cv._mplLines[0] is a: selected_id = crv_id # Use the plotmanager so that other plot sets get their current reset: self._controller._plotManager.setCurrentCurve(selected_id) def createAndAddLocalAction(self, icon_file, short_name): return self._toolbar.addAction( self._sgPyQt.loadIcon("CURVEPLOT", icon_file), short_name) def createPlotWidget(self): self._mplFigure = Figure((8.0, 5.0), dpi=100) self._mplCanvas = FigureCanvasQTAgg(self._mplFigure) self._mplCanvas.installEventFilter(self._eventHandler) self._mplCanvas.mpl_connect('pick_event', self.onPick) self._mplAxes = self._mplFigure.add_subplot(1, 1, 1) self._plotWidget = PlotWidget() self._toobarMPL = NavigationToolbar2QT(self._mplCanvas, None) for act in self._toobarMPL.actions(): actionName = str(act.text()).strip() self._mplNavigationActions[actionName] = act self._plotWidget.setCentralWidget(self._mplCanvas) self._toolbar = self._plotWidget.toolBar self.populateToolbar() self._popupMenu = QtGui.QMenu() self._popupMenu.addAction(self._actionLegend) # Connect evenement for the graphic scene self._mplCanvas.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self._mplCanvas.customContextMenuRequested.connect(self.onContextMenu) self._mplCanvas.mpl_connect('scroll_event', self.onScroll) self._mplCanvas.mpl_connect('button_press_event', self.onMousePress) def populateToolbar(self): # Action to dump view in a file a = self.createAndAddLocalAction("dump_view.png", trQ("DUMP_VIEW_TXT")) a.triggered.connect(self.dumpView) self._toolbar.addSeparator() # Actions to manipulate the scene a = self.createAndAddLocalAction("fit_all.png", trQ("FIT_ALL_TXT")) a.triggered.connect(self.autoFit) # Zoom and pan are mutually exclusive but can be both de-activated: self._zoomAction = self.createAndAddLocalAction( "fit_area.png", trQ("FIT_AREA_TXT")) self._zoomAction.triggered.connect(self.zoomArea) self._zoomAction.setCheckable(True) self._panAction = self.createAndAddLocalAction("zoom_pan.png", trQ("ZOOM_PAN_TXT")) self._panAction.triggered.connect(self.pan) self._panAction.setCheckable(True) self._toolbar.addSeparator() # Actions to change the representation of curves self._curveActionGroup = QtGui.QActionGroup(self._plotWidget) self._pointsAction = self.createAndAddLocalAction( "draw_points.png", trQ("DRAW_POINTS_TXT")) self._pointsAction.setCheckable(True) self._linesAction = self.createAndAddLocalAction( "draw_lines.png", trQ("DRAW_LINES_TXT")) self._linesAction.setCheckable(True) self._curveActionGroup.addAction(self._pointsAction) self._curveActionGroup.addAction(self._linesAction) self._linesAction.setChecked(True) self._curveActionGroup.triggered.connect(self.changeModeCurve) self._curveActionGroup.setExclusive(True) self._toolbar.addSeparator() # Actions to draw horizontal curves as linear or logarithmic self._horActionGroup = QtGui.QActionGroup(self._plotWidget) self._horLinearAction = self.createAndAddLocalAction( "hor_linear.png", trQ("HOR_LINEAR_TXT")) self._horLinearAction.setCheckable(True) self._horLogarithmicAction = self.createAndAddLocalAction( "hor_logarithmic.png", trQ("HOR_LOGARITHMIC_TXT")) self._horLogarithmicAction.setCheckable(True) self._horActionGroup.addAction(self._horLinearAction) self._horActionGroup.addAction(self._horLogarithmicAction) self._horLinearAction.setChecked(True) self._horActionGroup.triggered.connect(self.onViewHorizontalMode) self._toolbar.addSeparator() # Actions to draw vertical curves as linear or logarithmic self._verActionGroup = QtGui.QActionGroup(self._plotWidget) self._verLinearAction = self.createAndAddLocalAction( "ver_linear.png", trQ("VER_LINEAR_TXT")) self._verLinearAction.setCheckable(True) self._verLogarithmicAction = self.createAndAddLocalAction( "ver_logarithmic.png", trQ("VER_LOGARITHMIC_TXT")) self._verLogarithmicAction.setCheckable(True) self._verActionGroup.addAction(self._verLinearAction) self._verActionGroup.addAction(self._verLogarithmicAction) self._verLinearAction.setChecked(True) self._verActionGroup.triggered.connect(self.onViewVerticalMode) self._verActionGroup.setExclusive(True) self._toolbar.addSeparator() # Action to show or hide the legend self._actionLegend = self.createAndAddLocalAction( "legend.png", trQ("SHOW_LEGEND_TXT")) self._actionLegend.setCheckable(True) self._actionLegend.triggered.connect(self.showHideLegend) if self._DEFAULT_LEGEND_STATE: self._actionLegend.setChecked(True) self._toolbar.addSeparator() # Action to set the preferences a = self.createAndAddLocalAction("settings.png", trQ("SETTINGS_TXT")) a.triggered.connect(self.onSettings) pass def dumpView(self): # Choice of the view backup file filters = [] for form in [ "IMAGES_FILES", "PDF_FILES", "POSTSCRIPT_FILES", "ENCAPSULATED_POSTSCRIPT_FILES" ]: filters.append(trQ(form)) fileName = self._sgPyQt.getFileName(self._sgPyQt.getDesktop(), "", filters, trQ("DUMP_VIEW_FILE"), False) if not fileName.isEmpty(): name = str(fileName) self._mplAxes.figure.savefig(name) pass def autoFit(self, check=True, repaint=True): if self.__repaintOK(): self._mplAxes.relim() xm, xM = self._mplAxes.xaxis.get_data_interval() ym, yM = self._mplAxes.yaxis.get_data_interval() i = yM - ym self._mplAxes.axis([ xm, xM, ym - i * self.AUTOFIT_MARGIN, yM + i * self.AUTOFIT_MARGIN ]) if repaint: self.repaint() def zoomArea(self): if self._panAction.isChecked() and self._zoomAction.isChecked(): self._panAction.setChecked(False) # Trigger underlying matplotlib action: self._mplNavigationActions["Zoom"].trigger() def pan(self): if self._panAction.isChecked() and self._zoomAction.isChecked(): self._zoomAction.setChecked(False) # Trigger underlying matplotlib action: self._mplNavigationActions["Pan"].trigger() def getMarker(self, go_next=False): if go_next: self._lastMarkerID = (self._lastMarkerID + 1) % len( self.CURVE_MARKERS) return self.CURVE_MARKERS[self._lastMarkerID] def changeModeCurve(self, repaint=True): if not self.__repaintOK(): return action = self._curveActionGroup.checkedAction() if action is self._pointsAction: for crv_view in self._curveViews.values(): crv_view.setLineStyle("None") elif action is self._linesAction: for crv_view in self._curveViews.values(): crv_view.setLineStyle("-") else: raise NotImplementedError if repaint: self.repaint() def setXLog(self, log, repaint=True): if not self.__repaintOK(): return self._blockLogSignal = True if log: self._mplAxes.set_xscale('log') self._horLogarithmicAction.setChecked(True) else: self._mplAxes.set_xscale('linear') self._horLinearAction.setChecked(True) if repaint: self.autoFit() self.repaint() self._blockLogSignal = False def setYLog(self, log, repaint=True): if not self.__repaintOK(): return self._blockLogSignal = True if log: self._mplAxes.set_yscale('log') self._verLogarithmicAction.setChecked(True) else: self._mplAxes.set_yscale('linear') self._verLinearAction.setChecked(True) if repaint: self.autoFit() self.repaint() self._blockLogSignal = False def setXSciNotation(self, sciNotation, repaint=True): self._axisXSciNotation = sciNotation self.changeFormatAxis() if repaint: self.repaint() def setYSciNotation(self, sciNotation, repaint=True): self._axisYSciNotation = sciNotation self.changeFormatAxis() if repaint: self.repaint() def onViewHorizontalMode(self, checked=True, repaint=True): if self._blockLogSignal: return action = self._horActionGroup.checkedAction() if action is self._horLinearAction: self.setXLog(False, repaint) elif action is self._horLogarithmicAction: self.setXLog(True, repaint) else: raise NotImplementedError def onViewVerticalMode(self, checked=True, repaint=True): if self._blockLogSignal: return action = self._verActionGroup.checkedAction() if action is self._verLinearAction: self.setYLog(False, repaint) elif action is self._verLogarithmicAction: self.setYLog(True, repaint) else: raise NotImplementedError if repaint: self.repaint() def __adjustFigureMargins(self, withLegend): """ Adjust figure margins to make room for the legend """ if withLegend: leg = self._legend bbox = leg.get_window_extent() # In axes coordinates: bbox2 = bbox.transformed(leg.figure.transFigure.inverted()) if self._legendLoc == "right": self._mplFigure.subplots_adjust(right=1.0 - (bbox2.width + 0.02)) elif self._legendLoc == "bottom": self._mplFigure.subplots_adjust(bottom=bbox2.height + 0.1) else: # Reset to default (rc) values self._mplFigure.subplots_adjust(bottom=0.1, right=0.9) def setLegendVisible(self, visible, repaint=True): if visible and not self._actionLegend.isChecked(): self._actionLegend.setChecked(True) self.showHideLegend(repaint=repaint) if not visible and self._actionLegend.isChecked(): self._actionLegend.setChecked(False) self.showHideLegend(repaint=repaint) def showHideLegend(self, actionChecked=None, repaint=True): if not self.__repaintOK(): # Show/hide legend is extremely costly return show = self._actionLegend.isChecked() nCurves = len(self._curveViews) if nCurves > 10: fontSize = 'x-small' else: fontSize = None if nCurves == 0: # Remove legend leg = self._mplAxes.legend() if leg is not None: leg.remove() if show and nCurves > 0: # Recreate legend from scratch if self._legend is not None: self._legend = None self._mplAxes._legend = None if self._legendLoc == "bottom": self._legend = self._mplAxes.legend(loc="upper left", bbox_to_anchor=(0.0, -0.05, 1.0, -0.05), borderaxespad=0.0, mode="expand", fancybox=True, shadow=True, ncol=3, prop={ 'size': fontSize, 'style': 'italic' }) elif self._legendLoc == "right": self._legend = self._mplAxes.legend(loc="upper left", bbox_to_anchor=(1.02, 1.0), borderaxespad=0.0, ncol=1, fancybox=True, shadow=True, prop={ 'size': fontSize, 'style': 'italic' }) else: raise Exception( "Invalid legend placement! Must be 'bottom' or 'right'") # Canvas must be drawn so we can adjust the figure placement: self._mplCanvas.draw() self.__adjustFigureMargins(withLegend=True) else: if self._legend is None: # Nothing to do return else: self._legend.set_visible(False) self._legend = None self._mplAxes._legend = None self._mplCanvas.draw() self.__adjustFigureMargins(withLegend=False) curr_crv = self._model._currentCurve if curr_crv is None: curr_title = None else: curr_title = curr_crv.getTitle() if self._legend is not None: for label in self._legend.get_texts(): text = label.get_text() if (text == curr_title): label.set_backgroundcolor('0.85') else: label.set_backgroundcolor('white') if repaint: self.repaint() def onSettings(self, trigger=False, dlg_test=None): dlg = dlg_test or PlotSettings() dlg.titleEdit.setText(self._mplAxes.get_title()) dlg.axisXTitleEdit.setText(self._mplAxes.get_xlabel()) dlg.axisYTitleEdit.setText(self._mplAxes.get_ylabel()) dlg.gridCheckBox.setChecked( self._mplAxes.xaxis._gridOnMajor ) # could not find a relevant API to check this dlg.axisXSciCheckBox.setChecked(self._axisXSciNotation) dlg.axisYSciCheckBox.setChecked(self._axisYSciNotation) xmin, xmax = self._mplAxes.get_xlim() ymin, ymax = self._mplAxes.get_ylim() xminText = "%g" % xmin xmaxText = "%g" % xmax yminText = "%g" % ymin ymaxText = "%g" % ymax dlg.axisXMinEdit.setText(xminText) dlg.axisXMaxEdit.setText(xmaxText) dlg.axisYMinEdit.setText(yminText) dlg.axisYMaxEdit.setText(ymaxText) # List of markers dlg.markerCurve.clear() for marker in self.CURVE_MARKERS: dlg.markerCurve.addItem(marker) curr_crv = self._model.getCurrentCurve() if not curr_crv is None: dlg.colorCurve.setEnabled(True) dlg.markerCurve.setEnabled(True) name = curr_crv.getTitle() dlg.nameCurve.setText(name) view = self._curveViews[curr_crv.getID()] marker = view.getMarker() color = view.getColor() index = dlg.markerCurve.findText(marker) dlg.markerCurve.setCurrentIndex(index) rgb = colors.colorConverter.to_rgb(color) dlg.setRGB(rgb[0], rgb[1], rgb[2]) else: dlg.colorCurve.setEnabled(False) dlg.markerCurve.setEnabled(False) dlg.nameCurve.setText("") view = None if self._legend is None: dlg.showLegendCheckBox.setChecked(False) dlg.legendPositionComboBox.setEnabled(False) else: if self._legend.get_visible(): dlg.showLegendCheckBox.setChecked(True) dlg.legendPositionComboBox.setEnabled(True) if self._legendLoc == "bottom": dlg.legendPositionComboBox.setCurrentIndex(0) elif self._legendLoc == "right": dlg.legendPositionComboBox.setCurrentIndex(1) else: dlg.showLegendCheckBox.setChecked(False) dlg.legendPositionComboBox.setEnabled(False) if dlg.exec_(): # Title self._model.setTitle(dlg.titleEdit.text()) # Axis self._model.setXLabel(dlg.axisXTitleEdit.text()) self._model.setYLabel(dlg.axisYTitleEdit.text()) # Grid if dlg.gridCheckBox.isChecked(): self._mplAxes.grid(True) else: self._mplAxes.grid(False) # Legend if dlg.showLegendCheckBox.isChecked(): self._actionLegend.setChecked(True) if dlg.legendPositionComboBox.currentIndex() == 0: self._legendLoc = "bottom" elif dlg.legendPositionComboBox.currentIndex() == 1: self._legendLoc = "right" else: self._actionLegend.setChecked(False) xminText = dlg.axisXMinEdit.text() xmaxText = dlg.axisXMaxEdit.text() yminText = dlg.axisYMinEdit.text() ymaxText = dlg.axisYMaxEdit.text() self._mplAxes.axis([ float(xminText), float(xmaxText), float(yminText), float(ymaxText) ]) self._axisXSciNotation = dlg.axisXSciCheckBox.isChecked() self._axisYSciNotation = dlg.axisYSciCheckBox.isChecked() self.changeFormatAxis() # Color and marker of the curve if view: view.setColor(dlg.getRGB()) view.setMarker( self.CURVE_MARKERS[dlg.markerCurve.currentIndex()]) self.showHideLegend(repaint=True) self._mplCanvas.draw() pass def updateViewTitle(self): s = "" if self._model._title != "": s = " - %s" % self._model._title title = "CurvePlot (%d)%s" % (self._model.getID(), s) self._sgPyQt.setViewTitle(self._salomeViewID, title) def onCurrentPlotSetChange(self): """ Avoid a unnecessary call to update() when just switching current plot set! """ pass def onCurrentCurveChange(self): curr_crv2 = self._model.getCurrentCurve() if curr_crv2 != self._currCrv: if self._currCrv is not None: view = self._curveViews[self._currCrv.getID()] view.toggleHighlight(False) if not curr_crv2 is None: view = self._curveViews[curr_crv2.getID()] view.toggleHighlight(True) self._currCrv = curr_crv2 self.showHideLegend(repaint=False) # redo legend self.repaint() def changeFormatAxis(self): if not self.__repaintOK(): return # don't try to switch to sci notation if we are not using the # matplotlib.ticker.ScalarFormatter (i.e. if in Log for ex.) if self._horLinearAction.isChecked(): if self._axisXSciNotation: self._mplAxes.ticklabel_format(style='sci', scilimits=(0, 0), axis='x') else: self._mplAxes.ticklabel_format(style='plain', axis='x') if self._verLinearAction.isChecked(): if self._axisYSciNotation: self._mplAxes.ticklabel_format(style='sci', scilimits=(0, 0), axis='y') else: self._mplAxes.ticklabel_format(style='plain', axis='y') def update(self): if self._salomeViewID is None: self.createPlotWidget() self._salomeViewID = self._sgPyQt.createView( "CurvePlot", self._plotWidget) Logger.Debug("Creating SALOME view ID=%d" % self._salomeViewID) self._sgPyQt.setViewVisible(self._salomeViewID, True) self.updateViewTitle() # Check list of curve views: set_mod = set(self._model._curves.keys()) set_view = set(self._curveViews.keys()) # Deleted/Added curves: dels = set_view - set_mod added = set_mod - set_view for d in dels: self.removeCurve(d) if not len(self._curveViews): # Reset color cycle self._mplAxes.set_color_cycle(None) for a in added: self.appendCurve(a) # Axes labels and title self._mplAxes.set_xlabel(self._model._xlabel) self._mplAxes.set_ylabel(self._model._ylabel) self._mplAxes.set_title(self._model._title) self.onViewHorizontalMode(repaint=False) self.onViewVerticalMode(repaint=False) self.changeModeCurve(repaint=False) self.showHideLegend( repaint=False ) # The canvas is repainted anyway (needed to get legend bounding box) self.changeFormatAxis() # Redo auto-fit self.autoFit(repaint=False) self.repaint() def onDataChange(self): # the rest is done in the CurveView: self.autoFit(repaint=True) def onMousePress(self, event): if event.button == 3: if self._panAction.isChecked(): self._panAction.setChecked(False) if self._zoomAction.isChecked(): self._zoomAction.setChecked(False) def onContextMenu(self, position): pos = self._mplCanvas.mapToGlobal( QtCore.QPoint(position.x(), position.y())) self._popupMenu.exec_(pos) def onScroll(self, event): # Event location (x and y) xdata = event.xdata ydata = event.ydata cur_xlim = self._mplAxes.get_xlim() cur_ylim = self._mplAxes.get_ylim() base_scale = 2. if event.button == 'down': # deal with zoom in scale_factor = 1 / base_scale elif event.button == 'up': # deal with zoom out scale_factor = base_scale else: # deal with something that should never happen scale_factor = 1 new_width = (cur_xlim[1] - cur_xlim[0]) * scale_factor new_height = (cur_ylim[1] - cur_ylim[0]) * scale_factor relx = (cur_xlim[1] - xdata) / (cur_xlim[1] - cur_xlim[0]) rely = (cur_ylim[1] - ydata) / (cur_ylim[1] - cur_ylim[0]) self._mplAxes.set_xlim( [xdata - new_width * (1 - relx), xdata + new_width * (relx)]) self._mplAxes.set_ylim( [ydata - new_height * (1 - rely), ydata + new_height * (rely)]) self.repaint() pass def onPressEvent(self, event): if event.button == 3: #self._mplCanvas.emit(QtCore.SIGNAL("button_release_event()")) canvasSize = event.canvas.geometry() point = event.canvas.mapToGlobal( QtCore.QPoint(event.x, canvasSize.height() - event.y)) self._popupMenu.exec_(point) else: print "Press event on the other button" #if event.button == 3 : # canvasSize = event.canvas.geometry() # point = event.canvas.mapToGlobal(QtCore.QPoint(event.x,canvasSize.height()-event.y)) # self._popupMenu.move(point) # self._popupMenu.show() def onMotionEvent(self, event): print "OnMotionEvent ", event.button #if event.button == 3 : # event.button = None # return True def onReleaseEvent(self, event): print "OnReleaseEvent ", event.button