def __init__(self, axes: PlotItem, direction: Direction, range_type: Type[_Range], scale_type: Type[_Scale], *args, **kwargs): self._axes: PlotItem = axes self._raw: AxisItem = axes.getAxis(self._axis_name()) self._label: Axis._Label = Axis._Label(self._raw) self._scale: Axis._Scale = scale_type(self._axes, self._raw) self._range: Axis._Range = range_type(self._axes, self._raw) self._scale_line: Axis._ScaleLine = Axis._ScaleLine(self._raw) # self._ticks: Axis._Ticks = Axis._Ticks(self._raw, alt=(np.array([]), np.array([]))) self._ticks: np.ndarray = np.array([]) self._tick_labels: np.ndarray = np.array([]) self._minor_ticks: np.ndarray = np.array([]) self._minor_tick_labels: np.ndarray = np.array([]) self._tick_raw: List[Tuple[float, Any]] = [] super(Axis, self).__init__(direction, *args, **kwargs)
class SweepDataPlot(GraphicsView): GREEN = [0, 204, 153] BLUE = [100, 171, 246] RED = [221, 61, 53] PURPLE = [175, 122, 197] ASH = [52, 73, 94] GRAY = [178, 186, 187] COLORS = [BLUE, RED, GREEN, PURPLE, ASH, GRAY] if sys.platform == 'darwin': LW = 3 else: LW = 1.5 def __init__(self): GraphicsView.__init__(self) # create layout self.layout = pg.GraphicsLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setSpacing(-1.) self.setBackground(None) self.setCentralItem(self.layout) # create axes and apply formatting axisItems = dict() for pos in ['bottom', 'left', 'top', 'right']: axisItems[pos] = AxisItem(orientation=pos, maxTickLength=-7) self.p = PlotItem(axisItems=axisItems) self.setTitle('Sweep data', fontScaling=1.3, color='k') self.layout.addItem(self.p) self.p.vb.setBackgroundColor('w') self.p.setContentsMargins(10, 10, 10, 10) for pos in ['bottom', 'left', 'top', 'right']: ax = self.p.getAxis(pos) ax.setZValue(0) # draw on top of patch ax.setVisible(True) # make all axes visible ax.setPen(width=self.LW * 2 / 3, color=0.5) # grey spines and ticks try: ax.setTextPen('k') # black text except AttributeError: pass ax.setStyle(autoExpandTextSpace=True, tickTextOffset=4) self.p.getAxis('top').setTicks([]) self.p.getAxis('top').setHeight(0) self.p.getAxis('right').setTicks([]) self.x_axis = self.p.getAxis('bottom') self.y_axis = self.p.getAxis('left') self.x_axis.setLabel('Voltage', units='V', color='k', size='12pt') self.y_axis.setLabel('Current', units='A', color='k', size='12pt') self.y_axis.setStyle(tickTextWidth=35) # set auto range and mouse panning / zooming self.p.enableAutoRange(x=True, y=True) self.p.setLimits(xMin=-1e20, xMax=1e20, yMin=-1e20, yMax=1e20) def suggestPadding(axis): length = self.p.vb.width() if axis == 0 else self.p.vb.height() if length > 0: if axis == 0: padding = 0 else: padding = np.clip(1. / (length**0.5), 0.02, 0.1) else: padding = 0.02 return padding self.p.vb.suggestPadding = suggestPadding # set default ranges to start self.p.setXRange(-10, 10) self.p.setYRange(-10, 10) # add legend self.legend = LegendItem(brush=fn.mkBrush(255, 255, 255, 150), labelTextColor='k', offset=(20, -20)) self.legend.setParentItem(self.p.vb) def clear(self): self.p.clear() # clear current plot self.legend.clear() # clear current legend def plot(self, sweep_data): self.clear() xdata = sweep_data.get_column(0) xdata_title = sweep_data.titles[0] ydata = sweep_data.values()[1:] # format plot according to sweep type unit = xdata_title.unit if xdata_title.has_unit() else 'a.u.' self.x_axis.setLabel(xdata_title.name, unit=unit) self.y_axis.setLabel('Current', unit='A') if sweep_data.params['sweep_type'] == 'transfer': self.setTitle('Transfer curve') self.p.setLogMode(x=False, y=True) self.legend.setOffset((20, -20)) # legend in bottom-left corner ydata = [np.abs(y) for y in ydata] elif sweep_data.params['sweep_type'] == 'output': self.setTitle('Output curve') self.p.setLogMode(x=False, y=False) self.legend.setOffset((-20, 20)) # legend in top-right corner ydata = [np.abs(y) for y in ydata] else: self.setTitle('Sweep curve') self.p.setLogMode(x=False, y=False) ydata = [np.abs(y) for y in ydata] # plot data self.lines = [] for y, c in zip(ydata, itertools.cycle(self.COLORS)): p = self.p.plot(xdata, y, pen=fn.mkPen(color=c, width=self.LW)) self.lines.append(p) # add legend for l, t in zip(self.lines, sweep_data.column_names[1:]): self.legend.addItem(l, str(t)) self.p.autoRange() def setTitle(self, text, fontScaling=None, color=None, font=None): # work around pyqtplot which forces the title to be HTML if text is None: self.p.setTitle(None) # clears title and hides title column else: self.p.setTitle( '') # makes title column visible, sets placeholder text self.p.titleLabel.item.setPlainText( text) # replace HTML with plain text if color is not None: color = fn.mkColor(color) self.p.titleLabel.item.setDefaultTextColor(color) if font is not None: self.p.titleLabel.item.setFont(font) if fontScaling is not None: font = self.p.titleLabel.item.font() defaultFontSize = QtWidgets.QLabel('test').font().pointSize() fontSize = round(defaultFontSize * fontScaling, 1) font.setPointSize(fontSize) self.p.titleLabel.item.setFont(font)
class PyQtGraphPlot(ImageView): def __init__(self, *args, plot_data=None, **kwargs): # Setup GUI self.plot_view = PlotItem() super(PyQtGraphPlot, self).__init__(*args, view=self.plot_view, **kwargs) self._setup() self.model = PyQtGraphPlotModel(plot_data) self.refresh_plot() def plot(self, plot_data): self.model.plot_data = plot_data self.refresh_plot() def refresh_plot(self, keep_max_level=False): self.clear() if self.model.plot_data is None: return image, pos, scale, range_ = self.model.get_plot() if np.all(np.isnan(image)) == True: return old_level = self.get_levels() # Plot self.setImage(image, autoRange=True, autoLevels=True, pos=pos, scale=scale) self.set_range(range_) ratio = (range_[1][1] - range_[1][0]) / (range_[0][1] - range_[0][0]) if config.get_key('pyqtgraph', 'fixed_ratio') == 'True': self.set_aspect_ratio(ratio) else: self.set_aspect_ratio(None) if (keep_max_level or config.get_key('pyqtgraph', 'keep_max_level') == 'True'): levels = [np.nanmin(image.data), np.nanmax(image.data)] # Catch empty plots if old_level != (0, 1): levels[0] = old_level[0] if old_level[0] < levels[0] else \ levels[0] levels[1] = old_level[1] if old_level[1] > levels[1] else \ levels[1] self.set_levels(levels) def set_range(self, range_): padding = float(config.get_key('pyqtgraph', 'padding')) self.view.setRange(xRange=range_[0], yRange=range_[1], update=True, padding=padding) def set_levels(self, levels): self.getHistogramWidget().setLevels(*levels) self.setHistogramRange(*levels) def get_levels(self): return self.getHistogramWidget().getLevels() def set_aspect_ratio(self, ratio): if ratio is not None: self.plot_view.setAspectLocked(True, ratio=ratio) else: self.plot_view.setAspectLocked(False) def set_labels(self, x, y): color = config.get_key('pyqtgraph', 'axis_color') size = config.get_key('pyqtgraph', 'axis_size') if isinstance(x, list): self.set_label('bottom', x[0], x[1], color, size) elif isinstance(x, Axis): self.set_label('bottom', x.label, x.units, color, size) else: self.set_label('bottom', str(x), None, color, size) if isinstance(y, list): self.set_label('left', y[0], y[1], color, size) elif isinstance(y, Axis): self.set_label('left', y.label, y.units, color, size) else: self.set_label('left', str(y), None, color, size) def set_label(self, side, label, units=None, color='k', size=1): axis = AxisItem(side, text=label, units=units, **{ 'color': color, 'font-size': size }) if config.get_key('pyqtgraph', 'show_axis_label') == 'True': axis.showLabel(True) else: axis.showLabel(False) self.view.setAxisItems({side: axis}) def get_plot_data(self): return self.model.plot_data def get_LUT(self): colormap = self.getHistogramWidget().gradient.colorMap() nPts = int(config.get_key('pyqtgraph', 'nPts')) LUT = colormap.getLookupTable(mode='float', alpha=True, nPts=nPts) return LUT def get_label(self, side): return self.plot_view.getAxis(side).label.toHtml() def _setup(self): self.view.invertY(False) self.view.hideButtons() self.ui.roiBtn.hide() self.ui.menuBtn.hide()
class DataWidget(QWidget): def __init__(self, parent, shared_data): super(DataWidget, self).__init__(parent) self.__shared_data = shared_data self.__shared_data.update_sync.emit() # Add the file selection controls self.__dir_picker_button = QPushButton() self.__dir_picker_button.setEnabled(True) self.__dir_picker_button.setText("Load data") self.__dir_picker_button.setIcon(self.style().standardIcon(QStyle.SP_DirIcon)) self.__dir_picker_button.setToolTip('Select the directory using the file explorer') self.__dir_picker_button.clicked.connect(self.__open_dir_picker) # Add the sync controls self.__sync_time_label = QLabel() self.__sync_time_label.setText('Enter the timecode (HH:mm:ss:zzz) : ') self.__sync_time_edit = QTimeEdit() self.__sync_time_edit.setDisplayFormat('HH:mm:ss:zzz') self.__sync_time_edit.setEnabled(False) self.__sync_time_button = QPushButton() self.__sync_time_button.setText('Sync data') self.__sync_time_button.setEnabled(False) self.__sync_time_button.clicked.connect(self.__sync_data) # Create the layout for the file controls dir_layout = QHBoxLayout() dir_layout.setContentsMargins(0, 0, 0, 0) dir_layout.addWidget(self.__dir_picker_button) dir_layout.addStretch(1) dir_layout.addWidget(self.__sync_time_label) dir_layout.addWidget(self.__sync_time_edit) dir_layout.addWidget(self.__sync_time_button) # Create the axis and their viewbox self.__x_axis_item = AxisItem('left') self.__y_axis_item = AxisItem('left') self.__z_axis_item = AxisItem('left') self.__x_axis_viewbox = ViewBox() self.__y_axis_viewbox = ViewBox() self.__z_axis_viewbox = ViewBox() # Create the widget which will display the data self.__graphic_view = GraphicsView(background="#ecf0f1") self.__graphic_layout = GraphicsLayout() self.__graphic_view.setCentralWidget(self.__graphic_layout) # Add the axis to the widget self.__graphic_layout.addItem(self.__x_axis_item, row=2, col=3, rowspan=1, colspan=1) self.__graphic_layout.addItem(self.__y_axis_item, row=2, col=2, rowspan=1, colspan=1) self.__graphic_layout.addItem(self.__z_axis_item, row=2, col=1, rowspan=1, colspan=1) self.__plot_item = PlotItem() self.__plot_item_viewbox = self.__plot_item.vb self.__graphic_layout.addItem(self.__plot_item, row=2, col=4, rowspan=1, colspan=1) self.__graphic_layout.scene().addItem(self.__x_axis_viewbox) self.__graphic_layout.scene().addItem(self.__y_axis_viewbox) self.__graphic_layout.scene().addItem(self.__z_axis_viewbox) self.__x_axis_item.linkToView(self.__x_axis_viewbox) self.__y_axis_item.linkToView(self.__y_axis_viewbox) self.__z_axis_item.linkToView(self.__z_axis_viewbox) self.__x_axis_viewbox.setXLink(self.__plot_item_viewbox) self.__y_axis_viewbox.setXLink(self.__plot_item_viewbox) self.__z_axis_viewbox.setXLink(self.__plot_item_viewbox) self.__plot_item_viewbox.sigResized.connect(self.__update_views) self.__x_axis_viewbox.enableAutoRange(axis=ViewBox.XAxis, enable=True) self.__y_axis_viewbox.enableAutoRange(axis=ViewBox.XAxis, enable=True) self.__z_axis_viewbox.enableAutoRange(axis=ViewBox.XAxis, enable=True) # Create the final layout self.__v_box = QVBoxLayout() self.__v_box.addLayout(dir_layout) self.__v_box.addWidget(self.__graphic_view) self.setLayout(self.__v_box) self.__restore_state() def __open_dir_picker(self): self.__shared_data.data_file_path = QFileDialog.getOpenFileUrl(self, 'Open the Hexoskin data directory', QDir.homePath())[0] if self.__shared_data.data_file_path is not None: try: self.__load_data() self.__add_selector_acc_gyr() self.__show_data('ACC_X', 'ACC_Y', 'ACC_Z') except FileNotFoundError: pass except UnicodeDecodeError: pass def __load_data(self): if self.__shared_data.data_file_path is not None: self.__shared_data.import_parameter() def __show_data(self, field1, field2, field3): if self.__shared_data.parameter is not None: # Generate the timecodes if needed if len(self.__shared_data.parameter['TIMECODE']) == 0: if self.__shared_data.sampling_rate is None: result = False while not result and result == 0: result = self.__show_sampling_rate_picker() self.__shared_data.add_timecode() self.__x_axis_viewbox.clear() self.__y_axis_viewbox.clear() self.__z_axis_viewbox.clear() # Show the 3 selected fields self.__x_axis_viewbox.addItem(PlotCurveItem(list(map(int, self.__shared_data.parameter.get(field1))), pen='#34495e')) self.__y_axis_viewbox.addItem(PlotCurveItem(list(map(int, self.__shared_data.parameter.get(field2))), pen='#9b59b6')) self.__z_axis_viewbox.addItem(PlotCurveItem(list(map(int, self.__shared_data.parameter.get(field3))), pen='#3498db')) self.__x_axis_item.setLabel(field1, color="#34495e") self.__y_axis_item.setLabel(field2, color="#9b59b6") self.__z_axis_item.setLabel(field3, color="#3498db") # Add the middle line and the bottom timecodes timecodes = self.__shared_data.parameter['TIMECODE'] middle = [0] * len(timecodes) self.__plot_item_viewbox.addItem(PlotCurveItem(middle, pen='#000000')) self.__plot_item.getAxis('bottom').setTicks( self.__generate_time_ticks(timecodes, self.__shared_data.sampling_rate)) # Enable the controls self.__sync_time_edit.setEnabled(True) self.__sync_time_button.setEnabled(True) self.__dir_picker_button.setEnabled(False) self.__update_views() def __update_views(self): self.__x_axis_viewbox.setGeometry(self.__plot_item_viewbox.sceneBoundingRect()) self.__y_axis_viewbox.setGeometry(self.__plot_item_viewbox.sceneBoundingRect()) self.__z_axis_viewbox.setGeometry(self.__plot_item_viewbox.sceneBoundingRect()) def __generate_time_ticks(self, timecodes, rate): ticks = list() steps = [rate * 30, rate * 15, rate * 5, rate] for step in steps: temp = list() i = step while i in range(len(timecodes)): temp.append((i, timecodes[i].strftime('%H:%M:%S:') + str(int(timecodes[i].microsecond / 1000)))) i += step ticks.append(temp) return ticks def __sync_data(self): self.__shared_data.data_sync = self.__sync_time_edit.text() self.__shared_data.update_sync.emit() def __show_sampling_rate_picker(self) -> bool: self.__shared_data.sampling_rate, result = QInputDialog.getInt(self, 'Set sampling rate value', 'Sampling rate') return result def __add_selector_acc_gyr(self): if 'GYR_X' in self.__shared_data.parameter.keys() or 'GYR_Y' in self.__shared_data.parameter.keys() \ or 'GYR_Z' in self.__shared_data.parameter.keys(): show_acc = QPushButton() show_acc.setText('Show Accelerometer Axis') show_acc.clicked.connect(self.__show_acc) show_gyr = QPushButton() show_gyr.setText('Show Gyroscope Axis') show_gyr.clicked.connect(self.__show_gyr) layout = QHBoxLayout() layout.addWidget(show_acc) layout.addWidget(show_gyr) layout.addStretch(1) self.__v_box.addLayout(layout) def __show_acc(self): self.__show_data('ACC_X', 'ACC_Y', 'ACC_Z') def __show_gyr(self): self.__show_data('GYR_X', 'GYR_Y', 'GYR_Z') def __restore_state(self): if self.__shared_data.parameter is not None: self.__add_selector_acc_gyr() self.__show_data('ACC_X', 'ACC_Y', 'ACC_Z') print('trigger reimport') if self.__shared_data.data_sync is not None: text_time = self.__shared_data.data_sync.split(':') time = QTime() time.setHMS(int(text_time[0]), int(text_time[1]), int(text_time[2]), int(text_time[3])) self.__sync_time_edit.setTime(time)