How to annotate a broken_barh chart while hovering?

Viewed 623

I am trying to plot the Gantt chart using matplotlib in python, wherein there are two solutions suggested by different algorithms. Solution by each algorithm contains a group of batches (shown in different colors) starting and finishing at different points of time.

I am able to plot the same, but I want to annotate the graph in such a way that whenever I hover the mouse over the solution, it shows batch detail or length of the bar (processing time). I tried several ways, but not happening. [I would like to see (x,y)= (Batch Processing Time, Algorithm Name) value when I move the mouse over the batch solution.

import matplotlib.pyplot as plt 
import numpy as np
import pandas as pd

%matplotlib notebook
  
 
fig, gnt = plt.subplots() 
gnt.set_ylim(0, 50)
gnt.set_xlim(0, 65) 
 
    
# Setting labels for x-axis and y-axis 
gnt.set_xlabel('Batch Completion Time') 
gnt.set_ylabel('Solution by') 
  
# Setting ticks on y-axis 
gnt.set_yticks([10, 25])
gnt.set_yticklabels(['Algo_1', 'Algo_2']) 

# Setting graph attribute 
gnt.grid(True) 

     
#For Algo-1 Solution
gnt.broken_barh([(5,9), (14,1) , (15,4) , (19,9) , (28,4) , (34,4) , (38,5)],(5, 10),\
                facecolors = {'tab:blue','tab:red', 'tab:olive', 'tab:pink', 'tab:cyan', 'tab:brown', 'tab:orange'})
    

#For Algo-2 Solution
gnt.broken_barh([(14,6), (22,4) , (29,7) , (36,3) , (39,15)],(20,10),\
                facecolors = {'tab:blue','tab:red', 'tab:olive', 'tab:pink', 'tab:cyan'})
    
#upto here Gantt Chart is drawn
#Process of showing data while moving the mouse over the chart


annot = gnt.annotate("", xy=(0,0), xytext=(20,30),textcoords="offset points",
                    bbox=dict(boxstyle="round", fc="black", ec="b", lw=2),
                    arrowprops=dict(arrowstyle="->"))

annot.set_visible(False)

def update_annot(bar):
    
    x = bar.get_x() + bar.get_width()
    y = bar.get_y()+  (0.5*bar.get_height())
    
    annot.xy = (x,y) #box no (x,y) cordinate update karse
    text = "{:.2g},{:.2g}".format(x,y)
    annot.set_text(text)
    annot.get_bbox_patch().set_alpha(0.9)  

    
def hover(event):
    vis = annot.get_visible()
    if event.inaxes == gnt:
        for bar in gnt:
            cont, ind = bar.contains(event)
            if cont:
                update_annot(bar)
                annot.set_visible(True)
                fig.canvas.draw_idle()
                return
    if vis:
        annot.set_visible(False)
        fig.canvas.draw_idle()

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

plt.show()

I would like to see (x,y)= (Batch Processing Time, Algorithm Name) value when I move hover the over the batch solution.

2 Answers

broken_barh doesn't create individual bars, but one big BrokenBarHCollection object. When contains(event) is called, either False or True is returned, together with the index telling which of the small bars has been clicked on.

With .get_paths()[ind].get_extents() one can get the bounding box of that small bar. The coordinates of the bounding box lead to the start time and the duration.

import matplotlib.pyplot as plt

fig, ax_gnt = plt.subplots()
ax_gnt.set_ylim(0, 50)
ax_gnt.set_xlim(0, 65)

ax_gnt.set_yticks([10, 25])
ax_gnt.set_yticklabels(['Algo_1', 'Algo_2'])

ax_gnt.grid(True)

# For Algo-1 Solution
ax_gnt.broken_barh([(5, 9), (14, 1), (15, 4), (19, 9), (28, 4), (34, 4), (38, 5)], (5, 10),
                   facecolors={'tab:blue', 'tab:red', 'tab:olive', 'tab:pink', 'tab:cyan', 'tab:brown', 'tab:orange'})
# For Algo-2 Solution
ax_gnt.broken_barh([(14, 6), (22, 4), (29, 7), (36, 3), (39, 15)], (20, 10),
                   facecolors={'tab:blue', 'tab:red', 'tab:olive', 'tab:pink', 'tab:cyan'})

annot = ax_gnt.annotate("", xy=(0, 0), xytext=(20, 30), textcoords="offset points",
                        bbox=dict(boxstyle="round", fc="yellow", ec="b", lw=2),
                        arrowprops=dict(arrowstyle="->"))
annot.set_visible(False)

def update_annot(brokenbar_collection, coll_id, ind, x, y):
    annot.xy = (x, y)
    box = brokenbar_collection.get_paths()[ind].get_extents()
    text = f"{ax_gnt.get_yticklabels()[coll_id].get_text()} index:{ind} duration:{box.x1 - box.x0:.0f} "
    annot.set_text(text)
    annot.get_bbox_patch().set_alpha(0.9)

def hover(event):
    vis = annot.get_visible()
    if event.inaxes == ax_gnt:
        for coll_id, brokenbar_collection in enumerate(ax_gnt.collections):
            cont, ind = brokenbar_collection.contains(event)
            if cont:
                update_annot(brokenbar_collection, coll_id, ind['ind'][0], event.xdata, event.ydata)
                annot.set_visible(True)
                fig.canvas.draw_idle()
                return
    if vis:
        annot.set_visible(False)
        fig.canvas.draw_idle()

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

plt.show()

example plot

The JohanC , you saved my day ... there is a small correction required if using pyqt5

                sc.axes.broken_barh([(h[0][xc]["start"],h[0][xc]["interval"])], (h[0][xc]["id"], 0.5), facecolors =(u))
                annot = sc.axes.annotate(str(h[0][xc]["interval"]), xy=(h[0][xc]["start"],h[0][xc]["id"]), xytext=(20, 30), textcoords="offset points",
                    bbox=dict(boxstyle="round", fc="yellow", ec="b", lw=2),
                    arrowprops=dict(arrowstyle="->"))
                annot.set_visible(False)
                def update_annot(brokenbar_collection, coll_id, ind, x, y):
                    annot.xy = (x, y)
                    box = brokenbar_collection.get_paths()[ind].get_extents()
                    text = f"{sc.axes.get_yticklabels()[coll_id].get_text()} index:{ind} duration:{box.x1 - box.x0:.0f} "
                    annot.set_text(text)
                    annot.get_bbox_patch().set_alpha(0.9)

                def hover(event):
                    vis = annot.get_visible()
                    print("####",sc.axes.collections)
                    print("###",event.inaxes)
                    if event.inaxes == sc.axes:
                        print
                        for coll_id, brokenbar_collection in enumerate(sc.axes.collections):
                            cont, ind = brokenbar_collection.contains(event)
                            if cont:
                                update_annot(brokenbar_collection, coll_id, ind['ind'][0], event.xdata, event.ydata)
                                annot.set_visible(True)
                                sc.figure.canvas.draw_idle()
                                return
                    if vis:
                        annot.set_visible(False)
                        sc.figure.canvas.draw_idle()
                sc.figure.canvas.mpl_connect("motion_notify_event", hover)
Related