单击 matplotlib 中的步骤图子图点时如何使标签出现(或可能是 plotly)?

Posted

技术标签:

【中文标题】单击 matplotlib 中的步骤图子图点时如何使标签出现(或可能是 plotly)?【英文标题】:How to make labels appear when clicking on a step plot subplot point in matplotlib (or possibly plotly)? 【发布时间】:2021-12-12 06:12:30 【问题描述】:

我正在使用 matplotlib 基于数据框制作阶梯图,但我希望出现数据框的键/值之一 (signals_df['Gage']),而不是坐标作为注释,但我总是得到错误: AttributeError: 'Line2D' object has no attribute 'get_offsets' 当我从下到上单击第一个子图并且没有出现注释时。事实上,我注释掉了annot.set_visible(False) 并将示例中的"" 替换为val_gage,这样当单击子图中的某个点时,看起来我希望注释一一出现。 这是有问题的代码:

import pandas as pd
import numpy as np
import matplotlib as mtpl
from matplotlib import pyplot as plt
import matplotlib.ticker as ticker

annot = mtpl.text.Annotation

data = 
    # 'Name': ['Status', 'Status', 'HMI', 'Allst', 'Drvr', 'CurrTUBand', 'RUSource', 'RUReqstrPriority', 'RUReqstrSystem', 'RUResReqstStat', 'CurrTUBand', 'DSP', 'SetDSP', 'SetDSP', 'DSP', 'RUSource', 'RUReqstrPriority', 'RUReqstrSystem', 'RUResReqstStat', 'Status', 'Delay', 'Status', 'Delay', 'HMI', 'Status', 'Status', 'HMI', 'DSP'],
    # 'Value': [4, 4, 2, 1, 1, 1, 0, 7, 0, 4, 1, 1, 3, 0, 3, 0, 7, 0, 4, 1, 0, 1, 0, 1, 4, 4, 2, 3],
    # 'Gage': ['H1', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H1', 'H1', 'H3', 'H3', 'H3', 'H1', 'H3', 'H3', 'H3'],
    # 'Id_Par': [0, 0, 0, 0, 0, 0, 10, 10, 10, 10, 10, 0, 0, 22, 22, 28, 28, 28, 28, 0, 0, 38, 38, 0, 0, 0, 0, 0]
    'Name': ['Lamp_D_Rq', 'Status', 'Status', 'HMI', 'Lck_D_RqDrv3', 'Lck_D_RqDrv3', 'Lck_D_RqDrv3', 'Lck_D_RqDrv3', 'Lamp_D_Rq', 'Lamp_D_Rq', 'Lamp_D_Rq', 'Lamp_D_Rq'],
    'Value': [0, 4, 4, 2, 1, 1, 2, 2, 1, 1, 3, 3],
    'Gage': ['F1', 'H1', 'H3', 'H3', 'H3', 'F1', 'H3', 'F1', 'F1', 'H3', 'F1', 'H3'],
    'Id_Par': [0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0]
    

signals_df = pd.DataFrame(data)


