so Pyqtgraph automatically computes the axis and rescales upon zooming, and this is fine. However I have two axes, frequency and hour. Frequency can take any value between 0-100 and hour can take any value between 0-39. How can I limit the axis to these upper/lower bounds so that when the user zooms or pans they cannot go outside of these values?
My code is as follows (for 3 lines, my actual code will plot a lot more):
from pyqtgraph.Qt import QtGui, QtCore
import numpy as np
import pyqtgraph as pg
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
from random import randint
class CustomViewBox(pg.ViewBox):
def __init__(self, *args, **kwds):
pg.ViewBox.__init__(self, *args, **kwds)
self.setMouseMode(self.RectMode)
## reimplement right-click to zoom out
def mouseClickEvent(self, ev):
if ev.button() == QtCore.Qt.RightButton:
#self.autoRange()
self.setXRange(0,5)
self.setYRange(0,10)
def mouseDragEvent(self, ev):
if ev.button() == QtCore.Qt.RightButton:
ev.ignore()
else:
pg.ViewBox.mouseDragEvent(self, ev)
app = pg.mkQApp()
vb = CustomViewBox()
graph = pg.PlotWidget(viewBox=vb, enableMenu=False)
colour = []
for i in range(0,3):
colourvalue = [randint(0,255), randint(0,255), randint(0,255)]
tuple(colourvalue)
colour.append(colourvalue)
y_data = [
[['a',0],['b',1],['c',None],['d',6],['e',7]],
[['a',5],['b',2],['c',1],['d',None],['e',1]],
[['a',3],['b',None],['c',4],['d',9],['e',None]],
]
x_data = [0, 1, 2, 3, 4]
for i in range(3):
xv = []
yv = []
for j, v in enumerate(row[i][1] for row in y_data):
if v is not None:
xv.append(int(j))
yv.append(float(v))
graph.plot(xv, yv, pen = colour[i], name=y_data[0][i][0])
graph.show()
graph.setWindowTitle('Hourly Frequency Graph')
graph.setXRange(0,5)
graph.setYRange(0,10)
graph.setLabel('left', "Frequency", units='%')
graph.setLabel('bottom', "Hour")
graph.showGrid(x=True, y=True)
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
Thanks in advance for any help and advice!
I know this is an old one but just in case anyone else needs a solution. The user can be stopped from zooming out past the axis limits by getting the current axis (assumed to be the maximum out-zoom) and setting these values as the range for the view box.
e.g.
range_ = graph.getViewBox().viewRange()
graph.getViewBox().setLimits(xMin=range_[0][0], xMax=range_[0][1],
yMin=range_[1][0], yMax=range_[1][1])
Documentation: https://pyqtgraph.readthedocs.io/en/latest/graphicsItems/viewbox.html
Related
I'm trying to add a label to the top right corner of the plot showing the most recent data value. I've tried using pg.LabelItem and adding this to pg.PlotWidget and updating the label with each new data update but I'm unable to get the label to appear. Here's some pictures of what I'm trying to do:
What I have:
What I'm trying to do:
I can't get the white label to appear on the plot. Here's my code:
from PyQt4 import QtCore, QtGui
from threading import Thread
import pyqtgraph as pg
import numpy as np
import random
import sys
import time
class SimplePlot(QtGui.QWidget):
def __init__(self, parent=None):
super(SimplePlot, self).__init__(parent)
# Desired Frequency (Hz) = 1 / self.FREQUENCY
# USE FOR TIME.SLEEP (s)
self.FREQUENCY = .004
# Frequency to update plot (ms)
# USE FOR TIMER.TIMER (ms)
self.TIMER_FREQUENCY = self.FREQUENCY * 1000
# Set X Axis range. If desired is [-10,0] then set LEFT_X = -10 and RIGHT_X = 0
self.LEFT_X = -10
self.RIGHT_X = 0
self.X_Axis = np.arange(self.LEFT_X, self.RIGHT_X, self.FREQUENCY)
self.buffer = int((abs(self.LEFT_X) + abs(self.RIGHT_X))/self.FREQUENCY)
self.data = []
# Create Plot Widget
self.simple_plot_widget = pg.PlotWidget()
# Enable/disable plot squeeze (Fixed axis movement)
self.simple_plot_widget.plotItem.setMouseEnabled(x=False, y=False)
self.simple_plot_widget.setXRange(self.LEFT_X, self.RIGHT_X)
self.simple_plot_widget.setTitle('Plot')
self.simple_plot_widget.setLabel('left', 'Value')
self.simple_plot_widget.setLabel('bottom', 'Time (s)')
self.simple_plot = self.simple_plot_widget.plot()
self.simple_plot.setPen(197,235,255)
self.label_value = pg.LabelItem('', **{'color': '#FFF'})
self.simple_plot_widget.addItem(self.label_value)
self.layout = QtGui.QGridLayout()
self.layout.addWidget(self.simple_plot_widget)
self.read_position_thread()
self.start()
# Update plot
def start(self):
self.position_update_timer = QtCore.QTimer()
self.position_update_timer.timeout.connect(self.plot_updater)
self.position_update_timer.start(self.get_simple_plot_timer_frequency())
# Read in data using a thread
def read_position_thread(self):
self.current_position_value = 0
self.old_current_position_value = 0
self.position_update_thread = Thread(target=self.read_position, args=())
self.position_update_thread.daemon = True
self.position_update_thread.start()
def read_position(self):
frequency = self.get_simple_plot_frequency()
while True:
try:
# Add data
self.current_position_value = self.current_position_value + random.uniform(-1, -5)
self.old_current_position_value = self.current_position_value
time.sleep(frequency)
except:
self.current_position_value = self.old_current_position_value
def plot_updater(self):
self.dataPoint = float(self.current_position_value)
if len(self.data) >= self.buffer:
del self.data[:1]
self.data.append(self.dataPoint)
self.simple_plot.setData(self.X_Axis[len(self.X_Axis) - len(self.data):], self.data)
# Update label value
self.label_value.setText(str(self.dataPoint))
def clear_simple_plot(self):
self.data[:] = []
def get_simple_plot_frequency(self):
return self.FREQUENCY
def get_simple_plot_timer_frequency(self):
return self.TIMER_FREQUENCY
def get_simple_plot_layout(self):
return self.layout
def get_current_position_value(self):
return self.current_position_value
def get_simple_plot_widget(self):
return self.simple_plot_widget
if __name__ == '__main__':
app = QtGui.QApplication([])
mw = QtGui.QMainWindow()
mw.setWindowTitle('Plot')
simple_plot_widget = SimplePlot()
cw = QtGui.QWidget()
ml = QtGui.QGridLayout()
cw.setLayout(ml)
mw.setCentralWidget(cw)
ml.addLayout(simple_plot_widget.get_simple_plot_layout(),0,0)
mw.show()
# Start Qt event loop unless running in interactive mode or using pyside
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
It may be because your plot is rescaling constantly, and the size of the LabelItem doesn't change with it, also it seems to be positioned on the positive side of the x-axis, so you cant visualize the text.
Pyqtgraph recommends here to use TextItem instead of LabelItem, to display text inside a scaled view.
I tried using the TextItem and it worked fine, but its default position is bad, maybe because your plot is in the negative quadrant. Just use the setPos() method like this:
# Update label value
self.label_value.setPos(QtCore.QPointF(-9, 0.6*min(self.data)))
self.label_value.setText(str(self.dataPoint))
And it should do what you want.
I use pyqtgraph to draw the candles, but if I zoom graph very much, image hide.
I noticed that when zoom very much, “paint” method stops being called. Code with http request to exchange below.
image example
import pyqtgraph as pg
from pyqtgraph import QtCore, QtGui
from PyQt5 import QtWidgets
class CandlestickItem(QtWidgets.QGraphicsRectItem):
def __init__(self, data):
super(CandlestickItem, self).__init__()
self.data = data
self.generatePicture()
def generatePicture(self):
self.picture = QtGui.QPicture()
p = QtGui.QPainter(self.picture)
p.setPen(pg.mkPen('w'))
w = (self.data[1][0] - self.data[0][0]) / 3.
for (t, open, close, min, max) in self.data:
if max != min:
p.drawLine(QtCore.QPointF(t, min), QtCore.QPointF(t, max))
if open > close:
p.setBrush(pg.mkBrush('r'))
else:
p.setBrush(pg.mkBrush('g'))
p.drawRect(QtCore.QRectF(t - w, open, w * 2, close - open))
p.end()
def paint(self, p, *args):
print('paint call')
p.drawPicture(0, 0, self.picture)
def boundingRect(self):
return QtCore.QRectF(self.picture.boundingRect())
if __name__ == '__main__':
import sys
import urllib.request
import json
get_symbols_url = 'https://api.hitbtc.com/api/2/public/candles/BCNBTC?period=M30&limit=1000'
response = urllib.request.urlopen(get_symbols_url)
request_data = json.loads(response.read())
data_list = []
for i in range(0, len(request_data)):
data_list.append((float(i), float(request_data[i]['open']), float(request_data[i]['close']),
float(request_data[i]['min']), float(request_data[i]['max'])))
item = CandlestickItem(data_list)
plt = pg.plot()
plt.addItem(item)
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
I need to display very small values, for example, 0.0000001-0.00000015. What can I do to prevent image hidden at high zoom?
I was answered on the support forum and it works for me.
question on forum
It seems to be a rounding error. Need replace method of boundingRect.
# import numpy as np
def boundingRect(self):
data = np.array(self.data)
xmin = data[:,0].min()
xmax = data[:,0].max()
ymin = data[:,1:].min()
ymax = data[:,1:].max()
return QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin)
Is it possible link the auto-scale of several plots?
I want to scale all the plots with which ever is the biggest range on all curves of all plots.
Is there way to make it with a pyqtgraph function, or should I find the max, min and set the scale with a custom function?
I am using pyqtgraph on PyQt5
Pyqtgraph should automatically scale the y-axis on multiple plots with which ever has the largest range. Here's an example widget with auto-scaling plots in PyQt4 but the concept should be the same for PyQt5.
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
import sys
import random
class AutoScaleMultiplePlotWidget(QtGui.QWidget):
def __init__(self, parent=None):
super(AutoScaleMultiplePlotWidget, self).__init__(parent)
self.NUMBER_OF_PLOTS = 4
self.LEFT_X = 0
self.RIGHT_X = 5
self.SPACING = 1
self.x_axis = np.arange(self.LEFT_X, self.RIGHT_X + 1, self.SPACING)
self.buffer_size = int((abs(self.LEFT_X) + abs(self.RIGHT_X) + 1)/self.SPACING)
self.auto_scale_plot_widget = pg.PlotWidget()
self.auto_scale_plot_widget.setLabel('left', 'left axis')
# Create plots
self.left_plot1 = self.auto_scale_plot_widget.plot()
self.left_plot2 = self.auto_scale_plot_widget.plot()
self.left_plot3 = self.auto_scale_plot_widget.plot()
self.left_plot4 = self.auto_scale_plot_widget.plot()
self.left_plot1.setPen((173,255,129), width=1)
self.left_plot2.setPen((172,187,255), width=1)
self.left_plot3.setPen((255,190,116), width=1)
self.left_plot4.setPen((204,120,255), width=1)
self.initialize_plot_buffers()
self.initialize_data_buffers()
self.layout = QtGui.QGridLayout()
self.layout.addWidget(self.auto_scale_plot_widget)
self.start()
def initialize_data_buffers(self):
"""Create blank data buffers for each curve"""
self.data_buffers = []
for trace in range(self.NUMBER_OF_PLOTS):
self.data_buffers.append([0])
def initialize_plot_buffers(self):
"""Add plots into buffer for each curve"""
self.plots = []
self.plots.append(self.left_plot1)
self.plots.append(self.left_plot2)
self.plots.append(self.left_plot3)
self.plots.append(self.left_plot4)
def update_plot(self):
"""Generates new random value and plots curve onto plot"""
for trace in range(self.NUMBER_OF_PLOTS):
if len(self.data_buffers[trace]) >= self.buffer_size:
self.data_buffers[trace].pop(0)
data_point = self.data_buffers[trace][-1] + random.randint(10,50)
self.data_buffers[trace].append(float(data_point))
self.plots[trace].setData(self.x_axis[len(self.x_axis) - len(self.data_buffers[trace]):], self.data_buffers[trace])
def get_auto_scale_plot_layout(self):
return self.layout
def start(self):
self.multiple_axis_plot_timer = QtCore.QTimer()
self.multiple_axis_plot_timer.timeout.connect(self.update_plot)
self.multiple_axis_plot_timer.start(500)
if __name__ == '__main__':
# Create main application window
app = QtGui.QApplication([])
app.setStyle(QtGui.QStyleFactory.create("Cleanlooks"))
mw = QtGui.QMainWindow()
mw.setWindowTitle('Auto Scale Multiple Plot Example')
# Create plot
auto_scale_plot = AutoScaleMultiplePlotWidget()
# Create and set widget layout
# Main widget container
cw = QtGui.QWidget()
ml = QtGui.QGridLayout()
cw.setLayout(ml)
mw.setCentralWidget(cw)
# Add plot to main layout
ml.addLayout(auto_scale_plot.get_auto_scale_plot_layout(),0,0)
mw.show()
if(sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
I'm using pyqtgraph to plot tracks of a robot (the path that the bot drove). Now I want to add a marker to the plot to indicate the bots current position and heading. I thought ArrowItem would be the right choice, because it is scale invariant and can be rotated easily. However the local origin of the arrow is at its tip like this
but I want it to be in the center like this
How can I do that? I would also appreciate different solutions to this problem.
Update
After applying eyllansec's code I get some rendering problems. A minimal example (one has to zoom or move the view to disable the auto scaling):
import pyqtgraph as pg
import numpy as np
import time
class CenteredArrowItem(pg.ArrowItem):
def paint(self, p, *args):
p.translate(-self.boundingRect().center())
pg.ArrowItem.paint(self, p, *args)
if __name__ == '__main__':
app = pg.QtGui.QApplication([])
window = pg.GraphicsWindow(size=(1280, 720))
window.setAntialiasing(True)
tracker = window.addPlot(title='Tracker')
while True:
for i in range(300):
arrow = CenteredArrowItem(angle=i, headLen=40, tipAngle=45, baseAngle=30)
arrow.setPos(i / 300, i / 300)
tracker.addItem(arrow)
app.processEvents()
time.sleep(0.02)
tracker.removeItem(arrow)
As you may noticed I'm adding and removing the arrow each iteration. This is because arrow.setStyle(angle=i) is not working as it does not update the rotation of the arrow (probably a bug).
A possible solution is to overwrite the paint method of ArrowItem and move the QPainter:
import numpy as np
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
class MyArrowItem(pg.ArrowItem):
def paint(self, p, *args):
p.translate(-self.boundingRect().center())
pg.ArrowItem.paint(self, p, *args)
app = QtGui.QApplication([])
w = QtGui.QMainWindow()
p = pg.PlotWidget()
p.showGrid(x = True, y = True, alpha = 0.3)
w.show()
w.resize(640, 480)
w.setCentralWidget(p)
w.setWindowTitle('pyqtgraph example: Arrow')
a = pg.ArrowItem(angle=-160, tipAngle=60, headLen=40, tailLen=40, tailWidth=20, pen={'color': 'w', 'width': 3}, brush='r')
b = MyArrowItem(angle=-160, tipAngle=60, headLen=40, tailLen=40, tailWidth=20, pen={'color': 'w', 'width': 3})
a.setPos(10,0)
b.setPos(10,0)
p.addItem(a)
p.addItem(b)
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
As shown in the following figure, the red arrow is the default ArrowItem, and the blue is the offset, both are located in the same position with respect to the plot.
Update:
The problem is caused by the method that rotates the item used as the center of coordinates using the center of transformations by default, that is to say the (0, 0), we must move it:
import pyqtgraph as pg
import numpy as np
import time
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph import functions as fn
class CenteredArrowItem(pg.ArrowItem):
def setStyle(self, **opts):
# http://www.pyqtgraph.org/documentation/_modules/pyqtgraph/graphicsItems/ArrowItem.html#ArrowItem.setStyle
self.opts.update(opts)
opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']])
tr = QtGui.QTransform()
path = fn.makeArrowPath(**opt)
tr.rotate(self.opts['angle'])
p = -path.boundingRect().center()
tr.translate(p.x(), p.y())
self.path = tr.map(path)
self.setPath(self.path)
self.setPen(fn.mkPen(self.opts['pen']))
self.setBrush(fn.mkBrush(self.opts['brush']))
if self.opts['pxMode']:
self.setFlags(self.flags() | self.ItemIgnoresTransformations)
else:
self.setFlags(self.flags() & ~self.ItemIgnoresTransformations)
if __name__ == '__main__':
app = pg.QtGui.QApplication([])
window = pg.GraphicsWindow(size=(1280, 720))
window.setAntialiasing(True)
tracker = window.addPlot(title='Tracker')
while True:
for i in range(300):
arrow = CenteredArrowItem(angle=i, headLen=40, tipAngle=45, baseAngle=30)
arrow.setPos(i / 300, i / 300)
tracker.addItem(arrow)
app.processEvents()
time.sleep(0.02)
tracker.removeItem(arrow)
After digging through the source code of pyqtgraph I ended up with a special function that suits my needs. I apply the translation when creating the arrow path, instead when rendering it. Fortunately this also solves the roation bug (for whatever reason).
Example:
import pyqtgraph as pg
import numpy as np
import time
import pyqtgraph.functions
class CenteredArrowItem(pg.ArrowItem):
def setData(self, x, y, angle):
self.opts['angle'] = angle
opt = dict([(k, self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']])
path = pg.functions.makeArrowPath(**opt)
b = path.boundingRect()
tr = pg.QtGui.QTransform()
tr.rotate(angle)
tr.translate(-b.x() - b.width() / 2, -b.y() - b.height() / 2)
self.path = tr.map(path)
self.setPath(self.path)
self.setPos(x, y)
if __name__ == '__main__':
app = pg.QtGui.QApplication([])
window = pg.GraphicsWindow(size=(1280, 720))
window.setAntialiasing(True)
tracker = window.addPlot(title='Tracker')
arrow = CenteredArrowItem(headLen=40, tipAngle=45, baseAngle=30)
tracker.addItem(arrow)
tracker.addItem(pg.InfiniteLine(pos=(0,0), angle=45))
center = pg.ScatterPlotItem([], [], brush='r')
tracker.addItem(center)
while True:
for i in range(300):
arrow.setData(i, i, i)
center.setData([i], [i])
app.processEvents()
time.sleep(0.02)
I have large time-traces that must be inspected visually, so I need a fast scrolling tool.
How can I achieve the fastest Maplotlib/Pyside scrolling?
Right know, I added a PySide scroll-bar to a MPL figure and update the x-range of the plot with set_xlim() method. This is not fast enough especially because in the final application I have at least 8 time-traces in different subplots that must all scroll together. A figure of the plot is attached.
Is there room for improvement?
Here I attach the demo code that demonstrate the relatively low scrolling. It's long but it's almost all boiler-plate code. The interesting bit (that needs improvement) is in xpos_changed() method where the plot xlimits are changed.
EDIT: Below I incorporated some micro-optimizations suggested by tcaswell, but the update speed is not improved.
from PySide import QtGui, QtCore
import pylab as plt
import numpy as np
N_SAMPLES = 1e6
def test_plot():
time = np.arange(N_SAMPLES)*1e-3
sample = np.random.randn(N_SAMPLES)
plt.plot(time, sample, label="Gaussian noise")
plt.title("1000s Timetrace \n (use the slider to scroll and the spin-box to set the width)")
plt.xlabel('Time (s)')
plt.legend(fancybox=True)
q = ScrollingToolQT(plt.gcf(), scroll_step=10)
return q # WARNING: it's important to return this object otherwise
# python will delete the reference and the GUI will not respond!
class ScrollingToolQT(object):
def __init__(self, fig, scroll_step=10):
# Setup data range variables for scrolling
self.fig = fig
self.scroll_step = scroll_step
self.xmin, self.xmax = fig.axes[0].get_xlim()
self.width = 1 # axis units
self.pos = 0 # axis units
self.scale = 1e3 # conversion betweeen scrolling units and axis units
# Save some MPL shortcuts
self.ax = self.fig.axes[0]
self.draw = self.fig.canvas.draw
#self.draw_idle = self.fig.canvas.draw_idle
# Retrive the QMainWindow used by current figure and add a toolbar
# to host the new widgets
QMainWin = fig.canvas.parent()
toolbar = QtGui.QToolBar(QMainWin)
QMainWin.addToolBar(QtCore.Qt.BottomToolBarArea, toolbar)
# Create the slider and spinbox for x-axis scrolling in toolbar
self.set_slider(toolbar)
self.set_spinbox(toolbar)
# Set the initial xlimits coherently with values in slider and spinbox
self.ax.set_xlim(self.pos,self.pos+self.width)
self.draw()
def set_slider(self, parent):
self.slider = QtGui.QSlider(QtCore.Qt.Horizontal, parent=parent)
self.slider.setTickPosition(QtGui.QSlider.TicksAbove)
self.slider.setTickInterval((self.xmax-self.xmin)/10.*self.scale)
self.slider.setMinimum(self.xmin*self.scale)
self.slider.setMaximum((self.xmax-self.width)*self.scale)
self.slider.setSingleStep(self.width*self.scale/4.)
self.slider.setPageStep(self.scroll_step*self.width*self.scale)
self.slider.setValue(self.pos*self.scale) # set the initial position
self.slider.valueChanged.connect(self.xpos_changed)
parent.addWidget(self.slider)
def set_spinbox(self, parent):
self.spinb = QtGui.QDoubleSpinBox(parent=parent)
self.spinb.setDecimals(3)
self.spinb.setRange(0.001,3600.)
self.spinb.setSuffix(" s")
self.spinb.setValue(self.width) # set the initial width
self.spinb.valueChanged.connect(self.xwidth_changed)
parent.addWidget(self.spinb)
def xpos_changed(self, pos):
#pprint("Position (in scroll units) %f\n" %pos)
pos /= self.scale
self.ax.set_xlim(pos, pos+self.width)
self.draw()
def xwidth_changed(self, width):
#pprint("Width (axis units) %f\n" % step)
if width <= 0: return
self.width = width
self.slider.setSingleStep(self.width*self.scale/5.)
self.slider.setPageStep(self.scroll_step*self.width*self.scale)
old_xlim = self.ax.get_xlim()
self.xpos_changed(old_xlim[0]*self.scale)
if __name__ == "__main__":
q = test_plot()
plt.show()
As requested in the comments, here is a pyqtgraph demo which scrolls two large traces together (via mouse).
The documentation isn't complete for the pyqtgraph project but there are some good examples you can view with python -m pyqtgraph.examples which should point you in the right direction. The crosshair.py example might be particularly interesting for you.
If you go with pyqtgraph, connect your slider widget to the setXRange method in the last line of this demo.
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
import numpy as np
app = QtGui.QApplication([])
win = pg.GraphicsWindow()
x = np.arange(1e5)
y1 = np.random.randn(x.size)
y2 = np.random.randn(x.size)
p1 = win.addPlot(x=x, y=y1, name='linkToMe')
p1.setMouseEnabled(x=True, y=False)
win.nextRow()
p2 = win.addPlot(x=x, y=y2)
p2.setXLink('linkToMe')
p1.setXRange(2000,3000)
This seems a bit faster/more responsive:
from PySide import QtGui, QtCore
import pylab as plt
import numpy as np
N_SAMPLES = 1e6
def test_plot():
time = np.arange(N_SAMPLES)*1e-3
sample = np.random.randn(N_SAMPLES)
plt.plot(time, sample, label="Gaussian noise")
plt.legend(fancybox=True)
plt.title("Use the slider to scroll and the spin-box to set the width")
q = ScrollingToolQT(plt.gcf())
return q # WARNING: it's important to return this object otherwise
# python will delete the reference and the GUI will not respond!
class ScrollingToolQT(object):
def __init__(self, fig):
# Setup data range variables for scrolling
self.fig = fig
self.xmin, self.xmax = fig.axes[0].get_xlim()
self.step = 1 # axis units
self.scale = 1e3 # conversion betweeen scrolling units and axis units
# Retrive the QMainWindow used by current figure and add a toolbar
# to host the new widgets
QMainWin = fig.canvas.parent()
toolbar = QtGui.QToolBar(QMainWin)
QMainWin.addToolBar(QtCore.Qt.BottomToolBarArea, toolbar)
# Create the slider and spinbox for x-axis scrolling in toolbar
self.set_slider(toolbar)
self.set_spinbox(toolbar)
# Set the initial xlimits coherently with values in slider and spinbox
self.set_xlim = self.fig.axes[0].set_xlim
self.draw_idle = self.fig.canvas.draw_idle
self.ax = self.fig.axes[0]
self.set_xlim(0, self.step)
self.fig.canvas.draw()
def set_slider(self, parent):
# Slider only support integer ranges so use ms as base unit
smin, smax = self.xmin*self.scale, self.xmax*self.scale
self.slider = QtGui.QSlider(QtCore.Qt.Horizontal, parent=parent)
self.slider.setTickPosition(QtGui.QSlider.TicksAbove)
self.slider.setTickInterval((smax-smin)/10.)
self.slider.setMinimum(smin)
self.slider.setMaximum(smax-self.step*self.scale)
self.slider.setSingleStep(self.step*self.scale/5.)
self.slider.setPageStep(self.step*self.scale)
self.slider.setValue(0) # set the initial position
self.slider.valueChanged.connect(self.xpos_changed)
parent.addWidget(self.slider)
def set_spinbox(self, parent):
self.spinb = QtGui.QDoubleSpinBox(parent=parent)
self.spinb.setDecimals(3)
self.spinb.setRange(0.001, 3600.)
self.spinb.setSuffix(" s")
self.spinb.setValue(self.step) # set the initial width
self.spinb.valueChanged.connect(self.xwidth_changed)
parent.addWidget(self.spinb)
def xpos_changed(self, pos):
#pprint("Position (in scroll units) %f\n" %pos)
# self.pos = pos/self.scale
pos /= self.scale
self.set_xlim(pos, pos + self.step)
self.draw_idle()
def xwidth_changed(self, xwidth):
#pprint("Width (axis units) %f\n" % step)
if xwidth <= 0: return
self.step = xwidth
self.slider.setSingleStep(self.step*self.scale/5.)
self.slider.setPageStep(self.step*self.scale)
old_xlim = self.ax.get_xlim()
self.xpos_changed(old_xlim[0] * self.scale)
# self.set_xlim(self.pos,self.pos+self.step)
# self.fig.canvas.draw()
if __name__ == "__main__":
q = test_plot()
plt.show()