Dynamically rotate TextItem in pyqtgraph - python

I want to dynamically rotate TextItem but cannot get it to work. Changing position or anchor with setPos and setAnchor updates the item, but wanting to change angel with setAngle doesn't update the text. The strangest thing is that it does update once I drag the canvas.
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
app = QtGui.QApplication([])
w = pg.GraphicsView()
w.show()
w.resize(800,800)
view = pg.ViewBox()
w.setCentralItem(view)
view.setAspectLocked(True)
view.setRange(QtCore.QRectF(0, 0, 200, 200))
anchor = pg.TextItem()
anchor.setText("hey")
anchor.setColor(QtGui.QColor(255, 255, 255))
view.addItem(anchor)
def rotate():
x, y = anchor.pos()
anchor.setPos(x + 1, y + 1)
anchor.setAngle(anchor.angle + 10)
timer = QtCore.QTimer()
timer.timeout.connect(rotate)
timer.start(1000)
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
I am wondering what signal or function to call so that the item updates immediately.

It ended up being a bug. I solved it by just removing the old TextItem and creating a new one with the updatet angle.

Related

How to enable legends and change style in pyqtgraph?

Using pyQt5 I am continuously updating a plot with data using the self.graphicsView.clear() followed by self.graphicsView.plot() functions.
I changed the background color with the command pyqtgraph.setConfigOption('background', '#f0f0f0') before the widget is created, however, this does not apply to the legend items. The background is grey, and the legend appears black.
How do I change the style of this legend item?
I think I am implementing this wrong based upon how I reference each new plot item. I believe these need to be instantiated somehow, but the instance = self.graphicsView.plot(title = "example title") then referencing with instance.LegendItem and then access it with an HTML like tag. (Unable to find the reference anymore)
def plotGraph(self, value):
"""
plots value to graph
"""
self.graphQueue(self.plotDataBuffer, value) #buffered data input, max vals = value
self.graphicsView.clear() #clear data for continuous plot
self.graphicsView.addLegend()
self.graphicsView.plot(self.plotDataBuffer, pen='r', name='Data') #plot item
Note: This function is called in a loop
The addLengend() command creates a new legend each time it is called, however, my understanding is that this is only created once and if it is called again it only references the legend that was already created?
So, how do i properly initialize the legends once, and then format the style to match the background instead of black?
Simplified example:
import pyqtgraph as pg
from PyQt5 import QtGui
import numpy as np
import sys
pg.setConfigOption('background', '#f0f0f0')
plotWidget = pg.plot(title="Stackoverflow Simplified Example")
app = QtGui.QApplication(sys.argv)
while(1):
x = np.arange(50)
y = np.random.normal(size=(3, 50))
plotWidget.clear()
plotWidget.addLegend()
for i in range(3):
plotWidget.plot(x, y[i], pen=(i,3), name = "test {}".format(i))
app.processEvents()
if __name__ == '__main__':
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
app.exec_() # Start QApplication event loop ***
Checking the pyqtgraph LegendItem.py, one would find that the color is hard-coded into the paint function. You can replace that function with your user-defined function and change the color.
import pyqtgraph as pg
from PyQt5 import QtGui
import numpy as np
import sys
import types
pg.setConfigOption('background', '#f0f0f0')
plotWidget = pg.plot(title="Stackoverflow Simplified Example")
app = QtGui.QApplication(sys.argv)
leg = plotWidget.addLegend()
# replace legend paint
def paint(self, p, *args):
p.setPen(pg.mkPen(255,0,0,100))
p.setBrush(pg.mkBrush(0,200,0,50))
p.drawRect(self.boundingRect())
leg.paint = types.MethodType(paint,leg)
plotData = [0]*3
for i in range(3):
plotData[i] =plotWidget.plot([0],[0], pen=(i,3), name = "test {}".format(i))
while(1):
x = np.arange(50)
y = np.random.normal(size=(3, 50))
for i in range(3):
plotData[i].setData(x, y[i])
app.processEvents()
if __name__ == '__main__':
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
app.exec_() # Start QApplication event loop ***
If I understand correctly, you're wondering how to change the background color of a legend. Or at least that's how I stumbled upon this question.
I found that we can use the brush argument:
plotWidget.addLegend(brush=pg.mkBrush(255, 255, 255, 100))
where the first three values are your RGB color code and the last number is the alpha value that determines how transparent the background will be. In this example, the legend will be made with a white, somewhat transparent background.