def plot_signals(signals_df):
    print(signals_df)
    # Count signals by parallel
    signals_df['Count'] = signals_df.groupby('Id_Par').cumcount().add(1).mask(signals_df['Id_Par'].eq(0), 0)
    # Subtract Parallel values from the index column
    signals_df['Sub'] = signals_df.index - signals_df['Count']
    id_par_prev = signals_df['Id_Par'].unique()
    id_par = np.delete(id_par_prev, 0)
    signals_df['Prev'] = [1 if x in id_par else 0 for x in signals_df['Id_Par']]
    signals_df['Final'] = signals_df['Prev'] + signals_df['Sub']
    # Convert and set Subtract to index
    signals_df.set_index('Final', inplace=True)

    # Get individual names and variables for the chart
    names_list = [name for name in signals_df['Name'].unique()]
    num_names_list = len(names_list)
    num_axisx = len(signals_df["Name"])

    # Matplotlib's categorical feature to convert x-axis values to string
    x_values = [-1, ]
    x_values += (list(set(signals_df.index)))
    x_values = [str(i) for i in sorted(x_values)]

    # Creation Graphics
    fig, ax = plt.subplots(nrows=num_names_list, figsize=(10, 10), sharex=True)
    plt.xticks(np.arange(0, num_axisx), color='SteelBlue', fontweight='bold')

    # Loop to build the different graphs
    for pos, name in enumerate(names_list):
        # Creating a dummy plot and then remove it
        dummy, = ax[pos].plot(x_values, np.zeros_like(x_values))
        dummy.remove()

        # Get names by values and gage data
        data = signals_df[signals_df["Name"] == name]["Value"]
        data_gage = signals_df[signals_df["Name"] == name]["Gage"]

        # Get values axis-x and axis-y
        x_ = np.hstack([-1, data.index.values, len(signals_df) - 1])
        y_ = np.hstack([0, data.values, data.iloc[-1]])
        y_gage = np.hstack(["", "-", data_gage.values])
        # print(y_gage)

        # Plotting the data by position
        steps = ax[pos].plot(x_.astype('str'), y_, drawstyle='steps-post', marker='*', markersize=8, color='k', linewidth=2)
        ax[pos].set_ylabel(name, fontsize=8, fontweight='bold', color='SteelBlue', rotation=30, labelpad=35)
        ax[pos].yaxis.set_major_formatter(ticker.FormatStrFormatter('%0.1f'))
        ax[pos].yaxis.set_tick_params(labelsize=6)
        ax[pos].grid(alpha=0.4, color='SteelBlue')
        # Labeling the markers with Values and Gage
        xy_temp = []
        for i in range(len(y_)):
            if i == 0:
                xy = [x_[0].astype('str'), y_[0]]
                xy_temp.append(xy)
            else:
                xy = [x_[i - 1].astype('str'), y_[i - 1]]
                xy_temp.append(xy)

            # Creating values in text inside the plot
            ax[pos].text(x=xy[0], y=xy[1], s=str(xy[1]), color='k', fontweight='bold', fontsize=12)

            for val_gage, xy in zip(y_gage, xy_temp):
                annot = ax[pos].annotate(val_gage, xy=xy, xytext=(-20, 20), textcoords="offset points",
                                         bbox=dict(boxstyle="round", fc="w"),
                                         arrowprops=dict(arrowstyle="->"))
                # annot.set_visible(False)

    # Function for storing and showing the clicked values
    def update_annot(ind):
        print("Enter update_annot")
        coord = steps[0].get_offsets()[ind["ind"][0]]
        annot.xy = coord
        text = ", ".format(" ".join(list(map(str, ind["ind"]))),
                                " ".join([y_gage[n] for n in ind["ind"]]))
        annot.set_text(text)
        annot.get_bbox_patch().set_alpha(0.4)

    def on_click(event):
        print("Enter on_click")
        vis = annot.get_visible()
        # print(event.inaxes)
        # print(ax[pos])
        # print(event.inaxes == ax[pos])
        if event.inaxes == ax[pos]:
            cont, ind = steps[0].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("button_press_event",on_click)

    plt.show()

plot_signals(signals_df)

我已经测试并查看了许多答案和代码,如下所示:

How to add hovering annotations in matplotlib How to make labels appear when hovering over a point in multiple axis? ¿Es posible que aparezcan etiquetas al pasar el mouse sobre un punto en matplotlib? Matplotlib Cursor — How to Add a Cursor and Annotate Your Plot

我什至审查了 mplcursors 模块很长时间,因为它带有一个与我正在做的类似的步骤图的示例:https://mplcursors.readthedocs.io/en/stable/examples/step.html,但它给了我相同的结果,我找不到解决办法。

【问题讨论】:

您是否愿意/有兴趣使用 plotly 代替?这会更容易(也更强大) 谢谢@JohnCollins。是的,当然,我只是想,如果我没记错的话,plotly 仅适用于网络,而要求我的工作适用于桌面,除此之外,此功能只是我在开发中拥有的几个功能之一,我必须改变很多,没问题,但这需要我时间,只是知道我是如何用plotly做到的? 其实没有任何 plotly 是开源的并且可以启用(我相信这甚至可能是现在的默认设置——尽管它以前不是)完全“离线”(这意味着它将使没有连接到互联网/plotly 的服务器——所以不,它不仅适用于网络)。如果有人没有打败我,我会尝试发布一个答案,仅供参考,如何以完全离线的方式使用 plotly 来解决您的问题 我会注意的。非常感谢@JohnCollins @PureRangeIEncoding 好的,已发布答案。如您所见,不必大惊小怪。正如我在我的编辑/修订描述中评论的那样,我现在正在查看文档以编辑答案,以使悬停注释您的“量具”数据值,据我所知,这就是您所寻求的。这绝对是可能的。 Plotly.express 自动设置悬停数据,所以我只需要看看如何撤消它。一般推荐plotly.express,因为它简洁优雅 【参考方案1】:

