如何在 matplotlib 中添加悬停注释

Posted

技术标签:

【中文标题】如何在 matplotlib 中添加悬停注释【英文标题】:How to add hovering annotations in matplotlib 【发布时间】:2011-12-16 01:20:51 【问题描述】:

我正在使用 matplotlib 制作散点图。散点图上的每个点都与一个命名对象相关联。当我将光标悬停在与该对象关联的散点图上的点上时,我希望能够看到该对象的名称。特别是,如果能够快速查看异常点的名称,那就太好了。我在这里搜索时能找到的最接近的东西是 annotate 命令,但这似乎会在图上创建一个固定标签。不幸的是,由于我拥有的点数,如果我标记每个点,散点图将无法阅读。有谁知道创建仅在光标悬停在该点附近时出现的标签的方法?

【问题讨论】:

通过搜索到达这里的人可能还想检查this answer,这相当复杂,但根据要求可能合适。 【参考方案1】:

这里的其他答案似乎都没有真正回答这个问题。所以这里的代码使用 scatter 并在 悬停 时显示 注解

import matplotlib.pyplot as plt
import numpy as np; np.random.seed(1)

x = np.random.rand(15)
y = np.random.rand(15)
names = np.array(list("ABCDEFGHIJKLMNO"))
c = np.random.randint(1,5,size=15)

norm = plt.Normalize(1,4)
cmap = plt.cm.RdYlGn

fig,ax = plt.subplots()
sc = plt.scatter(x,y,c=c, s=100, cmap=cmap, norm=norm)

annot = ax.annotate("", xy=(0,0), xytext=(20,20),textcoords="offset points",
                    bbox=dict(boxstyle="round", fc="w"),
                    arrowprops=dict(arrowstyle="->"))
annot.set_visible(False)

def update_annot(ind):

    pos = sc.get_offsets()[ind["ind"][0]]
    annot.xy = pos
    text = ", ".format(" ".join(list(map(str,ind["ind"]))), 
                           " ".join([names[n] for n in ind["ind"]]))
    annot.set_text(text)
    annot.get_bbox_patch().set_facecolor(cmap(norm(c[ind["ind"][0]])))
    annot.get_bbox_patch().set_alpha(0.4)


def hover(event):
    vis = annot.get_visible()
    if event.inaxes == ax:
        cont, ind = sc.contains(event)
        if cont:
            update_annot(ind)
            annot.set_visible(True)
            fig.canvas.draw_idle()
        else:
            if vis:
                annot.set_visible(False)
                fig.canvas.draw_idle()

fig.canvas.mpl_connect("motion_notify_event", hover)

plt.show()

因为人们也想将此解决方案用于plot 行而不是散点图,所以以下是plot 的相同解决方案(工作方式略有不同)。

import matplotlib.pyplot as plt
import numpy as np; np.random.seed(1)

x = np.sort(np.random.rand(15))
y = np.sort(np.random.rand(15))
names = np.array(list("ABCDEFGHIJKLMNO"))

norm = plt.Normalize(1,4)
cmap = plt.cm.RdYlGn

fig,ax = plt.subplots()
line, = plt.plot(x,y, marker="o")

annot = ax.annotate("", xy=(0,0), xytext=(-20,20),textcoords="offset points",
                    bbox=dict(boxstyle="round", fc="w"),
                    arrowprops=dict(arrowstyle="->"))
annot.set_visible(False)

def update_annot(ind):
    x,y = line.get_data()
    annot.xy = (x[ind["ind"][0]], y[ind["ind"][0]])
    text = ", ".format(" ".join(list(map(str,ind["ind"]))), 
                           " ".join([names[n] for n in ind["ind"]]))
    annot.set_text(text)
    annot.get_bbox_patch().set_alpha(0.4)


def hover(event):
    vis = annot.get_visible()
    if event.inaxes == ax:
        cont, ind = line.contains(event)
        if cont:
            update_annot(ind)
            annot.set_visible(True)
            fig.canvas.draw_idle()
        else:
            if vis:
                annot.set_visible(False)
                fig.canvas.draw_idle()

fig.canvas.mpl_connect("motion_notify_event", hover)

plt.show()

如果有人正在寻找双轴线的解决方案,请参阅How to make labels appear when hovering over a point in multiple axis?

如果有人正在寻找条形图的解决方案,请参考例如this answer.

【讨论】:

非常好!注意,我注意到ind["ind"] 实际上是光标下所有点的索引列表。这意味着上面的代码实际上使您可以访问给定位置的所有点,而不仅仅是最高点。例如,如果您有两个重叠点,则文本可能会显示为 1 2, B C,如果您有 3 个重叠点,则甚至可能是 1 2 3, B C D @Jvinniec 没错,上图中故意存在这样一种情况(x ~ 0.4 处的绿点和红点)。如果将鼠标悬停,它将显示0 8, A I,(请参阅picture)。 @ImportanceOfBeingErnest 这是一个很棒的代码,但是当悬停并移动一个点时,它会多次调用fig.canvas.draw_idle()(它甚至将光标更改为空闲)。我解决了它存储以前的索引并检查是否ind["ind"][0] == prev_ind。然后仅当您从一个点移动到另一个点(更新文本)、停止悬停(使注释不可见)或开始悬停(使注释可见)时才更新。有了这个改变,它变得更加干净和高效。 @Konstantin 是的,当在 IPython/Jupyter 笔记本中使用 %matplotlib notebook 时,此解决方案将起作用。 @OriolAbril(以及其他所有人),如果您在修改此答案中的代码时遇到问题,请提出问题,链接到此答案并显示您尝试过的代码。如果没有实际看到,我无法知道您的每个代码有什么问题。【参考方案2】:

此解决方案适用于悬停一行而无需单击它:

import matplotlib.pyplot as plt

# Need to create as global variable so our callback(on_plot_hover) can access
fig = plt.figure()
plot = fig.add_subplot(111)

# create some curves
for i in range(4):
    # Giving unique ids to each data member
    plot.plot(
        [i*1,i*2,i*3,i*4],
        gid=i)

def on_plot_hover(event):
    # Iterating over each data member plotted
    for curve in plot.get_lines():
        # Searching which data member corresponds to current mouse position
        if curve.contains(event)[0]:
            print("over %s" % curve.get_gid())
            
fig.canvas.mpl_connect('motion_notify_event', on_plot_hover)           
plt.show()

【讨论】:

非常有用 +1ed。您可能需要对此进行“去抖动”,因为 motion_notify_event 将在曲线区域内重复运动。简单地检查曲线对象是否等于前一条曲线似乎有效。 嗯 - 这对我来说不是开箱即用的(matplotlib...) - 这对 ipython/jupyter 笔记本有用吗?当有多个子图时它也有效吗?在条形图而不是折线图上呢? 这会在悬停时将标签打印到控制台中。悬停时让标签出现在图片上怎么样?我明白这是个问题。 @mbernasocchi 非常感谢,如果我想查看直方图(散点图中的每个点都有不同的直方图),或者更好的是,我需要在 gid 参数中提供什么 -二维直方图的地图? @NikanaReklawyks 我添加了an answer,它实际上回答了这个问题。【参考方案3】:

来自http://matplotlib.sourceforge.net/examples/event_handling/pick_event_demo.html

from matplotlib.pyplot import figure, show
import numpy as npy
from numpy.random import rand


if 1: # picking on a scatter plot (matplotlib.collections.RegularPolyCollection)

    x, y, c, s = rand(4, 100)
    def onpick3(event):
        ind = event.ind
        print('onpick3 scatter:', ind, npy.take(x, ind), npy.take(y, ind))

    fig = figure()
    ax1 = fig.add_subplot(111)
    col = ax1.scatter(x, y, 100*s, c, picker=True)
    #fig.savefig('pscoll.eps')
    fig.canvas.mpl_connect('pick_event', onpick3)

show()
此配方在选择数据点时绘制注释:http://scipy-cookbook.readthedocs.io/items/Matplotlib_Interactive_Plotting.html。 这个配方绘制了一个工具提示,但它需要 wxPython: Point and line tooltips in matplotlib?

【讨论】:

这正是我所需要的,谢谢!作为奖励,为了实现它,我重写了我的程序,因此我没有在同一个图形上创建两个不同颜色的单独散点图来表示两组数据,而是复制了示例中为一个点分配颜色的方法。这使我的程序更易于阅读,代码更少。现在开始寻找将颜色转换为数字的指南! 这是散点图。线图呢?我试图让它对它们起作用,但它没有。有解决办法吗? @Sohaib 看我的回答 我对此有疑问。当我像这样散点图我的点时: plt.scatter(X_reduced[y == i, 0], X_reduced[y == i, 1], c=c, label=target_name, picker=True) i、c 和 target_name,那么我的索引顺序是否混乱?而且我无法再查找它属于哪个数据点? 这似乎不适用于带有 ipython 5 的 jupyter 5 笔记本。有没有简单的方法来解决这个问题? print 语句还应使用括号以与 python 3 兼容【参考方案4】:

我的解决方案很简单:

import matplotlib.pyplot as plt
import mplcursors
plt.plot(...)
mplcursors.cursor(hover=True)
plt.show()

你可以得到类似的东西

【讨论】:

迄今为止最好的解决方案,只有几行代码完全符合 OP 的要求 这不仅限于 jupyter 是什么?它适用于常规 python。 请问有什么方法可以使用 mplcursors 添加多个数据标签。当我尝试添加第二个数据标签时,第一个数据标签消失 我发现这是最好的代码。但是,一旦我放大,我就会失去显示值的悬停选项。 不需要 jupyter 工作!【参考方案5】:

其他答案没有解决我在最新版本的 Jupyter 内联 matplotlib 图中正确显示工具提示的需要。这个虽然有效:

import matplotlib.pyplot as plt
import numpy as np
import mplcursors
np.random.seed(42)

fig, ax = plt.subplots()
ax.scatter(*np.random.random((2, 26)))
ax.set_title("Mouse over a point")
crs = mplcursors.cursor(ax,hover=True)

crs.connect("add", lambda sel: sel.annotation.set_text(
    'Point ,'.format(sel.target[0], sel.target[1])))
plt.show()

用鼠标经过一个点时会导致如下图所示:

【讨论】:

来源(未注明出处)是mplcursors.readthedocs.io/en/stable/examples/hover.html 我无法在 jupyter 实验室中使用它。它是否可能在 jupyter notebook 中有效,但在 jupyter lab 中无效? 嗯...不知道从库文档中不归属代码 sn-ps 是不是很重要。 @MD004 请参阅 ***.com/questions/50149562/… 了解 jupyterlab - 您可以添加“%matplotlib 小部件”来完成这项工作。【参考方案6】:

对http://matplotlib.org/users/shell.html中提供的示例稍作修改:

import numpy as np
import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_title('click on points')

line, = ax.plot(np.random.rand(100), '-', picker=5)  # 5 points tolerance


def onpick(event):
    thisline = event.artist
    xdata = thisline.get_xdata()
    ydata = thisline.get_ydata()
    ind = event.ind
    print('onpick points:', *zip(xdata[ind], ydata[ind]))


fig.canvas.mpl_connect('pick_event', onpick)

plt.show()

正如 Sohaib 所要求的那样,这绘制了一条直线图

【讨论】:

【参考方案7】:

mpld3 为我解决它。 编辑(添加代码):

import matplotlib.pyplot as plt
import numpy as np
import mpld3

fig, ax = plt.subplots(subplot_kw=dict(axisbg='#EEEEEE'))
N = 100

scatter = ax.scatter(np.random.normal(size=N),
                 np.random.normal(size=N),
                 c=np.random.random(size=N),
                 s=1000 * np.random.random(size=N),
                 alpha=0.3,
                 cmap=plt.cm.jet)
ax.grid(color='white', linestyle='solid')

ax.set_title("Scatter Plot (with tooltips!)", size=20)

