Skip to content

Milestones and placeholders

Not every row of a plan is a bar. A milestone (type: 'milestone') is a point in time with no length, drawn as a diamond. A placeholder (type: 'placeholder') is a row with no bar at all: a heading for the rows under it, or work that nobody has scheduled yet. The plan here chains five milestones and four tasks with links, and keeps three placeholder rows.

Terminal window
pnpm add @ganttlane/dom # vanilla
pnpm add @ganttlane/react react react-dom # React
View on GitHub
data.ts
import type { Link, Task } from '@ganttlane/core';
// A plan with five milestones and three placeholder rows. 2026-03-02 is a Monday.
// `type: 'milestone'` a point in time, not a bar: it has a `start` and no length (an `end` or
// a `duration` is ignored). It is drawn as a diamond.
// `type: 'placeholder'` a row with no bar: its dates, if it has any, are ignored. It holds a
// place in the list: a heading for the rows under it, or work that is not
// scheduled yet. A task with no dates at all is a placeholder too, but
// the chart reports it as an issue; the type says you meant it.
export const tasks: Task[] = [
{ id: 'kickoff', name: 'Kick-off', type: 'milestone', start: '2026-03-02' },
{ id: 'design', name: 'Design', start: '2026-03-02', end: '2026-03-06' },
{ id: 'signoff', name: 'Design sign-off', type: 'milestone', start: '2026-03-09' },
{ id: 'build', name: 'Build', start: '2026-03-09', end: '2026-03-17' },
{ id: 'beta', name: 'Beta', type: 'milestone', start: '2026-03-18' },
// A heading: a placeholder with children, which are the rows below it.
{ id: 'launch', name: 'Launch', type: 'placeholder' },
{
id: 'marketing',
name: 'Marketing',
parentId: 'launch',
start: '2026-03-20',
end: '2026-03-25',
},
{ id: 'docs', name: 'Docs', parentId: 'launch', start: '2026-03-20', end: '2026-03-26' },
{ id: 'release', name: 'Release', type: 'milestone', parentId: 'launch', start: '2026-03-27' },
// Work that is not scheduled yet: rows with no bar. The button turns Hiring into a task.
{ id: 'backlog', name: 'Backlog', type: 'placeholder' },
{ id: 'hiring', name: 'Hiring', type: 'placeholder', parentId: 'backlog' },
{ id: 'support', name: 'Support rota', type: 'placeholder', parentId: 'backlog' },
];
// A milestone is linked like any task: it is the source of a link (what follows it) or its target
// (what it waits for). Its two edges are the same moment, so every link type reaches the same point.
export const links: Link[] = [
{ source: 'kickoff', target: 'design', lag: 0 },
{ source: 'design', target: 'signoff', lag: 2 },
{ source: 'signoff', target: 'build', lag: 0 },
{ source: 'build', target: 'beta', lag: 0 },
{ source: 'beta', target: 'marketing', lag: 2 },
{ source: 'beta', target: 'docs', lag: 2 },
{ source: 'marketing', target: 'release', lag: 1 },
{ source: 'docs', target: 'release', lag: 0 },
];

Files

  • data.ts: the tasks, with type and parentId, and the links, in comments.
  • options.ts: the calendar and the view mode.
  • helpers.ts: what the button does to the tasks, and the sentences of the status line.
  • vanilla.ts and react.tsx: the chart and two buttons.

Options

Option Value What it does
task.type 'milestone' A point in time at start; a zero length; drawn as a diamond
task.type 'placeholder' A row with no bar; its dates are ignored
task.parentId 'launch', 'backlog' A placeholder can be the group of the rows under it
link { source, target, lag } A milestone is the source or the target of a link, like a task
calendar.weekends [] Every day is a working day, so a milestone on a Saturday is no conflict
layout.rowHeight, layout.barHeight 34, 22 Compact rows, so that all twelve show
viewMode 'week' One column a week: the whole plan fits
scroll.initial 'first-task' Opens on the plan; the default, 'today', is far from these dates
scroll.align 0.04 Where the first task lands, as a fraction of the width: a little in from the edge, so that the first diamond is whole
onTaskClick a function (in the example) Called with the TaskRecord of a bar that was clicked
onTasksChange a function (in the example) One ChangeSet per drag, with every task it moved

What to try

  • Drag Kick-off a day or two to the right. Every link after it moves its target: Design, then Design sign-off, Build, Beta, Marketing, Docs and Release. The status line lists them.
  • Take a diamond by its side. There is nothing to resize: a milestone has no edges to drag, so it moves, and that is all it does.
  • Click Beta, and then Build. The status line reads the record: type, the date, and the length (none for a milestone).
  • Drag Marketing to the right. Release, the milestone that waits for it (and for Docs), follows it by its lag: a milestone is moved by links like any bar.
  • Press Schedule Hiring. Hiring was a placeholder, a row under Backlog with no bar; now it is a task with a start and a duration, and it has a bar. Press it again and the bar goes, and the row stays. The other placeholders, Launch and Support rota, have no bar either, and nothing to drag.

Notes

  • A milestone has a start and no length. Its end and duration are ignored. Its start and its end are the same moment, so a link of any type reaches the same point; lag counts from it. A milestone cannot be resized, but it can be moved, linked and given a progress.
  • A placeholder has no bar. A row without dates is a placeholder too, but then the chart reports a no-dates issue: the type says you meant it. A link that touches a placeholder is kept but inactive: it is not drawn, not propagated, and reported as an inactive-link issue. Give the row dates and a task type (as the button does) and the link works.
  • A placeholder as a heading. parentId makes Launch the parent of three rows, a milestone among them; the heading has no bar, so a child is never “outside” it. Children of a placeholder can be shut with collapse('launch') as any other group (see Groups and children).
  • Your tasks, your list. The button builds a new array with the one task replaced and gives it to setTasks; the chart diffs it by id. Nothing you pass in is changed.
  • React. defaultTasks and defaultLinks are where the chart starts, and then it owns the data (uncontrolled); chart.current.setTasks(next) gives it a new list.