Module audian.fulltraceplot

FullTracePlot

TODO

  • secs_to_str and secs_format to extra module or even thunderlab?
  • Have a class for a single channel that we could add to the toolbar.
  • Only use Data class

Functions

def secs_to_str(time)
Expand source code
def secs_to_str(time):
    hours = int(time//3600)
    time -= 3600*hours
    mins = int(time//60)
    time -= 60*mins
    secs = int(floor(time))
    time -= secs
    if hours > 0:
        return f'{hours}:{mins:02d}:{secs:02d}'
    elif mins > 0:
        return f'{mins:02d}:{secs:02d}'
    elif secs > 0:
        return f'{secs}.{1000*time:03.0f}s'
    elif time >= 0.01:
        return f'{1000*time:03.0f}ms'
    elif time >= 0.001:
        return f'{1000*time:.2f}ms'
    else:
        return f'{1e6*time:.0f}\u00b5s'
def secs_format(time)
Expand source code
def secs_format(time):
    if time >= 3600.0:
        return 'h:mm:ss'
    elif time >= 60.0:
        return 'mm:ss'
    elif time > 1.0:
        return 's.ms'
    else:
        return 'ms'

Classes

class FullTracePlot (data, axtraces, *args, **kwargs)
Expand source code
class FullTracePlot(pg.GraphicsLayoutWidget):

    
    def __init__(self, data, axtraces, *args, **kwargs):
        pg.GraphicsLayoutWidget.__init__(self, *args, **kwargs)

        self.data = data
        self.frames = self.data.frames
        self.tmax = self.frames/self.data.rate
        self.axtraces = axtraces
        self.no_signal = False

        self.setBackground(None)
        self.ci.layout.setContentsMargins(0, 0, 0, 0)
        self.ci.layout.setVerticalSpacing(0)

        # for each channel prepare a plot panel:
        xwidth = self.fontMetrics().averageCharWidth()
        self.axs = []
        self.lines = []
        self.regions = []
        self.labels = []
        for c in range(self.data.channels):
            # setup plot panel:
            axt = pg.PlotItem()
            axt.showAxes(True, False)
            axt.getAxis('left').setWidth(8*xwidth)
            axt.getViewBox().setBackgroundColor(None)
            axt.getViewBox().setDefaultPadding(padding=0.0)
            axt.hideButtons()
            axt.setMenuEnabled(False)
            axt.setMouseEnabled(False, False)
            axt.enableAutoRange(False, False)
            axt.setLimits(xMin=0, xMax=self.tmax,
                          minXRange=self.tmax, maxXRange=self.tmax)
            axt.setXRange(0, self.tmax)

            # add region marker:
            region = pg.LinearRegionItem(pen=dict(color='#110353', width=2),
                                         brush=(34, 6, 167, 127),
                                         hoverPen=dict(color='#aa77ff', width=2),
                                         hoverBrush=(34, 6, 167, 255),
                                         movable=True,
                                         swapMode='block')
            region.setZValue(50)
            region.setBounds((0, self.tmax))
            region.setRegion((self.axtraces[c].viewRange()[0]))
            region.sigRegionChanged.connect(self.update_time_range)
            self.axtraces[c].sigXRangeChanged.connect(self.update_region)
            axt.addItem(region)
            self.regions.append(region)

            # add time label:
            label = QGraphicsSimpleTextItem(axt.getAxis('left'))
            label.setToolTip(f'Total duration in {secs_format(self.tmax)}')
            label.setText(secs_to_str(self.tmax))
            label.setPos(int(xwidth), xwidth/2)
            self.labels.append(label)

            # init data:
            max_pixel = QApplication.desktop().screenGeometry().width()
            self.step = max(1, self.frames//max_pixel)
            self.index = 0
            self.nblock = int(20.0*self.data.rate//self.step)*self.step
            self.times = np.arange(0, self.frames, self.step/2)/self.data.rate
            self.datas = np.zeros((len(self.times), self.data.channels))
            
            # add data:
            line = pg.PlotDataItem(antialias=True,
                                   pen=dict(color='#2206a7', width=1.1),
                                   skipFiniteCheck=True, autDownsample=False)
            line.setZValue(10)
            axt.addItem(line)
            self.lines.append(line)

            # add zero line:
            zero_line = axt.addLine(y=0, movable=False, pen=dict(color='grey', width=1))
            zero_line.setZValue(20)
            
            self.addItem(axt, row=c, col=0)
            self.axs.append(axt)
            
        QTimer.singleShot(10, self.load_data)

        
    def polish(self):
        text_color = self.palette().color(QPalette.WindowText)
        for label in self.labels:
            label.setBrush(text_color)


    def load_data(self):
        i = 2*self.index//self.step
        n = min(self.nblock, self.frames - self.index)
        buffer = np.zeros((n, self.data.channels))
        self.data.load_buffer(self.index, n, buffer)
        for c in range(self.data.channels):
            data = down_sample_peak(buffer[:,c], self.step)
            self.datas[i:i+len(data), c] = data
            self.lines[c].setData(self.times, self.datas[:,c])
        self.index += n
        if self.index < self.frames:
            QTimer.singleShot(10, self.load_data)
        else:
            # TODO: do we really datas? Couldn't we take it from lines? 
            for c in range(self.data.channels):
                ymin = np.min(self.datas[:,c])
                ymax = np.max(self.datas[:,c])
                y = max(fabs(ymin), fabs(ymax))
                self.axs[c].setYRange(-y, y)
                self.axs[c].setLimits(yMin=-y, yMax=y,
                                      minYRange=2*y, maxYRange=2*y)
            self.times = None
            self.datas = None

        
    def update_layout(self, channels, data_height):
        first = True
        for c in range(self.data.channels):
            self.axs[c].setVisible(c in channels)
            if c in channels:
                self.ci.layout.setRowFixedHeight(c, data_height)
                self.labels[c].setVisible(first)
                first = False
            else:
                self.ci.layout.setRowFixedHeight(c, 0)
                self.labels[c].setVisible(False)
        self.setFixedHeight(len(channels)*data_height)


    def update_time_range(self, region):
        if self.no_signal:
            return
        self.no_signal = True
        xmin, xmax = region.getRegion()
        for ax, reg in zip(self.axtraces, self.regions):
            if reg is region:
                ax.setXRange(xmin, xmax)
                break
        self.no_signal = False


    def update_region(self, vbox, x_range):
        for ax, region in zip(self.axtraces, self.regions):
            if ax.getViewBox() is vbox:
                region.setRegion(x_range)
                break

        
    def mousePressEvent(self, ev):
        if ev.button() == Qt.MouseButton.LeftButton:
            for ax, region in zip(self.axs, self.regions):
                pos = ax.getViewBox().mapSceneToView(ev.pos())
                [xmin, xmax], [ymin, ymax] = ax.viewRange()
                if xmin <= pos.x() <= xmax and ymin <= pos.y() <= ymax:
                    dx = (xmax - xmin)/self.width()
                    x = pos.x()
                    xmin, xmax = region.getRegion()
                    if x < xmin-2*dx or x > xmax + 2*dx:
                        dx = xmax - xmin
                        xmin = max(0, x - dx/2)
                        xmax = xmin + dx
                        if xmax > self.tmax:
                            xmin = max(0, xmax - dx)
                        region.setRegion((xmin, xmax))
                        ev.accept()
                        return
                    break
        ev.ignore()
        super().mousePressEvent(ev)

Convenience class consisting of a :class:GraphicsView <pyqtgraph.GraphicsView> with a single :class:GraphicsLayout <pyqtgraph.GraphicsLayout> as its central item.

This widget is an easy starting point for generating multi-panel figures. Example::

w = pg.GraphicsLayoutWidget()
p1 = w.addPlot(row=0, col=0)
p2 = w.addPlot(row=0, col=1)
v = w.addViewBox(row=1, col=0, colspan=2)

========= ================================================================= parent (QWidget or None) The parent widget. show (bool) If True, then immediately show the widget after it is created. If the widget has no parent, then it will be shown inside a new window. size (width, height) tuple. Optionally resize the widget. Note: if this widget is placed inside a layout, then this argument has no effect. title (str or None) If specified, then set the window title for this widget. kargs All extra arguments are passed to :meth:GraphicsLayout.__init__ <pyqtgraph.GraphicsLayout.__init__> ========= =================================================================

This class wraps several methods from its internal GraphicsLayout: :func:nextRow <pyqtgraph.GraphicsLayout.nextRow> :func:nextColumn <pyqtgraph.GraphicsLayout.nextColumn> :func:addPlot <pyqtgraph.GraphicsLayout.addPlot> :func:addViewBox <pyqtgraph.GraphicsLayout.addViewBox> :func:addItem <pyqtgraph.GraphicsLayout.addItem> :func:getItem <pyqtgraph.GraphicsLayout.getItem> :func:addLabel <pyqtgraph.GraphicsLayout.addLabel> :func:addLayout <pyqtgraph.GraphicsLayout.addLayout> :func:removeItem <pyqtgraph.GraphicsLayout.removeItem> :func:itemIndex <pyqtgraph.GraphicsLayout.itemIndex> :func:clear <pyqtgraph.GraphicsLayout.clear>

============== ============================================================ Arguments: parent Optional parent widget useOpenGL If True, the GraphicsView will use OpenGL to do all of its rendering. This can improve performance on some systems, but may also introduce bugs (the combination of QGraphicsView and QOpenGLWidget is still an 'experimental' feature of Qt) background Set the background color of the GraphicsView. Accepts any single argument accepted by :func:mkColor <pyqtgraph.mkColor>. By default, the background color is determined using the 'backgroundColor' configuration option (see :func:setConfigOptions <pyqtgraph.setConfigOptions>). ============== ============================================================

Ancestors

  • pyqtgraph.widgets.GraphicsLayoutWidget.GraphicsLayoutWidget
  • pyqtgraph.widgets.GraphicsView.GraphicsView
  • PyQt5.QtWidgets.QGraphicsView
  • PyQt5.QtWidgets.QAbstractScrollArea
  • PyQt5.QtWidgets.QFrame
  • PyQt5.QtWidgets.QWidget
  • PyQt5.QtCore.QObject
  • sip.wrapper
  • PyQt5.QtGui.QPaintDevice
  • sip.simplewrapper

Methods

def polish(self)
Expand source code
def polish(self):
    text_color = self.palette().color(QPalette.WindowText)
    for label in self.labels:
        label.setBrush(text_color)
def load_data(self)
Expand source code
def load_data(self):
    i = 2*self.index//self.step
    n = min(self.nblock, self.frames - self.index)
    buffer = np.zeros((n, self.data.channels))
    self.data.load_buffer(self.index, n, buffer)
    for c in range(self.data.channels):
        data = down_sample_peak(buffer[:,c], self.step)
        self.datas[i:i+len(data), c] = data
        self.lines[c].setData(self.times, self.datas[:,c])
    self.index += n
    if self.index < self.frames:
        QTimer.singleShot(10, self.load_data)
    else:
        # TODO: do we really datas? Couldn't we take it from lines? 
        for c in range(self.data.channels):
            ymin = np.min(self.datas[:,c])
            ymax = np.max(self.datas[:,c])
            y = max(fabs(ymin), fabs(ymax))
            self.axs[c].setYRange(-y, y)
            self.axs[c].setLimits(yMin=-y, yMax=y,
                                  minYRange=2*y, maxYRange=2*y)
        self.times = None
        self.datas = None
def update_layout(self, channels, data_height)
Expand source code
def update_layout(self, channels, data_height):
    first = True
    for c in range(self.data.channels):
        self.axs[c].setVisible(c in channels)
        if c in channels:
            self.ci.layout.setRowFixedHeight(c, data_height)
            self.labels[c].setVisible(first)
            first = False
        else:
            self.ci.layout.setRowFixedHeight(c, 0)
            self.labels[c].setVisible(False)
    self.setFixedHeight(len(channels)*data_height)
def update_time_range(self, region)
Expand source code
def update_time_range(self, region):
    if self.no_signal:
        return
    self.no_signal = True
    xmin, xmax = region.getRegion()
    for ax, reg in zip(self.axtraces, self.regions):
        if reg is region:
            ax.setXRange(xmin, xmax)
            break
    self.no_signal = False
def update_region(self, vbox, x_range)
Expand source code
def update_region(self, vbox, x_range):
    for ax, region in zip(self.axtraces, self.regions):
        if ax.getViewBox() is vbox:
            region.setRegion(x_range)
            break
def mousePressEvent(self, ev)
Expand source code
def mousePressEvent(self, ev):
    if ev.button() == Qt.MouseButton.LeftButton:
        for ax, region in zip(self.axs, self.regions):
            pos = ax.getViewBox().mapSceneToView(ev.pos())
            [xmin, xmax], [ymin, ymax] = ax.viewRange()
            if xmin <= pos.x() <= xmax and ymin <= pos.y() <= ymax:
                dx = (xmax - xmin)/self.width()
                x = pos.x()
                xmin, xmax = region.getRegion()
                if x < xmin-2*dx or x > xmax + 2*dx:
                    dx = xmax - xmin
                    xmin = max(0, x - dx/2)
                    xmax = xmin + dx
                    if xmax > self.tmax:
                        xmin = max(0, xmax - dx)
                    region.setRegion((xmin, xmax))
                    ev.accept()
                    return
                break
    ev.ignore()
    super().mousePressEvent(ev)

mousePressEvent(self, event: Optional[QMouseEvent])