Search code examples
pythonmatplotlibannotations

How to annotate a broken_barh chart while hovering?


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.


Solution

  • Update: The code has been adapted to change the box's position when close to the borders. That way, the annotation stays better visible inside the plot. (Unfortunately, mplcursors doesn't work with broken_barh.)

    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
    
    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.8)
        xpos, ypos = annot.get_position()
        if x > xmax - (xmax - xmin) * 0.3:
            annot.set(position=(-20, ypos), anncoords="offset points", horizontalalignment='right')
        elif x < xmin + (xmax - xmin) * 0.3:
            annot.set(position=(20, ypos), anncoords="offset points", horizontalalignment='left')
    
        xpos, ypos = annot.get_position()
        if y > ymax - (ymax - ymin) * 0.3:
            annot.set(position=(xpos, -30), anncoords="offset points", verticalalignment='top')
        elif y < ymin + (ymax - ymin) * 0.3:
            annot.set(position=(xpos, 30), anncoords="offset points", verticalalignment='bottom')
    
    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, ax_gnt = plt.subplots()
    ymin, ymax = 0, 35
    ax_gnt.set_ylim(ymin, ymax)
    xmin, xmax = 0, 65
    ax_gnt.set_xlim(xmin, xmax)
    
    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)
    
    fig.canvas.mpl_connect("motion_notify_event", hover)
    
    plt.show()
    

    example plot