Generate richly styled Gantt charts as Excel (.xlsx) files
Project description
xlsx-gantt
Turn Python data into polished, presentation-ready Gantt charts — no Excel required, no GUI needed.
Built on openpyxl. Zero heavy dependencies.
Features
- Zero extra dependencies — built entirely on openpyxl; no pandas, no matplotlib, no Java bridge
- In-memory output —
generate_excel_bytes()returns raw.xlsxbytes without touching the file system; drop it straight into a Flask/Django response, e-mail it, or cache it in Redis - Eight production-ready themes — choose
ocean,midnight,forest,crimson,amber,slate,royal_purple, orteal; or generate a perfectly-matched palette from any hex colour withGanttTheme.from_color("#2E86C1") - Typed dataclass API —
Section,Task, andDateRangedataclasses give you IDE auto-completion and type checking; plain dicts still work and can be freely mixed in the same list - Multi-section layout — group tasks under labelled activities; section names merge vertically across their rows for a clean, structured look
- Multiple bars per row — paint several independent date ranges on a single task row, each with its own hex colour
- Milestones — any range where
start == endautomatically renders as a single filled cell - Solid progress DataBars — solid (gradient-free) Excel 2010 DataBar conditional formatting showing 0–100 % completion per task
- Resource / annotation columns — per-task
R: Responsible/S: Supportmarkers with individually configurable background colours - Time-estimate totals — numeric estimates accumulated automatically in a styled "Total" footer row
- Logo embedding — place a PNG or JPEG logo in the header area (A1:B2); scale and pixel-offset are fully configurable
- Freeze panes & auto-filter — header rows and task-name columns stay fixed while scrolling; Excel dropdown filters on every label column
- Colour utilities included —
darken,lighten,rotate_hue, andcontrast_text(WCAG 2.1) are exported for use in your own theme logic - Production-safe error handling — malformed sections, tasks, ranges, and annotations are silently skipped; no unhandled exceptions in live environments
Project Structure
xlsx-gantt/
├── xlsx_gantt/ # Library package
│ ├── __init__.py # Public re-exports
│ ├── style.py # GanttStyle configuration dataclass
│ ├── models.py # Section, Task, DateRange input dataclasses
│ ├── chart.py # GanttChart builder
│ ├── themes.py # GanttTheme + colour utilities
│ └── _xlsx_patch.py # Internal: solid DataBar XML post-processor
├── tests/
│ ├── __init__.py
│ ├── test_malformed_input.py
│ ├── test_new_features.py
│ └── test_readme_example.py
├── pyproject.toml
├── requirements.txt
├── requirements-dev.txt
├── LICENSE
└── README.md
Installation
pip install xlsx-gantt
With optional Pillow (enables non-PNG logo formats — JPEG, BMP, GIF, TIFF):
pip install "xlsx-gantt[logo]"
Development install
git clone https://github.com/trondegil/xlsx-gantt.git
cd xlsx-gantt
pip install -e ".[dev]"
Quick Start
from datetime import datetime
from xlsx_gantt import GanttChart, GanttTheme
sections = [
{
"name": "Design",
"tasks": [
{
"name": "Requirements gathering",
"time_estimate": 3,
"progress": 100,
"ranges": [
{"start": datetime(2026, 1, 5), "end": datetime(2026, 1, 9), "color": "0070C0"},
],
"annotations": {"Alice": "R", "Bob": "S"},
},
{
"name": "Architecture review",
"time_estimate": 2,
"progress": 80,
"ranges": [
{"start": datetime(2026, 1, 12), "end": datetime(2026, 1, 14), "color": "0070C0"},
],
"annotations": {"Alice": "R", "Bob": "R"},
},
],
},
{
"name": "Development",
"tasks": [
{
"name": "Backend",
"time_estimate": 10,
"progress": 50,
"ranges": [
{"start": datetime(2026, 1, 19), "end": datetime(2026, 2, 6), "color": "00B050"},
],
"annotations": {"Alice": "S", "Bob": "R"},
},
{
"name": "Frontend",
"time_estimate": 8,
"progress": 20,
"ranges": [
{"start": datetime(2026, 1, 26), "end": datetime(2026, 2, 13), "color": "00B050"},
],
"annotations": {"Alice": "R", "Bob": "S"},
},
],
},
{
"name": "Milestones",
"tasks": [
{
"name": "Beta release",
"time_estimate": None,
"progress": None,
"ranges": [
{"start": datetime(2026, 2, 9), "end": datetime(2026, 2, 9), "color": "FF0000"},
],
},
],
},
]
style = GanttTheme.get("ocean")
chart = GanttChart(
sections=sections,
start_date=datetime(2026, 1, 5), # Monday
end_date=datetime(2026, 2, 15), # Sunday
project_name="My Project",
resource_names=["Alice", "Bob"],
style=style,
logo_path="logo.png", # optional
)
# Save to disk
chart.generate_excel("gantt_chart.xlsx")
# — or — get raw bytes (no file written)
xlsx_bytes = chart.generate_excel_bytes()
print("Done!")
Typed Dataclass API (optional)
Instead of plain dicts you can use the typed Section, Task, and DateRange
dataclasses. Both forms are fully interchangeable and may be freely mixed in the
same sections list.
from datetime import datetime
from xlsx_gantt import GanttChart, GanttTheme, Section, Task, DateRange
sections = [
Section(
name="Design",
tasks=[
Task(
name="Requirements gathering",
time_estimate=3,
progress=100,
ranges=[
DateRange(
start=datetime(2026, 1, 5),
end=datetime(2026, 1, 9),
color="0070C0",
)
],
annotations={"Alice": "R", "Bob": "S"},
),
],
),
]
chart = GanttChart(
sections=sections,
start_date=datetime(2026, 1, 5),
end_date=datetime(2026, 2, 15),
project_name="My Project",
resource_names=["Alice", "Bob"],
style=GanttTheme.get("ocean"),
)
chart.generate_excel("gantt_chart.xlsx")
Mixing dicts and dataclasses
from xlsx_gantt import Section, Task
# Dict section alongside dataclass section — both work
sections = [
{"name": "Phase 1", "tasks": [{"name": "Kickoff", "progress": 100}]},
Section(name="Phase 2", tasks=[Task(name="Development", time_estimate=10)]),
]
to_dict / from_dict
Each dataclass provides to_dict() (serialize back to the dict API) and
from_dict() (construct from a raw dict, returning None for invalid input):
from xlsx_gantt import Section, Task, DateRange
from datetime import datetime
dr = DateRange(start=datetime(2026, 1, 5), end=datetime(2026, 1, 9), color="0070C0")
print(dr.to_dict())
# {'start': datetime(2026, 1, 5, 0, 0), 'end': datetime(2026, 1, 9, 0, 0), 'color': '0070C0'}
task = Task.from_dict({"name": "Research", "time_estimate": 3, "progress": 80})
print(task.name) # Research
In-Memory Output
generate_excel_bytes() returns the raw .xlsx bytes without writing to disk.
Useful for streaming from a web server or attaching to an e-mail:
chart = GanttChart(sections=sections, ...)
# Flask / Django example
xlsx_bytes = chart.generate_excel_bytes()
response = HttpResponse(
xlsx_bytes,
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
response["Content-Disposition"] = 'attachment; filename="gantt.xlsx"'
Data Structure
Sections (dict form)
{
"name": str, # section label (merged across all its task rows in column A)
"tasks": [ ... ] # list of task dicts (see below)
}
Tasks (dict form)
{
"name": str, # task label
"time_estimate": float | None, # hours / days / any unit; summed in the Total row
"progress": float | None, # 0–100 %; rendered as a solid DataBar
"ranges": [ # one or more coloured bars on the timeline
{
"start": datetime, # inclusive start date
"end": datetime, # inclusive end date (start == end → milestone)
"color": "RRGGBB", # 6-digit hex, no '#'; falls back to theme bar colour
},
...
],
"annotations": { # optional resource role markers
"Alice": "R", # R = Responsible
"Bob": "S", # S = Support
},
}
Note:
time_estimate,progress,ranges, andannotationsare all optional. Omitting them or passingNoneis handled gracefully.
Themes
Eight built-in themes are available:
| Name | Description | Base colour |
|---|---|---|
amber |
Warm corporate orange | FFC000 |
ocean |
Deep professional blue | 1A5276 |
forest |
Environmental green | 1E8449 |
crimson |
Bold, high-impact red | 922B21 |
slate |
Minimal neutral gray | 5D6D7E |
royal_purple |
Original default palette | 7030A0 |
midnight |
Modern near-black | 1B2631 |
teal |
Fresh blue-green | 148F77 |
from xlsx_gantt import GanttTheme
# By static method
style = GanttTheme.ocean()
# By name (case-insensitive; spaces and hyphens treated as underscores)
style = GanttTheme.get("royal_purple")
# Generated from any base hex colour
style = GanttTheme.from_color("#2E86C1")
# List all available theme names
print(GanttTheme.list_themes())
Custom Styles
Pass a GanttStyle dataclass instance to override any visual property:
from xlsx_gantt import GanttChart, GanttStyle
style = GanttStyle(
header_bg = "2C3E50",
header_fg = "FFFFFF",
bar_color = "E74C3C",
row_bg_even = "F2F3F4",
section_name_bg = "D5D8DC",
annotation_a_bg = "C8FFCC", # green tint for "R: Responsible"
annotation_s_bg = "FFE4B5", # amber tint for "S: Support"
col_label_bg = "FFC000",
col_label_fg = "000000",
)
chart = GanttChart(sections=..., style=style, ...)
Key GanttStyle fields
| Field | Default | Description |
|---|---|---|
header_bg |
"7030A0" |
Background for header rows (rows 1–2) |
header_fg |
"FFFFFF" |
Text colour for header rows |
bar_color |
"9966CC" |
Default Gantt bar fill |
progress_bar_color |
"7030A0" |
Solid DataBar fill colour |
weekend_bg |
"EFEFEF" |
Column fill for Sat/Sun |
row_bg_even |
None |
Even data-row background (None = white) |
row_bg_odd |
None |
Odd data-row background (None = white) |
section_name_bg |
None |
First row of each section |
col_label_bg |
"FFC000" |
Row 3 — Activity / Task / date numbers |
annotation_a_bg |
None |
Background for "R" annotation cells |
annotation_s_bg |
None |
Background for "S" annotation cells |
total_row_bg |
None |
Total row background (None → header_bg) |
logo_scale |
0.7 |
Logo size as a fraction of the A1:B2 area |
font_name |
"Calibri" |
Font used throughout the sheet |
col_width_task |
25.0 |
Task column width (Excel units) |
col_width_date |
3.0 |
Width of each day column |
All fields and their defaults are documented in the GanttStyle dataclass docstring.
Colour Utilities
The package also exports the colour helpers used internally by the theme engine:
from xlsx_gantt import contrast_text, darken, lighten, rotate_hue
# Pick white or black for maximum WCAG contrast against a background
text = contrast_text("1A5276") # → "FFFFFF"
# Blend a colour toward black or white
darker = darken("FFC000", 0.4)
lighter = lighten("FFC000", 0.6)
# Rotate the hue by a given number of degrees (0–360)
comp = rotate_hue("1A5276", 180)
Running the Tests
pip install -r requirements-dev.txt
pytest
The test suite covers:
- Valid input (dict API and dataclass API)
- In-memory output (
generate_excel_bytes) - Mixed dict + dataclass inputs
to_dict/from_dictround-tripspatch_solid_databarswith both file paths andBytesIO- Malformed sections, tasks, ranges, and annotations
- Edge-case chart configurations (empty date ranges,
Nonefields, reversed dates, etc.)
Requirements
| Package | Version | Notes |
|---|---|---|
| Python | ≥ 3.10 | |
| openpyxl | ≥ 3.1.0 | Required |
| Pillow | ≥ 10.0.0 | Optional — non-PNG logo formats |
Contributing
Contributions are welcome! Here is the recommended workflow:
-
Fork the repository and create a feature branch:
git checkout -b feature/my-improvement
-
Install the project in editable mode with dev dependencies:
pip install -e ".[dev]"
-
Make your changes. Keep the following guidelines in mind:
- Follow the existing code style (PEP 8, type hints, docstrings).
- Add or update tests in
tests/for any changed behaviour. - All colour values must be 6-digit hex strings without a leading
#.
-
Run the test suite and make sure all tests pass:
pytest
-
Commit with a clear, descriptive message:
git commit -m "feat: add X / fix Y"
-
Open a pull request against the
mainbranch describing what you changed and why.
Reporting issues
Please open a GitHub Issue and include:
- Python version (
python --version) - openpyxl version (
pip show openpyxl) - A minimal reproducible example or the full traceback.
License
This project is licensed under the MIT License — see the LICENSE file for the full text.
Dependency licences
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file xlsx_gantt-0.1.0.tar.gz.
File metadata
- Download URL: xlsx_gantt-0.1.0.tar.gz
- Upload date:
- Size: 37.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
749acca7622f94f855d1a9f029348f80b567ab7875199f4d130507cb4cb6e5d7
|
|
| MD5 |
9cd0438a2b6ee8dcf1cdbcefa529477d
|
|
| BLAKE2b-256 |
7a2506c591c32242233c163071640c5531015e8818adc920e4aa3129eb5d6bb5
|
Provenance
The following attestation bundles were made for xlsx_gantt-0.1.0.tar.gz:
Publisher:
publish.yml on trondegil/xlsx-gantt
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
xlsx_gantt-0.1.0.tar.gz -
Subject digest:
749acca7622f94f855d1a9f029348f80b567ab7875199f4d130507cb4cb6e5d7 - Sigstore transparency entry: 1295840880
- Sigstore integration time:
-
Permalink:
trondegil/xlsx-gantt@7fac7bf4ec78696fc8653a495d9db94bc26eeaeb -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/trondegil
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7fac7bf4ec78696fc8653a495d9db94bc26eeaeb -
Trigger Event:
release
-
Statement type:
File details
Details for the file xlsx_gantt-0.1.0-py3-none-any.whl.
File metadata
- Download URL: xlsx_gantt-0.1.0-py3-none-any.whl
- Upload date:
- Size: 26.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a22d66e277085a0f0c3f2720a2e3e30198501b15f5ebcc31096496f690fc0e4d
|
|
| MD5 |
cdaee26ac7050f6fdeae3e59bd3e7613
|
|
| BLAKE2b-256 |
f38ddc86af2988033ba2592de546e870df35d15124e4c06109fce26d9de383df
|
Provenance
The following attestation bundles were made for xlsx_gantt-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on trondegil/xlsx-gantt
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
xlsx_gantt-0.1.0-py3-none-any.whl -
Subject digest:
a22d66e277085a0f0c3f2720a2e3e30198501b15f5ebcc31096496f690fc0e4d - Sigstore transparency entry: 1295840945
- Sigstore integration time:
-
Permalink:
trondegil/xlsx-gantt@7fac7bf4ec78696fc8653a495d9db94bc26eeaeb -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/trondegil
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@7fac7bf4ec78696fc8653a495d9db94bc26eeaeb -
Trigger Event:
release
-
Statement type: