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