鼠标悬停在图形数据点上时使用 Plotly 进行数据注释标签动画

更不用说大量其他很棒的、易于使用的、广泛兼容的 JS 交互式绘图功能,全部免费,全部使用 Python。只需使用 conda(或 pip)安装,无需在线帐户,并且最新版本中的绘图默认为“离线模式”。


所以用plotly,特别是plotly express,很简单!

就坐标轴/数据的细节而言,我并不是 100% 你想要的,但我认为下面展示了 Plotly 可用于创建交互式图表的巨大易用性,以及非常强大的自定义功能。

通过the plotly docs 粗略阅读,您将能够轻松地将这些交互式图表调整为您想要的目的。

通过plotly.express,您仍然可以访问与所有其他子模块相关的内置Fig 功能。所以不要忽视那些[例如,上面的文档链接显示了特定于子图、自定义注释/悬停注释、自定义样式格式等的部分,所有这些仍然适用于plotly.express 中的对象! ])。

I - 数据结构设置

与您的相同... Plotly 旨在pandas.DataFrames 一起使用,特别是*。

*(与 matplotlib 不同——并不是说它仍然不受欢迎!,只是...    好吧,它老化得相当糟糕,让我们面对现实吧。)

例如,

import plotly.express as px
import plotly.graph_objs as go

import pandas as pd
import numpy as np

data = 
    "Name": [
        "Lamp_D_Rq", "Status", "Status", "HMI",
        "Lck_D_RqDrv3", "Lck_D_RqDrv3", "Lck_D_RqDrv3",
        "Lck_D_RqDrv3", "Lamp_D_Rq", "Lamp_D_Rq",
        "Lamp_D_Rq", "Lamp_D_Rq",
    ],
    "Value": [0, 4, 4, 2, 1, 1, 2, 2, 1, 1, 3, 3],
    "Gage": [
        "F1", "H1", "H3", "H3", "H3",
        "F1", "H3", "F1", "F1", "H3",
        "F1", "H3",
    ],
    "Id_Par": [0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0],


signals_df = pd.DataFrame(data)

注意:然后我通过您的绘图功能运行signals_df,并添加return signals_df 以获取更新的df,即:

Final Name Value Gage Id_Par Count Sub Prev
0 Lamp_D_Rq 0 F1 0 0 0 0
1 Status 4 H1 0 0 1 0
2 Status 4 H3 0 0 2 0
3 HMI 2 H3 11 1 2 1
4 Lck_D_RqDrv3 1 H3 0 0 4 0
5 Lck_D_RqDrv3 1 F1 0 0 5 0
6 Lck_D_RqDrv3 2 H3 0 0 6 0
7 Lck_D_RqDrv3 2 F1 0 0 7 0
8 Lamp_D_Rq 1 F1 0 0 8 0
9 Lamp_D_Rq 1 H3 0 0 9 0
10 Lamp_D_Rq 3 F1 0 0 10 0
11 Lamp_D_Rq 3 H3 0 0 11 0

II - 使用plotly.express (px) 绘制自定义悬停注释

这是一个相对(即mpl)使用 Plotly(通过 px)非常简单、可能的多功能、现代交互式显示数据的方法:

fig = px.line(
    signals_df,
    y="Value",
    x="Sub",
    color="Name",
    hover_data=["Gage"],
    custom_data=["Gage"],
    markers=True,
    height=500,
    render_mode="svg")

fig.update_traces(line="shape": 'hv')
fig.update_traces(
    hovertemplate="<br>".join([
        "Gage: %customdata[0]",
    ])
)
fig.show(config='displaylogo': False)

【讨论】:

