Reputation: 2795
I'm using PyQt and PyQtGraph to build a relatively simple plotting UI. As part of this I have a graphicsview (pyqtgraph's graphicslayoutwidget) that has PlotItems dynamically added to it by the user.
What I'm trying to achieve is allowing the user to select a PlotItem by double clicking on it.
It's simple enough to get if the user has double clicked somewhere within the widget window, but I can't seem to figure out how to return what was clicked on.
Most of my search results have come up with trying to reimplement mousePressEvent for certain pushbuttons. I've read a bit about event filters, but I'm not sure if that's the necessary solution here.
I'm not sure what other information might be useful for helping answer this question, so if it's unclear what I'm asking let me know so I can clarify.
Edit:
Duplicate of this:
pyqtgraph: When I click on a PlotItem how do I know which item has been clicked
Upvotes: 1
Views: 10228
Reputation: 636
A very straightforward alternative is to use a lambda function:
q_label = QLabel("MyLabel")
q_label.mousePressEvent = lambda e: on_name_clicked(q_label)
def on_name_clicked(self, q_label):
print(q_label.text())
Upvotes: 0
Reputation: 1402
After a lot of troubles working on this problem I figured out that the method items
of a QGraphicsView
cannot be used carelessly as reported in many answers around stackoverflow.
I came up with a custom solution to grab the nearest data point to click point. Here is the function
class DSP:
@staticmethod
def map_to_nearest(x: np.ndarray, x0: Union[np.ndarray, float]) -> np.ndarray:
"""This methods takes an array of time values and map it to nearest value in nidaq time array
It returns the indices"""
if type(x0) == float:
x0 = np.array([x0])
# A bit of magic
x_tiled = np.tile(x, (x0.size, 1))
x0_tiled = np.tile(x0, (x.size, 1)).T
diff = np.abs(x_tiled - x0_tiled)
idx = np.argmin(diff, axis=1)
return idx
class MyPlotWidget(pg.PlotWidget):
def nearest_data_index_to_mouse_click(self, click_scene_pos: QPointF):
"""
:param click_scene_pos: The position of the mouse click in Scene coordinate
:return:
- int - Nearest point data index (or None)
- view_rect (A rectangle in data coordinates of pixel_dist (equivalent scene coordinates) side
"""
# QPoint to numpy array
qpoint2np = lambda x: np.array([x.x(), x.y()])
# Filter out all not data-driven items in the list. Must be customized and improved!
get_data_items_only = lambda items: [item for item in items if
any([isinstance(item, x) for x in [pg.PlotDataItem, pg.PlotCurveItem, pg.ScatterPlotItem]])]
# Half side of the rectangular ROI around the click point
pixel_dist = 5
# Numpy click point
p_click = qpoint2np(self.plot_item.mapToView(click_scene_pos))
# Rectangle ROI in scene (pixel) coordinates
scene_rect = QRectF(click_scene_pos.x() - pixel_dist, click_scene_pos.y() - pixel_dist, 2*pixel_dist, 2*pixel_dist)
# Rectangle ROI in data coordinates - NB: transforming from scene_rect to view_rect allows for the different x-y scaling!
view_rect: QRectF = self.getPlotItem().mapRectToView(scene_rect)
# Get all items canonically intercepted thourgh the methods already discussed by other answers
items = get_data_items_only(self.scene().items(scene_rect))
if len(items) == 0:
return None, None, view_rect
# Make your own decisional criterion
item = items[0]
# p0: bottom-left p1: upper-right view_rect items (DO NOT USE bottomLeft() and topRight()! The scene coordinates are different!
# Y axis is upside-down
p0 = np.array([view_rect.x(), view_rect.y() - view_rect.height()])
p1 = np.array([view_rect.x() + view_rect.width(), view_rect.y()])
# Limit the analysis to the same x-interval as the ROI
_x_limits = np.array([p0[0], p1[0]])
_item_data_x, _item_data_y = item.getData()
xi = DSP.map_to_nearest(_item_data_x, _x_limits)
# If the point is out of the interval
if xi.size == 0:
return None, None, view_rect
xi = np.arange(xi[0], xi[1]+1).astype(np.int_)
x, y = _item_data_x[xi], _item_data_y[xi]
# (2,1) limited item data array
_item_data = np.array([x,y])
subitem = pg.PlotCurveItem(x=x, y=y)
# Now intersects is used again, but this time the path is limited to a few points near the click! Some error might remains, but it may works well in most cases
if subitem.getPath().intersects(view_rect):
# Find nearest point
delta = _item_data - p_click.reshape(2,1)
min_dist_arg = np.argmin(np.linalg.norm(delta, axis=0))
return item, xi[min_dist_arg], view_rect
# View_rect is returned just to allow me to plot the ROI for debug reason
return None, None, view_rect
Implementing a custom data-tip TextItem
, this is the results:
NOTE:
scene_rect
is automatically transformed, but I don't know so far how pyqtgraph
handles the underlying data of the PlotCurveItemPlotDataItem
, it will be turned in a couple of (ScatterPlotItem
and CurvePlotItem
) to handle both the lines and the symbols (markers) of the plotFirst of all, though not documented, it seems likely that QGraphicsView::items()
implements or uses the method QPainterPath::intersects()
. Checking the documentation of this method (with QRectF as an argument):
There is an intersection if any of the lines making up the rectangle crosses a part of the path or if any part of the rectangle overlaps with any area enclosed by the path.
By running some test scripts, it seems that QPainterPath
is considering always a closed path, possibly by connecting the last point with the first one. Indeed, the script below:
from PySide2.QtCore import QPoint, QRectF
from PySide2.QtGui import QPainterPath, QPicture, QPainter
from PySide2.QtWidgets import QApplication, QMainWindow
import pyqtgraph as pg
import numpy as np
app = QApplication([])
# Path1 made of two lines
x = [0, 5, 0]
y = [0, 5, 6]
path1 = pg.PlotCurveItem(x, y, name='Path1')
rect = QRectF(1,4,1,1)
# RectItem (here omitted) is taken from https://stackoverflow.com/questions/60012070/drawing-a-rectangle-in-pyqtgraph
rect_item = RectItem(rect)
pw = pg.PlotWidget()
pw.addItem(path1)
pw.addItem(rect_item)
text = f'path1.getPath().intersects(rect): {path1.getPath().intersects(rect)}'
pw.addItem(pg.TextItem(text))
# Need to replicate the item
rect_item = RectItem(rect)
path2 =pg.PlotCurveItem(x=[0,5,4], y=[0,5,6])
pw2 = pg.PlotWidget()
pw2.addItem(path2)
pw2.addItem(rect_item)
text = f'path2.getPath().intersects(rect): {path2.getPath().intersects(rect)}'
pw2.addItem(pg.TextItem(text))
pw.show()
pw2.show()
app.exec_()
gets this plots and results as output:
Upvotes: 1
Reputation: 11644
One strategy is to connect to GraphicsScene.sigMouseClicked
and then ask the scene which items are under the mouse cursor.
This should get you part way there:
import pyqtgraph as pg
w = pg.GraphicsWindow()
for i in range(4):
w.addPlot(0, i)
def onClick(event):
items = w.scene().items(event.scenePos())
print "Plots:", [x for x in items if isinstance(x, pg.PlotItem)]
w.scene().sigMouseClicked.connect(onClick)
Upvotes: 7