Built with ApexGantt
You already have the tasks. They are rows in a database, or an issue-tracker
export, with names like task_id and start_date and a pointer to a parent.
The shortest path to a timeline is not to rewrite that array into a new shape:
it is to declare where each field lives and hand over the rows you already have.
npm install apexgantt
What does ApexGantt actually need per task?
Two fields are required. Everything else is optional and most of it you already have.
| Field | Required | What it does |
|---|---|---|
id | yes | Stable string id. Dependencies and parents reference it. |
name | yes | Shown in the task-list column and on the bar. |
startTime | yes | Date string, parsed per inputDateFormat. See below, it is the one that bites. |
endTime | no | Omit it and the task renders as a milestone on startTime, so you do not need a separate milestone flag. |
progress | no | 0 to 100. Draws the filled portion of the bar. |
parentId | no | Id of the parent task. Produces a nested, collapsible task list. |
dependency | no | A task id string, or an object. See below. |
assignees | no | Consumed by the built-in avatar column renderer. |
Map your fields instead of rebuilding your rows
parsing tells ApexGantt where to find each field in whatever shape your data
already has, and series then takes your raw rows untouched. Dot-notation paths
work, and any field can carry a transform for coercion:
import ApexGantt from 'apexgantt'
// Straight from a task API: snake_case keys, ISO dates, a progress value that
// arrives as a string, and a parent pointer.
const rows = [
{ task_id: 'T-1', title: 'Discovery', start_date: '2026-01-05', end_date: '2026-01-16', pct_complete: '100' },
{ task_id: 'T-2', title: 'Build', start_date: '2026-01-19', end_date: '2026-02-27', pct_complete: '40', blocked_by: 'T-1' },
{ task_id: 'T-3', title: 'API', start_date: '2026-01-19', end_date: '2026-02-06', pct_complete: '80', parent: 'T-2' },
{ task_id: 'T-4', title: 'Launch', start_date: '2026-03-02', pct_complete: '0', blocked_by: 'T-2' },
]
const gantt = new ApexGantt(document.getElementById('gantt'), {
height: 420,
parsing: {
id: 'task_id',
name: 'title',
startTime: 'start_date',
endTime: 'end_date',
parentId: 'parent',
dependency: 'blocked_by',
// A path plus a transform, for the percentage that arrives as a string.
progress: { key: 'pct_complete', transform: Number },
},
series: rows,
})
gantt.render()
Three things in that snippet are worth calling out.
series is typed TaskInput[] | Record<string, unknown>[], so passing raw rows
alongside parsing type-checks cleanly. You are not fighting the types to avoid
a transform step.
The constructor takes an element, not a selector. document.getElementById
or a ref, never '#gantt'. One example in the shipped type definitions shows a
selector string; it is wrong.
T-4 has no end_date, so it renders as a milestone. If your source gives you
an explicit null rather than an absent key, map it through a transform that
returns undefined.
A failing transform does not take the chart down: the library logs
DataParser: Transform function failed for key "..." and falls back to the raw
value, so a bad coercion shows up as a wrong-looking task rather than a blank
screen. Worth knowing when only some rows look off.
Which date format? This is the one that will bite you
Dates are strings, parsed against inputDateFormat, whose default is
MM-DD-YYYY. The parse is forgiving in a way that matters: it tries your
configured format strictly first, and falls back to a permissive parse if that
fails.
That combination has one consequence worth building a rule around. Measured on
apexgantt 3.18.0, the same task rendered four ways:
startTime you pass | inputDateFormat | Renders on |
|---|---|---|
'05-01-2026' | default | 1 May 2026 |
'05-01-2026' | 'DD-MM-YYYY' | 5 January 2026 |
'2026-01-05' | default | 5 January 2026 |
'2026-01-05' | 'MM-DD-YYYY' | 5 January 2026 |
Read the first two rows again. The same string lands four months apart, with
no error, no warning, and a chart that looks entirely plausible either way. If
your data came from a European system and your inputDateFormat says
month-first, every date in the plan is silently wrong and nothing tells you.
The bottom two rows are the fix. Pass ISO YYYY-MM-DD and the ambiguity
cannot arise, because a four-digit leading year has only one reading and the
permissive fallback handles it whatever inputDateFormat says. Almost every task
system exports ISO already, so the rule is simply: do not reformat your dates on
the way in.
Note that the task-list Start column still displays dates in
inputDateFormat, so an ISO input shows as 01-05-2026. That is display
formatting, not a reparse.
Parents, and the dates you lose to them
parentId builds the hierarchy, and a task with children behaves differently
from a leaf. showSummaryBar defaults to true, which means a parent's bar
spans the earliest start and latest end of its descendants, and its own
startTime and endTime are ignored.
That matters when converting a real task list, because parent rows in an issue
tracker usually carry their own dates. Those dates will not be what you see.
Summary bars are also always read-only: drag, resize and progress editing are
disabled on them. If you need the parent's own dates respected, set
showSummaryBar: false on that task and it renders as an ordinary bar.
Dependencies: a string, or an object
The short form is a task id, which is read as finish-to-start with no lag:
{ id: 'T-2', name: 'Build', startTime: '2026-01-19', dependency: 'T-1' }
The long form adds a type and a lag:
dependency: { taskId: 'T-1', type: 'FS', lag: 2, lagUnit: 'working' }
lagUnit is the detail worth knowing. When a working calendar is configured it
defaults to 'working', so a lag of 2 means two working days and skips weekends
and holidays. Pass 'calendar' to force raw calendar days. Without a calendar
the two coincide, which is why this only surprises people once they add one.
What breaks first
| Symptom | Cause |
|---|---|
| Every date is off by months, chart looks fine | Ambiguous day/month strings against the wrong inputDateFormat. The table above. Switch the input to ISO. |
series is not a valid option / no tasks appear | You copied a framework example. The core option is series; the React wrapper takes a tasks prop instead and derives series from it. See the React guide. |
| Constructor throws or nothing mounts | A selector string was passed instead of an element. |
| A parent task ignores its own dates | showSummaryBar defaults to true. The section above. |
| A parent task cannot be dragged | Summary bars are read-only by design. |
| One task is a bar when it should be a diamond | It still has an endTime, probably an explicit null rather than an absent key. |
| Some rows have wrong values, others fine | A transform threw. Check the console for DataParser: Transform function failed. |
When a Gantt chart is the wrong shape
| What you have | Reach for |
|---|---|
| Tasks with dates, dependencies, and a hierarchy | A Gantt chart. This is the case. |
| Tasks with a status but no dates | A board, not a timeline. A Gantt chart with invented dates is a plan nobody agreed to. |
| A reporting hierarchy rather than a schedule | An org chart. Same flat-rows-to-hierarchy conversion, different renderer. |
| Resource allocation as the question, not sequence | A resource histogram, or the resource management demo alongside the timeline. |
| Thousands of tasks at once | Collapse by default and expand on demand. A timeline nobody can scan is a slow way to draw a table. |
| A single deadline countdown | Not a chart. A date and a number. |
Which plan covers this?
ApexGantt is a commercial library, included on every paid plan. The Community tier is free for organizations under $2M USD in annual revenue; at or above that a Commercial licence applies. Nothing in the family is open source, and source published on GitHub is not an open licence. Licensed features render in full without a key, watermarked, so this whole recipe is testable against your own data before you buy anything. The pricing page has the details.
The field-mapping demo, with sourceSee the pieces running
Reference documentation
Frequently Asked Questions
Do I have to reshape my task data for ApexGantt?
No. The parsing option maps ApexGantt's fields to paths in whatever shape your data already has, including dot-notation paths and a per-field transform for coercion, and series then accepts your raw rows. It is typed TaskInput[] | Record<string, unknown>[], so passing unmapped rows alongside parsing type-checks cleanly.
What date format does ApexGantt expect?
Dates are strings parsed against inputDateFormat, which defaults to MM-DD-YYYY. The parse tries your configured format strictly and falls back to a permissive parse, so ISO YYYY-MM-DD works whatever the setting is. Pass ISO: a leading four-digit year has only one reading, and ambiguous day/month strings are the one real hazard here.
Why are all my Gantt dates months out?
Because an ambiguous string was read against the wrong inputDateFormat. Measured on apexgantt 3.18.0, the string 05-01-2026 renders on 1 May under the default MM-DD-YYYY and on 5 January under DD-MM-YYYY, with no error and a plausible-looking chart either way. Switch the input to ISO rather than trying to match the format to your data.
How do I make a task a milestone?
Omit endTime. A task with a start and no end renders as a diamond on startTime, so no separate flag is needed. If your source supplies an explicit null rather than an absent key, map it through a transform that returns undefined.
Why does my parent task ignore its own start and end dates?
Because showSummaryBar defaults to true, so a task with children spans the earliest start and latest end of its descendants and its own dates are ignored. Summary bars are also read-only. Set showSummaryBar to false on that task if you need its own dates drawn.
Related
See the field mapping running
The data-parsing demo wires a non-standard task shape straight into a timeline, with source.