这里是关于 plotly 是如何(现在)完全离线、免费、开源的信息,默认情况下:plotly.com/python/is-plotly-free(他们的模型是他们提供额外的“企业”解决方案来增强 API 或使用合法的专业安全用户身份验证帐户和登录等处理 devops,只需 $;但核心代码完全免费?并且可以离线工作) 哇。非常感谢您的回答@JohnCollins,当我将所有内容迁移到网络时,它将对我有很大帮助,因为我提出了这个建议,他们告诉我这听起来不错,尽管他们最初要求我将名称和子图的值,但这是一个很好的开始。 太棒了!很高兴听到它可以为您利用 Plotly 解决问题。使用相关框架(从纯 Python 代码呈现 react.js)制作 Web 应用程序 Dash 也非常强大,开源/免费(如果您可以自己处理部署 - 这很容易,恕我直言;取决于预期的用户负载当然,我想),我也强烈推荐。我在这里写了很多关于 Dash on SO 的答案。此外,对于您的子图,您可以在上面的代码中将行 facet_rows="Name" 添加到 px 图中,仅此一项就会将输出分成四个图 你好@JohnCollins,你能告诉我如何删除添加facet_rows="Name"时出现在右侧的名称的图例吗?因为当它是一个长名称时,它们会被覆盖并且无法理解;我已经搜索了该页面:plotly express reference 并查看了示例但找不到。请和谢谢。 我用labels = 'Name': '', 'Value': ''试过了,只删除了值,但名称仍然会覆盖字符串的内容,因为它会在这种情况下?【参考方案2】:

在不太了解您正在使用的库的情况下,我可以看到您正在创建这些注释对象,然后将它们分配给稍后重新分配的全局变量,因此您失去了使其可见的正确对象。

相反,您可以将注释对象保存在字典中,并在以后需要时根据对象尝试检索它们。

我用一个列表向你展示了这个想法,但我猜你需要一本字典来识别正确的对象。

我稍微修改了您的代码,如果您调整窗口大小,它会显示所需的行为...我想您还必须找到一种方法来刷新绘图:

import pandas as pd
import numpy as np
import matplotlib as mtpl
from matplotlib import pyplot as plt
import matplotlib.ticker as ticker

annotations = []
data = 
    # 'Name': ['Status', 'Status', 'HMI', 'Allst', 'Drvr', 'CurrTUBand', 'RUSource', 'RUReqstrPriority', 'RUReqstrSystem', 'RUResReqstStat', 'CurrTUBand', 'DSP', 'SetDSP', 'SetDSP', 'DSP', 'RUSource', 'RUReqstrPriority', 'RUReqstrSystem', 'RUResReqstStat', 'Status', 'Delay', 'Status', 'Delay', 'HMI', 'Status', 'Status', 'HMI', 'DSP'],
    # 'Value': [4, 4, 2, 1, 1, 1, 0, 7, 0, 4, 1, 1, 3, 0, 3, 0, 7, 0, 4, 1, 0, 1, 0, 1, 4, 4, 2, 3],
    # 'Gage': ['H1', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H3', 'H1', 'H1', 'H3', 'H3', 'H3', 'H1', 'H3', 'H3', 'H3'],
    # 'Id_Par': [0, 0, 0, 0, 0, 0, 10, 10, 10, 10, 10, 0, 0, 22, 22, 28, 28, 28, 28, 0, 0, 38, 38, 0, 0, 0, 0, 0]
    'Name': ['Lamp_D_Rq', 'Status', 'Status', 'HMI', 'Lck_D_RqDrv3', 'Lck_D_RqDrv3', 'Lck_D_RqDrv3', 'Lck_D_RqDrv3', 'Lamp_D_Rq', 'Lamp_D_Rq', 'Lamp_D_Rq', 'Lamp_D_Rq'],
    'Value': [0, 4, 4, 2, 1, 1, 2, 2, 1, 1, 3, 3],
    'Gage': ['F1', 'H1', 'H3', 'H3', 'H3', 'F1', 'H3', 'F1', 'F1', 'H3', 'F1', 'H3'],
    'Id_Par': [0, 0, 0, 11, 0, 0, 0, 0, 0, 0, 0, 0]
    

signals_df = pd.DataFrame(data)


