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
import { controlAttributes, createControl } from '@revolist/ui';
import { defineCustomElements } from '@revolist/revogrid/loader';
defineCustomElements();
import {
defineGanttToolbar,
GanttPlugin,
GanttCalendarEditorPlugin,
type ProjectSnapshot,
} from '@revolist/gantt';
import { ExportExcelPlugin, RowSelectPlugin } from '@revolist/revogrid-pro';
import { currentTheme } from '../composables/useRandomData';
import {
SHOWCASE_ASSIGNMENTS,
SHOWCASE_BASELINES,
SHOWCASE_COLUMNS_WITH_ROW_SELECT,
SHOWCASE_DEPENDENCIES,
SHOWCASE_RESOURCES,
SHOWCASE_TOOLBAR_COLUMNS,
STANDARD_CALENDAR,
} from './shared/gantt-project-data';
import {
createProductionGanttConfig,
createProductionTasks,
createProductionPersistence,
productionError,
productionSourceRows,
} from './GanttProductionPatternsShared';
import './gantt-production-patterns.css';
export function load(parentSelector: string) {
const parent = document.querySelector(parentSelector);
if (!parent) return () => {};
const root = document.createElement('section');
const controls = document.createElement('div');
const status = document.createElement('p');
const toolbar = document.createElement('div');
const grid = document.createElement('revo-grid');
const persistence = createProductionPersistence();
let readOnly = false;
let localized = false;
let large = false;
let project: ProjectSnapshot | null = null;
const applyConfig = () => {
grid.gantt = createProductionGanttConfig(readOnly, localized, project);
};
const applyProject = () => {
applyConfig();
grid.ganttDependencies =
project?.dependencies ?? (large ? [] : SHOWCASE_DEPENDENCIES);
grid.ganttCalendars = project?.calendars ?? [STANDARD_CALENDAR];
grid.ganttResources = project?.resources ?? SHOWCASE_RESOURCES;
grid.ganttAssignments =
project?.assignments ?? (large ? [] : SHOWCASE_ASSIGNMENTS);
grid.ganttBaselines =
project?.baselines ?? (large ? [] : SHOWCASE_BASELINES);
grid.source = project
? productionSourceRows(project)
: createProductionTasks(large);
};
const toggle = (label: string, update: (value: boolean) => void) => {
const element = document.createElement('label');
const input = createControl(document, 'input', { type: 'checkbox' });
input.type = 'checkbox';
input.addEventListener('change', () => update(input.checked));
element.append(input, document.createTextNode(label));
return { element, input };
};
const run = async (action: () => unknown | Promise<unknown>) => {
try {
await action();
} catch (error) {
status.textContent = productionError(error);
}
};
const button = (label: string, action: () => unknown | Promise<unknown>) => {
const element = createControl(document, 'button', { type: 'button' });
element.type = 'button';
element.textContent = label;
element.className = controlAttributes('button', { class: 'rv-btn' }).class;
element.addEventListener('click', () => {
void run(action);
});
return element;
};
root.className = 'gantt-production-demo';
controls.className = 'gantt-production-controls';
status.className = 'gantt-production-status';
status.setAttribute('role', 'status');
toolbar.className = 'gantt-production-toolbar';
grid.className = 'gantt-production-grid';
grid.theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
grid.hideAttribution = true;
grid.plugins = [
GanttPlugin,
GanttCalendarEditorPlugin,
ExportExcelPlugin,
RowSelectPlugin,
];
grid.rowSelect = { rowOrder: true };
grid.columns = [...SHOWCASE_COLUMNS_WITH_ROW_SELECT];
const largeToggle = toggle('750 tasks', (value) => {
large = value;
project = null;
applyProject();
});
const restore = button('Restore', async () => {
project = await persistence.load();
large = false;
largeToggle.input.checked = false;
applyProject();
status.textContent =
'Restored the saved project, including tasks and project collections.';
});
restore.disabled = true;
const fileInput = createControl(document, 'input', { type: 'file' });
fileInput.type = 'file';
fileInput.hidden = true;
fileInput.accept = '.json,application/json';
fileInput.setAttribute('aria-label', 'Import JSON file');
fileInput.addEventListener('change', async () => {
const file = fileInput.files?.[0];
if (file)
await run(async () => {
project = await persistence.importFile(file);
large = false;
largeToggle.input.checked = false;
applyProject();
status.textContent = `Imported ${project.tasks.length} tasks from ${file.name}.`;
});
fileInput.value = '';
});
const options = document.createElement('div');
options.className = 'gantt-production-options';
const actions = document.createElement('div');
actions.className = 'gantt-production-actions';
options.append(
toggle('Read only', (value) => {
readOnly = value;
applyConfig();
}).element,
toggle('Español dates', (value) => {
localized = value;
applyConfig();
}).element,
largeToggle.element,
);
actions.append(
fileInput,
button('Import JSON', () => fileInput.click()),
button('JSON', () => persistence.exportFile(grid)),
button('Save', async () => {
await persistence.save(grid);
restore.disabled = false;
status.textContent =
'Saved the current edited project in browser memory.';
}),
restore,
button('Print', () => persistence.print(grid)),
);
controls.append(options, actions);
root.append(controls, status, toolbar, grid);
parent.appendChild(root);
applyProject();
defineGanttToolbar(toolbar, {
grid,
columns: SHOWCASE_TOOLBAR_COLUMNS,
controls: { baseline: true, calendars: true },
});
return () => {
toolbar.textContent = '';
grid.remove();
root.remove();
};
}

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,
},
};

GanttTimelineHeaderPlugin owns the custom RevoGrid header template used by the Gantt column. Other runtime plugins can discover it and append elements to the resolved timeline header without creating another render root:

import { GanttTimelineHeaderPlugin } from '@revolist/gantt';
// Inside another grid plugin, use its PluginProviders instance.
const timelineHeader = providers.plugins.getByClass(GanttTimelineHeaderPlugin);
const unregisterReleaseMarker = timelineHeader?.registerElement(
'release-marker',
(h) =>
h(
'div',
{
class: { 'my-release-marker': true },
style: { left: '420px' },
},
'Release',
),
);
// Call after closure-backed contribution state changes.
timelineHeader?.refresh();
// Call from the contributing plugin's teardown.
unregisterReleaseMarker?.();

Contribution renderers receive RevoGrid’s native HyperFunc<VNode> and the immutable, resolved header geometry. They render after the built-in header cells, non-working ranges, interaction range, and flag caps, in registration order. The contributing plugin owns its data and styling; timeline scale, viewport slicing, and scroll state remain owned by Gantt.