Repository: pyqtgraph/pyqtgraph

Issue:
ScatterPlotItem: hoverable=True does not create Tooltip
Tested with python -m pyqtgraph.examples

_--> GraphicsItems --> Scatter Plot_

Changed:
`g.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 120))`
to:
`g.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 120), hoverable=True)`
or:
`g.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 120), hoverable=True, tip='x: {x:.3g}\ny: {y:.3g}\ndata={data}'.format)`

Expected behaviour: When hovering over the data points a tooltip should be shown.
Experienced behaviour: Tooltip is **not** shown.

Tested on:
0.12.2 on Windows
0.11.1 on Linux (Arch)

The respective code lies in ScatterPlotItem.py (l. 1224):
```
           # Show information about hovered points in a tool tip
            vb = self.getViewBox()
            if vb is not None and self.opts['tip'] is not None:
                cutoff = 3
                tip = [self.opts['tip'](x=pt.pos().x(), y=pt.pos().y(), data=pt.data())
                       for pt in points[:cutoff]]
                if len(points) > cutoff:
                    tip.append('({} others...)'.format(len(points) - cutoff))
                print('\n\n'.join(tip)) # ADDED to TEST
                vb.setToolTip('\n\n'.join(tip))

            self.sigHovered.emit(self, points, ev)
```
The added line shows that the tip is created, but it is never shown.




Discussion:
I tried it out on Windows 10 / PyQt6 6.2.2 / pyqtgraph 0.12.2 and the tooltip does show on the upper left plot after adding `hoverable=True`.

The tooltip also does show on the bottom left and bottom right plots as `hoverable=True` is already set for them. Does the tooltip show on your setup for the bottom left and bottom right plots?
HI,

Thanks for your reply. Indeed, when checking again, it is working. However, there is still a glitch: when I used it on my graph, it worked on some points, on others not. However, I have more than one ScatterPlotItem in my view. In the example in question I had exactly five of them. Hence I have adopted the example by adding more than one up to four and while it still worked with two ScatterPlotItems, it does not work with four anymore:

```
# -*- coding: utf-8 -*-
"""
Example demonstrating a variety of scatter plot features.
"""



## Add path to library (just for examples; you do not need this)
import initExample

from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
import numpy as np
from collections import namedtuple
from itertools import chain

app = pg.mkQApp("Scatter Plot Item Example") 
mw = QtGui.QMainWindow()
mw.resize(800,800)
view = pg.GraphicsLayoutWidget()  ## GraphicsView with GraphicsLayout inserted by default
mw.setCentralWidget(view)
mw.show()
mw.setWindowTitle('pyqtgraph example: ScatterPlot')

## create four areas to add plots
w1 = view.addPlot()
w2 = view.addViewBox()
w2.setAspectLocked(True)
view.nextRow()
w3 = view.addPlot()
w4 = view.addPlot()
print("Generating data, this takes a few seconds...")

## Make all plots clickable
clickedPen = pg.mkPen('b', width=2)
lastClicked = []
def clicked(plot, points):
    global lastClicked
    for p in lastClicked:
        p.resetPen()
    print("clicked points", points)
    for p in points:
        p.setPen(clickedPen)
    lastClicked = points


## There are a few different ways we can draw scatter plots; each is optimized for different types of data:

## 1) All spots identical and transform-invariant (top-left plot).
## In this case we can get a huge performance boost by pre-rendering the spot
## image and just drawing that image repeatedly.

n = 300
s1 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 255), hoverable=True)
s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(60, 60, 60, 255), hoverable=True)
s3 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(120, 120, 120, 255), hoverable=True)
s4 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(180, 180, 180, 255), hoverable=True)
pos = np.random.normal(size=(2,n), scale=1e-5)
spots = [{'pos': pos[:,i], 'data': 1} for i in range(n)] + [{'pos': [0,0], 'data': 1}]
s1.addPoints(spots[0:5])
s2.addPoints(spots[6:10])
s3.addPoints(spots[11:15])
s4.addPoints(spots[16:20])
w1.addItem(s1)
w1.addItem(s2)
w1.addItem(s3)
w1.addItem(s4)

s1.sigClicked.connect(clicked)


## 2) Spots are transform-invariant, but not identical (top-right plot).
## In this case, drawing is almsot as fast as 1), but there is more startup
## overhead and memory usage since each spot generates its own pre-rendered
## image.

TextSymbol = namedtuple("TextSymbol", "label symbol scale")

def createLabel(label, angle):
    symbol = QtGui.QPainterPath()
    #symbol.addText(0, 0, QFont("San Serif", 10), label)
    f = QtGui.QFont()
    f.setPointSize(10)
    symbol.addText(0, 0, f, label)
    br = symbol.boundingRect()
    scale = min(1. / br.width(), 1. / br.height())
    tr = QtGui.QTransform()
    tr.scale(scale, scale)
    tr.rotate(angle)
    tr.translate(-br.x() - br.width()/2., -br.y() - br.height()/2.)
    return TextSymbol(label, tr.map(symbol), 0.1 / scale)

random_str = lambda : (''.join([chr(np.random.randint(ord('A'),ord('z'))) for i in range(np.random.randint(1,5))]), np.random.randint(0, 360))

s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True)
pos = np.random.normal(size=(2,n), scale=1e-5)
spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%10, 'size': 5+i/10.} for i in range(n)]
s2.addPoints(spots)
spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': label[1], 'size': label[2]*(5+i/10.)} for (i, label) in [(i, createLabel(*random_str())) for i in range(n)]]
s2.addPoints(spots)
w2.addItem(s2)
s2.sigClicked.connect(clicked)


## 3) Spots are not transform-invariant, not identical (bottom-left).
## This is the slowest case, since all spots must be completely re-drawn
## every time because their apparent transformation may have changed.

s3 = pg.ScatterPlotItem(
    pxMode=False,  # Set pxMode=False to allow spots to transform with the view
    hoverable=True,
    hoverPen=pg.mkPen('g'),
    hoverSize=1e-6
)
spots3 = []
for i in range(10):
    for j in range(10):
        spots3.append({'pos': (1e-6*i, 1e-6*j), 'size': 1e-6, 'pen': {'color': 'w', 'width': 2}, 'brush':pg.intColor(i*10+j, 100)})
s3.addPoints(spots3)
w3.addItem(s3)
s3.sigClicked.connect(clicked)

## Test performance of large scatterplots

s4 = pg.ScatterPlotItem(
    size=10,
    pen=pg.mkPen(None),
    brush=pg.mkBrush(255, 255, 255, 20),
    hoverable=True,
    hoverSymbol='s',
    hoverSize=15,
    hoverPen=pg.mkPen('r', width=2),
    hoverBrush=pg.mkBrush('g'),
)
n = 10000
pos = np.random.normal(size=(2, n), scale=1e-9)
s4.addPoints(
    x=pos[0],
    y=pos[1],
    # size=(np.random.random(n) * 20.).astype(int),
    # brush=[pg.mkBrush(x) for x in np.random.randint(0, 256, (n, 3))],
    data=np.arange(n)
)
w4.addItem(s4)
s4.sigClicked.connect(clicked)

if __name__ == '__main__':
    pg.exec()
```

I used this slightly modified bit:

```
           # Show information about hovered points in a tool tip
            vb = self.getViewBox()
            if vb is not None and self.opts['tip'] is not None:
                cutoff = 3
                tip = [self.opts['tip'](x=pt.pos().x(), y=pt.pos().y(), data=pt.data())
                       for pt in points[:cutoff]]
                if len(points) > cutoff:
                    tip.append('({} others...)'.format(len(points) - cutoff))
                if tip:
                    print('\n\n'.join(tip)) # ADDED to TEST
                else:
                    print('nothing')
                vb.setToolTip('\n\n'.join(tip))

            self.sigHovered.emit(self, points, ev)
```
And it shows that when selecting a point, which does not get a tooltip, the code actually produces the tip, but additionally produces at least one empty tip afterwards preventing the drawing of the tooltip.

This seems to be the problem.