def plot_signals(signals_df):
    print(signals_df)
    # Count signals by parallel
    signals_df['Count'] = signals_df.groupby('Id_Par').cumcount().add(1).mask(signals_df['Id_Par'].eq(0), 0)
    # Subtract Parallel values from the index column
    signals_df['Sub'] = signals_df.index - signals_df['Count']
    id_par_prev = signals_df['Id_Par'].unique()
    id_par = np.delete(id_par_prev, 0)
    signals_df['Prev'] = [1 if x in id_par else 0 for x in signals_df['Id_Par']]
    signals_df['Final'] = signals_df['Prev'] + signals_df['Sub']
    # Convert and set Subtract to index
    signals_df.set_index('Final', inplace=True)

    # Get individual names and variables for the chart
    names_list = [name for name in signals_df['Name'].unique()]
    num_names_list = len(names_list)
    num_axisx = len(signals_df["Name"])

    # Matplotlib's categorical feature to convert x-axis values to string
    x_values = [-1, ]
    x_values += (list(set(signals_df.index)))
    x_values = [str(i) for i in sorted(x_values)]

    # Creation Graphics
    fig, ax = plt.subplots(nrows=num_names_list, figsize=(10, 10), sharex=True)
    plt.xticks(np.arange(0, num_axisx), color='SteelBlue', fontweight='bold')

    # Loop to build the different graphs
    for pos, name in enumerate(names_list):
        print("name: %s" % name)
        print("pos: %s" % pos)
        # Creating a dummy plot and then remove it
        dummy, = ax[pos].plot(x_values, np.zeros_like(x_values))
        dummy.remove()

        # Get names by values and gage data
        data = signals_df[signals_df["Name"] == name]["Value"]
        data_gage = signals_df[signals_df["Name"] == name]["Gage"]

        # Get values axis-x and axis-y
        x_ = np.hstack([-1, data.index.values, len(signals_df) - 1])
        y_ = np.hstack([0, data.values, data.iloc[-1]])
        y_gage = np.hstack(["", "-", data_gage.values])
        # print(y_gage)

        # Plotting the data by position
        steps = ax[pos].plot(x_.astype('str'), y_, drawstyle='steps-post', marker='*', markersize=8, color='k', linewidth=2)
        ax[pos].set_ylabel(name, fontsize=8, fontweight='bold', color='SteelBlue', rotation=30, labelpad=35)
        ax[pos].yaxis.set_major_formatter(ticker.FormatStrFormatter('%0.1f'))
        ax[pos].yaxis.set_tick_params(labelsize=6)
        ax[pos].grid(alpha=0.4, color='SteelBlue')
        # Labeling the markers with Values and Gage
        xy_temp = []
        for i in range(len(y_)):
            if i == 0:
                xy = [x_[0].astype('str'), y_[0]]
                xy_temp.append(xy)
            else:
                xy = [x_[i - 1].astype('str'), y_[i - 1]]
                xy_temp.append(xy)

            # Creating values in text inside the plot
            ax[pos].text(x=xy[0], y=xy[1], s=str(xy[1]), color='k', fontweight='bold', fontsize=12)

            for val_gage, xy in zip(y_gage, xy_temp):
                print("val_gage: %s" % val_gage)
                annot = ax[pos].annotate(val_gage, xy=xy, xytext=(-20, 20), textcoords="offset points",
                                         bbox=dict(boxstyle="round", fc="w"),
                                         arrowprops=dict(arrowstyle="->"))

                annot.set_visible(False)
                annotations.append(annot)

    # Function for storing and showing the clicked values
    def update_annot(ind):
        print("Enter update_annot")
        coord = steps[0].get_offsets()[ind["ind"][0]]
        annot.xy = coord
        text = ", ".format(" ".join(list(map(str, ind["ind"]))),
                                " ".join([y_gage[n] for n in ind["ind"]]))
        annot.set_text(text)
        annot.get_bbox_patch().set_alpha(0.4)

    def on_click(event):
        print("Enter on_click")
        vis = annot.get_visible()
        # make the first three annotations visible
        for i in range(0, 3):
            print('elem visible')
            annotations[i].set_visible(True)
        print(event.inaxes)
        print(ax[pos])
        print(event.inaxes == ax[pos])
        if event.inaxes == ax[pos]:
            cont, ind = steps[0].contains(event)
            print (ind)
            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("button_press_event",on_click)

    plt.show()

plot_signals(signals_df)

我希望这会有所帮助,并能解决您的问题。如果我做对了,它看起来更像是一个 python/编程问题,并且与您使用的库没有太大关系:)

【讨论】:

我看到你是一个新的贡献者,这个答案显示了问题的深度。为你的努力点赞! 这也会导致与 OP 状态相同的错误:AttributeError: 'Line2D' object has no attribute 'get_offsets',尝试单击数据点时。 是的,我也收到了AttributeError: 'Line2D' object has no attribute 'get_offsets'的错误,但非常感谢@demorgan

以上是关于单击 matplotlib 中的步骤图子图点时如何使标签出现(或可能是 plotly)?的主要内容,如果未能解决你的问题,请参考以下文章

为啥将两个条形图子图合并为一个会改变轴,我该如何解决?

matplotlib 子图的行标题

使用 matplotlib 中的许多子图改进子图大小/间距

Matplotlib 不同大小的子图

如何在 matplotlib 中制作空白子图?

matplotlib如何画子图