labels = ['point 0'.format(i + 1) for i in range(N)]
tooltip = mpld3.plugins.PointLabelTooltip(scatter, labels=labels)
mpld3.plugins.connect(fig, tooltip)

mpld3.show()

你可以查看this例子

【讨论】:

请包含示例代码,不要仅仅链接到没有上下文或信息的外部资源。请参阅the Help Center 了解更多信息。 很遗憾,自 2017 年 7 月起,mpld3 不再被积极维护 代码示例因TypeError: array([1.]) is not JSON serializable 而失败。 @P-Gn 只需按照这里的技巧***.com/questions/48015030/mpld3-with-python-error MPLD3 是一个简单的解决方案,一旦遵循上述答案,它就可以工作。 @Zalakain 不幸的是,mpl3d seems to be abandonned.【参考方案8】:

mplcursors 为我工作。 mplcursors 为 matplotlib 提供可点击的注释。它受到 mpldatacursor (https://github.com/joferkington/mpldatacursor) 的极大启发,具有非常简化的 API

import matplotlib.pyplot as plt
import numpy as np
import mplcursors

data = np.outer(range(10), range(1, 5))

fig, ax = plt.subplots()
lines = ax.plot(data)
ax.set_title("Click somewhere on a line.\nRight-click to deselect.\n"
             "Annotations can be dragged.")

mplcursors.cursor(lines) # or just mplcursors.cursor()

plt.show()

【讨论】:

我自己使用这个,到目前为止,对于匆忙的人来说是最简单的解决方案。我刚刚绘制了 70 个标签,matplotlib 使每 10 行的颜色相同,真是太痛苦了。 mplcursors 解决了它。【参考方案9】:

在 matplotlib 状态栏中显示对象信息

特点

不需要额外的库 干净的地块 标签和艺术家没有重叠 支持多艺术家标签 可以处理来自不同绘图调用的艺术家(如scatterplotadd_patch) 库风格的代码

代码

### imports
import matplotlib as mpl
import matplotlib.pylab as plt
import numpy as np


# https://***.com/a/47166787/7128154
# https://matplotlib.org/3.3.3/api/collections_api.html#matplotlib.collections.PathCollection
# https://matplotlib.org/3.3.3/api/path_api.html#matplotlib.path.Path
# https://***.com/questions/15876011/add-information-to-matplotlib-navigation-toolbar-status-bar
# https://***.com/questions/36730261/matplotlib-path-contains-point
# https://***.com/a/36335048/7128154
class StatusbarHoverManager:
    """
    Manage hover information for mpl.axes.Axes object based on appearing
    artists.

    Attributes
    ----------
    ax : mpl.axes.Axes
        subplot to show status information
    artists : list of mpl.artist.Artist
        elements on the subplot, which react to mouse over
    labels : list (list of strings) or strings
        each element on the top level corresponds to an artist.
        if the artist has items
        (i.e. second return value of contains() has key 'ind'),
        the element has to be of type list.
        otherwise the element if of type string
    cid : to reconnect motion_notify_event
    """
    def __init__(self, ax):
        assert isinstance(ax, mpl.axes.Axes)


        def hover(event):
            if event.inaxes != ax:
                return
            info = 'x=:.2f, y=:.2f'.format(event.xdata, event.ydata)
            ax.format_coord = lambda x, y: info
        cid = ax.figure.canvas.mpl_connect("motion_notify_event", hover)

        self.ax = ax
        self.cid = cid
        self.artists = []
        self.labels = []

    def add_artist_labels(self, artist, label):
        if isinstance(artist, list):
            assert len(artist) == 1
            artist = artist[0]

        self.artists += [artist]
        self.labels += [label]

        def hover(event):
            if event.inaxes != self.ax:
                return
            info = 'x=:.2f, y=:.2f'.format(event.xdata, event.ydata)
            for aa, artist in enumerate(self.artists):
                cont, dct = artist.contains(event)
                if not cont:
                    continue
                inds = dct.get('ind')
                if inds is not None:  # artist contains items
                    for ii in inds:
                        lbl = self.labels[aa][ii]
                        info += ';   artist [:d, :d]: :'.format(
                            aa, ii, lbl)
                else:
                    lbl = self.labels[aa]
                    info += ';   artist [:d]: :'.format(aa, lbl)
            self.ax.format_coord = lambda x, y: info

        self.ax.figure.canvas.mpl_disconnect(self.cid)
        self.cid = self.ax.figure.canvas.mpl_connect(
            "motion_notify_event", hover)



def demo_StatusbarHoverManager():
    fig, ax = plt.subplots()
    shm = StatusbarHoverManager(ax)

    poly = mpl.patches.Polygon(
        [[0,0], [3, 5], [5, 4], [6,1]], closed=True, color='green', zorder=0)
    artist = ax.add_patch(poly)
    shm.add_artist_labels(artist, 'polygon')

    artist = ax.scatter([2.5, 1, 2, 3], [6, 1, 1, 7], c='blue', s=10**2)
    lbls = ['point ' + str(ii) for ii in range(4)]
    shm.add_artist_labels(artist, lbls)

    artist = ax.plot(
        [0, 0, 1, 5, 3], [0, 1, 1, 0, 2], marker='o', color='red')
    lbls = ['segment ' + str(ii) for ii in range(5)]
    shm.add_artist_labels(artist, lbls)

    plt.show()


# --- main
if __name__== "__main__":
    demo_StatusbarHoverManager()

【讨论】:

【参考方案10】:

我已经制作了一个多行注释系统以添加到:https://***.com/a/47166787/10302020。 对于最新版本: https://github.com/AidenBurgess/MultiAnnotationLineGraph

只需更改底部的数据。

import matplotlib.pyplot as plt


def update_annot(ind, line, annot, ydata):
    x, y = line.get_data()
    annot.xy = (x[ind["ind"][0]], y[ind["ind"][0]])
    # Get x and y values, then format them to be displayed
    x_values = " ".join(list(map(str, ind["ind"])))
    y_values = " ".join(str(ydata[n]) for n in ind["ind"])
    text = ", ".format(x_values, y_values)
    annot.set_text(text)
    annot.get_bbox_patch().set_alpha(0.4)


def hover(event, line_info):
    line, annot, ydata = line_info
    vis = annot.get_visible()
    if event.inaxes == ax:
        # Draw annotations if cursor in right position
        cont, ind = line.contains(event)
        if cont:
            update_annot(ind, line, annot, ydata)
            annot.set_visible(True)
            fig.canvas.draw_idle()
        else:
            # Don't draw annotations
            if vis:
                annot.set_visible(False)
                fig.canvas.draw_idle()


def plot_line(x, y):
    line, = plt.plot(x, y, marker="o")
    # Annotation style may be changed here
    annot = ax.annotate("", xy=(0, 0), xytext=(-20, 20), textcoords="offset points",
                        bbox=dict(boxstyle="round", fc="w"),
                        arrowprops=dict(arrowstyle="->"))
    annot.set_visible(False)
    line_info = [line, annot, y]
    fig.canvas.mpl_connect("motion_notify_event",
                           lambda event: hover(event, line_info))


# Your data values to plot
x1 = range(21)
y1 = range(0, 21)
x2 = range(21)
y2 = range(0, 42, 2)
# Plot line graphs
fig, ax = plt.subplots()
plot_line(x1, y1)
plot_line(x2, y2)
plt.show()

【讨论】:

以上是关于如何在 matplotlib 中添加悬停注释的主要内容,如果未能解决你的问题,请参考以下文章

Python和Matplotlib以及鼠标悬停的注释

Networkx 和 Matplotlib:如何访问节点属性并将它们显示为注释

pyqt 中的多个嵌入式 matplotlib 画布在鼠标悬停时改变大小

如何使用 mplcursors 在条形图上添加悬停注释

如何向 matplotlib 注释添加附加文本

如何在Kivy的matplotlib图表上实现鼠标悬停数据标签弹出窗口