Gantt Charts With Plotly

Generating customised Gantt charts using Plotly in offline mode.

Gantt Charts With Plotly

I got asked to set up a Gantt chart by a client, now I could do this in a variety of ways: using excel, drawing it in a graphics program, finding a dedicated Gantt chart generator. But I'm a python programmer and Gantt charts are basically tables, so the first thing I did was bung python gantt chart into a search engine and see what I came up with. I always check to see if someone else has done the hard work for me for simple things like this as reinventing the wheel isn't worth it and generally ends up costing clients more than if I'd just used an existing tool. The answer that popped out was plotly - or write a fair amount of code to do the same thing in MatPlotLib.

Setup

Create a virtualenv and pip install plotly numpy

Make an offline Gantt chart using some test data

from plotly.offline import plot
import plotly.figure_factory as ff

df = [dict(Task="Job A", Start='2009-01-01', Finish='2009-02-28'),
      dict(Task="Job B", Start='2009-03-05', Finish='2009-04-15'),
      dict(Task="Job C", Start='2009-02-20', Finish='2009-05-30')]

fig = ff.create_gantt(df)
plot(fig, filename='test_gantt', auto_open=True)
screenshot showing the basic output of the plotly gantt generator

Woohoo it works!

Display all of the yaxis labels not just a fragment of them

My test data has long task names (unlike the example above) and only the last bit of them is visible. This can be fixed by turning off autosize and setting the left margin to a sensible size.

import plotly.graph_objs as go

# insert this between the first fig statement and the plot statement.
fig['layout'].update(autosize=False, margin=go.Margin(l=100))

That works, but involves calculating the maximum length of the Task name manually each time which is a horrible idea. Lets automate that:

left_margin = max([len(i['Task']) for i in df])
left_margin = left_margin*8

Now you can replace the constant with a variable and let it work out how big the margin needs to be. Once you've sorted out the overflow on the left margin you may find that the rest of the plot is a bit cramped. This can be fixed by creating a variable called new_width and assigning it the value 800 + left_margin. You then need to add width=new_width to the layout update.

screenshot showing a gantt chart with a super long task name

Group tasks

If you need to group the tasks in your data - for example by phase or status you can do this by adding arbitrary meta data to each entry.

df = [dict(Task="Job A", Start='2009-01-01', Finish='2009-02-28', Phase="1"),
      dict(Task="Job B", Start='2009-03-05', Finish='2009-04-15', Phase="1"),
      dict(Task="Job C", Start='2009-02-20', Finish='2009-05-30', Phase="2")]

fig = ff.create_gantt(df, index_col="Phase", group_tasks="True")

Now when you plot the chart it will group together the tasks (by showing related tasks in the same colour) according to the values in index_col.

screenshot showing a gantt chart with grouped tasks

Override the default scale on the axes

By default plotly attempts to work out what the best scale is for your axis and applies that, it doesn't always get it right though.

For my chart, for example it gave me months and I wanted it to show a tick and associated label (in the format dd-mm-yy) for every week. This is fairly easy to do using the online graphical interface but that would mean that I needed to go in and manually adjust the whole thing every time I need to change anything else in the chart.

In order to do this I needed to:

  • set the tickformat to output %d-%m-%Y (This article really helped with this: How to make the messy date ticks organised)
  • set autotick to false so I could override it
  • set the distance between the ticks to one week (plotly uses milliseconds to work out times. There are 86400000 milliseconds in a day which is 604800000 milliseconds in a week).
  • set the 0 point of the ticks to be Monday instead of Thursday (-3 days).

That gave me this:

xaxis=dict(tickformat="%d-%m-%Y", autotick=False, tick0=-259200000, dtick=604800000)

Which displays exactly what I want it to.

screenshot showing the gantt chart with weekly x-axis divisions instead of monthly ones

Export an image

Adding image='svg' or png / jpeg / webp to the plot function causes it to output an image file as well as the interactive HTML view.

You can also set a filename here image_filename="test_gantt".

By default the image is generated at 800 x 600 pixels. If you've had to adjust the width you'll want to override this so that it matches the width you set for the HTML view of the chart so that your image doesn't look cramped.

You may also wish to set a bottom margin for your chart so that the generated image doesn't cut off immediately after the xaxis labels. I found 100px worked well for this.

plot(fig, filename='test_gantt', image="svg", image_filename="test_gantt", image_width=width, auto_open=True)
The png output from the script

Final code:

from plotly.offline import plot
import plotly.figure_factory as ff
import plotly.graph_objs as go

df = [dict(Task="Job A", Start='2009-01-01', Finish='2009-02-28', Phase="1"),
      dict(Task="Job B", Start='2009-03-05', Finish='2009-04-15', Phase="1"),
      dict(Task="Job C", Start='2009-02-20', Finish='2009-05-30', Phase="2")]

left_margin = max([len(i['Task']) for i in df])
left_margin = left_margin*8
width = 800+left_margin

fig = ff.create_gantt(df, index_col="Phase", group_tasks="True")
fig['layout'].update(autosize=False, margin=go.Margin(l=left_margin, b=100), xaxis=dict(tickformat="%d-%m-%Y", autotick=False, tick0=-259200000, dtick=604800000))
plot(fig, filename='test_gantt', image="png", image_filename="test_gantt", image_width=width, auto_open=True)

Comments powered by Talkyard.