How to Create a Gantt Chart in Plotly

Gantt charts remain the gold standard for visualizing project timelines, resource allocation, and task dependencies. Whether you're tracking a software development sprint, construction project, or...

Key Insights

  • Plotly offers two approaches for Gantt charts: figure_factory.create_gantt() for quick results and px.timeline() for more control and modern syntax
  • Proper date handling is critical—always use datetime objects and be explicit about timezones to avoid rendering errors
  • Interactive features like hover templates and range sliders transform static project timelines into powerful analytical tools

Introduction to Gantt Charts and Plotly

Gantt charts remain the gold standard for visualizing project timelines, resource allocation, and task dependencies. Whether you’re tracking a software development sprint, construction project, or marketing campaign, these horizontal bar charts instantly communicate what’s happening when and who’s responsible.

Plotly stands out as the best choice for creating Gantt charts in Python because it generates interactive, publication-ready visualizations that work seamlessly in Jupyter notebooks, dashboards, and web applications. Unlike static matplotlib charts, Plotly charts allow users to zoom, pan, and hover for details—essential when presenting to stakeholders who need to drill into specific timeframes.

The real power emerges when you need to share insights with non-technical audiences. Export your Plotly Gantt chart as a standalone HTML file, and anyone can explore the timeline in their browser without installing Python or any dependencies.

Setup and Dependencies

Install Plotly using pip. You’ll also want pandas for data manipulation and datetime for handling dates properly:

pip install plotly pandas

Import the necessary modules:

import plotly.express as px
import plotly.figure_factory as ff
import pandas as pd
from datetime import datetime, timedelta

Gantt charts require a specific data structure. At minimum, you need task names, start dates, and end dates. Most real-world applications also include resource assignments or task categories. Here’s the structure as a pandas DataFrame:

# Sample project data
data = pd.DataFrame([
    dict(Task="Requirements Gathering", Start='2024-01-01', Finish='2024-01-10', Resource='Planning'),
    dict(Task="System Design", Start='2024-01-08', Finish='2024-01-22', Resource='Architecture'),
    dict(Task="Backend Development", Start='2024-01-15', Finish='2024-02-15', Resource='Engineering'),
    dict(Task="Frontend Development", Start='2024-01-20', Finish='2024-02-20', Resource='Engineering'),
    dict(Task="Testing & QA", Start='2024-02-10', Finish='2024-03-05', Resource='Quality'),
    dict(Task="Deployment", Start='2024-03-01', Finish='2024-03-10', Resource='DevOps')
])

# Convert date strings to datetime objects
data['Start'] = pd.to_datetime(data['Start'])
data['Finish'] = pd.to_datetime(data['Finish'])

Always convert date strings to proper datetime objects. Plotly can technically handle string dates, but you’ll encounter inconsistent formatting and timezone issues that waste hours of debugging.

Creating a Basic Gantt Chart

Plotly provides two methods for creating Gantt charts. The modern approach uses plotly.express.timeline(), which offers cleaner syntax and better integration with the Plotly ecosystem:

# Modern approach with plotly.express
fig = px.timeline(
    data, 
    x_start="Start", 
    x_end="Finish", 
    y="Task",
    color="Resource"
)

# Update layout for better readability
fig.update_yaxes(autorange="reversed")  # Tasks from top to bottom
fig.update_layout(
    title="Project Timeline",
    xaxis_title="Date",
    yaxis_title="Tasks",
    height=400
)

fig.show()

The autorange="reversed" parameter is crucial—without it, tasks appear bottom-to-top, which feels counterintuitive for most users.

Alternatively, use figure_factory.create_gantt() for quick prototypes, though this method is considered legacy:

# Legacy approach (still works but less flexible)
fig = ff.create_gantt(
    data,
    colors='Resource',
    index_col='Resource',
    show_colorbar=True,
    group_tasks=True
)
fig.show()

Stick with px.timeline() for new projects. It receives active development and supports all modern Plotly features.

Customizing Chart Appearance

Basic charts work for quick analysis, but stakeholder presentations demand polish. Customize colors, hover information, and layout to create professional visualizations:

# Define custom color scheme
color_map = {
    'Planning': '#FF6B6B',
    'Architecture': '#4ECDC4',
    'Engineering': '#45B7D1',
    'Quality': '#FFA07A',
    'DevOps': '#98D8C8'
}

fig = px.timeline(
    data,
    x_start="Start",
    x_end="Finish",
    y="Task",
    color="Resource",
    color_discrete_map=color_map,
    hover_data={"Resource": True, "Start": "|%B %d, %Y", "Finish": "|%B %d, %Y"}
)

# Customize hover template
fig.update_traces(
    hovertemplate="<b>%{y}</b><br>" +
                  "Resource: %{customdata[0]}<br>" +
                  "Start: %{base|%b %d, %Y}<br>" +
                  "End: %{x|%b %d, %Y}<br>" +
                  "<extra></extra>"
)

# Professional layout adjustments
fig.update_yaxes(autorange="reversed")
fig.update_layout(
    title={
        'text': "Software Development Project Timeline",
        'x': 0.5,
        'xanchor': 'center',
        'font': {'size': 20}
    },
    xaxis=dict(
        title="Timeline",
        tickformat="%b %d\n%Y",
        gridcolor='lightgray'
    ),
    yaxis=dict(title=""),
    plot_bgcolor='white',
    height=500,
    font=dict(size=12)
)

fig.show()

The hovertemplate customization dramatically improves user experience. The default hover text often includes unnecessary technical details or poorly formatted dates. Custom templates let you show exactly what matters.

Advanced Features

Real projects require more sophisticated visualizations. Add task grouping, phase indicators, and annotations to communicate complex timelines:

# Enhanced dataset with phases and completion status
advanced_data = pd.DataFrame([
    dict(Task="Requirements Gathering", Start='2024-01-01', Finish='2024-01-10', 
         Resource='Planning', Phase='Phase 1', Complete=100),
    dict(Task="System Design", Start='2024-01-08', Finish='2024-01-22', 
         Resource='Architecture', Phase='Phase 1', Complete=100),
    dict(Task="Backend Development", Start='2024-01-15', Finish='2024-02-15', 
         Resource='Engineering', Phase='Phase 2', Complete=75),
    dict(Task="Frontend Development", Start='2024-01-20', Finish='2024-02-20', 
         Resource='Engineering', Phase='Phase 2', Complete=60),
    dict(Task="Testing & QA", Start='2024-02-10', Finish='2024-03-05', 
         Resource='Quality', Phase='Phase 3', Complete=30),
    dict(Task="Deployment", Start='2024-03-01', Finish='2024-03-10', 
         Resource='DevOps', Phase='Phase 3', Complete=0)
])

advanced_data['Start'] = pd.to_datetime(advanced_data['Start'])
advanced_data['Finish'] = pd.to_datetime(advanced_data['Finish'])

# Create chart with phase grouping
fig = px.timeline(
    advanced_data,
    x_start="Start",
    x_end="Finish",
    y="Task",
    color="Phase",
    pattern_shape="Resource",  # Add texture patterns
    hover_data=["Complete"]
)

# Add milestone marker for project kickoff
fig.add_vline(
    x=datetime(2024, 1, 1).timestamp() * 1000,
    line_dash="dash",
    line_color="red",
    annotation_text="Project Start"
)

# Add "today" indicator
today = datetime.now()
fig.add_vline(
    x=today.timestamp() * 1000,
    line_dash="dot",
    line_color="green",
    annotation_text="Today",
    annotation_position="top"
)

fig.update_yaxes(autorange="reversed")
fig.update_layout(
    title="Multi-Phase Project Timeline with Milestones",
    height=600,
    showlegend=True,
    legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
)

fig.show()

The pattern_shape parameter adds visual texture to distinguish resources even when printed in grayscale—a small detail that matters for accessibility.

Making Charts Interactive and Exportable

Plotly’s interactivity separates it from static charting libraries. Add range sliders and export functionality for maximum utility:

# Add range slider and export buttons
fig = px.timeline(
    data,
    x_start="Start",
    x_end="Finish",
    y="Task",
    color="Resource"
)

fig.update_yaxes(autorange="reversed")

# Add range slider for time navigation
fig.update_xaxes(
    rangeslider_visible=True,
    rangeselector=dict(
        buttons=list([
            dict(count=1, label="1m", step="month", stepmode="backward"),
            dict(count=3, label="3m", step="month", stepmode="backward"),
            dict(step="all", label="All")
        ])
    )
)

fig.update_layout(height=600)

# Save as interactive HTML
fig.write_html("project_timeline.html")

# Save as static image (requires kaleido: pip install kaleido)
fig.write_image("project_timeline.png", width=1200, height=600)

# Save as PDF for reports
fig.write_image("project_timeline.pdf", width=1200, height=600)

fig.show()

The HTML export creates a fully self-contained file with all JavaScript embedded. Share this with stakeholders who can then explore the timeline without any software installation.

Best Practices and Common Pitfalls

Date Handling: Always use timezone-aware datetime objects for projects spanning multiple regions. Implicit timezone assumptions cause off-by-one-day errors:

from datetime import timezone
data['Start'] = pd.to_datetime(data['Start']).dt.tz_localize('UTC')

Performance: For projects with 500+ tasks, Plotly can become sluggish. Consider aggregating tasks by week or month, or implement filtering to show only active tasks within a date range.

Responsive Design: Set height explicitly rather than relying on defaults. Auto-height calculations often produce cramped charts with overlapping labels. For 20 tasks, use height=600. Add 20-30 pixels per additional task.

Color Accessibility: Test your color schemes with a colorblind simulator. Use both color and pattern/texture to distinguish categories. The pattern_shape parameter exists for exactly this reason.

Data Validation: Validate that finish dates occur after start dates before plotting. Plotly will render invalid data but produce confusing visualizations:

assert (data['Finish'] >= data['Start']).all(), "Invalid date ranges detected"

Gantt charts in Plotly transform project data into actionable insights. The combination of Python’s data manipulation capabilities and Plotly’s interactive visualization creates a powerful workflow for project managers, analysts, and engineers. Start with basic charts, then progressively add customization as your needs evolve.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.