Search code examples
pythonpyqtpyqt5pyqtgraph

Update slider from PyQt when zooming in/out on a PyQtGraph


I would like to connect the PyQtGraph mouse wheel zoom function to a QSlider widget. So when i zoom in/out on the graph, I would get the range of the viewbox and the slider window should follow in sliding range. The closest example of how I would like it to be can be found in this PyQtGraph example: http://www.pyqtgraph.org/downloads/0.10.0/pyqtgraph-0.10.0-deb/pyqtgraph-0.10.0/examples/crosshair.py

So I would like to connect the following definitions in some way.

def update_plot(self):
    self.axX = self.p6.getAxis('bottom')
    self.xmin = self.axX.range[0]
    self.xmax = self.axX.range[0]
    print(self.axX.range)
    return xmin, xmax

def update_slider(self, xmin, xmax):
    self.size = self.w1.slider.value()
    self.p6.setXRange(self.xmin+self.size,self.xmax+self.size)
    print(self.size)

However, I can't seem to get it to work. I have attached the full code of my example below. Can you help me in any way?

import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QHBoxLayout, QLabel, QSizePolicy, QSlider, QSpacerItem, \
    QVBoxLayout, QWidget

import pyqtgraph as pg
import numpy as np


class Slider(QWidget):
    def __init__(self, minimum, maximum, parent=None):
        super(Slider, self).__init__(parent=parent)
        self.verticalLayout = QVBoxLayout(self)
        self.label = QLabel(self)
        self.verticalLayout.addWidget(self.label)
        self.horizontalLayout = QHBoxLayout()
        spacerItem = QSpacerItem(0, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem)
        self.slider = QSlider(self)
        self.slider.setOrientation(Qt.Vertical)
        self.horizontalLayout.addWidget(self.slider)
        spacerItem1 = QSpacerItem(0, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem1)
        self.verticalLayout.addLayout(self.horizontalLayout)
        self.resize(self.sizeHint())

        self.minimum = minimum
        self.maximum = maximum
        self.slider.valueChanged.connect(self.setLabelValue)
        self.x = None
        self.setLabelValue(self.slider.value())

    def setLabelValue(self, value):
        self.x = self.minimum + (float(value) / (self.slider.maximum() - self.slider.minimum())) * (
        self.maximum - self.minimum)
        self.label.setText("{0:.4g}".format(self.x))

class Widget(QWidget):
    def __init__(self, parent=None):
        super(Widget, self).__init__(parent=parent)
        self.horizontalLayout = QHBoxLayout(self)

        # plot (p6)
        self.win = pg.GraphicsWindow(title="Basic plotting examples")
        self.horizontalLayout.addWidget(self.win)
        self.p6 = self.win.addPlot(title="My Plot")
        x = np.arange(1e5)
        self.y1 = np.random.randn(x.size)
        self.p6.plot(self.y1, pen="r")
        self.p6.setMouseEnabled(x=True, y=False)
        self.p6.setXRange(0,300)
        self.p6.setLimits(xMin=0, xMax=len(self.y1))

        self.p6.sigRangeChanged.connect(self.update_plot)

        # slider (w1)
        self.w1 = Slider(0, len(self.y1))
        self.horizontalLayout.addWidget(self.w1)        
        self.w1.slider.setMinimum(0)
        self.w1.slider.setMaximum(len(self.y1))

        self.w1.slider.valueChanged.connect(self.update_slider)

    def update_plot(self):
        self.axX = self.p6.getAxis('bottom')
        self.xmin = self.axX.range[0]
        self.xmax = self.axX.range[0]
        print(self.axX.range)
        return self.xmin, self.xmax


    def update_slider(self, xmin, xmax):
        self.size = self.w1.slider.value()
        self.p6.setXRange(self.xmin+self.size,self.xmax+self.size)
        print(self.size)



if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = Widget()
    w.show()
    sys.exit(app.exec_())

