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

How to make labels appear when clicking on a step plot subplot point in matplotlib (or possibly plotly)?

我正在使用 matplotlib 基于数据框制作步骤图,但我希望数据框的 key/value 之一出现(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)

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

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

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

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

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

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

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/programming 问题,如果我没看错的话,与你正在使用的库没有太大关系:)

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

Not to mention a huge slew of other awesome, easy-to-use, widely-compatible JS-interactive graphing capabilities, all free, all in Python. Just install with conda (or pip) no online account required and the plots default to "offline mode" in latest version(s).


所以用plotly,具体plotly表达,很简单!

就您的 axes/data 的具体细节而言,我不是 100% 想要什么,但我认为下面展示了 Plotly 可用于创建交互式图形的巨大便利,以及非常强大的可用自定义功能.

通过 the plotly docs.

粗略浏览,您将能够轻松地将这些交互式图表调整到您想要的目的

And through plotly.express you still have access to the built-in Fig features relevant to all the other submodules, too. So don't overlook those [e.g., the docs link above shows sections specific for subplotting, custom annnotations/hover annotations, custom style formatting, etc., all which still apply to objects within plotly.express!]).

I - 数据结构设置

Identical to yours... Plotly is designed to work with pandas.DataFrames, specifically*.

*(Unlike matplotlib — not that it isn't still loved!, just...
    well, it's aging rather poorly let's face it.)

例如,

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)

绘制自定义悬停注释

Here's one relatively (i.e., to mpl) quite simple, possible multi-featured, modern interactive display of your data using Plotly (via 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})