在 pyqt5 中包含带有悬停标签的 matplotlib

Posted

技术标签:

【中文标题】在 pyqt5 中包含带有悬停标签的 matplotlib【英文标题】:Include matplotlib in pyqt5 with hover labels 【发布时间】:2017-06-23 16:40:09 【问题描述】:

我有一个来自 matplotlib 的图,我想在鼠标悬停时在标记点上显示标签。

我在 SO 上找到了this very helpful working example,我试图将完全相同的图集成到 pyqt5 应用程序中。 不幸的是,当在应用程序中有绘图时,悬停不再起作用。

这是基于上述 SO 帖子的完整工作示例:

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://***.com/a/4674445/190597 (Joe Kington)
    https://***.com/a/13306887/190597 (unutbu)
    https://***.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_())

当鼠标悬停在 pyqt 应用程序中时,我需要做什么才能使标签也显示?

【问题讨论】:

【参考方案1】:

第一个问题可能是您没有保留对 FollowDotCursor 的引用。

所以要确保FollowDotCursor 保持活动状态,您可以将其设为类变量

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

而不是cursor = ...

接下来,确保在 为图形提供画布之后实例化 Cursor 类。

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

最后,在 FollowDotCursor 中保留对回调的引用,不要使用 plt.connect,而是使用画布本身:

self.cid = self.fig.canvas.mpl_connect('motion_notify_event', self)

【讨论】:

以上是关于在 pyqt5 中包含带有悬停标签的 matplotlib的主要内容,如果未能解决你的问题,请参考以下文章

PyQt5 QTabWidget:如何在类和同一窗口中包含的选项卡之间切换?

如何在基于 Docbook 标签的 XSD 中包含 MathML 标签?

Highcharts悬停xAxis标签的独特价值

如何将代码模块中包含的VBA代码放在表单模块中

Mongodb数组查询:查找数组中包含局外人的记录[重复]

如何在静态文本标签中包含特殊字符?