Creating a quick Gantt chart for drilling schedules pt. 2

Creating a quick Gantt chart for drilling schedules pt. 2

February 22, 2024·Jack
Jack

In part 1 I worked on setting up a Gantt chart to plot executed drilling schedules from a quick drillhole database export. In this post I’ll be elaborating on the python code we wrote to make the chart more presentable, and to add some extra features such as break periods.

ℹ️
Check out part 1 to read about the intital set up if you haven’t already.

Format x-axis ticks

The default x-axis ticks and tick labels are often a bit cluttered and chaotic, so I usually find I have to format them to make it more presentable. matplotlib has a few ways to access axis ticks and and labels - here I’ve set a few ranges and list of tick lables, then applied them to the plot using set_xticks and set_xticklabels. I’ve also set the the number of minor x-ticks to 12 additional ticks, to fit in the bar labels, as we’ll see later. This might not be the most elegant solution, but it works for now.

First, we’ll need to define a start (0), stop (max +1), and step (10 days) for the major x-ticks, using np.arrange(). This will plot a major xtick every 10 days.
We’ll also need to set the labels for the x-ticks, using pd.date_range(), to label the x-ticks.
Finally we’ll also set the minor x-ticks to plot 1 for each day, plus 12 additional ticks to fit in the bar labels.

drillgantt.py
# format ticks
xdays = 10 # number of days between xticks
xticks = np.arange(0, df['end_num'].max()+1, xdays)
xticks_labels = pd.date_range(project_start, end=df['ENDDATE'].max()).strftime("%m/%d")
xticks_minor = np.arange(0, df['end_num'].max()+12, 1)

Next, we’ll need to apply to these ranges to the x-axis using ax.set_xticks() and ax.set_xticklabels().

drillgantt.py
ax.set_xticks(xticks)
ax.set_xticks(xticks_minor, minor=True)
ax.set_xticklabels(xticks_labels[::xdays], fontdict={'fontsize': 6, 'horizontalalignment': 'left'})

I also chose to remove the y-axis ticks, as there’s no real need for them in this case.

drillgantt.py
ax.set_yticks([])

Configure gridlines

I also chose to add vertical major gridlines to the plot, making it easier to read. These are set behind the plotted bars, as they’re not the main focus of the plot.

drillgantt.py
# set gridlines
ax.set_axisbelow(True)
ax.xaxis.grid(color='gray', linestyle='solid', alpha=0.2, which='major')

Add bar labels

Given a range of Hole IDs, it would become a terribly cluttered to note each hole in the legend, so I chose to add the Hole ID next to each bar. This is surprisingly straightforward in matplotlib bar charts, and can be done simply by accessing the bar_label attribute of the axis object (ax).

drillgantt.py
# add labels
ax.bar_label(gantt,  fontsize = 6, labels = df['HOLEID'], padding = 3)

Set a title

Next, you might want to add a title. I gave mine a bit of padding to make it look a bit neater.

drillgantt.py
# title 
plt.title('Good Title Here', loc = 'left', pad = 5)

I also tend to re-use the same title and axis label formatting across various plots used in the same report, so as to keep a consistent look and feel. This is easily achieved by setting a dictionary of font properties, and accessing them using fontdict in the set_title method.

For example:

drillgantt.py
# font dictionary
titlefm = {
    'weight': 'bold', 
    'size': 12, 
    'fontname': 'century gothic'
}

# title 
    plt.title('Good Title Here', loc = 'left', pad = 5, fontdict = titlefm)
⚠️
It is worth noting that (as of time of writing) using the fontdict parameter is no longer recommended. The matplotlib documentation recommends that parameters should be passed as individual keyword arguments or using dictionary-unpacking.

Add break periods

Our Gantt chart is looking almost complete, but something I noticed while I was creating one for a longer-term project was a series of large time gaps between different drilling periods on the project. While I could’ve explained this in the figure caption, I prefer to show as much info visually as possible, without cluttering things too much.

