Data formats
A task’s dates, its length, its precision and its links can be written in more than one way. The
chart reads all of them into one thing, a TaskRecord with real Dates, and this example writes
the same kind of plan in each shape, side by side. Each bar is named after how it was written.
Hover a bar for how the chart read it, or click it: the line under the chart shows the task as it is
in data.ts (record.task, the object you gave, which the chart never changes) and the days or
moments it covers. The checkbox flips dates.endInclusive, and the select zooms in far enough to
read the times of day.
pnpm add @ganttlane/dom # vanillapnpm add @ganttlane/react react react-dom # Reactnpm install @ganttlane/dom # vanillanpm install @ganttlane/react react react-dom # Reactyarn add @ganttlane/dom # vanillayarn add @ganttlane/react react react-dom # Reactbun add @ganttlane/dom # vanillabun add @ganttlane/react react react-dom # Reactimport type { Link, Task } from '@ganttlane/core';
/** What `task.data` holds: your own payload, which the library never reads. */export interface Meta { /** The part of the README the task belongs to. */ group: 'Start and end' | 'Duration' | 'Precision' | 'Links'; /** How the input is read, in a sentence: the tooltip shows it. */ note: string;}
// 2026-03-16 is a Monday. The tasks of a group say the same thing in different ways, so their// bars line up. Notes on the dates://// - `new Date(2026, 2, 16)` is local midnight of 16 March (months count from 0). A number is read// the same way, through the local clock, so it is made from a `Date` here: an epoch literal// would land on another day in some time zones.// - A value with a time of day (`'2026-03-23T09:00'`) makes a timed task. It must be a multiple// of `scheduling.granularity`, so `options.ts` sets it to one hour.export const tasks: Task<Meta>[] = [ // ---- start and end: the same five days, written four ways, and a timed task ---- { id: 'date', name: 'Date', start: new Date(2026, 2, 16), end: new Date(2026, 2, 20), data: { group: 'Start and end', note: 'A Date at local midnight is a date-only value: whole days.', }, }, { id: 'iso-date', name: 'ISO date', start: '2026-03-16', end: '2026-03-20', data: { group: 'Start and end', note: 'A date-only string is a whole-day value.', }, }, { id: 'epoch', name: 'Epoch ms', start: new Date(2026, 2, 16).getTime(), end: new Date(2026, 2, 20).getTime(), data: { group: 'Start and end', note: 'A number is read through the local clock, like a Date.', }, }, { id: 'iso-time', name: 'ISO date-time', start: '2026-03-23T09:00', end: '2026-03-24T17:00', data: { group: 'Start and end', note: 'A time of day makes a timed task: its end is an exact moment.', }, },
// ---- duration instead of an end ---- { id: 'days', name: 'duration 3', start: '2026-03-16', duration: 3, data: { group: 'Duration', note: 'A number is days.' }, }, { id: 'three-days', name: "duration '3d'", start: '2026-03-16', duration: '3d', data: { group: 'Duration', note: 'A string with a unit: the same three days.' }, }, { id: 'week', name: "duration '1w'", start: '2026-03-18', duration: '1w', data: { group: 'Duration', note: 'A week is seven calendar days, the weekend included.' }, }, { id: 'hours', name: "duration '6h'", start: '2026-03-23T09:00', duration: '6h', data: { group: 'Duration', note: 'Hours make a timed task, counted from its start.' }, },
// ---- precision: the same two dates, read as days or as moments ---- { id: 'precision-day', name: "precision 'day'", start: '2026-03-23', end: '2026-03-25', precision: 'day', data: { group: 'Precision', note: 'Whole days: the end is a day, whatever else it says.' }, }, { id: 'precision-time', name: "precision 'time'", start: '2026-03-23', end: '2026-03-25', precision: 'time', data: { group: 'Precision', note: 'Exact moments: the end is the midnight that starts 25 March.', }, },
// ---- links: in `links`, and as `dependencies` of the target ---- { id: 'links-a', name: 'links[] source', start: '2026-03-16', end: '2026-03-17', data: { group: 'Links', note: 'The predecessor of a link in the links array.' }, }, { id: 'links-b', name: 'links[] target', start: '2026-03-19', end: '2026-03-20', data: { group: 'Links', note: 'The successor: its link is in the links array, a lag of one day.', }, }, { id: 'deps-a', name: 'dependencies source', start: '2026-03-16', end: '2026-03-17', data: { group: 'Links', note: 'The predecessor of a link its successor lists as a dependency.', }, }, { id: 'deps-b', name: 'dependencies target', start: '2026-03-19', end: '2026-03-20', // The same link as `links-a` to `links-b`, written on the successor. dependencies: [{ id: 'deps-a', type: 'FS', lag: '1d' }], data: { group: 'Links', note: 'The successor: it lists the link as a dependency, a lag of one day.', }, },];
export const links: Link[] = [{ source: 'links-a', target: 'links-b', type: 'FS', lag: '1d' }];import type { GanttOptions } from '@ganttlane/dom';
import type { Meta } from './data';import { describe } from './helpers';
export const options = { // The scale of the timeline. The select also offers the half-day and quarter-day scales, where // the times of day of the timed tasks can be read. viewMode: 'day', // Open on the plan. The default, 'today', is far from these dates. scroll: { initial: 'first-task' },
// Every date, duration and lag is a multiple of the granularity. The default, '1d', would report // the timed tasks (`'2026-03-23T09:00'`) as `misaligned` and round them to midnight: one hour // lets them be what they are. Tasks given as whole days stay on whole days. scheduling: { granularity: '1h' }, // What a date-only `end` means. `true` (the default): the last day of the task. `false`: the day // after it, so that '2026-03-20' ends the task when 19 March is over. The checkbox flips it. dates: { endInclusive: true },
// Smaller rows, so that all fourteen fit. Labels go beside the bars. layout: { rowHeight: 28, barHeight: 18 }, display: { barLabel: 'outside' },
tooltip: { // `record.task` is the task as given, with its own typed `data` (`Meta`); the rest of the // record is what the chart made of it. render: ({ task: record }) => ({ title: record.name, subtitle: describe(record), details: record.task.data?.note, }), },} satisfies Partial<GanttOptions<Meta>>;import '@ganttlane/dom/styles.css';
import { createGantt } from '@ganttlane/dom';
import { links, type Meta, tasks } from './data';import { describe, inputOf } from './helpers';import { options } from './options';
const VIEW_MODES = ['day', 'half-day', 'quarter-day'] as const;const HINT = 'Click a bar to see how its input was written, and how the chart read it.';
export function mount(container: HTMLElement): () => void { const root = document.createElement('div'); const toolbar = document.createElement('div'); toolbar.setAttribute('role', 'toolbar'); // The chart fills its container and scrolls inside it, so the container needs a height. const chart = document.createElement('div'); chart.style.height = '470px'; const status = document.createElement('p'); status.textContent = HINT; root.append(toolbar, chart, status); container.append(root);
// `Meta` is the type of `task.data`: it flows to every handler and render function. const gantt = createGantt<Meta>(chart, { tasks, links, ...options, onTaskClick: (record) => { // `record.task` is the task as you gave it, never changed by the chart; the rest of the // record is what the chart made of it. status.textContent = `${record.name}: written as ${inputOf(record.task, links)}. Read as ${describe(record)}.`; }, });
const controller = new AbortController(); const { signal } = controller;
// `dates.endInclusive`: what a date-only `end` means. const endInclusive = document.createElement('input'); endInclusive.type = 'checkbox'; endInclusive.checked = options.dates.endInclusive; endInclusive.addEventListener( 'change', () => { // `dates` is merged field by field with the one set before: only this field changes. gantt.setOptions({ dates: { endInclusive: endInclusive.checked } }); status.textContent = HINT; // what the last click said is out of date now }, { signal }, ); const endLabel = document.createElement('label'); endLabel.append(endInclusive, ' dates.endInclusive');
// The scale: the finer ones show the times of day of the timed tasks. const view = document.createElement('select'); for (const id of VIEW_MODES) { const option = document.createElement('option'); option.value = id; option.textContent = id; view.append(option); } view.value = options.viewMode; view.addEventListener( 'change', () => { gantt.setViewMode(view.value); }, { signal }, ); const viewLabel = document.createElement('label'); viewLabel.append('View ', view);
toolbar.append(viewLabel, endLabel);
return () => { controller.abort(); gantt.destroy(); root.remove(); };}import '@ganttlane/react/styles.css';
import { Gantt } from '@ganttlane/react';import { useState } from 'react';
import { links, tasks } from './data';import { describe, inputOf } from './helpers';import { options } from './options';
const VIEW_MODES = ['day', 'half-day', 'quarter-day'];const HINT = 'Click a bar to see how its input was written, and how the chart read it.';
export default function Example() { // The two options the controls change are state. `viewMode` is a controlled prop in React: the // chart shows the one your state holds. const [viewMode, setViewMode] = useState<string>(options.viewMode); const [endInclusive, setEndInclusive] = useState<boolean>(options.dates.endInclusive); const [status, setStatus] = useState(HINT);
return ( <> <div role="toolbar"> <label> View{' '} <select value={viewMode} onChange={(event) => { setViewMode(event.target.value); }} > {VIEW_MODES.map((id) => ( <option key={id} value={id}> {id} </option> ))} </select> </label> <label> <input type="checkbox" checked={endInclusive} onChange={(event) => { setEndInclusive(event.target.checked); setStatus(HINT); // what the last click said is out of date now }} />{' '} dates.endInclusive </label> </div> <Gantt // The chart fills its container and scrolls inside it, so the container needs a height. style={{ height: 470 }} // The chart owns the data (uncontrolled): these are where it starts. `Meta`, the type of // `task.data`, is inferred from them and flows to every handler. defaultTasks={tasks} defaultLinks={links} {...options} // After `options`, so that the state wins over what it holds. viewMode={viewMode} dates={{ endInclusive }} onTaskClick={(record) => { // `record.task` is the task as you gave it, never changed by the chart; the rest of // the record is what the chart made of it. setStatus( `${record.name}: written as ${inputOf(record.task, links)}. Read as ${describe(record)}.`, ); }} /> <p>{status}</p> </> );}import type { Link, Task, TaskRecord } from '@ganttlane/dom';
const day = new Intl.DateTimeFormat('en', { weekday: 'short', day: 'numeric', month: 'short' });const moment = new Intl.DateTimeFormat('en', { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23',});
/** A value written the way the data file writes it: `'2026-03-16'`, `new Date(2026, 2, 16)`, `3`. */function literal(value: unknown): string { if (value instanceof Date) { const parts = [value.getFullYear(), value.getMonth(), value.getDate()]; if (value.getHours() !== 0 || value.getMinutes() !== 0) { parts.push(value.getHours(), value.getMinutes()); } return `new Date(${parts.join(', ')})`; } if (typeof value === 'string') return `'${value}'`; if (Array.isArray(value)) return `[${value.map(literal).join(', ')}]`; if (typeof value === 'object' && value !== null) { const fields = Object.entries(value).map(([key, item]) => `${key}: ${literal(item)}`); return `{ ${fields.join(', ')} }`; } return String(value);}
/** * How a task was written, as the data file has it: its dates, its duration and precision, its * `dependencies`, and the entry of `links` that ends at it. `record.task` is the task you gave, * never changed by the chart. * * @param task - A task as given. * @param links - The links as given. * @returns The task's input, as source text. */export function inputOf(task: Task, links: readonly Link[]): string { const written: string[] = []; for (const key of ['start', 'end', 'duration', 'precision', 'dependencies'] as const) { if (task[key] !== undefined) written.push(`${key}: ${literal(task[key])}`); } for (const link of links) { if (link.target === task.id) written.push(`links[]: ${literal(link)}`); } return written.join(', ');}
/** * How the chart read a task, from its computed record: the precision, and the days or the * moments the task covers (`endExclusive` is the end of the half-open interval, whichever way * `end` was written). * * @param record - A task of the chart, or `undefined`. * @returns A sentence. */export function describe(record: TaskRecord | undefined): string { if (!record?.start || !record.endExclusive || !record.duration) return ''; if (record.precision === 'time') { const hours = Math.round(record.duration.calendarDays * 24); return `a timed task: ${moment.format(record.start)} to ${moment.format(record.endExclusive)} (${String(hours)} h)`; } const { endExclusive } = record; const last = new Date( endExclusive.getFullYear(), endExclusive.getMonth(), endExclusive.getDate() - 1, ); const days = Math.round(record.duration.calendarDays); return `whole days: ${day.format(record.start)} to ${day.format(last)} (${String(days)} ${days === 1 ? 'day' : 'days'})`;}Files
data.ts: fourteen tasks in four groups, thelinksarray, andMeta, the type oftask.data.options.ts: the granularity,dates, the size of the rows, and a tooltip that reads the typeddata.helpers.ts:inputOfwrites a task as source text,describesays how the chart read it from its computed record.vanilla.tsandreact.tsx: the chart, the checkbox and the select.
Options
| Option | Value | What it does |
|---|---|---|
scheduling.granularity |
'1h' |
The step of the data: every date, duration and lag is a multiple of it, so timed tasks fit |
dates.endInclusive |
true |
Whether a date-only end is the last day (the default) or the day after it; the checkbox |
viewMode |
'day' |
The scale; the select adds 'half-day' and 'quarter-day' (React: state, a controlled prop) |
scroll.initial |
'first-task' |
Opens on the plan |
layout.rowHeight, layout.barHeight |
28, 18 |
Smaller rows, so that all fourteen fit |
display.barLabel |
'outside' |
The name goes beside the bar, whatever its length |
tooltip.render |
a function | Shows how the chart read the task, and the note of its typed data |
onTaskClick |
a function | Writes the task’s input and its record in the line under the chart |
How each shape is read
| You write | The chart reads |
|---|---|
start and end as a Date, '2026-03-16' or a number, at local midnight |
A date-only value: precision 'day'. The task covers whole days, and an end is the last day (dates.endInclusive: true) |
'2026-03-23T09:00', or a Date or a number with a time of day |
A time: precision 'time'. The task starts and ends at exact moments, and an end is the moment it is over |
duration: 3 |
A number of days, counted from start. It is used when there is no end: end wins if both are given |
duration: '3d', '1w', '6h' |
A length with a unit. A week is seven calendar days. Hours make a timed task, whatever start looks like |
precision: 'time' on date-only values |
The same strings read as exact moments: '2026-03-25' is the midnight that starts that day, so the task ends there |
precision: 'day' |
Whole days. It is what date-only values are anyway; a time of day under it would be misaligned |
dates.endInclusive: false |
A date-only end is the day after the task, which ends when the day before it is over. Timed tasks and durations do not change |
an entry of links |
A link: source, target, a type ('FS' by default) and a lag (a number of days, or a duration such as '1d') |
dependencies on the successor |
The same link, written on the task that follows: ids, or { id, type, lag }. It is merged into the links |
data |
Yours. The chart never reads it; Task<Meta> types it everywhere it comes back |
What to try
- Compare the first three bars.
Date, an ISO date and an epoch number are the same five days (16 to 20 March). Click each to see the source text next to what the chart read. - Flip
dates.endInclusive. Every bar with a date-onlyend(the first three, precision ‘day’ and the four link rows) loses its last day: withfalsethe end is where the task stops, not its last day. The timed tasks, the durations and precision ‘time’ do not move. - Look at the times. ISO date-time and duration ‘6h’ are timed tasks: they begin and end inside a day. Choose quarter-day in the select to read them.
- Compare the two precisions. Both tasks are written
'2026-03-23'to'2026-03-25'. One is read as three whole days, the other as two days that end at midnight. - Compare the two links. The arrows between the links[] rows and between the dependencies rows are the same link, a finish-to-start with a lag of a day. Click the targets to see where each was written.
Notes
- The dates are floating wall-clock time.
'2026-03-23T09:00'is 09:00 for every viewer, in every time zone, and strings with aZor a UTC offset are rejected. ADateor a number is read through the local clock of the machine, sonew Date(2026, 2, 16)(months count from 0) is 16 March everywhere. Avoidnew Date('2026-03-16'): JavaScript reads that string as UTC, which is the evening of the 15th west of Greenwich. The epoch numbers of this example are made from aDatefor that reason. - The granularity decides what a time of day may be. With the default
'1d', a timed task is reported asmisalignedand widened to whole days.'1h'accepts the hours of this plan; a finer value accepts finer times. Tasks given as whole days stay on whole days (scheduling.keepDayPrecision, on by default). - Precision is derived from the input, and
precisionoverrides it. A date-only value is a day; a time of day makes the task timed, and so does a duration in hours. recordhas the computed side.start,endandendExclusiveareDates,endExclusivebeing the end of the half-open interval whichever wayendwas written, anddurationcounts days and working days.record.taskis your task, untouched, with its typeddata.linksis the canonical form,dependenciesa shorthand. Use whichever fits your data: the chart merges them, thelinksarray first, then the dependencies in task order. If you keep the links in your own state (see Controlled data), prefer the array: a dependency is not in the list the chart hands back, and deleting one is only reported toonLinkDelete.Task<Meta>is the type of your payload. It flows throughcreateGantt<Meta>or the tasks of<Gantt>into the records, the tooltip’s context and every handler, hererecord.task.data?.note.- React: the two options the controls change are state.
viewModeis a controlled prop in React (the chart shows the one your state holds), anddatescomes from state too. The component passes both after the spreadoptions, so they win. The vanilla chart changes them withsetViewModeandsetOptions.