In general, the state of labeling points/tooltips in pyqtgraph seems to be rather not yet implemented or did I miss something (e.g. I would like to have labels of values in BarGraphItem and maybe have labels instead of tooltips in ScatterPlotItem. Additionally, having a possibility to format timestamps to dates in the code of question here would also be very fine. Seems to be not implemented by now.).
I have simplified your example. Here there are 4 `ScatterPlotItem`s in the same view but the data points are separated. All the tooltips show.

If 2 spots (A and B) from different `ScatterPlotItem`s (S1 and S2 respectively) are close together and the mouse is over A, then S1 would send a tooltip with A *and* S2 would send an empty tooltip, nullifying the effect of S1's tooltip. Perhaps that's the effect you are seeing.

```python
import pyqtgraph as pg
import numpy as np

rng = np.random.default_rng(0)

pg.mkQApp()
pw = pg.PlotWidget()
pw.show()
w1 = pw.getPlotItem()

n = 5*4
s1 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush('r'), hoverable=True)
s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush('g'), hoverable=True)
s3 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush('b'), hoverable=True)
s4 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush('m'), hoverable=True)
pos = rng.normal(size=(2,n), scale=1e-5)
s1.addPoints([{'pos': pos[:,i] + [0,0], 'data': 1} for i in range(0,5)])
s2.addPoints([{'pos': pos[:,i] + [0,5e-5], 'data': 2} for i in range(5,10)])
s3.addPoints([{'pos': pos[:,i] + [5e-5,0], 'data': 3} for i in range(10,15)])
s4.addPoints([{'pos': pos[:,i] + [5e-5,5e-5], 'data': 4} for i in range(15,20)])
w1.addItem(s1)
w1.addItem(s2)
w1.addItem(s3)
w1.addItem(s4)

pg.exec()
```
I have moved s3 and s4 closer together and the problem occurs (again, it worked with the initial simplified code):

```
import pyqtgraph as pg
import numpy as np

rng = np.random.default_rng(0)

pg.mkQApp()
pw = pg.PlotWidget()
pw.show()
w1 = pw.getPlotItem()

n = 5*4
s1 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush('r'), hoverable=True)
s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush('g'), hoverable=True)
s3 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush('b'), hoverable=True)
s4 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush('m'), hoverable=True)
pos = rng.normal(size=(2,n), scale=1e-5)
s1.addPoints([{'pos': pos[:,i] + [0,0], 'data': 1} for i in range(0,5)])
s2.addPoints([{'pos': pos[:,i] + [0,4e-5], 'data': 2} for i in range(5,10)])
s3.addPoints([{'pos': pos[:,i] + [0,5e-5], 'data': 2} for i in range(5,10)])
#s3.addPoints([{'pos': pos[:,i] + [5e-5,0], 'data': 3} for i in range(10,15)])
s4.addPoints([{'pos': pos[:,i] + [5e-5,5e-5], 'data': 4} for i in range(15,20)])
w1.addItem(s1)
w1.addItem(s2)
w1.addItem(s3)
w1.addItem(s4)

pg.exec()
```

Can you confirm this? So this is a BUG?

(BTW. how can I enable syntax highlighting here as you have done?)
>(BTW. how can I enable syntax highlighting here as you have done?)

put "python" (without the quotes) immediately after the 3 backticks that you used to start the block.

>Can you confirm this? So this is a BUG?

Yes, I can confirm the behavior that you are seeing. Whether it is a bug is arguable. The tooltip is set on the viewbox, so if you have multiple `ScatterPlotItem`s trying to set a tooltip on the same viewbox, then you would have this problem.

In the top right plot of `examples/ScatterPlot.py`, you can have different symbols and colors for the spots in a single `ScatterPlotItem`, so you don't really need to be to using multiple `ScatterPlotItem`s. 
Hi All!

I can confirm this strange bug with 2 scatter(or more) ovelapping each other and hover doesn't work for some points. Based on my observations hover doesn't work for scatter which was placed first and it seems like only for spots which lay in the area between spots of second scatter.
Please see below code example for this to reproduce. In this code all green dots have hover working fine but only 2 red dots on the right can be hovered, others don't work.
In my project i need to use multiple scatters as i have option to control visibility of one scatter or another or borh, so when i have both scatters visible many points are not hoverable due to this issue. Is there a way or workaround to solve this problem?

