Gantt Timeline and Zoom
The Gantt timeline can be configured at two levels:
zoomPresetfor fast setup with built-in levels.zoomfor 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 { 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 = document.createElement('input');
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 = document.createElement('button');
element.type = 'button';
element.textContent = label;
element.className = 'rv-btn';
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');
status.textContent = 'Save and restore use browser memory only; no server is contacted.';
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 = document.createElement('input');
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(); };
}
<template>
<section class="gantt-production-demo">
<div class="gantt-production-controls">
<div class="gantt-production-options">
<label><input v-model="readOnly" type="checkbox" />Read only</label>
<label><input v-model="localized" type="checkbox" />Español dates</label>
<label><input v-model="large" type="checkbox" @change="project = null" />750 tasks</label>
</div>
<div class="gantt-production-actions">
<input ref="importInput" type="file" hidden accept=".json,application/json" aria-label="Import JSON file" @change="importJson" />
<button type="button" class="rv-btn" @click="importInput?.click()">Import JSON</button>
<button type="button" class="rv-btn" @click="run(() => persistence.exportFile(getGrid()))">JSON</button>
<button type="button" class="rv-btn" @click="save">Save</button>
<button type="button" class="rv-btn" :disabled="!hasSaved" @click="restore">Restore</button>
<button type="button" class="rv-btn" @click="run(() => persistence.print(getGrid()))">Print</button>
</div>
</div>
<p class="gantt-production-status" role="status">{{ status }}</p>
<div ref="toolbarRef" class="gantt-production-toolbar"></div>
<RevoGrid ref="gridRef" class="gantt-production-grid" hide-attribution :theme="isDark ? 'darkCompact' : 'compact'"
:plugins="plugins" :row-select.prop="rowSelect" :source="tasks" :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" />
</section>
</template>
<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import { defineGanttToolbar, GanttPlugin, GanttCalendarEditorPlugin, type ProjectSnapshot } from '@revolist/gantt';
import { ExportExcelPlugin, RowSelectPlugin } from '@revolist/revogrid-pro';
import { currentThemeVue } 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';
const { isDark } = currentThemeVue();
const plugins = [GanttPlugin, GanttCalendarEditorPlugin, ExportExcelPlugin, RowSelectPlugin];
const rowSelect = { rowOrder: true };
const columns = SHOWCASE_COLUMNS_WITH_ROW_SELECT;
const readOnly = ref(false);
const localized = ref(false);
const large = ref(false);
const project = ref<ProjectSnapshot | null>(null);
const status = ref('Save and restore use browser memory only; no server is contacted.');
const hasSaved = ref(false);
const persistence = createProductionPersistence();
const tasks = computed(() => project.value ? productionSourceRows(project.value) : createProductionTasks(large.value));
const dependencies = computed(() => project.value?.dependencies ?? (large.value ? [] : SHOWCASE_DEPENDENCIES));
const calendars = computed(() => project.value?.calendars ?? [STANDARD_CALENDAR]);
const resources = computed(() => project.value?.resources ?? SHOWCASE_RESOURCES);
const assignments = computed(() => project.value?.assignments ?? (large.value ? [] : SHOWCASE_ASSIGNMENTS));
const baselines = computed(() => project.value?.baselines ?? (large.value ? [] : SHOWCASE_BASELINES));
const ganttConfig = computed(() => createProductionGanttConfig(readOnly.value, localized.value, project.value));
const gridRef = ref<InstanceType<typeof RevoGrid> | HTMLRevoGridElement | null>(null);
const importInput = ref<HTMLInputElement | null>(null);
const toolbarRef = ref<HTMLElement | null>(null);
let frame = 0;
function getGrid(): HTMLRevoGridElement {
const value = gridRef.value as (InstanceType<typeof RevoGrid> & { $el?: HTMLRevoGridElement }) | HTMLRevoGridElement | null;
const grid = value && '$el' in value ? value.$el : value;
if (!(grid instanceof HTMLElement)) throw new Error('The grid is still loading.');
return grid as HTMLRevoGridElement;
}
async function run(action: () => unknown | Promise<unknown>) {
try { await action(); } catch (error) { status.value = productionError(error); }
}
async function save() { await run(async () => { await persistence.save(getGrid()); hasSaved.value = true; status.value = 'Saved the current edited project in browser memory.'; }); }
async function restore() { await run(async () => { project.value = await persistence.load(); large.value = false; status.value = 'Restored the saved project, including tasks and project collections.'; }); }
async function importJson(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (file) await run(async () => { project.value = await persistence.importFile(file); large.value = false; status.value = `Imported ${project.value.tasks.length} tasks from ${file.name}.`; });
input.value = '';
}
function mountToolbar() {
let grid: HTMLRevoGridElement;
try { grid = getGrid(); } catch { frame = requestAnimationFrame(mountToolbar); return; }
if (toolbarRef.value) defineGanttToolbar(toolbarRef.value, { grid, columns: SHOWCASE_TOOLBAR_COLUMNS, controls: { baseline: true, calendars: true } });
}
onMounted(async () => { await nextTick(); mountToolbar(); });
onBeforeUnmount(() => { cancelAnimationFrame(frame); if (toolbarRef.value) toolbarRef.value.textContent = ''; });
</script>
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
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 default function GanttProductionPatterns() {
const gridRef = useRef<HTMLRevoGridElement>(null);
const importInput = useRef<HTMLInputElement>(null);
const toolbarRef = useRef<HTMLDivElement>(null);
const [readOnly, setReadOnly] = useState(false);
const [localized, setLocalized] = useState(false);
const [large, setLarge] = useState(false);
const [project, setProject] = useState<ProjectSnapshot | null>(null);
const [status, setStatus] = useState('Save and restore use browser memory only; no server is contacted.');
const [hasSaved, setHasSaved] = useState(false);
const persistence = useMemo(() => createProductionPersistence(), []);
const plugins = useMemo(() => [GanttPlugin, GanttCalendarEditorPlugin, ExportExcelPlugin, RowSelectPlugin], []);
const rowSelect = useMemo(() => ({ rowOrder: true }), []);
const columns = useMemo(() => SHOWCASE_COLUMNS_WITH_ROW_SELECT, []);
const tasks = useMemo(() => project ? productionSourceRows(project) : createProductionTasks(large), [project, large]);
const gantt = useMemo(() => createProductionGanttConfig(readOnly, localized, project), [readOnly, localized, project]);
const dependencies = useMemo(() => project?.dependencies ?? (large ? [] : SHOWCASE_DEPENDENCIES), [project, large]);
const calendars = useMemo(() => project?.calendars ?? [STANDARD_CALENDAR], [project]);
const resources = useMemo(() => project?.resources ?? SHOWCASE_RESOURCES, [project]);
const assignments = useMemo(() => project?.assignments ?? (large ? [] : SHOWCASE_ASSIGNMENTS), [project, large]);
const baselines = useMemo(() => project?.baselines ?? (large ? [] : SHOWCASE_BASELINES), [project, large]);
function getGrid(): HTMLRevoGridElement {
if (!gridRef.current) throw new Error('The grid is still loading.');
return gridRef.current;
}
async function run(action: () => unknown | Promise<unknown>) {
try { await action(); } catch (error) { setStatus(productionError(error)); }
}
async function save() {
await run(async () => {
await persistence.save(getGrid());
setHasSaved(true);
setStatus('Saved the current edited project in browser memory.');
});
}
async function restore() {
await run(async () => {
setProject(await persistence.load());
setLarge(false);
setStatus('Restored the saved project, including tasks and project collections.');
});
}
async function importJson(event: React.ChangeEvent<HTMLInputElement>) {
const input = event.currentTarget;
const file = input.files?.[0];
if (file) await run(async () => {
const imported = await persistence.importFile(file);
setProject(imported);
setLarge(false);
setStatus(`Imported ${imported.tasks.length} tasks from ${file.name}.`);
});
input.value = '';
}
useEffect(() => {
const toolbar = toolbarRef.current;
if (!toolbar || !gridRef.current) return;
defineGanttToolbar(toolbar, { grid: gridRef.current, columns: SHOWCASE_TOOLBAR_COLUMNS, controls: { baseline: true, calendars: true } });
return () => { toolbar.textContent = ''; };
}, []);
return <section className="gantt-production-demo">
<div className="gantt-production-controls">
<div className="gantt-production-options">
<label><input type="checkbox" checked={readOnly} onChange={(event) => setReadOnly(event.currentTarget.checked)} />Read only</label>
<label><input type="checkbox" checked={localized} onChange={(event) => setLocalized(event.currentTarget.checked)} />Español dates</label>
<label><input type="checkbox" checked={large} onChange={(event) => { setLarge(event.currentTarget.checked); setProject(null); }} />750 tasks</label>
</div>
<div className="gantt-production-actions">
<input ref={importInput} type="file" hidden accept=".json,application/json" aria-label="Import JSON file" onChange={importJson} />
<button type="button" className="rv-btn" onClick={() => importInput.current?.click()}>Import JSON</button>
<button type="button" className="rv-btn" onClick={() => { void run(() => persistence.exportFile(getGrid())); }}>JSON</button>
<button type="button" className="rv-btn" onClick={save}>Save</button>
<button type="button" className="rv-btn" disabled={!hasSaved} onClick={restore}>Restore</button>
<button type="button" className="rv-btn" onClick={() => { void run(() => persistence.print(getGrid())); }}>Print</button>
</div>
</div>
<p className="gantt-production-status" role="status">{status}</p>
<div ref={toolbarRef} className="gantt-production-toolbar" />
<RevoGrid ref={gridRef} className="gantt-production-grid" theme={currentTheme().isDark() ? 'darkCompact' : 'compact'} hideAttribution
plugins={plugins} rowSelect={rowSelect} source={tasks} columns={columns} gantt={gantt}
ganttDependencies={dependencies} ganttCalendars={calendars} ganttResources={resources}
ganttAssignments={assignments} ganttBaselines={baselines} />
</section>;
}
import { ChangeDetectorRef, inject, AfterViewInit, Component, ElementRef, NO_ERRORS_SCHEMA, OnDestroy, ViewChild, ViewEncapsulation } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
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';
@Component({
selector: 'gantt-production-patterns-grid', standalone: true, schemas: [NO_ERRORS_SCHEMA], imports: [RevoGrid],
styleUrls: ['./gantt-production-patterns.css'], encapsulation: ViewEncapsulation.None,
template: `<section class="gantt-production-demo">
<div class="gantt-production-controls">
<div class="gantt-production-options">
<label><input type="checkbox" [checked]="readOnly" (change)="setReadOnly($any($event.target).checked)" />Read only</label>
<label><input type="checkbox" [checked]="localized" (change)="setLocalized($any($event.target).checked)" />Español dates</label>
<label><input type="checkbox" [checked]="large" (change)="setLarge($any($event.target).checked)" />750 tasks</label>
</div>
<div class="gantt-production-actions">
<input #importFile type="file" hidden accept=".json,application/json" aria-label="Import JSON file" (change)="importJson($event)" />
<button type="button" class="rv-btn" (click)="importFile.click()">Import JSON</button>
<button type="button" class="rv-btn" (click)="exportJson()">JSON</button>
<button type="button" class="rv-btn" (click)="save()">Save</button>
<button type="button" class="rv-btn" [disabled]="!hasSaved" (click)="restore()">Restore</button>
<button type="button" class="rv-btn" (click)="print()">Print</button>
</div>
</div>
<p class="gantt-production-status" role="status">{{ status }}</p>
<div #toolbar class="gantt-production-toolbar"></div>
<revo-grid #grid class="gantt-production-grid" [theme]="theme" [hideAttribution]="true" [plugins]="plugins"
[rowSelect]="rowSelect" [source]="tasks" [columns]="columns" [gantt]="ganttConfig" [ganttDependencies]="dependencies"
[ganttCalendars]="calendars" [ganttResources]="resources" [ganttAssignments]="assignments" [ganttBaselines]="baselines"></revo-grid>
</section>`,
})
export class GanttProductionPatternsGridComponent implements AfterViewInit, OnDestroy {
private readonly changeDetector = inject(ChangeDetectorRef);
@ViewChild('grid', { read: ElementRef }) gridRef!: ElementRef<HTMLRevoGridElement>;
@ViewChild('toolbar', { read: ElementRef }) toolbarRef!: ElementRef<HTMLElement>;
theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
plugins = [GanttPlugin, GanttCalendarEditorPlugin, ExportExcelPlugin, RowSelectPlugin]; rowSelect = { rowOrder: true }; columns = SHOWCASE_COLUMNS_WITH_ROW_SELECT;
calendars = [STANDARD_CALENDAR]; resources = [...SHOWCASE_RESOURCES]; assignments = [...SHOWCASE_ASSIGNMENTS]; baselines = [...SHOWCASE_BASELINES];
readOnly = false; localized = false; large = false; tasks = createProductionTasks(); dependencies = SHOWCASE_DEPENDENCIES;
project: ProjectSnapshot | null = null;
status = 'Save and restore use browser memory only; no server is contacted.';
hasSaved = false;
private persistence = createProductionPersistence();
ganttConfig = createProductionGanttConfig();
private refresh() { this.ganttConfig = createProductionGanttConfig(this.readOnly, this.localized, this.project); }
private refreshProject() {
this.tasks = this.project ? productionSourceRows(this.project) : createProductionTasks(this.large);
this.dependencies = [...(this.project?.dependencies ?? (this.large ? [] : SHOWCASE_DEPENDENCIES))];
this.calendars = [...(this.project?.calendars ?? [STANDARD_CALENDAR])];
this.resources = [...(this.project?.resources ?? SHOWCASE_RESOURCES)];
this.assignments = [...(this.project?.assignments ?? (this.large ? [] : SHOWCASE_ASSIGNMENTS))];
this.baselines = [...(this.project?.baselines ?? (this.large ? [] : SHOWCASE_BASELINES))];
this.refresh();
}
private getGrid(): HTMLRevoGridElement {
const grid = this.gridRef?.nativeElement;
if (!grid) throw new Error('The grid is still loading.');
return grid;
}
private async run(action: () => unknown | Promise<unknown>) {
try { await action(); } catch (error) { this.status = productionError(error); }
finally { this.changeDetector.markForCheck(); }
}
setReadOnly(value: boolean) { this.readOnly = value; this.refresh(); }
setLocalized(value: boolean) { this.localized = value; this.refresh(); }
setLarge(value: boolean) { this.large = value; this.project = null; this.refreshProject(); }
exportJson() { return this.run(() => this.persistence.exportFile(this.getGrid())); }
print() { return this.run(() => this.persistence.print(this.getGrid())); }
save() {
return this.run(async () => {
await this.persistence.save(this.getGrid()); this.hasSaved = true;
this.status = 'Saved the current edited project in browser memory.';
});
}
restore() {
return this.run(async () => {
this.project = await this.persistence.load(); this.large = false; this.refreshProject();
this.status = 'Restored the saved project, including tasks and project collections.';
});
}
async importJson(event: Event) {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (file) await this.run(async () => {
this.project = await this.persistence.importFile(file); this.large = false; this.refreshProject();
this.status = `Imported ${this.project.tasks.length} tasks from ${file.name}.`;
});
input.value = '';
}
ngAfterViewInit() { defineGanttToolbar(this.toolbarRef.nativeElement, { grid: this.gridRef.nativeElement, columns: SHOWCASE_TOOLBAR_COLUMNS, controls: { baseline: true, calendars: true } }); }
ngOnDestroy() { if (this.toolbarRef?.nativeElement) this.toolbarRef.nativeElement.textContent = ''; }
}
import {
exportProjectSnapshot,
parseProjectSnapshotJson,
type GanttPluginConfig,
type GanttTaskSourceRow,
type ProjectSnapshot,
} from '@revolist/gantt';
type ISODateString = import('@revolist/gantt').TaskEntity['startDate'];
import { getGanttScheduleSnapshot } from './recipes/current-schedule-snapshot';
import { ProjectSnapshotRestAdapter } from './recipes/rest-adapter';
import { printGanttContainer } from './recipes/print-pdf-recipe';
import {
SHOWCASE_ASSIGNMENTS,
SHOWCASE_BASELINES,
SHOWCASE_DEPENDENCIES,
SHOWCASE_GANTT_CONFIG,
SHOWCASE_RESOURCES,
SHOWCASE_TASKS,
STANDARD_CALENDAR,
} from './shared/gantt-project-data';
export function createProductionGanttConfig(readOnly = false, localized = false, project?: ProjectSnapshot | null): GanttPluginConfig {
return {
...SHOWCASE_GANTT_CONFIG,
...(project ? { id: project.id, name: project.name, version: project.version, currency: project.currency, timeZone: project.timeZone, primaryCalendarId: project.primaryCalendarId, updatedAt: project.updatedAt, durationSettings: project.durationSettings, calendars: project.calendars } : {}),
allowTaskCreate: true,
taskCreateRow: true,
readOnly,
labels: localized ? {
taskMode: { auto: 'Automática', manual: 'Manual' },
workflowStatus: { 'not-started': 'Sin iniciar', 'in-progress': 'En curso', done: 'Finalizada', blocked: 'Bloqueada' },
} : undefined,
dateFormats: localized ? {
locale: 'es-ES',
table: { dateStyle: 'medium' },
tooltip: { dateStyle: 'long' },
} : undefined,
visuals: {
...SHOWCASE_GANTT_CONFIG.visuals,
showBaseline: true,
showTaskLabels: true,
timeRanges: [{ id: 'release-window', startDate: '2026-02-16', endDate: '2026-02-20', label: 'Release window', color: '#8b5cf633' }],
milestoneLines: [{ id: 'go-live', date: '2026-02-27', label: 'Go live', color: '#e11d48' }],
taskMarkerHook: (task) => task.deadlineDate ? [{ id: `deadline-${task.id}`, date: task.deadlineDate, label: 'Deadline' }] : [],
taskBarColorHook: ({ row }) => row.id === SHOWCASE_TASKS[1].id ? { barColor: '#64748b', className: 'gantt-production-locked' } : undefined,
taskTooltipFields: ['startDate', 'endDate', 'duration', 'percentDone', 'assignees'],
},
} as GanttPluginConfig;
}
export function createProductionTasks(large = false): GanttTaskSourceRow[] {
if (!large) {
return SHOWCASE_TASKS.map((task, index) => ({ ...task, ...(index === 1 ? { locked: true } : {}) })) as GanttTaskSourceRow[];
}
return Array.from({ length: 750 }, (_, index) => {
const day = 2 + (index % 20);
return {
id: `large-${index + 1}`,
name: `Virtualized task ${index + 1}`,
startDate: `2026-02-${String(day).padStart(2, '0')}` as ISODateString,
duration: 1 + (index % 5),
durationUnit: 'day',
percentDone: (index * 7) % 101,
taskMode: 'manual',
locked: index % 17 === 0,
} satisfies GanttTaskSourceRow;
});
}
export function productionSourceRows(project: ProjectSnapshot): GanttTaskSourceRow[] {
return project.tasks.map((task) => ({
...task,
durationUnit: task.durationUnit ?? 'hour',
percentDone: task.progressPercent,
taskMode: task.manuallyScheduled ? 'manual' : 'auto',
}));
}
export async function getProductionProject(grid: HTMLRevoGridElement): Promise<ProjectSnapshot> {
return getGanttScheduleSnapshot(grid);
}
export function downloadProductionSnapshot(project: ProjectSnapshot): void {
const href = URL.createObjectURL(new Blob([exportProjectSnapshot(project, { space: 2 })], { type: 'application/json' }));
const link = document.createElement('a');
link.href = href;
link.download = 'gantt-project.json';
link.click();
setTimeout(() => URL.revokeObjectURL(href), 0);
}
/** A real REST recipe with an injected browser-memory transport; no server is contacted. */
export function createProductionPersistence() {
let savedJson: string | undefined;
const adapter = new ProjectSnapshotRestAdapter({
url: '/example/project',
fetchImpl: (async (_input, init) => {
if (init?.method === 'PUT') {
savedJson = String(init.body);
return new Response(null, { status: 204 });
}
return new Response(savedJson ?? 'No saved project', { status: savedJson ? 200 : 404 });
}) as typeof fetch,
});
return {
save: async (grid: HTMLRevoGridElement) => adapter.save(await getProductionProject(grid)),
load: () => adapter.load(),
importFile: async (file: Pick<File, 'text'>): Promise<ProjectSnapshot> => {
const parsed = parseProjectSnapshotJson(await file.text());
if (parsed.ok === false) throw new Error(parsed.error);
return parsed.project;
},
exportFile: async (grid: HTMLRevoGridElement) => downloadProductionSnapshot(await getProductionProject(grid)),
print: (grid: HTMLRevoGridElement) => {
const result = printGanttContainer(grid.parentElement, { cleanupDelayMs: 60_000, hideSelectors: ['.gantt-production-controls', '.gantt-production-status', '.gantt-production-toolbar', '.gantt-toolbar', '.rgContextMenu'], metadata: { title: grid.gantt?.name ?? 'Project schedule' } });
if (!result.ok) throw new Error('The print preview could not be opened.');
},
};
}
export function productionError(error: unknown): string {
return error instanceof Error ? error.message : 'The operation failed.';
}
Quick Start with zoomPreset
Section titled “Quick Start with zoomPreset”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 Start
Section titled “Week Start”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.
Time Modes
Section titled “Time Modes”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-hourfor short execution windows and handoffs. - Opt into
hour-dayfor same-day and next-day coordination. - Use
day-weekor coarser for baseline and milestone planning.
Hour Mode
Section titled “Hour Mode”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.workingTimecontrols whether snapping can use working-time rules. Closed weekdays, holidays, and closed hours are skipped only whenscheduling.excludeHolidaysFromDurationis alsotrue.
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:00snaps forward to09:00. - A drag after
17:00snaps 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.
Full Zoom Configuration
Section titled “Full Zoom Configuration”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, },};Custom Zoom Levels
Section titled “Custom Zoom Levels”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', },};Wheel Zoom Options
Section titled “Wheel Zoom Options”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'
Anchor Behavior During Zoom
Section titled “Anchor Behavior During Zoom”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 Overlays
Section titled “Timeline Overlays”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, },};Plugin Header Contributions
Section titled “Plugin Header Contributions”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.