To that end, I wanted to colour sections of the chart to represent these breaks, and then explain them briefly in the legend. Again, the full code for this chart is designed in such a way that it can be iterated over multiple datasets, so I designed the break plotting as a function that is able to handle multiple breaks. I’ve broken it up a bit here for simplicity, but the logic is similar.

First, in order to add the break to the ledend, we’re first going to define a list of elements (we’ll come back to this in the next step).

drillgantt.py
legendelems = [Patch(facecolor=cdict[i], label=i)  for i in cdict]

Next, we define the function that takes the parameters of the break period (start, end, reason, and a colour) and creates a Rectangle patch object to draw to the axes. As parameters, the Rectangle takes the xy coordinates of the bottom left corner, width, height, and colour. The dates of the break period are converted to numbers (as the x-axis essentially still plots dates in number format) using mdates.datestr2num. The bottom left corner of rect is set using the coordinates of the axis object (ax) and the start date, the height is the full height of the y-axis, and the width is the difference between the start and end dates.

I also made the rectangle slightly transparent, to make it easier to see the gridlines behind it, gave it a z-order (zorder) of 0 to place it behind the bars.

drillgantt.py
# create break period rectangle
def set_break(start, end, project_start, rectcolor):

    # convert project start datetime object (timestamp) to matplotlib date format
    project_start = mdates.date2num(project_start)

    # convert datestrings to matplotlib date format
    xstart = mdates.datestr2num(start) - project_start
    xend = mdates.datestr2num(end) - project_start
    
    # get ax limits
    ystart = ax.get_ylim()[0]
    yend = ax.get_ylim()[1]

    # create rectangle patch
    rect = Rectangle((xstart, ystart), xend-xstart, yend-ystart, facecolor=rectcolor, alpha=0.25, zorder=0)   
    
    return rect

The following then creates the Rectangle patch objects, adds them to the axis, then sets the break reason and patch colour to add to the legend elements list legendelems. This is copied twice for two different breaks here, though this could easily be looped through if a list of break parameters is set.

drillgantt.py
# add break periods
rect = set_break('2022-10-17', '2022-12-18', project_start, 'grey')
ax.add_patch(rect)
break_reason = 'Drilling other projects'
legendelems.append(Patch(facecolor=rect.get_facecolor(), label=break_reason))

rect2 = set_break('2022-12-19', '2023-01-11', project_start, 'lightgreen')
ax.add_patch(rect2)
break_reason = 'Holiday Break'
legendelems.append(Patch(facecolor=rect2.get_facecolor(), label=break_reason))

Set up and format the legend

Now we just have to make a legend so we can tell what all the coloured bars actually represent. The list we created in the previous step legendelems does a little bit of list comprehension to create a list of Patch objects with labels from the previously defined cdict colour dictionary. As we created the Rectangle patches, we also appended the colour and break reason of each object as a Patch object to the legendelems list.

The following then just creates a Legend object leg using plt.legend(), which uses the legendelems list as the handles. I’ve also put a fair bit of formatting, positioning, and and padding in there, as I find the default legends a bit ugly. Once the Legend object is created, we can then format the frame and border of the legend using leg.get_frame().

drillgantt.py
# plot legend
leg = plt.legend(
    handles=legendelems, 
    ncols=len(legendelems),
    fancybox=False, 
    fontsize=6, 
    loc='lower right', 
    bbox_to_anchor=(1, 1.025), 
    borderaxespad=0, 
    edgecolor='black',
    shadow=False,
)

leg.get_frame().set_linestyle('solid')
leg.get_frame().set_linewidth(0.8)

Plot the figure

Finally, make sure the whole thing actually prints out:

drillgantt.py
# show
plt.show()

# or save
fig.savefig('drillgantt')

And we’re done!

Okay, so maybe that wasn’t so quick after all…

If you ignore half the formatting, its a pretty neat way to make a quick-and-dirty Gantt chart, but as with all matplotlib stuff, there’s a million ways to drill down into the details and get pretty specific with the formatting.

So thanks for taking the time to check out this post, and I hope you found it useful. If you have any questions or comments, feel free to reach out to me on Twitter