```python from PyQt6.QtWidgets import QApplication
import pyqtgraph as pg

import sys  # We need sys so that we can pass argv to QApplication


scatter1 = pg.ScatterPlotItem(
            size=5, brush=pg.mkBrush(30, 255, 35, 255), hoverable=True,
            tip='x: {x:.6g}\ny: {y:.6g}\n{data}'.format)

scatter2 = pg.ScatterPlotItem(
            size=5, brush=pg.mkBrush(255, 0, 0, 255), hoverable=True,
            tip='x: {x:.6g}\ny: {y:.6g}\n{data}'.format)

plot = pg.plot()
plot.setBackground('w')

x_data1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
y_data1 = [5, 4, 6, 4, 3, 5, 6, 6, 7, 8]

x_data2 = [2, 3, 4, 5, 6, 7, 1, 8, 9, 10]
y_data2 = [5, 4, 6, 4, 3, 5, 6, 6, 7, 8]

scatter1.addPoints(x_data1, y_data1)
scatter2.addPoints(x_data2, y_data2)
plot.addItem(scatter1)
plot.addItem(scatter2)

app = QApplication([])
sys.exit(app.exec())

```


Thanks!
@devoxy1 made a slight change to your post for the code block, the triple back-ticks need to be on their own separate line.  

I replicated the issue; thanks for the MWE.  I'm not seeing an easy way to "fix" this, but I'd have to think about it some more, I suspect that one scatter item is capturing the mouse hover event, and it's not relaying the event to the second item.  @lidstrom83 is our resident scatter plot expert so I'll tag him in case something stands out (but he's been taking a break from pyqtgraph work recently).

I do want to point out that QToolTip's, in general, have some issues, and I generally try and avoid them when feasible, and using some kind of TextItem may be preferable.

- I've never had good luck with setting QToolTip duration to remain longer than ~7 seconds.  I've tried a variety of ways, never could do it.
- Many window-sharing services don't recognize QToolTip's as part of the "share" display, so they often don't get captured
- They don't work in our jupyter notebook functionality (for likely the same reason as the previous bullet point)
- Having a QToolTip display right away has also shown to be problematic.

That said, that is supported functionality in this library, so this is a completely legit bug-report.

Resolving patch:
diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py
--- a/pyqtgraph/graphicsItems/ScatterPlotItem.py
+++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py
@@ -443,6 +443,10 @@ def __init__(self, *args, **kargs):
 
         #self.setCacheMode(self.DeviceCoordinateCache)
 
+        # track when the tooltip is cleared so we only clear it once
+        # this allows another item in the VB to set the tooltip
+        self._toolTipCleared = True
+
     def setData(self, *args, **kargs):
         """
         **Ordered Arguments:**
@@ -1217,12 +1221,17 @@ def hoverEvent(self, ev):
             # Show information about hovered points in a tool tip
             vb = self.getViewBox()
             if vb is not None and self.opts['tip'] is not None:
-                cutoff = 3
-                tip = [self.opts['tip'](x=pt.pos().x(), y=pt.pos().y(), data=pt.data())
-                       for pt in points[:cutoff]]
-                if len(points) > cutoff:
-                    tip.append('({} others...)'.format(len(points) - cutoff))
-                vb.setToolTip('\n\n'.join(tip))
+                if len(points) > 0:
+                    cutoff = 3
+                    tip = [self.opts['tip'](x=pt.pos().x(), y=pt.pos().y(), data=pt.data())
+                           for pt in points[:cutoff]]
+                    if len(points) > cutoff:
+                        tip.append('({} others...)'.format(len(points) - cutoff))
+                    vb.setToolTip('\n\n'.join(tip))
+                    self._toolTipCleared = False
+                elif not self._toolTipCleared:
+                    vb.setToolTip("")
+                    self._toolTipCleared = True
 
             self.sigHovered.emit(self, points, ev)
 


Test patch: