1

I have a plot from matplotlib for which I would like to display labels on the marker points when hover over with the mouse.

I found this very helpful working example on SO and I was trying to integrate the exact same plot into a pyqt5 application. Unfortunately when having the plot in the application the hovering doesn't work anymore.

Here is a full working example based on the mentioned SO post:

import matplotlib.pyplot as plt
import scipy.spatial as spatial
import numpy as np
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
import sys

pi = np.pi
cos = np.cos


def fmt(x, y):
    return 'x: {x:0.2f}\ny: {y:0.2f}'.format(x=x, y=y)

class FollowDotCursor(object):
    """Display the x,y location of the nearest data point.
    https://stackoverflow.com/a/4674445/190597 (Joe Kington)
    https://stackoverflow.com/a/13306887/190597 (unutbu)
    https://stackoverflow.com/a/15454427/190597 (unutbu)
    """
    def __init__(self, ax, x, y, tolerance=5, formatter=fmt, offsets=(-20, 20)):
        try:
            x = np.asarray(x, dtype='float')
        except (TypeError, ValueError):
            x = np.asarray(mdates.date2num(x), dtype='float')
        y = np.asarray(y, dtype='float')
        mask = ~(np.isnan(x) | np.isnan(y))
        x = x[mask]
        y = y[mask]
        self._points = np.column_stack((x, y))
        self.offsets = offsets
        y = y[np.abs(y-y.mean()) <= 3*y.std()]
        self.scale = x.ptp()
        self.scale = y.ptp() / self.scale if self.scale else 1
        self.tree = spatial.cKDTree(self.scaled(self._points))
        self.formatter = formatter
        self.tolerance = tolerance
        self.ax = ax
        self.fig = ax.figure
        self.ax.xaxis.set_label_position('top')
        self.dot = ax.scatter(
            [x.min()], [y.min()], s=130, color='green', alpha=0.7)
        self.annotation = self.setup_annotation()
        plt.connect('motion_notify_event', self)

    def scaled(self, points):
        points = np.asarray(points)
        return points * (self.scale, 1)

    def __call__(self, event):
        ax = self.ax
        # event.inaxes is always the current axis. If you use twinx, ax could be
        # a different axis.
        if event.inaxes == ax:
            x, y = event.xdata, event.ydata
        elif event.inaxes is None:
            return
        else:
            inv = ax.transData.inverted()
            x, y = inv.transform([(event.x, event.y)]).ravel()
        annotation = self.annotation
        x, y = self.snap(x, y)
        annotation.xy = x, y
        annotation.set_text(self.formatter(x, y))
        self.dot.set_offsets((x, y))
        bbox = ax.viewLim
        event.canvas.draw()

    def setup_annotation(self):
        """Draw and hide the annotation box."""
        annotation = self.ax.annotate(
            '', xy=(0, 0), ha = 'right',
            xytext = self.offsets, textcoords = 'offset points', va = 'bottom',
            bbox = dict(
                boxstyle='round,pad=0.5', fc='yellow', alpha=0.75),
            arrowprops = dict(
                arrowstyle='->', connectionstyle='arc3,rad=0'))
        return annotation

    def snap(self, x, y):
        """Return the value in self.tree closest to x, y."""
        dist, idx = self.tree.query(self.scaled((x, y)), k=1, p=1)
        try:
            return self._points[idx]
        except IndexError:
            # IndexError: index out of bounds
            return self._points[0]


class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        self.width = 1000
        self.height = 800
        self.setGeometry(0, 0, self.width, self.height)

        canvas = self.get_canvas()

        w = QWidget()
        w.layout = QHBoxLayout()
        w.layout.addWidget(canvas)
        w.setLayout(w.layout)

        self.setCentralWidget(w)

        self.show()


    def get_canvas(self):
        fig, ax = plt.subplots()
        x = np.linspace(0.1, 2*pi, 10)
        y = cos(x)
        markerline, stemlines, baseline = ax.stem(x, y, '-.')
        plt.setp(markerline, 'markerfacecolor', 'b')
        plt.setp(baseline, 'color','r', 'linewidth', 2)
        cursor = FollowDotCursor(ax, x, y, tolerance=20)

        canvas = FigureCanvas(fig)

        return canvas


app = QApplication(sys.argv)
win = MainWindow()
sys.exit(app.exec_())

What would I have to do to make the labels also show when hovering over in the pyqt application?

1 Answer 1

3

The first problem may be that you don't keep a reference to the FollowDotCursor.

So to make sure the FollowDotCursor stays alive, you can make it a class variable

self.cursor = FollowDotCursor(ax, x, y, tolerance=20)

instead of cursor = ....

Next make sure you instatiate the Cursor class after giving the figure a canvas.

canvas = FigureCanvas(fig)
self.cursor = FollowDotCursor(ax, x, y, tolerance=20)

Finally, keep a reference to the callback inside the FollowDotCursor and don't use plt.connect but the canvas itself:

self.cid = self.fig.canvas.mpl_connect('motion_notify_event', self)
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.