Skip to content

Gantt Timeline and Zoom

The Gantt timeline can be configured at two levels:

  1. zoomPreset for fast setup with built-in levels.
  2. zoom for full control over levels, wheel behavior, and anchor strategy.

Use weekStartsOn when week headers or week ticks should align to a specific weekday. Gantt defaults to Sunday-start weeks.

Source code
TypeScript ts
// src/components/gantt/GanttShowcase.ts
import './gantt-showcase.scss';
import { defineCustomElements } from '@revolist/revogrid/loader';
defineCustomElements();

import { GanttPlugin } from '@revolist/revogrid-enterprise';
import { ExportExcelPlugin, RowSelectPlugin } from '@revolist/revogrid-pro';
import {
  STANDARD_CALENDAR,
  SHOWCASE_ASSIGNMENTS,
  SHOWCASE_BASELINES,
  SHOWCASE_COLUMNS_POLISHED,
  SHOWCASE_DEPENDENCIES,
  SHOWCASE_GANTT_CONFIG,
  SHOWCASE_RESOURCES,
  SHOWCASE_TASKS,
  renderShowcaseTaskBarContent,
} from './gantt-project-data';
import { currentTheme } from '../composables/useRandomData';

// ─── Entry point ──────────────────────────────────────────────────────────────

const rowSelect = { rowOrder: true };

