交互式注释仅显示在 matplotlib 中的一个数据集

Interactive annotations only show up for one data set in matplotlib

我编写了一个带有注释的脚本,根据用户 ImportanceOfBeingErnest 对类似问题的一些回答,将鼠标悬停在数据点上时会显示这些注释。我所做的更改之一是我只更改单个注释的文本和位置并将其用于多个数据集。这似乎导致了所有数据集/绘图仪列表中仅针对最后一个数据集(或绘图仪,我在脚本中称它们为绘图仪)显示注释的问题。

如何在我的脚本中为两个散点图的所有数据点显示注释?我是否必须为每个数据集做一个新的注释并分别更新它们?

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.ticker import AutoMinorLocator, MultipleLocator
from scipy.stats import linregress


# All data in pA*s
gc_data = {
'KAL1':{'Toluol':400754.594,'1-Octen':53695.014,'Decan':6443.483,'1-Nonannitril':48984.504},
'KAL2':{'Toluol':417583.343,'1-Octen':29755.3,'Decan':16264.896,'1-Nonannitril':16264.896},
'KAL3':{'Toluol':442378.88,'1-Octen':18501.12,'Decan':19226.245,'1-Nonannitril':16200.611},
'KAL4':{'Toluol':389679.589,'1-Octen':13381.415,'Decan':68549.002,'1-Nonannitril':11642.123},
'KAL5':{'Toluol':423982.487,'1-Octen':6263.286,'Decan':53580.809,'1-Nonannitril':4946.271},
'KAL6':{'Toluol':351754.329,'1-Octen':8153.602,'Decan':105408.823,'1-Nonannitril':7066.718}
}

# All data in mg
mass_data = {
'KAL1':{'1-Octen':149.3,'Decan':17.8,'1-Nonannitril':154.7},
'KAL2':{'1-Octen':80.6,'Decan':43.7,'1-Nonannitril':82.8},
'KAL3':{'1-Octen':50.4,'Decan':51.8,'1-Nonannitril':51.5},
'KAL4':{'1-Octen':40.9,'Decan':206.9,'1-Nonannitril':40.8},
'KAL5':{'1-Octen':18.0,'Decan':155.2,'1-Nonannitril':16.4},
'KAL6':{'1-Octen':23.4,'Decan':301.4,'1-Nonannitril':23.6},
}





def update_annot(line, annot, ind):

    if isinstance(line, matplotlib.collections.PathCollection):
        x,y = line.get_offsets().transpose()
    elif isinstance(line, matplotlib.lines.Line2D):
        x,y = line.get_data()
    else:
        quit('No getter of x,y Data for this type of plotter.')

    annot.xy = (x[ind["ind"][0]], y[ind["ind"][0]])
    text = "x = {}\ny= {}".format(x[ind["ind"][0]], y[ind["ind"][0]])
    annot.set_text(text)


def hover(event,fig,annot):

    if event.inaxes in fig.axes:
        plotters = fig.axes[0].collections
        for plotter in plotters:
            cont, ind = plotter.contains(event)
            if cont:
                update_annot(plotter, annot, ind)
                annot.set_visible(True)
                fig.canvas.draw_idle()
            else:
                if annot.get_visible():
                    annot.set_visible(False)
                    fig.canvas.draw_idle()




def get_data(substance,standard):

    m = [mass_data[i][substance]/mass_data[i][standard] for i in mass_data] 
    A = [gc_data[i][substance]/gc_data[i][standard] for i in mass_data]

    return A,m



def plot(substance,standard,save=None):

    A,m = get_data(substance,standard)

    A_baddata = A.pop(1)
    m_baddata = m.pop(1)

    # Linear regression 
    a,b,rval,pval,stdev = linregress(A,m)

    # Plotting
    fig, ax = plt.subplots(figsize=(6,6))

    # Data inputs
    ax.scatter(A,m,marker='o') # Measured data
    ax.scatter(A_baddata,m_baddata,marker='o',c='r')

    xmin,xmax = ax.get_xlim()
    ymin,ymax = ax.get_ylim()
    ax.plot(np.array([-2*max(A),2*max(A)]),np.array([-2*max(A),2*max(A)])*a + b) # graph from regression parameters
    ax.set_ylim(ymin,ymax)
    ax.set_xlim(xmin,xmax)

    # General formatting
    ax.tick_params(axis='both',which='both',labelsize=12,direction='in')

    ax.xaxis.set_major_locator(MultipleLocator(1))
    ax.yaxis.set_major_locator(MultipleLocator(1))
    ax.xaxis.set_minor_locator(AutoMinorLocator())
    ax.yaxis.set_minor_locator(AutoMinorLocator())

    ax.set_ylabel(r'$m_{\mathrm{Substanz}}\quad/\quadm_{\mathrm{Standard}}$')
    ax.set_xlabel(r'$A_{\mathrm{Substanz}}\quad/\quadA_{\mathrm{Standard}}$')

    # Description Box
    textstr='{}{}\n'.format('Substanz: ',substance)
    textstr+='{}{}\n'.format('Standard: ',standard)
    textstr+='{}{:.5f}\n'.format('a = ',a)
    textstr+='{}{:.5f}\n'.format('b = ',b)
    textstr+='{}{:.5f}\n'.format(r'$R^{2}$ = ',rval)
    textstr+='{}{:.5f}\n'.format(r'$p$ = ',pval)
    textstr+='{}{:.5f}'.format(r'$\bar X = $',stdev)

    props = dict(boxstyle='round', fc='#96FBFF', ec='#3CF8FF', alpha=0.5)
    ax.text(0.05, 0.95, textstr, transform=ax.transAxes, fontsize=10,
            verticalalignment='top', bbox=props)


    if save:
        plt.savefig(substance+'.svg' ,bbox_inches='tight', transparent=True)
    else:
        # Hovering annotation
        ################################################################################################

        # for i in range(len())
        annot = ax.annotate("", xy=(0,0), xytext=(1,1),textcoords="offset points",
                            bbox=dict(boxstyle="round", fc="w", alpha=0.4),
                            arrowprops=dict(arrowstyle="->"))
        annot.set_visible(False)
        ################################################################################################

        fig.canvas.mpl_connect("motion_notify_event", lambda event: hover(event, fig, annot))
        plt.show()

