3

I'm using matplotlib to make step graphs based on a dataframe, but I want one of the key/value of the dataframe to appear (signals_df['Gage']), instead of coordinates as annotation, but I always get the error: AttributeError: 'Line2D' object has no attribute 'get_offsets' when I click on the first subplot from bottom to top and the annotation does not appear. In fact, I commented out the annot.set_visible(False)and replaced the "" of the examples with val_gage, so that it will look like I want the annotation to appear one by one, when clicking on some point within the subplots. This is the code in question:

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)

I've tested and reviewed many answers and code like the following:

I even reviewed the mplcursors module for a long time, since it comes with an example with a graph of steps similar to what I'm doing: https://mplcursors.readthedocs.io/en/stable/examples/step.html, but it gives me the same result and I can't find the solution.

John Collins
  • 2,067
  • 9
  • 17
MayEncoding
  • 87
  • 1
  • 12
  • 2
    Are you willing/interested in using plotly instead? It would be much easier (and more powerful) – John Collins Nov 01 '21 at 21:42
  • Thanks @JohnCollins. Well yes, of course, I just think, if I'm not mistaken, plotly is only for web and the work asked me is for desktop, apart this function is only one of several that I have in my development and I’ve to change a lot, no problem, but it would take me time, it would only be to know how I do it with plotly? – MayEncoding Nov 02 '21 at 16:16
  • Well actually no plotly is open source and can be enabled (I believe this may even now be the default -- although it did not used to be) to be entirely "offline" (meaning it will make no connections to the internet/plotly's servers -- so no, it's not only for web). If someone doesn't beat me to it, I will try to post an answer demonstrating, just FYI, how your question could be achieved using plotly in a totally offline fashion – John Collins Nov 03 '21 at 05:50
  • I'll be attentive. Thank you very much @JohnCollins – MayEncoding Nov 03 '21 at 16:54
  • @PureRangeIEncoding OK, answer posted. Much less fussing around necessary, as you can see. As I comment in my edits/revisions description, I am looking now through the docs to edit the answer to have the hover annotation be _only_ your "Gage" data value, as I understand is what you seek. It's definitely possible. Plotly.express auto sets up the hoverdata, so I just need to see how to undo that. Generally `plotly.express` is recommended, due to its elegant brevity – John Collins Nov 05 '21 at 04:45

2 Answers2

1

Without knowing much about the libraries you are using I can see you are creating these annotation objects and then assigning them to a global variable that is re-assigned later and thus you lose the right object to make it visible.

Instead you could keep the annotation objects into a dictionary and try to retrieve them later when you need them based on an object.

I used a list to show you the idea, but you need a dictionary I guess to identify the right objects.

I modified your code a bit and it shows the desired behaviour if you resize the window...I guess you have to find a way to refresh the plot also:

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)

I hope this helps and it will solve your problem. It looks more like a python/programming problem and not so much related to the libraries you are using if I get this right :)

demorgan
  • 79
  • 4
  • 2
    I see that you are a new contributor and this answer shows depth to the problem. Thumbs up for your effort! – Vojtech Stas Nov 04 '21 at 13:14
  • This also results in the same error the OP states, though: `AttributeError: 'Line2D' object has no attribute 'get_offsets'`, when trying to click on the data points. – John Collins Nov 05 '21 at 05:30
  • It's true, I also get the error of `AttributeError: 'Line2D' object has no attribute 'get_offsets'` but thank you very much @demorgan – MayEncoding Nov 05 '21 at 15:03
1

Using Plotly for data annotation labels animation upon mouse hovering over graph data points

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).


So with plotly, specifically plotly express, it's very simple!

I am not 100% what you want as far as specifics for your axes/data, but I think below demonstrates the tremendous ease with which Plotly can be used to create interactive graphs, & the very powerful customization available.

You will easily be able to adapt these interactive graphs to your desired purposes via cursory perusal through 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 - Data Structures Setup

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

E.g.,

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)

NOTE: I then ran signals_df through your plotting function, and added return signals_df to get the updated df, which was:

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 - Plotting custom hover annotations with 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})

example plotly express

added gif animation

John Collins
  • 2,067
  • 9
  • 17
  • Here is the information on how plotly is (now) totally offline, free, open source, by default: https://plotly.com/python/is-plotly-free/ (their model is that they offer additional "enterprise" solutions which enhance the APIs or handle devops with legitimate professional secure user authentication accounts and sign-on, etc., for a $; but the core code is totally free and works offline) – John Collins Nov 04 '21 at 23:39
  • 1
    Woow. Infinitely grateful for your answer @JohnCollins, it will help me a lot when I migrate everything to the web, because I made that proposal and they told me that it did not sound bad, although what they initially asked me is to separate the names and values by subplots, but it is a very good start . – MayEncoding Nov 05 '21 at 14:59
  • Awesome! Glad to hear it could work out for you leveraging Plotly. Making web apps with their associated framework (which renders react.js, from pure Python code) Dash is also very powerful, open source/free (if you can handle the deployment yourself - which is pretty easy, IMHO; depending on expected user load of course, I suppose), which I would highly recommend as well. I have written a number of answers in regards to Dash on SO here. Also, for your subplots, you can add just the line `facet_rows="Name"` to the `px` graph in code above, and that alone will split the output into four plots – John Collins Nov 05 '21 at 15:17
  • Hello @JohnCollins, could you tell me how to remove the legends of the names that appear on the right side when added `facet_rows="Name"`?, since when it is a long name they are overwritten and not understood; I have searched the page: [plotly express reference](https://plotly.com/python-api-reference/generated/plotly.express.line.html) and reviewed examples but can't find. Please and thanks. – MayEncoding Dec 01 '21 at 01:33
  • I tried it with labels = {'Name': '', 'Value': ''}, only value is removed, but the name still overwrites the content of the strings, as it would be in this case? – MayEncoding Dec 01 '21 at 01:50