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.
Related
I have trouble using pygtgraph scrolling plots
Expected Results
The expected results are quite similar to the pyqtgraph-examples-scrolling plots-plot5
X-values are times, which can be generated by a simple function. Y-Values are random values.
Each 10 seconds samples as one chunk and each plot can have max. 30 seconds samples, which means 3 chunks. The current plot window only shows the latest 10 seconds samples
For example, now there are total 60 seconds samples:
Data between 50s-60s will be viewed in the current window
Data between 30s-50s could be viewed by using the mouse to drag the x-axis backward
Data between 0-30s will not be displayed
My Code
My current code is below, it can only show latest 30s data.
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
import random
win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('Scrolling Plots')
p1 = win.addPlot()
p1.setYRange(0,10)
xVal = [0]
yVal = [0]
def genTime(): # used to generate time
t = 0
while True:
t += np.random.random_sample()
yield t
t = np.ceil(t)
xTime = genTime()
#=====================================================
viewSize = 10 # current window show only latest 10s data
plotSize = 30 # plot 30s data -> 3 chunk
lstCurves = [] # List for Curves
def update():
global p1, xVal, yVal, lstCurves
#for c in lstCurves:
# c.setPos(xVal[-1], 0)
i = np.ceil(xVal[-1]) % viewSize # e.g. when time is 9.2s -> one 10s view size is full, append to curves list as one chunk
if i == 0:
curve = p1.plot()
lstCurves.append(curve)
xValLast = xVal[-1]
yValLast = yVal[-1]
xVal = [xValLast]
yVal = [yValLast]
while len(lstCurves) > 3: # max 3 chunk (30 s)
p1.removeItem(lstCurves.pop(0)) # remove the oldest 10s
else:
curve = lstCurves[-1] # latest 10s curve
xVal.append(next(xTime))
yVal.append(random.randint(0,9))
curve.setData(xVal, yVal)
print(len(lstCurves))
#======================================================
timer = pg.QtCore.QTimer()
timer.timeout.connect(update)
timer.start(1000)
## 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_()
Problem
I have tried using curve.setPos(xx, 0), It looks like the whole curve is moving along the x-axis, but the mapping relationship between X-value and Y-value is broken
I have also tried using setXRange() to dynamically change x-axis display-range in update() func. But in this case, I can't use the mouse to drag the x-axis back to view the old data any more.
My English is not good, I hope you can understand my question. Any suggestions would be sincerely appreciated!
Problem
The reasons your code don't do what you want are:
When you drag to view the other chunks, you disable the automatic auto-range of the plot, after that, you will have to manually drag the plot every time you want to see the new data. Also, by default, the auto-range of the plot will cover all the data that you are plotting.
When you use the setRange() method inside the update function, it will force that range every time you add another value to the data. Then the drag will not work as you want
What can you do
Well, from my perspective using the mouse drag to visualize the other data is not very convenient, I suggest to use an external widget to control the range of the data you want to view, like, a slider, scroll bar, spin box, ...
A QScrollBar can do the job and it will look esthetic in a GUI.
Before my Alternative Solution, I have a suggestion for you:
Use objects to create your widget, generate a class and use the variables as attributes, with this you avoid the use of the keyword global, and you could reuse the widget for other purposes.
Alternative Solution
Try this:
import sys
import random
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
class MyApp(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
## Creating the Widgets and Layouts
self.plot_widget = pg.PlotWidget()
self.layout = QtGui.QVBoxLayout()
self.sbutton = QtGui.QPushButton("Start / Continue")
self.ebutton = QtGui.QPushButton("Stop")
self.timer = pg.QtCore.QTimer()
self.scroll = QtGui.QScrollBar(QtCore.Qt.Horizontal)
## Creating the variables and constants
self.data = [[0], [random.randint(0,9)]] ## [xVal, yVal] to create less variables
self.plot_item = self.plot_widget.plot(*self.data)
self.plot_widget.setYRange(0, 10)
self.xTime = self.genTime()
self.vsize = 10
self.psize = 30
## Building the Widget
self.setLayout(self.layout)
self.layout.addWidget(self.sbutton)
self.layout.addWidget(self.ebutton)
self.layout.addWidget(self.plot_widget)
self.layout.addWidget(self.scroll)
## Changing some properties of the widgets
self.plot_widget.setMouseEnabled(x=False, y=False)
self.ebutton.setEnabled(False)
self.scroll.setEnabled(False)
self.scroll.setMaximum(self.psize-self.vsize)
self.scroll.setValue(self.psize-self.vsize)
## Coneccting the signals
self.sbutton.clicked.connect(self.start)
self.ebutton.clicked.connect(self.stop)
self.timer.timeout.connect(self.update)
self.scroll.valueChanged.connect(self.upd_scroll)
def genTime(self): # used to generate time
t = 0
while True:
t += np.random.random_sample()
yield t
t = np.ceil(t)
def upd_scroll(self):
val = self.scroll.value()
xmax = np.ceil(self.data[0][-1+self.vsize-self.psize+val])-1
xmin = xmax-self.vsize
self.plot_widget.setXRange(xmin, xmax)
def update(self):
num = len(self.data[0])
if num <= self.psize:
self.plot_item.setData(*self.data)
else:
self.plot_item.setData(
self.data[0][-self.psize:],
self.data[1][-self.psize:]
)
if num == self.vsize:
self.scroll.setEnabled(True)
self.data[0].append(next(self.xTime))
self.data[1].append(random.randint(0,9))
if num > self.vsize :
self.upd_scroll()
def start(self):
self.sbutton.setEnabled(False)
self.ebutton.setEnabled(True)
self.timer.start(100)
def stop(self):
self.sbutton.setEnabled(True)
self.ebutton.setEnabled(False)
self.timer.stop()
self.upd_scroll()
def closeEvent(self, event):
self.timer.stop()
event.accept()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = MyApp()
window.show()
sys.exit(app.exec_())
It may look like this:
I'm writing a medium-sized application to review some data. The structure is that plots will be held in a QTabWidget interface, with a plot control widget to adjust x limits (and later, there may be more features in the control widget). I have included a minimum reproducible example below.
Currently, I pass the axis of a figure to my control widget, and within that widget change the x limits after clicking a button. I have verified that the axis is being passed to the widget (with a print statement). I can programmatically set the axis limits (see line 91: self.Fig_ax.set_xlim(5, 20000) ) in the widget __init__ function and it works, but in the button click function, that same syntax does not do anything. However, with print statements, I verified that the axis is still being passed to the button click function.
I am very confused as to why the set_xlims method works in __init__ but not upon button press. Use: Run the code, enter a number in the X Min and X Max fields, click the Apply X Limits button. For the sake of the example, I hardcoded the button click axis shift to have defined limits rather than use what is entered into the fields, but those fields do get printed to the console for debugging purposes.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created
"""
import sys
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
## import matplotlib and animation
import functools
import random as rd
import numpy as np
from numpy import array, sin, pi, arange
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
import pandas as pd
## import threading
import time
from matplotlib.backends.qt_compat import QtCore, QtWidgets
from matplotlib.backends.backend_qt5agg import (FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
## New:
from PyQt5.QtWebEngineWidgets import *
######################################################8
class AppWindow(QMainWindow):
def __init__(self):
super().__init__()
self.title = 'DDBRT'
self.setWindowTitle(self.title)
self.DDBRT_Widget = DDBRT(self) # Call the DDBRT
self.setCentralWidget(self.DDBRT_Widget) # set it as the central widget in the window
self.show()
####
####
''' End AppWindow '''
# August 27 2019 Start building a custom QWidget that can be put into the tool in multiple instances to adjust xlims.
# This may also serve as a templatge for other custom widgets that can go in
class XLimControlWidget(QWidget):
def __init__(self, parent, **kwargs):
super(QWidget, self).__init__(parent)
# set layout:
self.XLCWLayout = QVBoxLayout(self)
# Insert X Min Box Label
self.XMinSelectLbl = QLabel('Set X Min:')
self.XLCWLayout.addWidget(self.XMinSelectLbl)
# Insert X Min Entry Field
self.XMinEntryField = QLineEdit('X Min')
self.XLCWLayout.addWidget(self.XMinEntryField)
# Insert X Max Box Label
self.XMaxSelectLbl = QLabel('Set X Min:')
self.XLCWLayout.addWidget(self.XMaxSelectLbl)
# Insert X Max Box Entry Field
self.XMaxEntryField = QLineEdit('X Max')
self.XLCWLayout.addWidget(self.XMaxEntryField)
# Insert Set Button
self.SetXLimsBtn = QPushButton('Apply X Limits')
self.XLCWLayout.addWidget(self.SetXLimsBtn)
# Adjust layout so this widget is compact:
self.XLCWLayout.setSpacing(0)
self.XLCWLayout.setContentsMargins(0, 0, 0, 0)
# Note, that doesn't actually work and it still looks ugly
# That's annoying, but not worth figuring out how to fix right now.
# Need to focus on programming the behavior.
# Try out the kwargs pass to make sure passing something works.
for key, value in kwargs.items():
print('%s = %s' %(key, value))
####
self.Fig_ax = kwargs['Fig_ax_Key']
print('self.Fig_ax = %s of type %s' %(self.Fig_ax, type(self.Fig_ax)))
# Try the fig ax set xlim, which does work but doesn't.
self.Fig_ax.set_xlim(5, 20000)
self.SetXLimsBtn.clicked.connect(self.SetXLimsBtnClkd)
####
def SetXLimsBtnClkd(self): # Define what happens when the button is clicked.
self.xmin = float(self.XMinEntryField.text())
print('X Min will be ', self.xmin, ' of type ', type(self.xmin))
self.xmax = float(self.XMaxEntryField.text())
print('X Max will be ', self.xmax, ' of type ', type(self.xmax))
print('self.Fig_ax = %s of type %s' %(self.Fig_ax, type(self.Fig_ax)))
self.Fig_ax.set_xlim(20, 45)
# End desired goal:
# self.Fig_ax.set_xlim(self.xmin, self.xmax)
####
####
class DDBRT(QWidget):
def __init__(self, parent):
super(QWidget, self).__init__(parent)
#%% Set up multithreading
self.threadpool = QThreadPool() # Set up QThreadPool for multithreading so the GIL doesn't freeze the GUI
print("Multithreading with maximum %d theads" % self.threadpool.maxThreadCount())
#%% Layout:
## Set layout
self.MainLayout = QGridLayout(self)
## Create and embed CheckBox Placeholder
self.ChkBx_Placeholder = QCheckBox('ChkBxPlcholdr1');
self.MainLayout.addWidget(self.ChkBx_Placeholder, 3, 0)
## Create and embed tab container to hold plots, etc.
# Initialize tab container
self.TabsContainer = QTabWidget()
# Initialize tabs
self.tab0 = QWidget()
# Add tabs
self.TabsContainer.addTab(self.tab0, "Tab 0")
# Populate 0th tab
self.tab0.layout = QGridLayout(self)
self.pushButton0 = QPushButton("PyQt5 button")
self.tab0.layout.addWidget(self.pushButton0)
self.tab0.setLayout(self.tab0.layout)
# Add TabsContainer to widget
self.MainLayout.addWidget(self.TabsContainer, 3, 1) # self.MainLayout.addWidget(self.TabsContainer, 2, 2) # Works just fine too, but I can worry about layout finessing later because it's not that difficult, important, or urgent right now
self.setLayout(self.MainLayout)
#%% Plot XLs (accelerations)
XL_t = np.arange(0, 200000, 1)
XL_X = np.sin(XL_t/20000)
XL_Y = np.sin(XL_t/2000)
XL_Z = np.sin(XL_t/200)
self.tab8 = QWidget()
self.TabsContainer.addTab(self.tab8, "Tab 8: Acceleration mpl subplots")
self.tab8.layout = QHBoxLayout(self)
self.XL_Fig = Figure()
self.XL_X_ax = self.XL_Fig.add_subplot(3, 1, 1)
self.XL_X_ax.plot(XL_t, XL_X)
self.XL_X_ax.set_title('Acceleration X')
# self.XL_X_ax.grid(True)
self.XL_X_ax.set_xlabel('Time (s)')
self.XL_X_ax.set_ylabel('Acceleration')
#
self.XL_Y_ax = self.XL_Fig.add_subplot(3, 1, 2, sharex=self.XL_X_ax)
self.XL_Y_ax.plot(XL_t, XL_Y)
self.XL_Y_ax.set_title('Acceleration Y')
# self.XL_Y.grid(True)
self.XL_Y_ax.set_xlabel('Time (s)')
self.XL_Y_ax.set_ylabel('Acceleration')
#
self.XL_Z_ax = self.XL_Fig.add_subplot(3, 1, 3, sharex=self.XL_X_ax)
self.XL_Z_ax.plot(XL_t, XL_Z)
self.XL_Z_ax.set_title('Acceleration Z')
# self.XL_Z.grid(True)
self.XL_Z_ax.set_xlabel('Time (s)')
self.XL_Z_ax.set_ylabel('Acceleration')
#
self.XL_Canvas = FigureCanvas(self.XL_Fig)
self.XL_Canvas.print_figure('test')
# # Create an XLPlot container widget and add the canvas and navigation bar to it
self.XL_PlotContainer = QWidget()
self.XL_PlotContainer.layout = QVBoxLayout(self)
self.XL_PlotContainer.layout.addWidget(self.XL_Canvas)
self.XLMPLToolbar = NavigationToolbar(self.XL_Canvas, self)
self.XL_PlotContainer.layout.addWidget(self.XLMPLToolbar, 3)
self.XL_PlotContainer.setLayout(self.XL_PlotContainer.layout) # Looks redundant but it's needed to display the widgets
# add XLPlotContainer Widget to tab
self.tab8.layout.addWidget(self.XL_PlotContainer, 1)
self.tab8.setLayout(self.tab8.layout)
# add XLCWidget to tab
self.kwargs = {"Fig_ax_Key": self.XL_X_ax}
self.XLXLCW = XLimControlWidget(self, **self.kwargs)
self.tab8.layout.addWidget(self.XLXLCW)
####
####
#%%
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = AppWindow()
sys.exit(app.exec_())
I expect the button click to change the axes on all subplots (since I linked the subplots when I set them up), but the x limits do not change at all. The button click function does run, as shown by the print statements.
In case anyone else finds this question later, here is the solution that worked: I passed the entire figure to my control widget, used a = self.Fig.get_axes() to get a list of my axes, and a[0].set_xlim(20, 200) ; self.Fig.canvas.draw_idle() to update the figure with the button click.
I'm trying to do something similar to what is done in the pyqtgraph example 'Crosshair/Mouse Interaction'. Basically I want to connect a linear region item on one plot, to the x-axis on another plot. then one plot will show the data that's in the linearregionitem, and you can zoom in and out by changing the linearregionitem, and vice-versa.
My problem is that it crashes with:
RecursionError: maximum recursion depth exceeded while calling a
Python object
Here is the code from the example if you want to try it to give you an idea of what I want to do...
"""
Demonstrates some customized mouse interaction by drawing a crosshair that follows
the mouse.
"""
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.Point import Point
#generate layout
app = QtGui.QApplication([])
win = pg.GraphicsWindow()
win.setWindowTitle('pyqtgraph example: crosshair')
label = pg.LabelItem(justify='right')
win.addItem(label)
p1 = win.addPlot(row=1, col=0)
p2 = win.addPlot(row=2, col=0)
region = pg.LinearRegionItem()
region.setZValue(10)
# Add the LinearRegionItem to the ViewBox, but tell the ViewBox to exclude this
# item when doing auto-range calculations.
p2.addItem(region, ignoreBounds=True)
#pg.dbg()
p1.setAutoVisible(y=True)
#create numpy arrays
#make the numbers large to show that the xrange shows data from 10000 to all the way 0
data1 = 10000 + 15000 * pg.gaussianFilter(np.random.random(size=10000), 10) + 3000 * np.random.random(size=10000)
data2 = 15000 + 15000 * pg.gaussianFilter(np.random.random(size=10000), 10) + 3000 * np.random.random(size=10000)
p1.plot(data1, pen="r")
p1.plot(data2, pen="g")
p2.plot(data1, pen="w")
def update():
region.setZValue(10)
minX, maxX = region.getRegion()
p1.setXRange(minX, maxX, padding=0)
region.sigRegionChanged.connect(update)
def updateRegion(window, viewRange):
rgn = viewRange[0]
region.setRegion(rgn)
p1.sigRangeChanged.connect(updateRegion)
region.setRegion([1000, 2000])
#cross hair
vLine = pg.InfiniteLine(angle=90, movable=False)
hLine = pg.InfiniteLine(angle=0, movable=False)
p1.addItem(vLine, ignoreBounds=True)
p1.addItem(hLine, ignoreBounds=True)
vb = p1.vb
def mouseMoved(evt):
pos = evt[0] ## using signal proxy turns original arguments into a tuple
if p1.sceneBoundingRect().contains(pos):
mousePoint = vb.mapSceneToView(pos)
index = int(mousePoint.x())
if index > 0 and index < len(data1):
label.setText("<span style='font-size: 12pt'>x=%0.1f, <span style='color: red'>y1=%0.1f</span>, <span style='color: green'>y2=%0.1f</span>" % (mousePoint.x(), data1[index], data2[index]))
vLine.setPos(mousePoint.x())
hLine.setPos(mousePoint.y())
proxy = pg.SignalProxy(p1.scene().sigMouseMoved, rateLimit=60, slot=mouseMoved)
#p1.scene().sigMouseMoved.connect(mouseMoved)
## 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_()
If you don't want to read all that, the linearregionitem and the plotitem are connected via the lines...
def update():
region.setZValue(10)
minX, maxX = region.getRegion()
p1.setXRange(minX, maxX, padding=0)
region.sigRegionChanged.connect(update)
def updateRegion(window, viewRange):
rgn = viewRange[0]
region.setRegion(rgn)
p1.sigRangeChanged.connect(updateRegion)
Here's a minimal working example of my code...I'm doing pretty much the same thing, but I'm doing it in a class...
When you run it, it will crash if you adjust the linearregionitem, or if you change the axis of plotA. If you comment out either of the 'connect' lines, then the program will work (half-way).
import pyqtgraph as pg
import sys
# PyQt5 includes
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication
class MyApplicationWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.__buildUI()
def __buildUI(self):
plotWidget = pg.GraphicsLayoutWidget()
self.PlotA = pg.PlotItem()
self.PlotA.setXRange(10, 20)
self.PlotB = pg.PlotItem()
self.PlotB.setXRange(0, 100)
self.lri = pg.LinearRegionItem()
self.lri.setRegion((10, 20))
self.PlotB.addItem(self.lri)
# The following two connections set up a recursive loop
self.lri.sigRegionChanged.connect(self.update)
self.PlotA.sigRangeChanged.connect(self.update_lri)
plotWidget.addItem(self.PlotA)
plotWidget.nextRow()
plotWidget.addItem(self.PlotB)
self.setCentralWidget(plotWidget)
self.show()
def update(self):
minX, maxX = self.lri.getRegion()
self.PlotA.setXRange(minX, maxX)
def update_lri(self, window, viewRange):
A_xrange = viewRange[0]
self.lri.setRegion(A_xrange)
if __name__ == '__main__':
app = QApplication(sys.argv)
widget = MyApplicationWindow()
sys.exit(app.exec_())
What's happening? Can anyone tell me how to get this working? This is in Python 3.6
+1 for proving a good MVCE. This allowed me to experiment a bit and I found the issue. Couldn't have solved it without it.
You must set the padding to zero when updating the x range of the plot. So change the update method to:
def update(self):
minX, maxX = self.lri.getRegion()
self.PlotA.setXRange(minX, maxX, padding=0)
Typically with QT these infinite signal loops are prevented by only updating a variable (and emitting the corresponding signal) when the new value is different from the old value. Somewhere in Qt/PyQtGraph this check is also done. But since your padding isn't zero, the new xrange will be a little bigger than the old xrange every iteration, and the loop doesn't end.
BTW, it is common in Python to let variable names start with a lower case character, and class names with upper case. I recommend to rename self.PlotA to self.plotA. This makes your code better readable for other Python programmers. Also it will give better syntax highlighting here on Stack Overflow.
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 a very simple stacked matplotlib bar plot embedded in PyQt Canvas. I am trying to get the corresponding label of the bar area (rectangle) based on the click. But I would always be getting _nolegend_ when I try to print the information from the event. Ideally I would like to see the corresponding label on the bar attached in the code.
For example when you click the gray bar it should print a2
import sys
import matplotlib.pyplot as plt
from PyQt4 import QtGui
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
def on_pick(event):
print event.artist.get_label()
def main():
app = QtGui.QApplication(sys.argv)
w = QtGui.QWidget()
w.resize(640, 480)
w.setWindowTitle('Pick Test')
fig = Figure((10.0, 5.0), dpi=100)
canvas = FigureCanvas(fig)
canvas.setParent(w)
axes = fig.add_subplot(111)
# bind the pick event for canvas
fig.canvas.mpl_connect('pick_event', on_pick)
p1 = axes.bar(1,6,picker=2,label='a1')
p2 = axes.bar(1,2, bottom=6,color='gray',picker=1,label='a2')
axes.set_ylim(0,10)
axes.set_xlim(0,5)
w.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
This gets a little tricky because bar is a complex plot object that is really composed of multiple components.
You can use get_legend_handles_labels to get all the artists and labels for the axes. Then you can look so see which group your current artist belongs to.
So your callback may look something like this.
def on_pick(event)
rect = event.artist
# Get the artists and the labels
handles,labels = rect.axes.get_legend_handles_labels()
# Search for your current artist within all plot groups
label = [label for h,label in zip(handles, labels) if rect in h.get_children()]
# Should only be one entry but just double check
if len(label) == 1:
label = label[0]
else:
label = None
print label