plot('1-Nonannitril','Decan',0)

主要问题是悬停事件是由线而不是附近的散点触发的。因此,连接 motion_notify_event.

时应排除此行

由于 ImportanceOfBeingErnest 和其他人发布了有关如何创建注释的帖子,他们开发了 mplcursors 库来大大简化此类注释的创建。

使用 mplcursors 您可以简单地调用 mplcursors.cursor(ax.collections, hover=True) 并自动创建一个带有 x 和 y 位置的注释。但很容易可以走得更远。下面的示例还显示了如何显示艺术家的标签(这里 'artist' 是一组散点)。此外,如何使用艺术家的颜色作为注释的背景。此外,一个额外的属性被添加到带有名称列表的艺术家。然后将这些名称添加到注释中。

代码省略了一些与注释无关的元素,例如大文本。

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.ticker import AutoMinorLocator, MultipleLocator
from scipy.stats import linregress
import mplcursors
from matplotlib.colors import to_rgb

# All data in pA*s
gc_data = {
    'KAL1': {'Toluol': 400754.594, '1-Octen': 53695.014, 'Decan': 6443.483, '1-Nonannitril': 48984.504},
    'KAL2': {'Toluol': 417583.343, '1-Octen': 29755.3, 'Decan': 16264.896, '1-Nonannitril': 16264.896},
    'KAL3': {'Toluol': 442378.88, '1-Octen': 18501.12, 'Decan': 19226.245, '1-Nonannitril': 16200.611},
    'KAL4': {'Toluol': 389679.589, '1-Octen': 13381.415, 'Decan': 68549.002, '1-Nonannitril': 11642.123},
    'KAL5': {'Toluol': 423982.487, '1-Octen': 6263.286, 'Decan': 53580.809, '1-Nonannitril': 4946.271},
    'KAL6': {'Toluol': 351754.329, '1-Octen': 8153.602, 'Decan': 105408.823, '1-Nonannitril': 7066.718}
}

# All data in mg
mass_data = {
    'KAL1': {'1-Octen': 149.3, 'Decan': 17.8, '1-Nonannitril': 154.7},
    'KAL2': {'1-Octen': 80.6, 'Decan': 43.7, '1-Nonannitril': 82.8},
    'KAL3': {'1-Octen': 50.4, 'Decan': 51.8, '1-Nonannitril': 51.5},
    'KAL4': {'1-Octen': 40.9, 'Decan': 206.9, '1-Nonannitril': 40.8},
    'KAL5': {'1-Octen': 18.0, 'Decan': 155.2, '1-Nonannitril': 16.4},
    'KAL6': {'1-Octen': 23.4, 'Decan': 301.4, '1-Nonannitril': 23.6},
}

def update_annot(sel):
    x, y = sel.target
    label = sel.artist.get_label()
    new_text = f'{label}\nx: {x:.2f}\ny: {y:.2f}'
    # append the name
    new_text += '\n' + sel.artist.data_names[sel.target.index]
    sel.annotation.set_text(new_text)
    # get the color of the scatter dots, make them whiter and use that as background color for the annotation
    r, g, b = to_rgb(sel.artist.get_facecolor())
    sel.annotation.get_bbox_patch().set(fc=((r + 2) / 3, (g + 2) / 3, (b + 2) / 3), alpha=0.7)

def get_data(substance, standard):
    m = [mass_data[i][substance] / mass_data[i][standard] for i in mass_data]
    A = [gc_data[i][substance] / gc_data[i][standard] for i in mass_data]
    return A, m

def plot(substance, standard, save=None):
    global measured_names, baddata_names
    A, m = get_data(substance, standard)
    measured_names = list(mass_data.keys())

    A_baddata = A.pop(1)
    m_baddata = m.pop(1)
    baddata_names = [measured_names.pop(1)]

    # Linear regression
    a, b, rval, pval, stdev = linregress(A, m)

    # Plotting
    fig, ax = plt.subplots(figsize=(6, 6))

    # Data inputs
    scat1 = ax.scatter(A, m, marker='o', label='Measured data')  # Measured data
    scat1.data_names = measured_names
    scat2 = ax.scatter(A_baddata, m_baddata, marker='o', c='r', label='Bad data')
    scat2.data_names = baddata_names

    xmin, xmax = ax.get_xlim()
    ymin, ymax = ax.get_ylim()
    ax.plot(np.array([-2 * max(A), 2 * max(A)]),
            np.array([-2 * max(A), 2 * max(A)]) * a + b)  # graph from regression parameters
    ax.set_ylim(ymin, ymax)
    ax.set_xlim(xmin, xmax)

    ax.set_ylabel(r'$m_{\mathrm{Substanz}}\quad/\quadm_{\mathrm{Standard}}$')
    ax.set_xlabel(r'$A_{\mathrm{Substanz}}\quad/\quadA_{\mathrm{Standard}}$')

    # Hovering annotation
    # cursor = mplcursors.cursor(ax.collections, hover=True)
    cursor = mplcursors.cursor([scat1, scat2], hover=True)
    cursor.connect("add", update_annot)
    plt.show()

plot('1-Nonannitril', 'Decan', 0)