Recipe

Build a project timeline from a task list

You already have the tasks, as rows with names like task_id and start_date. The shortest path to a timeline is to declare where each field lives, not to rewrite the array.

Data ParsingOpen in new tab

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.

FieldRequiredWhat it does
idyesStable string id. Dependencies and parents reference it.
nameyesShown in the task-list column and on the bar.
startTimeyesDate string, parsed per inputDateFormat. See below, it is the one that bites.
endTimenoOmit it and the task renders as a milestone on startTime, so you do not need a separate milestone flag.
progressno0 to 100. Draws the filled portion of the bar.
parentIdnoId of the parent task. Produces a nested, collapsible task list.
dependencynoA task id string, or an object. See below.
assigneesnoConsumed 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 passinputDateFormatRenders on
'05-01-2026'default1 May 2026
'05-01-2026''DD-MM-YYYY'5 January 2026
'2026-01-05'default5 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

SymptomCause
Every date is off by months, chart looks fineAmbiguous day/month strings against the wrong inputDateFormat. The table above. Switch the input to ISO.
series is not a valid option / no tasks appearYou 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 mountsA selector string was passed instead of an element.
A parent task ignores its own datesshowSummaryBar defaults to true. The section above.
A parent task cannot be draggedSummary bars are read-only by design.
One task is a bar when it should be a diamondIt still has an endTime, probably an explicit null rather than an absent key.
Some rows have wrong values, others fineA transform threw. Check the console for DataParser: Transform function failed.

When a Gantt chart is the wrong shape

What you haveReach for
Tasks with dates, dependencies, and a hierarchyA Gantt chart. This is the case.
Tasks with a status but no datesA board, not a timeline. A Gantt chart with invented dates is a plan nobody agreed to.
A reporting hierarchy rather than a scheduleAn org chart. Same flat-rows-to-hierarchy conversion, different renderer.
Resource allocation as the question, not sequenceA resource histogram, or the resource management demo alongside the timeline.
Thousands of tasks at onceCollapse by default and expand on demand. A timeline nobody can scan is a slow way to draw a table.
A single deadline countdownNot 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 source

See 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.

Get started