Metadata-Version: 2.4
Name: yml2gantt
Version: 0.1.0
Summary: Draw gantt charts with matplotlib from a simple YAML file (or from Python).
License-Expression: MIT
License-File: LICENSE
Keywords: gantt,gantt-chart,yaml,matplotlib,project-management,planning,timeline
Author: Louis Devillez
Author-email: louis.devillez@gmail.com
Requires-Python: >=3.11
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Office/Business :: Scheduling
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Dist: matplotlib (>=3.5)
Requires-Dist: pyyaml (>=6.0)
Project-URL: Homepage, https://github.com/ldevillez/yml2gantt
Project-URL: Issues, https://github.com/ldevillez/yml2gantt/issues
Project-URL: Repository, https://github.com/ldevillez/yml2gantt
Description-Content-Type: text/markdown

# yml2gantt

[![PyPI](https://img.shields.io/pypi/v/yml2gantt)](https://pypi.org/project/yml2gantt/)
[![Python](https://img.shields.io/pypi/pyversions/yml2gantt)](https://pypi.org/project/yml2gantt/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://github.com/ldevillez/yml2gantt/blob/main/LICENSE)

Write your project plan in a small YAML file, get a clean gantt chart
(PDF, PNG, SVG, …) drawn with matplotlib.

```yaml
gantt:
  start: 2026-01-05
  duration: 4w

tasks:
  - {name: Write, duration: 2w}
  - {name: Review, start: 2w, duration: 1w}
  - {name: Ship, start: 3w, milestone: true}
```

```sh
yml2gantt plan.yml -o plan.png
```

![Gantt chart of the roadmap example](https://raw.githubusercontent.com/ldevillez/yml2gantt/main/docs/images/roadmap.png)

## Features

- Plain YAML input: easy to write by hand, to diff and to keep in git
- Tasks with subtasks (a task without duration spans its subtasks)
- Milestones, on their own row or placed on a task's bar
- Interrupted tasks: several bar segments on one row
- Custom text in the bars (`label`), e.g. who works on the task
- Day, week, month, trimester or abstract (numbered) time scales
- Positions as dates or as offsets (`3`, `"2w"`), lengths as durations or end dates
- A "we are here" line at today's date or at any position
- Export to any format matplotlib supports (PDF, PNG, SVG, …)
- A Python API to build charts from code, or to draw into your own figures

## Installation

Requires Python 3.11 or newer.

```sh
pip install yml2gantt
```

To use only the command line tool, `pipx install yml2gantt` or
`uv tool install yml2gantt` install it in its own environment.

## Command line

```sh
yml2gantt examples/project.yml
```

This draws the chart, saves it as `examples/project.pdf` and opens a
window with it.

| Option            | Effect |
|-------------------|--------|
| `-o, --output F`  | Output file (default: `<file>.pdf`). The extension sets the format: `.pdf`, `.png`, `.svg`, … |
| `--no-show`       | Do not open a window (for scripts, CI, servers). |
| `--version`       | Print the version and exit. |
| `-h, --help`      | Show the help. |

```sh
yml2gantt examples/workpackages.yml -o plan.png --no-show
python -m yml2gantt examples/sprint.yml     # same thing
```

On an error in the file (unknown key, bad date, …) the tool prints a
message such as `yml2gantt: error: task: unknown keys ['durtion']` and
exits with status 1.

## The YAML format

A file has two sections: `gantt` (the time axis) and `tasks` (the rows).

```yaml
gantt:
  start: 2026-01-05
  duration: 12w
  scale: week
  title: My project

tasks:
  - name: Kick-off
    start: 0
    duration: 3d
    label: Team A                 # text in the bar (default: the name)
    milestones: [Go]              # diamond at the end of the bar

  - name: Development             # no duration: spans its subtasks
    start: 2026-01-12
    subtasks:
      - {name: Backend, start: 0, duration: 4w}
      - {name: Frontend, start: 2w, duration: 5w,
         milestones: [{name: Demo, at: 50%}]}

  - {name: Release, start: 2026-03-20, milestone: true}
```

Keys are case-insensitive, and `task` is accepted instead of `name`.
Unknown keys are reported as errors, so typos do not go unnoticed.

### Dates, offsets and durations

| You write        | Meaning |
|------------------|---------|
| `2026-01-05`     | A date (ISO format). |
| `"2026-W02-1"`   | An ISO week date: Monday of week 2 of 2026 (quote it). |
| `3` or `2.5`     | A number of days (on the `abstract` scale: a number of units). |
| `"3d"`, `"2w"`   | 3 days, 2 weeks. Decimals work: `"1.5w"`. |

A position (`start`, `end`, a milestone's `at`) is either a date or an
**offset**. Offsets count from the chart start for tasks, from the
parent start for subtasks, and from the task start for segments and
milestones. There is no month unit, because months have different
lengths: use dates for that.

On the `abstract` scale, all positions and lengths are plain numbers.

### `gantt` section

| Key            | Description |
|----------------|-------------|
| `start`        | Start date (`2026-01-05`, `"2025-W38-7"`), or a number for the `abstract` scale. Default `0`. |
| `end`          | End of the chart (exclusive). Give `end` **or** `duration`. |
| `duration`     | Length of the chart: days (`90`) or `"3d"`, `"13w"`. |
| `scale`        | `day`, `week` (default), `month`, `trimester` or `abstract`. |
| `title`        | Chart title. |
| `show_today`   | Draw a red line at today's date (default `true`, dated scales only, ignored when `now` is set). Nothing is drawn if today is outside the chart. |
| `now`          | Draw the "we are here" line at this position instead of today: a date or an offset (e.g. `5.5` on an abstract scale). Works on every scale. |
| `now_label`    | Text above that line. Default: `Now` (with `now`) or `Today`. `""` for no text. |
| `label_format` | Period label, see below. |
| `label_every`  | Label only one period out of N (default `1`). |
| `step`         | Period size for the `abstract` scale (default `1`). |

### Time scales and labels

| Scale       | One period is | Default label | Light grid lines |
|-------------|---------------|---------------|------------------|
| `day`       | a day         | `Jan 05`      | none |
| `week`      | a week (Monday to Sunday) | `W02` | days |
| `month`     | a month       | `Jan 2026`    | weeks |
| `trimester` | a quarter     | `Q1 2026`     | months |
| `abstract`  | `step` units  | `1, 2, 3…`    | none |

`label_format` is a Python format string with these placeholders:

| Placeholder | Value |
|-------------|-------|
| `{date}`    | First day of the period. Takes [strftime codes](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes): `{date:%b}` → `Jan`, `{date:%d/%m}` → `05/01`. |
| `{q}`       | Quarter number (1 to 4). |
| `{n}`       | Period number, from 1. The only placeholder on the `abstract` scale. |

Use `\n` (in a double-quoted string) for two-line labels, e.g.
`"{date:%a}\n{date:%d}"` or `"Q{q}\n{date:%Y}"`. A period cut to less
than half by the chart start or end is left unlabelled.

### Tasks and subtasks

| Key          | Description |
|--------------|-------------|
| `name`       | Shown on the y axis, and inside the bar unless `label` is set. Required. |
| `label`      | Text written inside the bar instead of `name` (next to the diamond for a milestone row). `""` leaves the bar empty. |
| `start`      | A date or an offset (see above). Default `0`. |
| `duration`   | Length: a number, `"3d"` or `"2w"`. |
| `end`        | End date or offset, instead of `duration`. |
| `color`      | Any [matplotlib color](https://matplotlib.org/stable/users/explain/colors/colors.html) (`tab:red`, `"#3366cc"`, `lightgrey`, …). |
| `subtasks`   | List of subtasks (same keys, one level deep). |
| `milestone`  | `true` turns the entry into a milestone row (see below). |
| `milestones` | Milestones drawn on the bar (see below). |
| `segments`   | Several pieces of bar on the same row (see below). |

- A task without `duration`/`end` spans its subtasks (or its segments).
- Tasks take the next color of the matplotlib color cycle. Subtasks use
  a lighter version of their parent's color, and write their text in the
  parent's color.
- Subtasks can be milestones too (`milestone: true`), and get the
  parent's color.

### Milestones

There are two kinds of milestone.

**On a task's bar**, with `milestones` (tasks and subtasks):

```yaml
milestones:
  - Done                    # at the end of the bar
  - {name: Review, at: 40%} # 40% of the bar length
  - {name: Demo, at: 4}     # 4 units after the task start
  - {name: Freeze, at: 2026-02-01, color: tab:red}   # a date
```

`at` defaults to `100%`. Percentages use the drawn bar, so they also
work on a task that spans its subtasks. Milestones are black unless
`color` is given. The label goes to the right of the diamond, or to its
left in the last 20% of the chart.

**On their own row**, with `milestone: true`: the entry takes only a
`start` (no `duration`, `end`, `subtasks`, `segments` or `milestones`)
and is drawn as a diamond.

```yaml
- {name: Grand opening, start: 89, milestone: true, color: tab:red}
```

### Segments

A task interrupted in the middle, or split into phases, can have
several pieces of bar on its row:

```yaml
- name: Field trials
  label: "Team B"           # written in each segment
  start: 2
  segments:
    - {start: 0, duration: 3}               # offsets from the task start
    - {start: 5, end: 8, label: "Retest"}   # own label
  milestones: [Report]      # end of the last segment
```

| Key          | Description |
|--------------|-------------|
| `start`      | A date, or an offset from the task start. Default `0`. |
| `duration` / `end` | Length, or end date/offset (one of them is required). |
| `color`      | Default: color of the task. |
| `label`      | Text in the segment. Default: the task `label` (or its name). `""` leaves it empty. |

The task spans from its `start` to the end of its last segment: this is
what its parent and its percentage milestones use. Touching segments are
separated by a thin white line. A task with segments takes no
`duration`/`end` and no `subtasks`.

### Abstract scale

Use `scale: abstract` when the chart has no calendar dates, e.g. a
project plan in quarters or a course in weeks. All positions and
lengths are plain numbers.

```yaml
gantt:
  start: 0
  duration: 16
  scale: abstract
  step: 1               # one column per unit
  label_format: "Q{n}"  # Q1, Q2, …
  title: Work plan
  now: 5.5              # "we are here": middle of Q6
```

## Examples

Each example in [`examples/`](https://github.com/ldevillez/yml2gantt/tree/main/examples)
is commented and shows a different part of the format.

| File | Shows |
|------|-------|
| [`minimal.yml`](https://github.com/ldevillez/yml2gantt/blob/main/examples/minimal.yml) | The smallest useful file |
| [`project.yml`](https://github.com/ldevillez/yml2gantt/blob/main/examples/project.yml) | Week scale, ISO week date, subtasks, milestone rows, offsets in days, short bar labels |
| [`sprint.yml`](https://github.com/ldevillez/yml2gantt/blob/main/examples/sprint.yml) | Day scale, two-line labels, `end` dates, people in the bars, `now` at a date, an interrupted task |
| [`roadmap.yml`](https://github.com/ldevillez/yml2gantt/blob/main/examples/roadmap.yml) | Month scale, dates only, colored milestones on bars, a paused task, no "today" line |
| [`house.yml`](https://github.com/ldevillez/yml2gantt/blob/main/examples/house.yml) | Trimester scale with `{q}`, percentage milestones, a seasonal task as segments |
| [`course.yml`](https://github.com/ldevillez/yml2gantt/blob/main/examples/course.yml) | Abstract scale in weeks, `label_every`, fractional positions |
| [`workpackages.yml`](https://github.com/ldevillez/yml2gantt/blob/main/examples/workpackages.yml) | Abstract scale in quarters, a research project in work packages, partners in the bars, many milestones and segments |
| [`from_python.py`](https://github.com/ldevillez/yml2gantt/blob/main/examples/from_python.py) | Building a chart from Python |

**sprint.yml**: day scale

![Sprint example](https://raw.githubusercontent.com/ldevillez/yml2gantt/main/docs/images/sprint.png)

**house.yml**: trimester scale

![House building example](https://raw.githubusercontent.com/ldevillez/yml2gantt/main/docs/images/house.png)

**course.yml**: abstract scale

![Course example](https://raw.githubusercontent.com/ldevillez/yml2gantt/main/docs/images/course.png)

**workpackages.yml**: abstract scale, work packages

![Work packages example](https://raw.githubusercontent.com/ldevillez/yml2gantt/main/docs/images/workpackages.png)

## Python API

### Building a chart

```python
from yml2gantt import Gantt

g = Gantt(start="2026-01-01", end="2027-01-01", scale="trimester",
          title="Board game launch")

g.add_task("Market research", start=0, duration="6w", label="Alice")

design = g.add_task("Game design", start="2026-02-15")
design.add_subtask("Rules", duration="3w")
proto = design.add_subtask("Prototype", start="3w", duration=60)
proto.add_milestone("Demo", at="50%")

tests = g.add_task("Playtests", start="2026-06-01")
tests.add_segment(duration="3w")
tests.add_segment(start="8w", duration="2w", label="Retest")

g.add_task("Shipping", start="2026-12-15", milestone=True)

fig, ax = g.draw()
fig.savefig("plan.pdf")
```

| Call | Description |
|------|-------------|
| `Gantt(start=0, end=None, duration=None, scale="week", title=None, show_today=True, label_format=None, label_every=1, step=1, now=None, now_label=None)` | Same keys as the `gantt` section. |
| `g.add_task(name, start=0, duration=None, end=None, color=None, milestone=False, label=None)` | Adds a top-level task and returns it. |
| `task.add_subtask(...)` | Same arguments as `add_task`; `start` is relative to the parent. |
| `task.add_milestone(name, at="100%", color=None)` | Adds a milestone on the bar. |
| `task.add_segment(start=0, duration=None, end=None, color=None, label=None)` | Adds a piece of bar on the task's row. |
| `g.draw(ax=None, figsize=None, lighten=0.3)` | Draws and returns `(fig, ax)`. `lighten` sets how light the subtask colors are (0 = white, 1 = parent color). |

Wrong input raises `ValueError` (or `TypeError` for a value of the
wrong type) with a message naming the task.

### Loading YAML from Python

```python
from yml2gantt import Gantt, load_yaml, from_dict

g = load_yaml("examples/project.yml")       # or Gantt.from_yaml(...)
g = from_dict({"gantt": {"start": 0, "duration": 10, "scale": "abstract"},
               "tasks": [{"name": "A", "duration": 4}]})   # or Gantt.from_dict(...)

# Edit it before drawing
g.add_task("Extra task", start=5, duration=2)
```

### Customizing the figure

`draw` returns plain matplotlib objects, so anything matplotlib can do
works afterwards:

```python
import matplotlib.pyplot as plt
from yml2gantt import load_yaml

plt.style.use("seaborn-v0_8-whitegrid")     # styles and the color cycle apply

fig, ax = load_yaml("examples/roadmap.yml").draw(figsize=(14, 6))
ax.set_xlabel("2026 - 2027")
ax.tick_params(axis="y", labelsize=11)
fig.savefig("roadmap.svg", bbox_inches="tight")
```

To place a gantt inside a larger figure, pass your own axes:

```python
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(11, 10))
load_yaml("examples/sprint.yml").draw(ax=ax1)
load_yaml("examples/roadmap.yml").draw(ax=ax2)
```

## Limitations

- Subtasks go one level deep (a subtask cannot have subtasks).
- Text in a bar is not shortened: on a short bar, use a short `label`
  or `label: ""`.
- Milestone labels can cover the text of their bar: move the milestone
  (`at`) or shorten the bar `label`.
- Dated scales work with whole days (no hours).

## Development

```sh
git clone https://github.com/ldevillez/yml2gantt
cd yml2gantt
poetry install          # or: pip install -e .
poetry run yml2gantt examples/minimal.yml
```

Project layout:

```
yml2gantt/
  cli.py          command line (yml2gantt, python -m yml2gantt)
  model.py        Gantt, Task, segment and milestone objects
  scale.py        time scales: dates → x positions, grid and labels
  io.py           loading from a dict or a .yml file
  plot.py         drawing with matplotlib
  colors.py       color helpers
examples/         example charts
docs/images/      pictures used in this README
```

### Releasing a new version

Releases are made by GitHub Actions
([`release.yml`](https://github.com/ldevillez/yml2gantt/blob/main/.github/workflows/release.yml))
when a version tag is pushed. It builds the package, publishes it on
PyPI and creates the GitHub release, with the changelog entry as notes.

```sh
poetry version minor        # or patch / major: updates pyproject.toml
# move the "Unreleased" notes of CHANGELOG.md under the new version
git commit -am "Release v$(poetry version -s)"
git tag "v$(poetry version -s)"
git push origin main --tags
```

The release stops if the tag does not match the version in
`pyproject.toml`.

## License

[MIT](https://github.com/ldevillez/yml2gantt/blob/main/LICENSE)