Window updates only when it resizes in PyQt5

The application should display an image from a grid of pixels. The color of all pixels should change 30 times per second. After starting the app works for a few seconds and after that the pixels updates will stop. When window resized the pixels updating resumes. With a long-term update of the pixel network, the CPU consumption increases greatly. I tested it on Windows and there the pixel update stops almost immediately. Used the Threading library, and the PyQt5 library to display the interface. How can I make a stable pixels updates in grid?
Here is my code:
from random import choice, randint
from sys import argv
from threading import Thread
from time import sleep
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtGui import QIcon, QPalette
from PyQt5.QtWidgets import (QApplication, QFrame, QGridLayout, QMainWindow,
QMenu, QToolBar, QWidget)
class EmulatorWindow(QMainWindow):
spacing = None
app_running = True
def __init__(self, spacing=1, screen_resolution=(16, 16)):
super().__init__()
self.spacing = spacing
# Pixel Grid
self.grid = QGridLayout()
self.grid.setContentsMargins(0, 0, 0, 0)
self.grid.setSpacing(self.spacing)
for x in range(0, screen_resolution[0]):
for y in range(0, screen_resolution[1]):
pixel = QWidget()
pixel.setAutoFillBackground(True)
self.grid.addWidget(pixel, y, x)
# Application thread
self.applicationThread = Thread(target=self.applicationRunner, args=())
self.applicationThread.start()
# Window Properties
self.setGeometry(300, 300, 450, 495)
self.setWindowTitle('Pixels Grid')
widget = QWidget()
widget.setLayout(self.grid)
self.setCentralWidget(widget)
self.setMinimumSize(QSize(450, 495))
self.show()
def applicationRunner(self):
color = 0
while True:
if self.app_running == False:
break
for x in range(0, 16):
for y in range(0, 16):
self.grid.itemAtPosition(x, y).widget().setPalette(QPalette([Qt.red, Qt.blue, Qt.green][color]))
sleep(1 / 30)
color = color + 1
if color == 3:
color = 0
def switchSpacing(self):
self.grid.setSpacing(self.spacing if self.grid.spacing() == 0 else 0)
if __name__ == '__main__':
app = QApplication(argv)
ex = EmulatorWindow()
app.exec_()
ex.app_running = False
Activity Monitor Screenshot
In the screenshot is MenuBar and ToolBar, but, they do not affect the problem
Application Screenshot
The reason why the GUI is not updated with the thread is that Qt prohibits the updating of graphic elements from another thread, for more information read GUI Thread and Worker Thread. A thread should not be used if the task is not heavy, for example if we test when it consumes changing a color using the following code:
t = QtCore.QElapsedTimer()
t.start()
pal = QtGui.QPalette([QtCore.Qt.red, QtCore.Qt.blue, QtCore.Qt.green][self._color])
for x in range(self.grid.rowCount()):
for y in range(self.grid.columnCount()):
w = self.grid.itemAtPosition(x, y).widget()
if w is not None:
w.setPalette(pal)
self._color = (self._color +1) % 3
print(t.elapsed(), " milliseconds")
Obtaining the following results:
4 milliseconds
2 milliseconds
2 milliseconds
3 milliseconds
2 milliseconds
3 milliseconds
3 milliseconds
2 milliseconds
3 milliseconds
# ...
Supporting my statement that it is not a heavy task so in this case you should use a QTimer that allows you to do periodic tasks:
from PyQt5 import QtCore, QtGui, QtWidgets
class EmulatorWindow(QtWidgets.QMainWindow):
def __init__(self, spacing=1, screen_resolution=(16, 16)):
super().__init__()
self.spacing = spacing
# Pixel Grid
self.grid = QtWidgets.QGridLayout()
self.grid.setContentsMargins(0, 0, 0, 0)
self.grid.setSpacing(self.spacing)
for x in range(screen_resolution[0]):
for y in range(screen_resolution[1]):
pixel = QtWidgets.QWidget(autoFillBackground=True)
self.grid.addWidget(pixel, y, x)
# Window Properties
self.setGeometry(300, 300, 450, 495)
self.setWindowTitle('Pixels Grid')
widget = QtWidgets.QWidget()
self.setCentralWidget(widget)
widget.setLayout(self.grid)
self.setMinimumSize(QtCore.QSize(450, 495))
self._color = 0
timer = QtCore.QTimer(self, interval=1000/30, timeout=self.applicationRunner)
timer.start()
#QtCore.pyqtSlot()
def applicationRunner(self):
pal = QtGui.QPalette([QtCore.Qt.red, QtCore.Qt.blue, QtCore.Qt.green][self._color])
for x in range(self.grid.rowCount()):
for y in range(self.grid.columnCount()):
w = self.grid.itemAtPosition(x, y).widget()
if w is not None:
w.setPalette(pal)
self._color = (self._color +1) % 3
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
ex = EmulatorWindow()
ex.show()
sys.exit(app.exec_())

pyqtgraph - move origin of ArrowItem to local center

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)

pyqtgraph : how to know the size of a TextItem?

I'm using pyqtgraph to plot graphics where I put some text (for example the max of the data...)
My question is how to know the size (height, width) of a TextItem input? It's for controling its place for avoiding being out of the window.
For example my code could be this :
# -*- coding: utf-8 -*-
from pyqtgraph.Qt import QtGui, QtCore
import numpy as np
import pyqtgraph as pg
app = QtGui.QApplication([])
win = pg.GraphicsWindow()
pg.setConfigOptions(antialias=True)
voieTT=np.ones(100)
voieTemps=np.ones(100)
for i in range(100):
voieTT[i]=np.random.random_sample()
voieTemps[i]=i
signemax=np.max(voieTT)
tmax=np.where(voieTT==np.max(voieTT))[0][0]
grapheTT = win.addPlot(enableMenu=False)
grapheTTVB=grapheTT.getViewBox()
grapheTT.plot(voieTemps, voieTT)
grapheTT.plot([tmax],[signemax],symbol='o',symbolSize=8)
grapheTT.showGrid(x=True, y=True)
textVmax = pg.TextItem(anchor=(0.5,1.5), border='w', fill=(0,0,255))
textVmax.setHtml('<div style="text-align: center"><span style="color: #FFF;">Vmax=%0.1f mm/s # %0.1f s</span></div>'%(np.abs(signemax),tmax))
grapheTT.addItem(textVmax)
textVmax.setPos(tmax, signemax)
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
#changed original code to simple plot() because manual window structure
#somehow didn't automatically cooperate with viewbox' map methods
from pyqtgraph.Qt import QtGui, QtCore
import numpy as np
import pyqtgraph as pg
voieTT=np.ones(100)
voieTemps=np.ones(100)
for i in range(100):
voieTT[i]=np.random.random_sample()
voieTemps[i]=i
signemax=np.max(voieTT)
tmax=np.where(voieTT==np.max(voieTT))[0][0]
grapheTT = pg.plot(enableMenu=False)
grapheTTVB=grapheTT.getViewBox()
grapheTT.plot(voieTemps, voieTT)
grapheTT.plot([tmax],[signemax],symbol='o',symbolSize=8)
grapheTT.showGrid(x=True, y=True)
textVmax = pg.TextItem(anchor=(0.5,1.5), border='w', fill=(0,0,255))
textVmax.setHtml('<div style="text-align: center"><span style="color: #FFF;">Vmax=%0.1f mm/s # %0.1f s</span></div>'%(np.abs(signemax),tmax))
grapheTT.addItem(textVmax)
textVmax.setPos(tmax, signemax)
#DISCLAIMER: there's likely a better way, but this works too:
#you can get pixel dimensions x,y,w,h from bounding rectangle
br = textVmax.boundingRect()
print br
#and scale them into viewbox data dimensions
sx, sy = grapheTTVB.viewPixelSize() #scaling factors
print sx, sy
print sx*br.width(), sy*br.height()
#ALSO, you can just use ViewBox autorange
#ViewBox autoranges based on its contained items' dataBounds() info
#however TextItem.py is a UIGraphicsItem.py with dataBounds() return None
"""Called by ViewBox for determining the auto-range bounds.
By default, UIGraphicsItems are excluded from autoRange."""
#so, following example of... ArrowItem.py
#here is a simple dataBounds() for TextItem
def Autoranging_TextItem_dataBounds(axis, frac=1.0, orthoRange=None):
br = textVmax.boundingRect()
sx, sy = grapheTTVB.viewPixelSize()
w, h = sx*br.width(), sy*br.height()
if axis == 0: return [-w/2, w/2]
if axis == 1: return [0, h]
textVmax.dataBounds = Autoranging_TextItem_dataBounds
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