Solution

  • First of all, the question you asked first was actually more useful for the StackOverflow "conduct". After the editing you just left what is an useless plain copy/paste from a pyqtgraph example.

    Don't do that.

    People rarely go through editings, and seeing the current status of your question, they would think that you didn't put any efforts at all in trying to solve your problem (leading them to just ignore your question). I suggest you to undo your editing or, at least, restore the original question example code.


    The main problem with your question is that zooming is not linear. From a normal user perspective, each step of a zoom slider should increase or decrease by a zoom factor (pyqtgraph does that, actually).

    A slider is usually a linear interface, which means that each of its step should be equally proportional to its previous or next steps.
    Simply put, if the "middle step" of the slider equals to the original size of an image and the "next step" equals to a double size, the user expects that the step next to that will result in doubling the size again. The same works backwards too, obviously.

    Imagine it as a spinbox, or even a couple of +/- buttons.

        size = 2
        print(size)
    Out[1]: 2
    
        def doZoom(size, step):
            zoomFactor = 2
            if step > 0:
                size *= zoomFactor
            else:
                size /= zoomFactor
            return size
    
        size = doZoom(size, +1)
        print(size)
    Out[2]: 4
    
        size = doZoom(size, +1)
        print(size)
    Out[3]: 8
    
        size = doZoom(size, -1)
        print(size)
    Out[4]: 4
    

    A possible solution is to implement a slider that takes into account all of this.

    import sys
    from math import log
    from PyQt5.QtCore import Qt
    from PyQt5.QtWidgets import (QApplication, QLabel, QSlider, QWidget, 
        QHBoxLayout, QVBoxLayout)
    
    import pyqtgraph as pg
    import numpy as np
    
    class LabelSlider(QWidget):
        def __init__(self, *args, **kwargs):
            super(LabelSlider, self).__init__(*args, **kwargs)
            layout = QVBoxLayout(self)
            # IMPORTANT! QStyles sometimes create *huge* margins (6-10 pixels) around
            # layout contents; we don't really need those in here
            layout.setContentsMargins(0, 0, 0, 0)
    
            self.label = QLabel()
            layout.addWidget(self.label, alignment=Qt.AlignHCenter)
            # use an arbitrary text for the minimum width to minimize size flickering
            # on value changes
            self.label.setMinimumWidth(self.fontMetrics().width("8.8888e+88"))
            self.label.setAlignment(Qt.AlignCenter)
    
            layout.addSpacing(10)
    
            self.slider = QSlider()
            # when adding a QSlider to a QLayout and specifying an alignment, the
            # opposite of the orientation *has* to be omitted to ensure that it's
            # centered in the other direction
            layout.addWidget(self.slider, alignment=Qt.AlignHCenter)
            # set the slider "transparent" for mouse events, so that the user will
            # still see it as enabled but won't be able to interact with it
            self.slider.setAttribute(Qt.WA_TransparentForMouseEvents)
    
            #set a range high enough to limit rounding errors
            self.slider.setMaximum(100000)
    
            # expose the basic slider signal/methods for transparency, so that
            # this LabelSlider will have a similar interface to that of a QSlider
            self.value = self.slider.value
            self.valueChanged = self.slider.valueChanged
    
        def setValue(self, value, xLimit):
            sliderValue = self.slider.maximum() - value * self.slider.maximum()
            self.slider.setValue(sliderValue)
    
            xLimitMin, xLimitMax = xLimit
            limitRange = xLimitMax - xLimitMin
            floatValue = xLimitMin + (value * limitRange / (limitRange)) * (
                limitRange)
            self.label.setText("{0:.4g}".format(floatValue))
            # ensure that the widget is resized to fit the label contents too;
            # sizeHint will be called afterwards
            self.updateGeometry()
    
        def sizeHint(self):
            hint = super(LabelSlider, self).sizeHint()
            # adjust the minimum hint width to accomodate the label contents
            if hint.width() < self.label.width():
                hint.setWidth(self.label.width())
            return hint
    
    
    class Widget(QWidget):
        def __init__(self, parent=None):
            super(Widget, self).__init__(parent=parent)
            self.horizontalLayout = QHBoxLayout(self)
    
            self.win = pg.GraphicsWindow(title="Basic plotting examples")
            self.horizontalLayout.addWidget(self.win)
            self.p6 = self.win.addPlot(title="My Plot")
            x = np.arange(1e5)
            self.y1 = np.random.randn(x.size)
            self.p6.plot(self.y1, pen="r")
            self.p6.setMouseEnabled(x=True, y=False)
            self.p6.setXRange(0,300)
            # set a minimum x range for the plot
            # this value *HAS* to be > 0
            self.p6.setLimits(xMin=0, xMax=len(self.y1), minXRange=1)
    
            self.p6.sigRangeChanged.connect(self.update_plot)
    
            self.slider = LabelSlider()
            self.horizontalLayout.addWidget(self.slider)
    
        def update_plot(self):
            state = self.p6.getViewBox().state
    
            # get the limits of the plot's ViewBox
            limits = state["limits"]
            minZoom, maxZoom = xLimit = limits["xLimits"]
            xRangeMin, xRangeMax = limits["xRange"]
    
            # if the minimum x range is set, use that instead
            if xRangeMin is not None:
                minZoom = xRangeMin
            # ensure that the minimum x range is > 0
            minZoom = max(1, minZoom)
            if xRangeMax is not None:
                maxZoom = xRangeMax
            xMin, xMax = self.p6.getAxis("bottom").range
            diff = xMax - xMin
    
            # get the possible minimum and maximum values based on the wheel factor
            factor = abs(state["wheelScaleFactor"])
            minimum = log(maxZoom / 100000., factor)
            maximum = log(minZoom / 100000., factor)
            value = log(diff / 100000., factor)
    
            # adjust the factor to a 0.0-1.0 range according to the possible zoom
            realValue = (value - minimum) / (maximum - minimum)
    
            # set the slider value according to the above value
            self.slider.setValue(realValue, xLimit)
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        w = Widget()
        w.show()
        sys.exit(app.exec_())