I am trying to zoom in a chart that consists from lots of sine waves close to each other, but when I accidentally click on one of the curves (they are tightly together as in the image) the rubberband isn't created and therefore the zoom is ignored, it only allows me to zoom on the white borders of the chart.
Any ideas how to fix it, so if I click on the curve then it will zoom as well?
The overrided function:
class aview(QChartView):
def __init__(self, chart, parent):
super(aview, self).__init__(chart, parent)
self.setMouseTracking(True)
self.setInteractive(True)
self.setRubberBand(self.HorizontalRubberBand)
Call to the overrided function:
class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()
...
curve = QLineSeries()
curve.setUseOpenGL(True)
curve.append(.........) # this isn't important for this question
...
self.current = QWidget(self)
self.chart = QChart()
self.chart.legend().hide()
self.chart.addSeries(curve)
self.chart_view = aview(self.chart, self.current)
self.chart_view.setRenderHint(QtGui.QPainter.Antialiasing)
...
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Window()
ex.show()
sys.exit(app.exec_())
Chart without zoom:
Horizontally zoomed chart (consisting of sinewaves):
Okay, the problem was OpenGL, it creates a transparent widget on top of all others which is a separate widget that can't be animated:
https://doc.qt.io/qt-5/qabstractseries.html#useOpenGL-prop
The OpenGL acceleration of series drawing is meant for use cases that
need fast drawing of large numbers of points. It is optimized for
efficiency, and therefore the series using it lack support for many
features available to non-accelerated series:
Series animations are not supported for accelerated series.
...
So simply commenting out curve.setUseOpenGL(True) solved it.
Related
I wrote a script for modeling the evolution of a pandemic (with graphs and scatter plots).
I tried several libraries to display results in real-time (8 countries x 500 particles):
Matplotlib (not fast enough)
PyQtGraph (better but still not fast enough)
OpenGL (good, but I did not find how to use it in 2D efficiently, using subplots, titles, legends...)
Bokeh (good, but the scatter plots "blink" each time their particles turn color. Code is here if you are interested)
That is why I am turning now to VisPy.
I am using a class Visualizer to display the results, with the method app.Timer().connect to manage the real-time side. Pandemic code is here.
from Pandemic import *
from vispy.plot import Fig
from vispy import app
class Visualizer:
def __init__(self, world):
self.fig = Fig()
self.world = world
self.traces = {}
#Scatter plots
for idx, c in world.countries.items():
pos_x = idx % self.world.nb_cols
pos_y = idx // self.world.nb_cols
subplot = self.fig[pos_y, pos_x]
data = np.array([c.x_coord, c.y_coord]).reshape(-1,2)
self.traces[idx] = subplot.plot(data, symbol='o', width=0, face_color=c.p_colors, title='Country {}'.format(idx+1))
def display(self):
for idx, c in self.world.countries.items():
data = np.array([c.x_coord, c.y_coord]).reshape(-1,2)
self.traces[idx].set_data(data, face_color=c.p_colors)
def update(self, event):
self.world.update(quarantine=False)
self.display()
def animation(self):
self.timer = app.Timer()
self.timer.connect(self.update)
self.timer.start(0)
self.start()
def start(self):
if (sys.flags.interactive != 1):
self.status = app.run()
if __name__ == '__main__':
w = World(move=0.001)
for i in range(8):
w.add_country(nb_S=500)
v = Visualizer(w)
v.animation()
The scatter plots "blink" each time their particles turn color, as with Bokeh. Am I doing something wrong?
Is there a more efficient way for real-time display, maybe using vispy.gloo or vispy.scene? (It is slower than pyqtgraph.opengl for the moment)
We can efficiently plot in real time by using vispy.gloo module to leverage the power of GPU. Here is one way of doing it :
1) Build a class that inherits vispy.app.Canvas class.
2) Create an OpenGL Program whose inputs are shaders. This object allows us to link our data to shader variables. Each dot on the canvas depends on these variable values (describing its coordinate, color, etc). For example, it is way harder for displaying text (titles, labels, etc) than with Matplotlib library. Here is a deeper explanation of the process.
3) Set a timer connected to the function we want to call repeatedly (real-time side).
The vispy.scene module, dedicated to the high-level visualization interfaces for scientists, is still experimental. Maybe this is the reason why my first code got some bugs.
Here is my new code.
I have a time history of images (2 + 1)D arrays that I take various slices of and examine using ipython and each view is a matplotlib figure.
I have a custom class that uses matplotlib widgets (specifically a Slider) to allow an interactive window to open and view the images frame by frame as selected by the Slider. The widget works fine, but uses the plt.show() command to block, which is also fine until I'm done with the widget.
In order for control to pass back to the ipython command line, I have to close all matplotlib figures--I would like to be able to only close the window associated with the widget. Is there some method to enable this functionality?
Something like fig.show(blocking=True) would be what I imagine I want, i.e. limit the blocking of the GUI mainloop to only look for plt.close() of that window, but that does not appear to be currently implemented.
#ImportanceOfBeingEarnest, thanks for the response. I've added the code I use for the viewer widget. To initialize the object, you just need to provide a 3D array of [frames (t), y, x] values. i.e.
randomData = np.random.rand((5,5,5))
class showFrames(object):
def __init__(self, timeData):
self.data = timeData # 3D array of t, y, x values
self.fig, self.ax = plt.subplots(1)
self.im = None
self.frameStr = None
self.start()
def start(self):
# initialize GUI
Tmin = self.data.min()
Tmax = self.data.max()
frameInit = self.data.shape[0] - 1
self.im = self.ax.imshow(self.data[frameInit])
self.im.set_clim(Tmin, Tmax)
self.fig.colorbar(self.im)
self.frameStr = self.ax.text(0.1, 0.1, '', transform=self.ax.transAxes, color='white')
axis_color = 'yellow'
# Add frame and radius slider for tweaking the parameters
frame_slider_ax = self.fig.add_axes([0.25, 0.05, 0.65, 0.03], axisbg=axis_color)
frame_slider = Slider(frame_slider_ax, 'Frame', 0, frameInit, valinit=frameInit)
frame_slider.on_changed(self.frame_slider_on_changed)
plt.show()
def frame_slider_on_changed(self, i):
self.im.set_data(self.data[int(i)])
self.frameStr.set_text(str(int(i)))
self.fig.canvas.draw_idle()
Your Slider instance is being garbage collected because you don't retain a reference to it.
From the Slider documentation:
For the slider to remain responsive you must maintain a reference to it.
In this case self.slider=Slider(...) instead of slider=Slider(...).
First project in qt.
I'm having trouble translating/rotating a rect along a line. Basically i would want to align the rect with the position of the line. When i change position of the circle the rect should translate along the line. See images below.
What i have at the moment
w_len = len(str(weight)) / 3 * r + r / 3
weight_v = Vector(r if w_len <= r else w_len, r)
weight_rectangle = QRectF(*(mid - weight_v), *(2 * weight_v))
painter.drawRect(weight_rectangle)
*mid is just a vector with coordinates at half of the link , weight_v is a vector based on the text size.
Any pointers , should i look at adding a translate to the painter ? Whenever i try to add translation to the painter it breaks the other shapes as well.
t = QTransform()
t.translate(-5 ,-5)
t.rotate(90)
painter.setTransform(t)
painter.drawRect(weight_rectangle)
painter.resetTransform()
Update:
With below answer i was able to fix the rotation. Many thanks, looks like my text is not displaying correctly.
I have the following code:
painter.translate(center_of_rec_x, center_of_rec_y);
painter.rotate(- link_paint.angle());
rx = -(weight_v[0] * 0.5)
ry = -(weight_v[1] )
new_rec = QRect(rx , ry, weight_v[0], 2 * weight_v[1])
painter.drawRect(QRect(rx , ry, weight_v[0] , 2 * weight_v[1] ))
painter.drawText(new_rec, Qt.AlignCenter, str(weight))
Update2:
All is fine , was a mistake in my code. I was taking the wrong link angle.
Thx.
Rotation is always done according to the origin point (0, 0), so you need to translate to the origin point of the rotation and then apply it.
Also, when applying any temporary change to the painter, save() and restore() should be used: in this way the current state of the painter is stored, and that state will be restored afterwards (including any transformation applied in the meantime). Painter states can be nested, and one could save multiple times to apply multiple "layers" of painter state modifications. Just remember that the all states must be restored to the base status before releasing (ending) the painter.
Since you didn't provide an MRE, I created a small widget to show how this works:
class AngledRect(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.setMinimumSize(200, 200)
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.setRenderHints(qp.Antialiasing)
contents = self.contentsRect()
# draw a line from the top left to the bottom right of the widget
line = QtCore.QLineF(contents.topLeft(), contents.bottomRight())
qp.drawLine(line)
# save the current state of the painter
qp.save()
# translate to the center of the painting rectangle
qp.translate(contents.center())
# apply an inverted rotation, since the line angle is counterclockwise
qp.rotate(-line.angle())
# create a rectangle that is centered at the origin point
rect = QtCore.QRect(-40, -10, 80, 20)
qp.setPen(QtCore.Qt.white)
qp.setBrush(QtCore.Qt.black)
qp.drawRect(rect)
qp.drawText(rect, QtCore.Qt.AlignCenter, '{:.05f}'.format(line.angle()))
qp.restore()
# ... other painting...
For simple transformations, using translate and rotate is usually enough, but the above is almost identical to:
transform = QtGui.QTransform()
transform.translate(contents.center().x(), contents.center().y())
transform.rotate(-line.angle())
qp.save()
qp.setTransform(transform)
# ...
I want to create a real-time, point plotting GUI. I am using the Scanse Sweep LiDAR, and at each sweep of this LiDAR (working between 1 - 10Hz) I receive approximately 1000 points (x, y) describing the LiDARs surrounding. This is a 2D LiDAR.
I have looked everywhere and tried countless of code snippets for pyqtgraph, but either it crashes, is super slow or doesn't work at all.
Is there a straight-forward way of creating a plotter window and upon each new scan/data delivery, push those points to the plotter window?
Thankful for any kind of help
It is unclear to me what exactly you want to do, so I assume that you want to make a scatter plot with a 1000 points that are refreshed 10 times a second. Next time please include your code so that we can reproduce your issues and see what you want to achieve.
In my experience PyQtGraph is the fastest option in Python. It can easily plot a 1000 points at 10 Hz. See the example below.
#!/usr/bin/env python
from PyQt5 import QtCore, QtWidgets
import pyqtgraph as pg
import numpy as np
class MyWidget(pg.GraphicsWindow):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.mainLayout = QtWidgets.QVBoxLayout()
self.setLayout(self.mainLayout)
self.timer = QtCore.QTimer(self)
self.timer.setInterval(100) # in milliseconds
self.timer.start()
self.timer.timeout.connect(self.onNewData)
self.plotItem = self.addPlot(title="Lidar points")
self.plotDataItem = self.plotItem.plot([], pen=None,
symbolBrush=(255,0,0), symbolSize=5, symbolPen=None)
def setData(self, x, y):
self.plotDataItem.setData(x, y)
def onNewData(self):
numPoints = 1000
x = np.random.normal(size=numPoints)
y = np.random.normal(size=numPoints)
self.setData(x, y)
def main():
app = QtWidgets.QApplication([])
pg.setConfigOptions(antialias=False) # True seems to work as well
win = MyWidget()
win.show()
win.resize(800,600)
win.raise_()
app.exec_()
if __name__ == "__main__":
main()
The way it works is as follows. By plotting an empty list a PlotDataItem is created. This represents a collection of points. When new data points arrive, the setData method is used to set them as the data of the PlotDataItem, which removes the old points.
I want to implement a fast scrolling timetrace tool in python. The timetrace data is already all in memory in a numpy array and is big (>1e6 samples). I need a tool for quick visual inspection.
I already tried using Matplotlib+PySide but the update speed is not fast enough.
Can you reproduce the Matplotlib+Pyside demo in another toolkit like pygraphqt/chaco/quiqwt? I don't know any of them and I'm willing to learn the one that perform better in this application.
To be useful in my workflow, the chosen framework should allow to run the plot from an interactive ipython session and should be fast and extensible (eventually I will need several plots scrolled in sync on the same windows). In principle pyqtgraph, guiqwt or chaco all seem good candidates. But let judge on a real example.
Thanks.
Here's the pyqtgraph version. I tried to keep the code as similar as I could to the original demo. On my system, pyqtgraph only runs about 5x faster than matplotlib, and is still pretty slow (~1fps) when all of the data is visible. The major performance differences between matplotlib and pyqtgraph are in throughput--how rapidly new data can be plotted.
For better performance, I'd recommend looking at some of the GPU-based plotting libraries like visvis or galry. Pyqtgraph will be adding GPU support in the future, but it's not there yet. There are some efforts to bring matplotlib to the GPU as well, but I haven't seen any results from that yet..
## adapted from http://stackoverflow.com/questions/16824718/python-matplotlib-pyside-fast-timetrace-scrolling
from PySide import QtGui, QtCore
import numpy as np
import pyqtgraph as pg
N_SAMPLES = 1e6
def test_plot():
time = np.arange(N_SAMPLES)*1e-3
sample = np.random.randn(N_SAMPLES)
plt = pg.PlotWidget(title="Use the slider to scroll and the spin-box to set the width")
plt.addLegend()
plt.plot(time, sample, name="Gaussian noise")
q = ScrollingToolQT(plt)
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.plotItem.vb.childrenBounds()[0]
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
self.win = QtGui.QMainWindow()
self.win.show()
self.win.resize(800,600)
self.win.setCentralWidget(fig)
self.toolbar = QtGui.QToolBar()
self.win.addToolBar(QtCore.Qt.BottomToolBarArea, self.toolbar)
# Create the slider and spinbox for x-axis scrolling in toolbar
self.set_slider(self.toolbar)
self.set_spinbox(self.toolbar)
# Set the initial xlimits coherently with values in slider and spinbox
self.set_xlim = self.fig.setXRange
self.set_xlim(0, self.step)
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, padding=0)
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.fig.plotItem.vb.viewRange()[0]
self.xpos_changed(old_xlim[0] * self.scale)
if __name__ == "__main__":
app = pg.mkQApp()
q = test_plot()
app.exec_()