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:

Open the live example: Product roadmap - a year in quarters (Gantt)

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:

Open the live example: Resource load & folded axis (Gantt)

See also

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