Gantt: the axis and working time
The time axis: its five presets and how to move between them, the window it spans, the today line, non-working days and holidays, and the Enterprise option that folds weekends out of the chart.
<script lang="ts">
import { SvGrid, type GridColumns } from '@svgrid/grid'
import { enableGanttView, type GanttProConfig } from '@svgrid/enterprise'
enableGanttView()
type Task = {
id: string
name: string
owner?: string
start: string
end?: string
progress?: number
parentId?: string | null
milestone?: boolean
}
const pad = (n: number) => String(n).padStart(2, '0')
const isoDay = (d: Date) => `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
const base = new Date()
base.setHours(0, 0, 0, 0)
base.setDate(base.getDate() - ((base.getDay() + 6) % 7))
const at = (dayOffset: number) => {
const d = new Date(base)
d.setDate(d.getDate() + dayOffset)
return isoDay(d)
}
let data = $state<Task[]>([
{ id: 'p1', name: 'Discovery', parentId: null, start: at(0), owner: 'Priya' },
{ id: 't1', name: 'Interviews', parentId: 'p1', start: at(0), end: at(4), progress: 100, owner: 'Priya' },
{ id: 't2', name: 'Synthesis', parentId: 'p1', start: at(5), end: at(11), progress: 40, owner: 'Marco' },
{ id: 'p2', name: 'Build', parentId: null, start: at(12), owner: 'Sven' },
{ id: 't3', name: 'Implementation', parentId: 'p2', start: at(12), end: at(25), progress: 10, owner: 'Sven' },
{ id: 't4', name: 'Hardening', parentId: 'p2', start: at(26), end: at(39), progress: 0, owner: 'Lena' },
{ id: 'm1', name: 'Launch', parentId: null, start: at(40), milestone: true },
])
const holidays = [at(17)]
const columns: GridColumns<Task> = [
{ field: 'name', header: 'Task', width: 200 },
{ field: 'owner', header: 'Owner', width: 100 },
]
const pro = (c: GanttProConfig<any, Task>) => c
</script>
Zoom presets
Five axis presets, each a tick unit under a coarser grouping row:
zoom |
Ticks | Grouped by | Tick width |
|---|---|---|---|
day |
days (named) | weeks | 48 px |
week (default) |
days | months | 28 px |
month |
weeks | months | 40 px |
quarter |
months | quarters | 96 px |
year |
months | years | 72 px |
<SvGrid {data} {columns} getRowId={(r) => r.id}
gantt={{ startField: 'start', endField: 'end', parentField: 'parentId', zoom: 'month' }} />
The toolbar shows a zoom stepper over zoomLevels (all five by default; a
single entry hides it), and Ctrl/Cmd+wheel over the chart steps it too,
keeping the date under the pointer fixed. onZoomChange reports the change.
zoom is the preset the chart opens on; the stepper owns it afterwards.
<SvGrid {data} {columns} getRowId={(r) => r.id}
gantt={{
startField: 'start', endField: 'end', parentField: 'parentId',
zoom: 'quarter',
zoomLevels: ['month', 'quarter', 'year'],
onZoomChange: (z) => console.log('zoom', z),
}} />
Pick the ladder for the plan. A sprint reads at day / week; a roadmap at
month / quarter / year, which is what the roadmap demo does:
The window
The axis spans the first start to the last finish plus rangePaddingDays (7)
of slack on each side, clamped by minDate / maxDate. A drag cannot leave
the clamped window either.
Tick widths are the axis's natural size, the one it scrolls at. When the whole window is narrower than the pane beside the task table the ticks stretch to fill it, so a short plan at a coarse zoom never stops two thirds of the way across. The chart opens with today a third of the way in (or the first task, when today is outside the window).
A dashed today line crosses the chart; todayLine: false hides it.
Non-working days and holidays
Non-working days are shaded, and every duration and cascade calculation skips
them: a durationField of three days starting Thursday ends Tuesday, and a
cascaded start never lands on a Sunday.
<SvGrid {data} {columns} getRowId={(r) => r.id}
gantt={{
startField: 'start', endField: 'end', parentField: 'parentId',
nonWorkingDays: [0, 6], // Sun + Sat, the default
holidays,
weekStartsOn: 1,
zoom: 'week',
}} />
| Option | Meaning |
|---|---|
nonWorkingDays |
Weekday numbers, 0 = Sunday. Default [0, 6]. |
holidays |
Specific dates (a Date, epoch-ms or ISO string each) that are off. |
showNonWorking |
Shade the columns. Default true. |
weekStartsOn |
First day of the week for the day and month presets' tick grouping, 0-6. Default 0. |
respectWorkingTime |
Schedule in working time: a moved or cascaded task lands on a working day and keeps its working length; slack is in working days. Default true. |
Shading is drawn for the day-granular presets. At month and coarser a tick
spans several days and is never wholly off, so the shading is skipped rather
than painting a partial column.
Folding weekends out of the axis
Enterprise. collapseWeekends takes the whole non-working days out of the
timeline and leaves a narrow hatched marker where each run was, so a quarter
fits in the width a month used to take. Working days keep their real size -
nothing is squashed - and a task that does run over a folded weekend still
draws across it.
<SvGrid {data} {columns} getRowId={(r) => r.id}
gantt={pro({
startField: 'start', endField: 'end', parentField: 'parentId',
zoom: 'week', collapseWeekends: true, collapsedGapPx: 10,
})} />
collapsedGapPx: 0 removes the folded days outright: Friday's finish and
Monday's start land on the same pixel. Only the day-granular presets (day,
week) have weekend columns to fold; at month and coarser a tick is never
wholly non-working, so the option is ignored rather than shrinking a week by
part of itself. The resource demo has a toggle for it:
See also
- Getting started - how dates and durations are read.
- Dependencies - the cascade that respects working time.
- Config reference - the axis options in one table.
Live examples
- Product roadmap - a year in quarters - The Gantt as a roadmap: three teams, their initiatives across four quarters, and the releases that cut across them as milestones. A `task` snippet draws the owner and a status chip inside each bar, colour is by status, a custom tooltip carries the effort and a summary, the header chips filter by status, and a month / quarter / year zoom ladder drills in with Ctrl+wheel.
- Resource load & folded axis - A field-service quarter with a load strip under the chart: one row per crew, one bar per column, red where a crew is booked past its capacity - the specialist is one person with two sign-offs in the same week. Capacity is per resource, so a two-van crew takes two jobs at once. Fold the weekends out and the quarter fits in the width a month used to take.
Related articles
- Throttling Live Grid Updates to Animation Frames - High-frequency WebSocket feeds will wreck grid performance if you apply every message immediately. One rAF buffer and a Map coalescer cuts thousands of renders down to 60 per second.
- We Asked Claude Code for a Svelte Table. It Wrote Its Own Every Time, Until the Project Said Otherwise - Ten fresh Claude Code sessions, one prompt, a bare SvelteKit app. With no hint it hand-wrote a DataTable component, even for 10,000 rows. One paragraph in AGENTS.md changed that.
- Building a Real-Time Trading Grid in Svelte - How to wire a WebSocket tick feed into SvGrid without dropping frames - one array replacement per animation frame, stable row identity, up/down flashes, and which of the grid's two update paths to use for what.