Margins in PyQtGraph's GraphicsLayout

Having a simple graphics layout with PyQtGraph:
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
app = QtGui.QApplication([])
view = pg.GraphicsView()
l = pg.GraphicsLayout(border='g')
view.setCentralItem(l)
view.show()
view.resize(800,600)
l.addPlot(0, 0)
l.addPlot(1, 0)
l.layout.setSpacing(0.)
l.setContentsMargins(0., 0., 0., 0.)
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()
Whichs outputs:
How could I get rid of the small margins which are between the green external line and the window borders?
I could do the trick and use l.setContentsMargins(-10., -10., -10., -10.), and that works:
But it seems to me like a dirty trick and there should be another parameter which is setting that margin. Could this be possible? Is there another margin parameter which I could set to 0 to get the same results?
I think this might be a Qt bug. There's an easy workaround:
l = pg.GraphicsLayout()
l.layout.setContentsMargins(0, 0, 0, 0)
To understand this, let's look at a modified example:
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
app = QtGui.QApplication([])
view = pg.GraphicsView()
view.show()
view.resize(800,600)
class R(QtGui.QGraphicsWidget):
# simple graphics widget that draws a rectangle around its geometry
def boundingRect(self):
return self.mapRectFromParent(self.geometry()).normalized()
def paint(self, p, *args):
p.setPen(pg.mkPen('y'))
p.drawRect(self.boundingRect())
l = QtGui.QGraphicsGridLayout()
r1 = R()
r2 = R()
r3 = R()
r1.setLayout(l)
l.addItem(r2, 0, 0)
l.addItem(r3, 1, 0)
view.scene().addItem(r1)
In this example, calling l.setContentsMargins(...) has the expected effect, but calling r1.setContentsMargins(...) does not. The Qt docs suggest that the effect should have been the same, though: http://qt-project.org/doc/qt-4.8/qgraphicswidget.html#setContentsMargins
For anyone going through this in 2022, use a pg.GraphicsLayoutWidget :
# GraphicsLayoutWidget is now recommended
w = pg.GraphicsLayoutWidget(border=(30,20,255))
win.centralWidget.layout.setContentsMargins(0,0,0,0)
win.centralWidget.layout.setSpacing(0)
Notice how there is no spacing between each blue border of each plot :
A little update.
self.graphicsView = pg.GraphicsLayoutWidget(self)
self.graphicsView.ci.layout.setContentsMargins(0, 0, 0, 0)
self.graphicsView.ci.layout.setSpacing(0)
For those who are using pyqtgraph.jupyter.GraphicsLayoutWidget, the layout object is a variable named gfxLayout
from pyqtgraph.jupyter import GraphicsLayoutWidget
win = GraphicsLayoutWidget()
win.gfxLayout.setContentsMargins(10,10,10,10)
win.gfxLayout.setSpacing(0)

Categories