export function load(parentSelector: string): void {
  const parent = document.querySelector(parentSelector);
  if (!parent) return;
  const darkTheme = currentTheme().isDark();

  const container = document.createElement('div');
  container.className = `gantt-showcase-shell grow h-full ${darkTheme ? 'gantt-showcase-shell--dark' : 'gantt-showcase-shell--light'}`;
  parent.appendChild(container);

  const grid = document.createElement('revo-grid') as HTMLRevoGridElement;
  const controls = document.createElement('div');
  controls.className = 'gantt-showcase-controls';
  container.appendChild(controls);

  grid.theme          = darkTheme ? 'darkCompact' : 'compact';
  grid.readonly       = false;
  grid.range          = true;
  grid.resize         = true;
  grid.rowSize        = 42;
  grid.rowHeaders     = false;
  grid.hideAttribution = true;
  grid.autoSizeColumn = true;
  grid.classList.add('gantt-showcase-grid');
  grid.plugins        = [GanttPlugin, ExportExcelPlugin, RowSelectPlugin];
  grid.rowSelect      = rowSelect;
  grid.columns        = [...SHOWCASE_COLUMNS_POLISHED];
  grid.source         = [...SHOWCASE_TASKS];
  grid.ganttDependencies = [...SHOWCASE_DEPENDENCIES];
  grid.ganttCalendars    = [{ ...STANDARD_CALENDAR }];
  grid.ganttResources    = [...SHOWCASE_RESOURCES];
  grid.ganttAssignments  = [...SHOWCASE_ASSIGNMENTS];
  grid.ganttBaselines    = [...SHOWCASE_BASELINES];
  let showCriticalPath = Boolean(SHOWCASE_GANTT_CONFIG.visuals.showCriticalPath);
  let showBaseline = false;

  function applyGanttConfig() {
    grid.gantt = {
      ...SHOWCASE_GANTT_CONFIG,
      visuals: {
        ...SHOWCASE_GANTT_CONFIG.visuals,
        showCriticalPath,
        showBaseline,
        taskBarContentHook: renderShowcaseTaskBarContent,
      },
    } as typeof grid.gantt;
  }

  function createToggle(label: string, checked: () => boolean, onChange: (value: boolean) => void) {
    const control = document.createElement('label');
    control.className = 'gantt-showcase-control';
    const input = document.createElement('input');
    input.type = 'checkbox';
    input.className = 'gantt-showcase-control__input';
    const text = document.createElement('span');
    text.className = 'gantt-showcase-control__label';
    text.textContent = label;
    const sync = () => {
      input.checked = checked();
    };
    input.addEventListener('change', () => {
      onChange(input.checked);
      applyGanttConfig();
    });
    control.append(input, text);
    sync();
    return control;
  }

  controls.append(
    createToggle('Critical path', () => showCriticalPath, (value) => {
      showCriticalPath = value;
    }),
    createToggle('Baselines', () => showBaseline, (value) => {
      showBaseline = value;
    }),
  );

  applyGanttConfig();
  container.appendChild(grid);
}
React tsx
// src/components/gantt/GanttShowcase.tsx
import './gantt-showcase.scss';
import React, { useMemo, useRef, useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { GanttPlugin } from '@revolist/revogrid-enterprise';
import { ExportExcelPlugin, RowSelectPlugin } from '@revolist/revogrid-pro';
import {
  STANDARD_CALENDAR,
  SHOWCASE_ASSIGNMENTS,
  SHOWCASE_BASELINES,
  SHOWCASE_COLUMNS_POLISHED,
  SHOWCASE_DEPENDENCIES,
  SHOWCASE_GANTT_CONFIG,
  SHOWCASE_RESOURCES,
  SHOWCASE_TASKS,
  renderShowcaseTaskBarContent,
} from './gantt-project-data';
import type { GanttPluginConfig } from '@revolist/revogrid-enterprise';
import { currentTheme } from '../composables/useRandomData';

const plugins = [GanttPlugin, ExportExcelPlugin, RowSelectPlugin];
const rowSelect = { rowOrder: true };
const source      = [...SHOWCASE_TASKS];
const dependencies = [...SHOWCASE_DEPENDENCIES];
const calendars    = [{ ...STANDARD_CALENDAR }];
const resources    = [...SHOWCASE_RESOURCES];
const assignments  = [...SHOWCASE_ASSIGNMENTS];
const baselines    = [...SHOWCASE_BASELINES];
const columns      = [...SHOWCASE_COLUMNS_POLISHED];

function GanttShowcase() {
  const { isDark } = currentTheme();
  const darkTheme = isDark();
  const gridRef = useRef<HTMLRevoGridElement>(null);
  const [showCriticalPath, setShowCriticalPath] = useState(Boolean(SHOWCASE_GANTT_CONFIG.visuals.showCriticalPath));
  const [showBaseline, setShowBaseline] = useState(false);
  const ganttConfig: GanttPluginConfig = useMemo(() => ({
    ...SHOWCASE_GANTT_CONFIG,
    visuals: {
      ...SHOWCASE_GANTT_CONFIG.visuals,
      showCriticalPath,
      showBaseline,
      taskBarContentHook: renderShowcaseTaskBarContent,
    },
  } as GanttPluginConfig), [showCriticalPath, showBaseline]);

  return (
    <div className={`gantt-showcase-shell grow h-full ${darkTheme ? 'gantt-showcase-shell--dark' : 'gantt-showcase-shell--light'}`}>
      <div className="gantt-showcase-controls">
        <label className="gantt-showcase-control">
          <input
            className="gantt-showcase-control__input"
            type="checkbox"
            checked={showCriticalPath}
            onChange={(event) => setShowCriticalPath(event.currentTarget.checked)}
          />
          <span className="gantt-showcase-control__label">Critical path</span>
        </label>
        <label className="gantt-showcase-control">
          <input
            className="gantt-showcase-control__input"
            type="checkbox"
            checked={showBaseline}
            onChange={(event) => setShowBaseline(event.currentTarget.checked)}
          />
          <span className="gantt-showcase-control__label">Baselines</span>
        </label>
      </div>
      <RevoGrid
        ref={gridRef}
        className="gantt-showcase-grid"
        theme={darkTheme ? 'darkCompact' : 'compact'}
        hideAttribution
        readonly={false}
        range
        resize
        rowSize={42}
        rowHeaders={false}
        plugins={plugins}
        rowSelect={rowSelect}
        source={source}
        columns={columns}
        gantt={ganttConfig}
        ganttDependencies={dependencies}
        ganttCalendars={calendars}
        ganttResources={resources}
        ganttAssignments={assignments}
        ganttBaselines={baselines}
      />
    </div>
  );
}

export default GanttShowcase;
Vue vue
<template>
  <div :class="shellClass">
    <div class="gantt-showcase-controls">
      <label class="gantt-showcase-control">
        <input v-model="showCriticalPath" class="gantt-showcase-control__input" type="checkbox" />
        <span class="gantt-showcase-control__label">Critical path</span>
      </label>
      <label class="gantt-showcase-control">
        <input v-model="showBaseline" class="gantt-showcase-control__input" type="checkbox" />
        <span class="gantt-showcase-control__label">Baselines</span>
      </label>
    </div>
    <RevoGrid
      ref="gridRef"
      class="gantt-showcase-grid skip-style cell-border"
      hide-attribution
      :readonly="false"
      :range="true"
      :resize="true"
      :row-size="42"
      :row-headers="false"
      :theme="gridTheme"
      :plugins="plugins"
      :row-select.prop="rowSelect"
      :source="source"
      :columns="columns"
      :gantt.prop="ganttConfig"
      :gantt-dependencies.prop="dependencies"
      :gantt-calendars.prop="calendars"
      :gantt-resources.prop="resources"
      :gantt-assignments.prop="assignments"
      :gantt-baselines.prop="baselines"
    />
  </div>
</template>

<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import { ExportExcelPlugin, RowSelectPlugin } from '@revolist/revogrid-pro';
import {
  STANDARD_CALENDAR,
  SHOWCASE_ASSIGNMENTS,
  SHOWCASE_BASELINES,
  SHOWCASE_COLUMNS_POLISHED,
  SHOWCASE_DEPENDENCIES,
  SHOWCASE_GANTT_CONFIG,
  SHOWCASE_RESOURCES,
  SHOWCASE_TASKS,
  renderShowcaseTaskBarContent,
} from './gantt-project-data';
import { currentThemeVue } from '../composables/useRandomData';

// ── Static grid data ──────────────────────────────────────────────────────────
const plugins = ref<unknown[]>([]);
const rowSelect = { rowOrder: true };
const source      = ref([...SHOWCASE_TASKS]);
const dependencies = ref([...SHOWCASE_DEPENDENCIES]);
const calendars    = ref([{ ...STANDARD_CALENDAR }]);
const resources    = ref([...SHOWCASE_RESOURCES]);
const assignments  = ref([...SHOWCASE_ASSIGNMENTS]);
const baselines    = ref([...SHOWCASE_BASELINES]);
const columns      = ref([...SHOWCASE_COLUMNS_POLISHED]);
const showCriticalPath = ref(Boolean(SHOWCASE_GANTT_CONFIG.visuals.showCriticalPath));
const showBaseline = ref(false);
const { isDark } = currentThemeVue();
const gridTheme = computed(() => (isDark.value ? 'darkCompact' : 'compact'));
const shellClass = computed(() => [
  'gantt-showcase',
  'gantt-showcase-shell',
  'grow',
  'h-full',
  isDark.value ? 'gantt-showcase-shell--dark' : 'gantt-showcase-shell--light',
]);

const ganttConfig = computed(() => ({
  ...SHOWCASE_GANTT_CONFIG,
  visuals: {
    ...SHOWCASE_GANTT_CONFIG.visuals,
    showCriticalPath: showCriticalPath.value,
    showBaseline: showBaseline.value,
    taskBarContentHook: renderShowcaseTaskBarContent,
  },
}));

// ── Refs ──────────────────────────────────────────────────────────────────────
const gridRef    = ref<InstanceType<typeof RevoGrid> | HTMLRevoGridElement | null>(null);

onMounted(async () => {
  const { GanttPlugin } = await import('@revolist/revogrid-enterprise');

  plugins.value = [GanttPlugin, ExportExcelPlugin, RowSelectPlugin];
});
</script>

<style src="./gantt-showcase.scss" lang="scss"></style>

<style scoped>
.gantt-showcase :deep(revo-grid) {
  flex: 1;
  min-height: 0;
}
</style>
Angular ts
// src/components/gantt/GanttShowcaseAngular.ts
import {
  Component,
  NO_ERRORS_SCHEMA,
  ViewEncapsulation,
} from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import { GanttPlugin } from '@revolist/revogrid-enterprise';
import { ExportExcelPlugin, RowSelectPlugin } from '@revolist/revogrid-pro';
import type { GanttPluginConfig } from '@revolist/revogrid-enterprise';
import {
  STANDARD_CALENDAR,
  SHOWCASE_ASSIGNMENTS,
  SHOWCASE_BASELINES,
  SHOWCASE_COLUMNS_POLISHED,
  SHOWCASE_DEPENDENCIES,
  SHOWCASE_GANTT_CONFIG,
  SHOWCASE_RESOURCES,
  SHOWCASE_TASKS,
  renderShowcaseTaskBarContent,
} from './gantt-project-data';
import { currentTheme } from '../composables/useRandomData';

function createGanttConfig(showCriticalPath: boolean, showBaseline: boolean): GanttPluginConfig {
  return {
    ...SHOWCASE_GANTT_CONFIG,
    visuals: {
      ...SHOWCASE_GANTT_CONFIG.visuals,
      showCriticalPath,
      showBaseline,
      taskBarContentHook: renderShowcaseTaskBarContent,
    },
  } as GanttPluginConfig;
}

@Component({
  selector: 'gantt-showcase-grid',
  standalone: true,
  host: {
    class: 'gantt-showcase-angular-host',
  },
  // Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs.
  schemas: [NO_ERRORS_SCHEMA],
  imports: [RevoGrid],
  encapsulation: ViewEncapsulation.None,
  styleUrls: ['./gantt-showcase.scss'],
  template: `
    <div [class]="shellClass">
      <div class="gantt-showcase-controls">
        <label class="gantt-showcase-control">
          <input
            class="gantt-showcase-control__input"
            type="checkbox"
            [checked]="showCriticalPath"
            (change)="setCriticalPath($any($event.target).checked)"
          />
          <span class="gantt-showcase-control__label">Critical path</span>
        </label>
        <label class="gantt-showcase-control">
          <input
            class="gantt-showcase-control__input"
            type="checkbox"
            [checked]="showBaseline"
            (change)="setBaseline($any($event.target).checked)"
          />
          <span class="gantt-showcase-control__label">Baselines</span>
        </label>
      </div>
      <revo-grid
        class="gantt-showcase-grid skip-style cell-border"
        [theme]="theme"
        [hideAttribution]="true"
        [readonly]="false"
        [range]="true"
        [resize]="true"
        [rowSize]="42"
        [rowHeaders]="false"
        [plugins]="plugins"
        [rowSelect]="rowSelect"
        [source]="source"
        [columns]="columns"
        [gantt]="ganttConfig"
        [ganttDependencies]="dependencies"
        [ganttCalendars]="calendars"
        [ganttResources]="resources"
        [ganttAssignments]="assignments"
        [ganttBaselines]="baselines"
      ></revo-grid>
    </div>
  `,
})
export class GanttShowcaseGridComponent {
  readonly isDark       = currentTheme().isDark();
  readonly theme        = this.isDark ? 'darkCompact' : 'compact';
  readonly shellClass   = `gantt-showcase-shell grow h-full ${this.isDark ? 'gantt-showcase-shell--dark' : 'gantt-showcase-shell--light'}`;
  readonly plugins      = [GanttPlugin, ExportExcelPlugin, RowSelectPlugin];
  readonly rowSelect    = { rowOrder: true };
  showCriticalPath      = Boolean(SHOWCASE_GANTT_CONFIG.visuals.showCriticalPath);
  showBaseline          = false;
  ganttConfig           = createGanttConfig(this.showCriticalPath, this.showBaseline);
  readonly source       = [...SHOWCASE_TASKS];
  readonly dependencies = [...SHOWCASE_DEPENDENCIES];
  readonly calendars    = [{ ...STANDARD_CALENDAR }];
  readonly resources    = [...SHOWCASE_RESOURCES];
  readonly assignments  = [...SHOWCASE_ASSIGNMENTS];
  readonly baselines    = [...SHOWCASE_BASELINES];
  readonly columns      = [...SHOWCASE_COLUMNS_POLISHED];

  setCriticalPath(value: boolean): void {
    this.showCriticalPath = value;
    this.ganttConfig = createGanttConfig(this.showCriticalPath, this.showBaseline);
  }

  setBaseline(value: boolean): void {
    this.showBaseline = value;
    this.ganttConfig = createGanttConfig(this.showCriticalPath, this.showBaseline);
  }
}

Use a built-in preset when you only need a standard timeline scale:

grid.gantt = {
// ...required project fields
zoomPreset: 'week-month',
};

Supported presets:

  • 'minute-hour' (15-minute ticks grouped by hour)
  • 'hour-day' (hour ticks grouped by day)
  • 'day-week'
  • 'week-month'
  • 'month-quarter'
  • 'quarter-year'
  • 'year-quarter'
  • 'multi-year-quarter'

By default, the interactive zoom ladder starts at day-week and excludes the intraday presets for performance. Use zoomPreset: 'hour-day', zoomPreset: 'minute-hour', or explicit zoom.levels when an intraday timeline is required.

Week-based timeline headers, week ticks, and padded week ranges use Sunday by default:

grid.gantt = {
// ...required project fields
weekStartsOn: 0, // Sunday, default
};

Set weekStartsOn: 1 when the Gantt should use Monday-start weeks:

grid.gantt = {
// ...required project fields
weekStartsOn: 1, // Monday
zoomPreset: 'week-month',
};

weekStartsOn accepts 0 for Sunday or 1 for Monday. It affects timeline week boundaries only; task calendars still use their own workingDays configuration.

Gantt timeline modes are defined by tickUnit + headerRows in each zoom level.

  • minute-hour: 15-minute timeline resolution for intraday planning windows.
  • hour-day: hourly timeline resolution grouped under day headers.
  • day-week: daily planning view.
  • week-month: weekly planning view.
  • month-quarter: monthly planning view.
  • quarter-year, year-quarter, multi-year-quarter: portfolio and long-range views.

Recommended usage pattern:

  • Opt into minute-hour for short execution windows and handoffs.
  • Opt into hour-day for same-day and next-day coordination.
  • Use day-week or coarser for baseline and milestone planning.

Use hour mode when task bars should move, resize, and create on hour boundaries instead of whole-day boundaries. The important settings are:

  • timelinePrecision: 'hour' makes timeline interactions use hour precision by default.
  • zoomPreset: 'hour-day' starts the visible timeline with one column per hour.
  • snap.unit: 'hour' forces task create, move, and resize snapping to hours even if the active zoom level changes later.
  • snap.workingTime controls whether snapping can use working-time rules. Closed weekdays, holidays, and closed hours are skipped only when scheduling.excludeHolidaysFromDuration is also true.
grid.gantt = {
// ...required project fields
timelinePrecision: 'hour',
zoomPreset: 'hour-day',
snap: {
unit: 'hour',
workingTime: true,
},
scheduling: {
excludeHolidaysFromDuration: true,
},
calendars: [
{
id: 'standard',
name: 'Standard',
workingDays: [1, 2, 3, 4, 5],
workingHours: [{ start: '09:00', end: '17:00' }],
hoursPerDay: 8,
},
],
tasks: [
{
id: 'task-1',
name: 'Same-day implementation',
startDate: '2026-04-06T09:00:00.000Z',
endDate: '2026-04-06T13:00:00.000Z',
},
],
};

When snap.workingTime is true or omitted and scheduling.excludeHolidaysFromDuration is true, hour snapping uses the primary project calendar:

  • A drag before 09:00 snaps forward to 09:00.
  • A drag after 17:00 snaps forward to the next working opening.
  • Weekend or holiday drags snap to the next or previous working opening depending on the interaction direction.

Set snap.workingTime: false when users should be able to place tasks in non-working hours while still snapping to the start of each hour.

If scheduling.excludeHolidaysFromDuration is false or omitted, hour snapping rounds to the start of the hour and does not move drags out of non-working time.

Use zoom when you need custom levels, min/max bounds, locale, or wheel interaction rules.

grid.gantt = {
// ...required project fields
zoomPreset: 'week-month',
zoom: {
enabled: true,
defaultLevelId: 'hour-day',
minLevelId: 'minute-hour',
maxLevelId: 'quarter-year',
locale: 'en-US',
zoomAnchorMode: 'pointer',
wheelZoomEnabled: true,
wheelZoomTrigger: 'ctrlKey',
wheelZoomMode: 'discrete',
invertWheelDirection: false,
},
};

You can define your own zoom.levels list from finest to coarsest.

grid.gantt = {
// ...required project fields
zoom: {
levels: [
{
id: 'minute-hour',
label: '15 Min / Hour',
tickUnit: 'minute',
tickCount: 15,
tickWidth: 44,
headerRows: [
{ id: 'day', unit: 'day' },
{ id: 'hour', unit: 'hour' },
{ id: 'minute', unit: 'minute', count: 15 },
],
},
{
id: 'hour-day',
label: 'Hour / Day',
tickUnit: 'hour',
tickWidth: 52,
headerRows: [
{ id: 'day', unit: 'day' },
{ id: 'hour', unit: 'hour' },
],
},
{
id: 'day-week',
label: 'Day / Week',
tickUnit: 'day',
tickWidth: 56,
headerRows: [
{ id: 'week', unit: 'week' },
{ id: 'day', unit: 'day' },
],
},
{
id: 'week-month',
label: 'Week / Month',
tickUnit: 'week',
tickWidth: 84,
headerRows: [
{ id: 'month', unit: 'month' },
{ id: 'week', unit: 'week' },
],
},
{
id: 'month-quarter',
label: 'Month / Quarter',
tickUnit: 'month',
tickWidth: 120,
headerRows: [
{ id: 'year', unit: 'year' },
{ id: 'month', unit: 'month' },
],
},
],
defaultLevelId: 'hour-day',
},
};

Control whether users can zoom with the mouse wheel and which modifier key is required.

grid.gantt = {
// ...required project fields
zoom: {
wheelZoomEnabled: true,
wheelZoomTrigger: 'metaKey', // macOS Cmd key
wheelZoomMode: 'smooth-discrete',
invertWheelDirection: false,
},
};

Available wheelZoomTrigger values:

  • 'ctrlKey'
  • 'metaKey'
  • 'altKey'
  • 'shiftKey'
  • 'none'

zoomAnchorMode controls which point in the viewport stays stable when switching levels:

  • 'pointer': keeps the date under the mouse pointer fixed.
  • 'center': keeps the center date fixed.
  • 'start': keeps the left edge date fixed.
grid.gantt = {
// ...required project fields
zoom: {
zoomAnchorMode: 'center',
},
};

Timeline visuals are configured in visuals.

grid.gantt = {
// ...required project fields
visuals: {
projectLineDate: '2026-04-06',
timeRanges: [
{
id: 'sprint-1',
startDate: '2026-04-06',
endDate: '2026-04-17',
label: 'Sprint 1',
color: '#5B8DEF',
},
{
id: 'freeze',
startDate: '2026-05-18',
endDate: '2026-05-22',
label: 'Code Freeze',
color: '#F59E0B',
},
],
shadeNonWorkingTime: true,
},
};