class PlottrMain(QMainWindow): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle(getAppTitle()) self.setWindowIcon(QIcon('./plottr_icon.png')) self.activateWindow() # layout of basic widgets self.logger = Logger() self.frame = QFrame() layout = QVBoxLayout(self.frame) layout.addWidget(self.logger) # self.setLayout(layout) self.setCentralWidget(self.frame) self.frame.setFocus() # basic setup of the data handling self.dataHandlers = {} # setting up the Listening thread self.listeningThread = QThread() self.listener = DataReceiver() self.listener.moveToThread(self.listeningThread) # communication with the ZMQ thread self.listeningThread.started.connect(self.listener.loop) self.listener.sendInfo.connect(self.logger.addMessage) self.listener.sendData.connect(self.processData) # go! self.listeningThread.start() @pyqtSlot(dict) def processData(self, data): dataId = data['id'] if dataId not in self.dataHandlers: self.dataHandlers[dataId] = DataWindow(dataId=dataId) self.dataHandlers[dataId].show() self.logger.addMessage(f'Started new data window for {dataId}') self.dataHandlers[dataId].windowClosed.connect( self.dataWindowClosed) w = self.dataHandlers[dataId] w.addData(data) def closeEvent(self, event): self.listener.running = False self.listeningThread.quit() hs = [h for d, h in self.dataHandlers.items()] for h in hs: h.close() @pyqtSlot(str) def dataWindowClosed(self, dataId): self.dataHandlers[dataId].close() del self.dataHandlers[dataId]
class App(QWidget): """ GUI """ def __init__(self): super().__init__() self.title = 'Lung areas scoring' self.left = 50 self.top = 50 self.top_row_height = 600 self.bottom_row_height = 250 self.left_column_width = 250 self.right_column_width = 800 self.width = self.left_column_width + self.right_column_width self.height = self.top_row_height + self.bottom_row_height self.video_label_width = 600 # height computed automatically to preserve aspect ratio self.tmp_dir = os.getcwd() + "/tmp" self.lungs_template_page_name = "image_webView.html" self.lungs_customized_page_name = "image_customized.html" self.lungs_template_page = os.getcwd( ) + "/resources/" + self.lungs_template_page_name self.lungs_customized_page = os.getcwd( ) + "/resources/" + self.lungs_customized_page_name # Mac self.ffmpeg_bin = os.getcwd() + "/bin/ffmpeg" # Windows #self.ffmpeg_bin = os.getcwd()+"/bin/ffmpeg.exe" self.init_ui() self.threadpool = QThreadPool() self._dialogs = [] self.html = "" self.task = "" self.create_directory(self.tmp_dir) # used when user wants to reset everything. Needed to choose the image file page to load self.startNewSession = True self.clickedAreaName = "" def init_ui(self): """ Initialize the window and add content """ self.setFixedSize(self.width, self.height) self.setWindowTitle(self.title) self.setGeometry(self.left, self.top, self.width, self.height) # Set background color for Mac compatibility p = self.palette() p.setColor(self.backgroundRole(), Qt.white) self.setPalette(p) # Style white = "#fff" yellow = "#ff0" orange = "#fa0" red = "#f00" grey = "#7c7c7c" #button_style = "background-color: #D7D7D7; border-style: solid; border-width: 2px; border-color: black; border-radius: 5px; font: bold; font-size: 17px" # Mac compatibility button_style = "color: black; background-color: #D7D7D7; border-style: solid; border-width: 2px; border-color: black; border-radius: 5px; font: bold; font-size: 17px" #text_style = "font-size: 17px; color: black" # Mac compatibility text_style = "font-size: 17px; color: black; background-color: rgb(255, 255, 255);" text_bold_style = "font-size: 17px; color: black; font-weight: bold" text_totals_style = "font-size: 17px; color: black; font-weight: bold; margin-left: 10px" header_style = "font-size: 17px; color: black; font-weight: bold" clickable_label_style = "font-size: 17px; color: black; background-color:#ffffff; padding-left: 20px" button_height = 40 self.int_to_color_map = { 0: "white", 1: "yellow", 2: "orange", 3: "red", 4: "grey" } layout = QGridLayout() panel_top_left = QVBoxLayout() self.reset_btn = QPushButton(text="RESTART") self.reset_btn.setFixedHeight(button_height) self.reset_btn.setStyleSheet(button_style) self.reset_btn.clicked.connect(self.reset_session) panel_top_left.addWidget(self.reset_btn) registry = QVBoxLayout() name_label = QLabel("Name") name_label.setStyleSheet(text_style) registry.addWidget(name_label) self.name = QLineEdit() self.name.setStyleSheet(text_style) registry.addWidget(self.name) surname_label = QLabel("Last name") surname_label.setStyleSheet(text_style) registry.addWidget(surname_label) self.surname = QLineEdit() self.surname.setStyleSheet(text_style) registry.addWidget(self.surname) dob_label = QLabel("Date of birth") dob_label.setStyleSheet(text_style) registry.addWidget(dob_label) dob_grid = QGridLayout() dob_btn = QPushButton(icon=QIcon("resources/calendar_icon.png")) dob_btn.clicked.connect(self.set_date_of_birth) dob_btn.setFixedWidth(button_height) dob_btn.setFixedHeight(button_height) dob_btn.setIconSize(QSize(button_height, button_height)) dob_btn.setStyleSheet("border:none") dob_grid.addWidget(dob_btn, 0, 0) self.date_of_birth = ClickableQLabel(self) self.date_of_birth.clicked.connect(self.set_date_of_birth) self.date_of_birth.setFixedHeight(self.surname.sizeHint().height()) self.date_of_birth.setStyleSheet(clickable_label_style) dob_grid.addWidget(self.date_of_birth, 0, 1) dob_frame = QFrame() dob_frame.setLayout(dob_grid) registry.addWidget(dob_frame) doa_label = QLabel("Date of acquisition") doa_label.setStyleSheet(text_style) registry.addWidget(doa_label) doa_grid = QGridLayout() doa_btn = QPushButton(icon=QIcon("resources/calendar_icon.png")) doa_btn.clicked.connect(self.set_date_of_acquisition) doa_btn.setFixedWidth(button_height) doa_btn.setFixedHeight(button_height) doa_btn.setIconSize(QSize(button_height, button_height)) doa_btn.setStyleSheet("border:none") doa_grid.addWidget(doa_btn, 0, 0) self.date_of_acquisition = ClickableQLabel(self) self.date_of_acquisition.clicked.connect(self.set_date_of_acquisition) self.date_of_acquisition.setFixedHeight( self.surname.sizeHint().height()) self.date_of_acquisition.setStyleSheet(clickable_label_style) doa_grid.addWidget(self.date_of_acquisition, 0, 1) doa_frame = QFrame() doa_frame.setLayout(doa_grid) registry.addWidget(doa_frame) registry_frame = QFrame() registry_frame.setFrameShape(QFrame.StyledPanel) registry_frame.setLineWidth(0.6) registry_frame.setLayout(registry) panel_top_left.addWidget(registry_frame) legend = QVBoxLayout() header_legend = QLabel("Legend") header_legend.setStyleSheet(header_style) legend.addWidget(header_legend) legend_grid = QGridLayout() legend_grid.addWidget(header_legend) whitelabel = self.get_label(white) legend_grid.addWidget(whitelabel, 0, 0) score0_label = QLabel("Score 0") score0_label.setStyleSheet(text_style) legend_grid.addWidget(score0_label, 0, 1) yellowlabel = self.get_label(yellow) legend_grid.addWidget(yellowlabel, 1, 0) score1_label = QLabel("Score 1") score1_label.setStyleSheet(text_style) legend_grid.addWidget(score1_label, 1, 1) orangelabel = self.get_label(orange) legend_grid.addWidget(orangelabel, 2, 0) score2_label = QLabel("Score 2") score2_label.setStyleSheet(text_style) legend_grid.addWidget(score2_label, 2, 1) redlabel = self.get_label(red) legend_grid.addWidget(redlabel, 3, 0) score3_label = QLabel("Score 3") score3_label.setStyleSheet(text_style) legend_grid.addWidget(score3_label, 3, 1) greylabel = self.get_label(grey) legend_grid.addWidget(greylabel, 4, 0) score_nm_label = QLabel("Not measured") score_nm_label.setStyleSheet(text_style) legend_grid.addWidget(score_nm_label, 4, 1) legend.addLayout(legend_grid) legend_frame = QFrame() legend_frame.setFrameShape(QFrame.StyledPanel) legend_frame.setLineWidth(0.6) legend_frame.setLayout(legend) panel_top_left.addWidget(legend_frame) panel_bottom_left = QVBoxLayout() totals = QVBoxLayout() header_totals = QLabel("Totals") header_totals.setStyleSheet(header_style) self.pathological_areas = QLabel("Pathological areas: ") self.pathological_areas.setStyleSheet(text_style) totals.addWidget(header_totals) totals.addWidget(self.pathological_areas) totals_grid = QGridLayout() totals_grid.addWidget(self.get_label(white), 0, 0) self.number_whites = QLabel("") self.number_whites.setStyleSheet(text_totals_style) totals_grid.addWidget(self.number_whites, 0, 1) totals_grid.addWidget(self.get_label(yellow), 1, 0) self.number_yellow = QLabel("") self.number_yellow.setStyleSheet(text_totals_style) totals_grid.addWidget(self.number_yellow, 1, 1) totals_grid.addWidget(self.get_label(orange), 2, 0) self.number_orange = QLabel("") self.number_orange.setStyleSheet(text_totals_style) totals_grid.addWidget(self.number_orange, 2, 1) totals_grid.addWidget(self.get_label(red), 3, 0) self.number_red = QLabel("") self.number_red.setStyleSheet(text_totals_style) totals_grid.addWidget(self.number_red, 3, 1) totals.addLayout(totals_grid) self.number_grey = QLabel("") totals_frame = QFrame() totals_frame.setFrameShape(QFrame.StyledPanel) totals_frame.setLineWidth(0.6) totals_frame.setLayout(totals) panel_bottom_left.addWidget(totals_frame) self.generate_report_btn = QPushButton(text="GENERATE REPORT") self.generate_report_btn.setFixedHeight(button_height) self.generate_report_btn.setStyleSheet(button_style) self.generate_report_btn.clicked.connect(self.generate_report) panel_bottom_left.addWidget(self.generate_report_btn) panel_top_left_frame = QFrame() panel_top_left_frame.setLayout(panel_top_left) panel_bottom_left_frame = QFrame() panel_bottom_left_frame.setLayout(panel_bottom_left) layout.addWidget(panel_top_left_frame, 0, 0) layout.addWidget(panel_bottom_left_frame, 1, 0) self.webview = QWebEngineView(None) self.webpage = ClickableWebPage() self.webview.setPage(self.webpage) self.webpage.load( QUrl(Path(self.lungs_template_page).absolute().as_uri())) self.webpage.signals.id.connect(self.choose_video_file) layout.addWidget(self.webview, 0, 1) self.webview.show() self.video_crop_window = VideoCropWindow(self) self.video_crop_window.layout = QVBoxLayout() # show video frame for cropping panel_video = QVBoxLayout() self.panel_video_frame = QFrame() self.panel_video_frame.setFrameStyle(QFrame.NoFrame) self.panel_video_frame.setLayout(panel_video) self.video_crop_window.layout.addWidget(self.panel_video_frame) self.video_crop_window.setCentralWidget(self.panel_video_frame) self.video_crop_window.setLayout(self.video_crop_window.layout) self.video_label = VideoView() self.video_label.setScaledContents(True) self.video_label.setMinimumSize(1, 1) panel_video.addWidget(self.video_label) self.crop_btn = QPushButton(text="CROP VIDEO") self.crop_btn.setFixedHeight(button_height) self.crop_btn.setStyleSheet(button_style) self.crop_btn.clicked.connect(self.crop_video) panel_video.addWidget(self.crop_btn) self.gray_label = QLabel(self.video_label) self.gray_label.setStyleSheet( "border: 0px; background-color: rgba(124, 124, 124, 160);") self.gray_label.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) self.gray_label.setScaledContents(True) self.gray_label.hide() self.m_label_gif = QLabel() self.m_movie_gif = QMovie("resources/loadingGif.gif") self.m_label_gif.setMovie(self.m_movie_gif) #self.m_label_gif.setScaledContents(True) self.m_label_gif.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.m_label_gif.setAlignment(Qt.AlignCenter) self.m_movie_gif.setScaledSize(QSize(75, 75)) gifLayout = QVBoxLayout() self.video_label.setLayout(gifLayout) gifLayout.addWidget(self.m_label_gif, alignment=Qt.AlignCenter) self.m_movie_gif.stop() self.m_label_gif.hide() note_layout = QVBoxLayout() n_header = QLabel("Notes of the clinician") n_header.setStyleSheet(header_style) self.clinician_notes = QTextEdit() self.clinician_notes.setPlainText("") self.clinician_notes.setStyleSheet(text_style) note_layout.addWidget(n_header) note_layout.addWidget(self.clinician_notes) note_frame = QFrame() note_frame.setLayout(note_layout) layout.addWidget(note_frame, 1, 1) layout.setRowStretch layout.setRowMinimumHeight(0, self.top_row_height) layout.setRowMinimumHeight(1, self.bottom_row_height) layout.setColumnMinimumWidth(0, self.left_column_width) layout.setColumnMinimumWidth(1, self.right_column_width) layout.setRowStretch(0, 5) layout.setRowStretch(1, 3) self.setLayout(layout) self.show() def select_date(self, label): widget = Calendar() widget.setWindowModality(Qt.ApplicationModal) widget.exec_() result = widget.selectedDate label.setText(result) def set_date_of_birth(self): self.select_date(self.date_of_birth) def set_date_of_acquisition(self): self.select_date(self.date_of_acquisition) @pyqtSlot() def choose_file(self): """ Opens the file chooser and starts processing in a separate thread """ data_path = QFileDialog.getExistingDirectory(self, "Select folder") if len(data_path) == 0: return worker = Worker(data_path, TFClassifier) worker.signals.result.connect(self.process_result) self.threadpool.start(worker) def choose_video_file(self, areaID): """ Opens the file chooser and starts processing in a separate thread """ dialogResult = QFileDialog.getOpenFileName( self, "Open Video", "", "Video Files (*.MOV *.mov *.AVI *.avi)") if len(dialogResult[0]) == 0: return self.clickedAreaName = areaID # to be used later in HTML substitution self.video_file_path = dialogResult[0] self.frame_path = "" self.extract_video_frame(self.video_file_path) @pyqtSlot() def reset_session(self): """ In case user wants to start from scracth, template image webview page has to be loaded""" self.startNewSession = True # set the template page in the webView self.webpage.load( QUrl(Path(self.lungs_template_page).absolute().as_uri())) # delete the customized page if present if (os.path.exists(self.lungs_customized_page)): os.remove(self.lungs_customized_page) def process_result(self, task): """ Retrieves the output of a task """ task_dict = json.loads(task) self.task = task #html = customize_report("resources/image.html", task) html = "" if (self.startNewSession): shutil.copy(self.lungs_template_page, self.lungs_customized_page) self.startNewSession = False with open(self.lungs_customized_page) as file: html = "".join(file.readlines()) if len(html) == 0: raise RuntimeError("The HTML template must not be empty") if (self.clickedAreaName == ""): raise RuntimeError("Clicked area name must not be empty") newColorInt = task_dict[ self.clickedAreaName[1:]] # strip initial _ character newColorClass = self.int_to_color_map[ newColorInt] # must be same as in html CSS color class name # find id attribute in HTML path tag to change color specifically for one area matches = re.findall( r'<path id=\"' + self.clickedAreaName + '\" class=\"(.+?)\"', html) oldPath = "" newPath = "" paths = re.findall(r'<path.+?</path>', html) for path in paths: if (self.clickedAreaName in path): oldPath = path matches = re.findall(r'class=\"(.+?)\"', path) if (len(matches) == 1): newPath = path.replace(matches[0], newColorClass) if (oldPath != "" and newPath != ""): html = html.replace(oldPath, newPath) # replace in the template also the page name in the links # TODO: do it only if it is start of new session html = html.replace(self.lungs_template_page_name, self.lungs_customized_page_name) self.html = html export_html(html, self.lungs_customized_page) self.webpage.load( QUrl(Path(self.lungs_customized_page).absolute().as_uri())) self.pathological_areas.setText( "Pathological areas: <b>{}/14</b>".format( task_dict['pathological_areas'])) self.number_whites.setText(str(task_dict['n_score_0'])) self.number_yellow.setText(str(task_dict['n_score_1'])) self.number_orange.setText(str(task_dict['n_score_2'])) self.number_red.setText(str(task_dict['n_score_3'])) self.number_grey.setText(str(task_dict['n_not_measured'])) self.video_crop_window.hide() self.gray_label.hide() self.m_movie_gif.stop() self.m_label_gif.hide() else: raise RuntimeError("Error while customizing html template") def show_alert(self, task_dict, fields): """ Shows a generic alert. task_dict is a dictionary containing at least the "name" field and the one contained in fields """ new_window = QDialog() new_window.setWindowTitle("{}".format(task_dict['name'])) layout = QVBoxLayout() for field in fields: label = QLabel("{}: {}".format(field.capitalize(), task_dict[field])) layout.addWidget(label) new_window.setLayout(layout) self._dialogs.append(new_window) new_window.show() # Remove unused dialogs to_remove = [] for d in self._dialogs: # Check if some dialogs have been closed if not d.isVisible(): to_remove.append(d) for tr in to_remove: self._dialogs.remove(tr) def get_label(self, color): """ Returns a label of the color specified (in HTML) """ tmp = QLabel() tmp.setStyleSheet( "border: 1px solid #444;background-color:{};".format(color)) tmp.setFixedWidth(50) tmp.setFixedHeight(20) return tmp def generate_report(self): output_dir = QFileDialog.getExistingDirectory(self, "Select folder") if len(output_dir) == 0: return html = customize_report("resources/report.html", self.task) totals = [ k.text() for k in [ self.number_whites, self.number_yellow, self.number_orange, self.number_red, self.number_grey ] ] name = self.name.text() surname = self.surname.text() dob = self.date_of_birth.text() doa = self.date_of_acquisition.text() html = generate_output_html(html, name, surname, dob, doa, self.pathological_areas.text(), totals, self.clinician_notes.toPlainText()) export_html(html, os.path.join(output_dir, "Report.html")) export_pdf( html, os.path.join(output_dir, "{}{}_{}_{}.pdf".format(surname, name, dob, doa))) def extract_video_frame(self, file_name): """ Open video with ffmpeg and get file_number frame. Not efficient for multiple calls""" self.delete_folder_contents(self.tmp_dir) commandStringList = [ self.ffmpeg_bin, '-i', file_name, '-vframes', '1', '-f', 'image2', self.tmp_dir + '/frame%03d.jpeg' ] videoworker = VideoWorker(commandStringList, "success", "fail") videoworker.signals.result.connect(self.process_video_frame_result) self.threadpool.start(videoworker) def process_video_frame_result(self, task): """ Async call to process frame extraction result """ task_dict = json.loads(task) if (task_dict["value"] == "success"): frame_basenames = sorted( list( filter( re.compile(r'frame').search, os.listdir(self.tmp_dir)))) self.frame_path = os.path.join(self.tmp_dir, frame_basenames[0]) else: self.frame_path = "" QMessageBox.about(self, "Extraction Result", "Could not extract frame from video") if (self.frame_path != ""): pixmap = QPixmap(self.frame_path) # calculate a view that preserves aspect ratio of video frame video_aspect_ratio = pixmap.height() / pixmap.width() video_label_height = int( (video_aspect_ratio * self.video_label_width)) self.video_label.setFixedWidth(pixmap.width()) self.video_label.setFixedHeight(pixmap.height()) self.crop_btn.setFixedWidth(pixmap.width()) self.gray_label.setGeometry(0, 0, pixmap.width(), pixmap.height()) self.video_label.setPixmap(pixmap) # center crop square only at beginning of each session if (self.startNewSession): self.video_label.rubberBand.setGeometry( (pixmap.width() / 2) - (self.video_label.minSquareSide / 2), (pixmap.height() / 2) - (self.video_label.minSquareSide / 2), self.video_label.minSquareSide, self.video_label.minSquareSide) self.panel_video_frame.setFocus() self.video_crop_window.show() def crop_video(self): """Crop from ffmpeg using scaled pixels of rubber band""" self.gray_label.show() self.m_movie_gif.start() self.m_label_gif.show() labelX = self.video_label.rubberBand.geometry().x() labelY = self.video_label.rubberBand.geometry().y() labelRight = self.video_label.rubberBand.geometry().x( ) + self.video_label.rubberBand.geometry().width() labelBottom = self.video_label.rubberBand.geometry().y( ) + self.video_label.rubberBand.geometry().height() labelWidth = self.video_label.width labelHeight = self.video_label.height # need to compensate in case video view has aspect ratio different than the original video, for UI reasons # a scale factor has to be applied from label space to video space, asuming # that the aspect ratio of the label was the same of the video labelToVideoScaleWidth = self.video_label.pixmap().width( ) / self.video_label.width() labelToVideoScaleHeight = self.video_label.pixmap().height( ) / self.video_label.height() videoWidth = self.video_label.pixmap().width() videoHeight = self.video_label.pixmap().height() videoFrameSpaceX = int((labelToVideoScaleWidth * labelX)) videoFrameSpaceY = int((labelToVideoScaleHeight * labelY)) videoFrameSpaceRight = int((labelToVideoScaleWidth * labelRight)) videoFrameSpaceBottom = int((labelToVideoScaleHeight * labelBottom)) pre, ext = os.path.splitext(self.video_file_path) new_video_name = pre + "_cropped" + ext #note: -crf 15 is the quality of exported video. See ffmpeg doc commandStringList = [ self.ffmpeg_bin, '-y', '-i', self.video_file_path, '-filter:v', 'crop=' + str(videoFrameSpaceRight - videoFrameSpaceX) + ':' + str(videoFrameSpaceBottom - videoFrameSpaceY) + ':' + str(videoFrameSpaceX) + ':' + str(videoFrameSpaceY) + '', '-crf', '15', new_video_name ] videoworker = VideoWorker(commandStringList, "success", "Error exporting cropped video") videoworker.signals.result.connect(self.process_video_crop_result) self.threadpool.start(videoworker) def process_video_crop_result(self, task): """ Retrieves the output of a task """ task_dict = json.loads(task) if (task_dict["value"] != "success"): QMessageBox.about(self, "Crop Result", task_dict["value"]) # clean tmp folder self.delete_folder_contents(self.tmp_dir) dir = os.path.dirname(self.video_file_path) worker = Worker(dir, TFClassifier) worker.signals.result.connect(self.process_result) self.threadpool.start(worker) #TODO put in utils def create_directory(self, directory): if not os.path.exists(directory): os.makedirs(directory) def delete_folder_contents(self, directory): for filename in os.listdir(directory): file_path = os.path.join(directory, filename) try: if os.path.isfile(file_path) or os.path.islink(file_path): os.unlink(file_path) elif os.path.isdir(file_path): shutil.rmtree(file_path) except Exception as e: print('Failed to delete %s. Reason: %s' % (file_path, e))
class DataWindow(QMainWindow): dataAdded = pyqtSignal(dict) dataActivated = pyqtSignal(dict) windowClosed = pyqtSignal(str) def __init__(self, dataId, parent=None): super().__init__(parent) self.dataId = dataId self.setWindowTitle(getAppTitle() + f" ({dataId})") self.data = {} self.addingQueue = {} self.currentlyProcessingPlotData = False self.pendingPlotData = False # plot settings setMplDefaults() # data chosing widgets self.structureWidget = DataStructure() self.plotChoice = PlotChoice() chooserLayout = QVBoxLayout() chooserLayout.addWidget(self.structureWidget) chooserLayout.addWidget(self.plotChoice) # plot control widgets self.plot = MPLPlot() plotLayout = QVBoxLayout() plotLayout.addWidget(self.plot) plotLayout.addWidget(NavBar(self.plot, self)) # Main layout self.frame = QFrame() mainLayout = QHBoxLayout(self.frame) mainLayout.addLayout(chooserLayout) mainLayout.addLayout(plotLayout) # data processing threads self.dataAdder = DataAdder() self.dataAdderThread = QThread() self.dataAdder.moveToThread(self.dataAdderThread) self.dataAdder.dataUpdated.connect(self.dataFromAdder) self.dataAdder.dataUpdated.connect(self.dataAdderThread.quit) self.dataAdderThread.started.connect(self.dataAdder.run) self.plotData = PlotData() self.plotDataThread = QThread() self.plotData.moveToThread(self.plotDataThread) self.plotData.dataProcessed.connect(self.updatePlot) self.plotData.dataProcessed.connect(self.plotDataThread.quit) self.plotDataThread.started.connect(self.plotData.processData) # signals/slots for data selection etc. self.dataAdded.connect(self.structureWidget.update) self.dataAdded.connect(self.updatePlotChoices) self.dataAdded.connect(self.updatePlotData) self.structureWidget.itemSelectionChanged.connect(self.activateData) self.dataActivated.connect(self.plotChoice.setOptions) self.plotChoice.choiceUpdated.connect(self.updatePlotData) # activate window self.frame.setFocus() self.setCentralWidget(self.frame) self.activateWindow() @pyqtSlot() def activateData(self): item = self.structureWidget.selectedItems()[0] self.activeDataSet = item.text(0) self.dataActivated.emit(self.dataStructure[self.activeDataSet]) def updatePlotChoices(self, dataStructure): n = self.structureWidget.getSelected() if n is not None: self.plotChoice.updateData(dataStructure[n]) @pyqtSlot() def updatePlotData(self): if self.plotDataThread.isRunning(): self.pendingPlotData = True else: self.currentPlotChoiceInfo = self.plotChoice.choiceInfo self.pendingPlotData = False self.plotData.setData(self.data[self.activeDataSet], self.currentPlotChoiceInfo) self.plotDataThread.start() def _plot1D(self, x, data): self.plot.axes.plot(x, data, 'o') self.plot.axes.set_xlabel(self.currentPlotChoiceInfo['xAxis']['name']) self.plot.axes.set_ylabel(self.activeDataSet) def _plot2D(self, x, y, data): xx, yy = pcolorgrid(x, y) if self.currentPlotChoiceInfo['xAxis'][ 'idx'] < self.currentPlotChoiceInfo['yAxis']['idx']: im = self.plot.axes.pcolormesh(xx, yy, data.T) else: im = self.plot.axes.pcolormesh(xx, yy, data) cb = self.plot.fig.colorbar(im) self.plot.axes.set_xlabel(self.currentPlotChoiceInfo['xAxis']['name']) self.plot.axes.set_ylabel(self.currentPlotChoiceInfo['yAxis']['name']) cb.set_label(self.activeDataSet) @pyqtSlot(object, object, object) def updatePlot(self, data, x, y): self.plot.clearFig() if x is None and y is None: self.plot.draw() elif x is not None and y is None: self._plot1D(x, data) elif y.size < 2: self._plot1D(x, data) elif x is not None and y is not None and x.size > 1: self._plot2D(x, y, data) self.plot.axes.set_title("{} [{}]".format(self.dataId, self.activeDataSet), size='x-small') self.plot.fig.tight_layout() self.plot.draw() if self.pendingPlotData: self.updatePlotData() # # Data adding # def addData(self, dataDict): """ Here we receive new data from the listener. We'll use a separate thread for processing and combining (numerics might be costly). If the thread is already running, we'll put the new data into a queue that will be resolved during the next call of addData (i.e, queue will grow until current adding thread is done.) """ doUpdate = dataDict.get('update', False) and self.data != {} dataDict = dataDict.get('datasets', {}) if self.dataAdderThread.isRunning(): if self.addingQueue == {}: self.addingQueue = dataDict else: self.addingQueue = combineDicts(self.addingQueue, dataDict) else: if self.addingQueue != {}: dataDict = combineDicts(self.addingQueue, dataDict) if dataDict != {}: self.dataAdder.setData(self.data, dataDict, doUpdate) self.dataAdderThread.start() self.addingQueue = {} @pyqtSlot(object, dict) def dataFromAdder(self, data, dataStructure): self.data = data self.dataStructure = dataStructure self.dataAdded.emit(self.dataStructure) # clean-up def closeEvent(self, event): self.windowClosed.emit(self.dataId)