I want to display a real-time graph displaying a few curves. I would like some mouse interaction:
I have been able to handle this hoverable curve behaviour using a combination of:
PlotCurveItem
that displays the curve for all dataScatterPlotItem
with opacity of 0 to detect hovered points for all data and display their value in a tooltipScatterPlotItem
for displaying the hovered data point(Feel free to give some tips on the methodology, I am new to PyQt and pyqtgraph). Now I want to have several of these hoverable curves, so ideally I would like to create a class that encompasses this behaviour.
How do I do that? What class do I need to extend?
Here's some code I wrote for illustration:
import pyqtgraph as pg
from pyqtgraph import QtCore, QtGui
from PyQt5.QtWidgets import QApplication, QMainWindow
import numpy as np
class HoverableCurveItem(pg.PlotCurveItem):
sigCurveHovered = QtCore.Signal(object, object)
sigCurveNotHovered = QtCore.Signal(object, object)
def __init__(self, hoverable=True, *args, **kwargs):
super(HoverableCurveItem, self).__init__(*args, **kwargs)
self.hoverable = hoverable
self.setAcceptHoverEvents(True)
def hoverEvent(self, ev):
if self.hoverable:
if self.mouseShape().contains(ev.pos()):
self.sigCurveHovered.emit(self, ev)
else:
self.sigCurveNotHovered.emit(self, ev)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
print('he')
self.view = pg.GraphicsLayoutWidget()
self.setCentralWidget(self.view)
self.makeplot()
def makeplot(self):
x = list(range(10))
y = [np.random.randint(10) for _ in x]
self.data = y
plot = self.view.addPlot()
self.plotitem = HoverableCurveItem(x, y, pen=pg.mkPen('w', width=10))
self.scatter = pg.ScatterPlotItem(pen=pg.mkPen('g', width=25))
self.scatter.setOpacity(0.0)
self.scatter.setData(x, y, hoverable=True, tip=lambda x, y, data: f"{y}°C" )
self.plotitem.setClickable(True, width=10)
self.plotitem.sigCurveHovered.connect(self.hovered)
self.plotitem.sigCurveNotHovered.connect(self.leaveHovered)
self.hovered = pg.ScatterPlotItem(pen=pg.mkPen('g', width=5))
self.hovered.setOpacity(0.)
plot.addItem(self.plotitem)
plot.addItem(self.scatter)
plot.addItem(self.hovered)
self.plot = plot
def hovered(self, item, event):
x = int(np.round(event.pos()[0]))
y = self.data[int(np.round(x))]
self.plotitem.setToolTip(f"{x}: {y}")
self.plotitem.setPen(pg.mkPen('b', width=10))
self.hovered.setData([x], [y])
self.hovered.setOpacity(1)
def leaveHovered(self):
self.plotitem.setPen(pg.mkPen('w', width=10))
self.point.setOpacity(0)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec())
Your implementation is acceptable, but too convoluted and not very modular.
Most importantly, the scatter items should be part of the curve, not separate entities, since they share the same points.
A more appropriate approach should set the scatter items as child items of the curve, then the hover event would eventually make them visible depending on the current point.
class HoverableCurveItem(pg.PlotCurveItem):
def __init__(self, hoverable=True, *args, **kwargs):
super(HoverableCurveItem, self).__init__(*args, **kwargs)
self.basePen = self.opts['pen']
self.hoverPen = pg.mkPen('b', width=10)
self.hoverable = hoverable
self.setAcceptHoverEvents(True)
self.hoverItem = pg.ScatterPlotItem(pen=pg.mkPen('g', width=5))
self.hoverItem.setParentItem(self)
self.hoverItem.setData(self.xData, self.yData)
self.hoverItem.setVisible(False)
def hoverEvent(self, ev):
if self.hoverable:
if self.mouseShape().contains(ev.pos()):
self.setPen(self.hoverPen)
x = int(round(ev.pos()[0]))
y = self.yData[x]
self.setToolTip("{}: {}".format(x, y))
self.hoverItem.setPointsVisible([x == i for i in self.xData])
self.hoverItem.setVisible(True)
else:
self.setPen(self.basePen)
self.setToolTip('')
self.hoverItem.setVisible(False)
As an unrelated note, be aware that you made a very important mistake: in your MainWindow
class you defined a hovered
function, but then you actually overwrite that attribute to create the scatter item, which you also named hovered
.
Luckily, that issue didn't affect your program in its current state because you've been connecting the self.hovered
function before creating the self.hovered
item, but if you had connected the function after that point, you'd have got a fatal error, since self.hovered
wouldn't refer to a callable anymore.
Always consider very carefully all the names you use (including functions), so that you can avoid mistakes like these, which are very annoying and often difficult to track down. Ask yourself if something is (an instance or a data container, so it should have a noun) or it does (a function, so it is a verb).