Gantt
A project-plan Gantt built on a framework-free kernel: a virtualized treegrid task pane and a timeline chart share one scroll model, with summary brackets, milestone diamonds, baseline bars, FS/SS/FF/SF dependency arrows routed orthogonally, critical-path highlighting, forward auto-scheduling and calendar-true hour/day/week/month scales. Bars drag to move, resize and set progress — Escape cancels mid-gesture — link dots draw dependencies with cycle rejection, and every edit is one snapshot undo/redo step. Dates are plain local Dates — no date library, no adapter — and every string, aria labels included, lives in the messages config. No WAI-ARIA APG gantt pattern exists, so the widget composes the treegrid pattern with roving-tabindex rows — the focused row drives its bar with Ctrl+Arrow keyboard move/resize — and a polite live region announcing every change.
@oge-ui/gantt is a commercial package — free for evaluation and development, with no watermark and no runtime license checks. See licensing for the terms.
Getting started
One element, a working Gantt. Drag a bar to move it (Escape cancels mid-drag), pull its edges to resize, drag the bottom knob to set progress, drag a link dot onto another bar to draw a dependency. Double-click a bar or row for the task dialog — or double-click / drag on empty chart space to create a task right there. Right-click opens the built-in menu (edit, new subtask, indent/outdent, delete); Alt+Shift+Left/Right reparents from the keyboard.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeGantt } from '@oge-ui/gantt';
@Component({
selector: 'demo-root',
imports: [OgeGantt],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- One element, a working Gantt: task tree pane + timeline chart
with summary brackets, milestone diamonds and dependency arrows. Drag a
bar to move it (Escape cancels mid-drag), pull its edges to resize, drag
the bottom knob to set progress, drag a link dot onto another bar to draw
a dependency. Double-click opens the task dialog. -->
<oge-gantt
[tasks]="tasks"
[dependencies]="links"
style="height: 480px"
/>
`,
})
export class Demo {
protected readonly tasks = [
{ id: 1, title: 'Release 1.0', start: new Date(2026, 7, 3), end: new Date(2026, 7, 21) },
{
id: 2,
parentId: 1,
title: 'Design',
start: new Date(2026, 7, 3),
end: new Date(2026, 7, 7),
progress: 100,
},
{
id: 3,
parentId: 1,
title: 'Implementation',
start: new Date(2026, 7, 7),
end: new Date(2026, 7, 17),
progress: 45,
},
{
id: 4,
parentId: 1,
title: 'Ship',
start: new Date(2026, 7, 21),
end: new Date(2026, 7, 21), // zero-length => milestone diamond
},
];
protected readonly links = [
{ id: 'a', predecessorId: 2, successorId: 3 }, // FS is the default type
{ id: 'b', predecessorId: 3, successorId: 4 },
];
}Field mapping
Any item shape binds through the *Expr inputs — field names, dotted paths or getter functions — for tasks and dependency links alike. String dates parse as local wall time and write back in the same storage shape after edits.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeGantt } from '@oge-ui/gantt';
@Component({
selector: 'demo-root',
imports: [OgeGantt],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Any item shape binds through the *Expr inputs — field names,
dotted paths or getter functions — for tasks AND dependency links.
String dates parse as local wall time and write back in the same
storage shape after edits. -->
<oge-gantt
[tasks]="workItems"
[dependencies]="relations"
keyExpr="code"
parentKeyExpr="parentCode"
titleExpr="subject"
startExpr="plan.begin"
endExpr="plan.finish"
progressExpr="done"
dependencyKeyExpr="relId"
predecessorKeyExpr="fromCode"
successorKeyExpr="toCode"
style="height: 360px"
/>
`,
})
export class Demo {
protected readonly workItems = [
{
code: 'EPIC-1',
subject: 'Checkout revamp',
plan: { begin: '2026-08-03', finish: '2026-08-14' },
},
{
code: 'T-1',
parentCode: 'EPIC-1',
subject: 'Payment API',
plan: { begin: '2026-08-03', finish: '2026-08-07' },
done: 80,
},
{
code: 'T-2',
parentCode: 'EPIC-1',
subject: 'Wallet UI',
plan: { begin: '2026-08-07', finish: '2026-08-14' },
done: 20,
},
];
protected readonly relations = [{ relId: 1, fromCode: 'T-1', toCode: 'T-2' }];
}Dependencies & critical path
showCriticalPath outlines the zero-slack chain; autoScheduling pushes successors forward whenever a predecessor moves, honoring FS/SS/FF/SF semantics. Drawing a link that would close a cycle is rejected and announced.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeGantt } from '@oge-ui/gantt';
@Component({
selector: 'demo-root',
imports: [OgeGantt],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- showCriticalPath outlines the tasks with zero slack;
autoScheduling pushes successors forward whenever a predecessor moves
(FS/SS/FF/SF respected). Drawing a link that would close a cycle is
rejected and announced — try dragging a link dot backwards. -->
<oge-gantt
[tasks]="tasks"
[dependencies]="links"
[showCriticalPath]="true"
[autoScheduling]="true"
style="height: 420px"
/>
`,
})
export class Demo {
protected readonly tasks = [
{ id: 1, title: 'Foundation', start: new Date(2026, 7, 3), end: new Date(2026, 7, 6), progress: 100 },
{ id: 2, title: 'Framing', start: new Date(2026, 7, 6), end: new Date(2026, 7, 12), progress: 60 },
{ id: 3, title: 'Electrical', start: new Date(2026, 7, 12), end: new Date(2026, 7, 15) },
{ id: 4, title: 'Landscaping', start: new Date(2026, 7, 6), end: new Date(2026, 7, 10) },
{ id: 5, title: 'Inspection', start: new Date(2026, 7, 17), end: new Date(2026, 7, 18) },
];
protected readonly links = [
{ id: 1, predecessorId: 1, successorId: 2 },
{ id: 2, predecessorId: 2, successorId: 3 },
{ id: 3, predecessorId: 3, successorId: 5 },
{ id: 4, predecessorId: 4, successorId: 5 },
];
}Baselines, strip lines & resources
Baseline bars render the original plan under the live bars for slippage at a glance; stripLines mark deadlines (a line) or freeze windows (a range); resources label the bars; weekends shade automatically and holidays join the off-day shading.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeGantt } from '@oge-ui/gantt';
import type { OgeGanttStripLine } from '@oge-ui/gantt';
@Component({
selector: 'demo-root',
imports: [OgeGantt],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Baseline bars render the original plan under the live bars;
stripLines mark deadlines (a line) or freeze windows (a range) on the
chart; resources label the bars. Weekends shade automatically and
holidays add to the off-day shading. -->
<oge-gantt
[tasks]="tasks"
[stripLines]="stripLines"
[resources]="people"
[holidays]="holidays"
style="height: 380px"
/>
`,
})
export class Demo {
protected readonly tasks = [
{
id: 1,
title: 'Data migration',
start: new Date(2026, 7, 4),
end: new Date(2026, 7, 11),
baselineStart: new Date(2026, 7, 3),
baselineEnd: new Date(2026, 7, 7),
progress: 70,
resourceId: 'ada',
},
{
id: 2,
title: 'Cutover rehearsal',
start: new Date(2026, 7, 11),
end: new Date(2026, 7, 14),
baselineStart: new Date(2026, 7, 10),
baselineEnd: new Date(2026, 7, 12),
resourceId: 'grace',
},
];
protected readonly stripLines: OgeGanttStripLine[] = [
{ start: new Date(2026, 7, 18), label: 'Go-live', color: '#dc2626' },
{
start: new Date(2026, 7, 14),
end: new Date(2026, 7, 17),
label: 'Freeze',
},
];
protected readonly people = [
{ id: 'ada', text: 'Ada', color: '#7c3aed' },
{ id: 'grace', text: 'Grace', color: '#0891b2' },
];
protected readonly holidays = [new Date(2026, 7, 10)];
}Editing pipeline
Every mutation runs a cancelable -ing event before the store changes; the past-tense event fires only for applied changes — persist from there. This demo locks finished tasks, asks before deleting, and disables dependency drawing.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeGantt } from '@oge-ui/gantt';
import type { OgeGanttTaskDeletingEvent, OgeGanttTaskUpdatingEvent } from '@oge-ui/gantt';
@Component({
selector: 'demo-root',
imports: [OgeGantt],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Every mutation runs a cancelable '-ing' event before the
store changes; the past-tense event fires only for applied changes —
persist from there. allow* flags gate each capability; readOnly turns
the whole widget display-only. This demo protects finished tasks and
asks before deleting. -->
<oge-gantt
[tasks]="tasks"
[allowDependencyAdding]="false"
(taskUpdating)="protectDone($event)"
(taskDeleting)="confirmDelete($event)"
style="height: 360px"
/>
`,
})
export class Demo {
protected readonly tasks = [
{ id: 1, title: 'Audit (done — locked)', start: new Date(2026, 7, 3), end: new Date(2026, 7, 6), progress: 100 },
{ id: 2, title: 'Remediation', start: new Date(2026, 7, 6), end: new Date(2026, 7, 13), progress: 30 },
];
protected protectDone(
event: OgeGanttTaskUpdatingEvent<Record<string, unknown>>,
): void {
if ((event.oldData['progress'] as number) === 100) event.cancel = true;
}
protected confirmDelete(
event: OgeGanttTaskDeletingEvent<Record<string, unknown>>,
): void {
event.cancel = !confirm('Delete this task?');
}
}Toolbar, scales & undo/redo
The toolbar adds tasks, zooms between calendar-true hour/day/week/month scales (Ctrl+wheel on the chart works too), fits the whole plan, expands/collapses the tree and drives snapshot undo/redo — every edit, drags included, is one undo step. The task pane is a virtualized treegrid with configurable columns and a draggable splitter.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeGantt } from '@oge-ui/gantt';
import type { OgeGanttScaleType } from '@oge-ui/gantt';
@Component({
selector: 'demo-root',
imports: [OgeGantt],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The toolbar adds tasks, zooms between hour/day/week/month
scales (Ctrl+wheel on the chart works too), fits the whole plan,
expands/collapses the tree and drives snapshot undo/redo — every edit,
including drags, is one undo step. scaleType is two-way. The task list
is a virtualized treegrid: columns are configurable and the splitter
between the panes drags. -->
<oge-gantt
[tasks]="tasks"
[(scaleType)]="scale"
[columns]="columns"
[taskListWidth]="300"
style="height: 420px"
/>
`,
})
export class Demo {
protected readonly scale = signal<OgeGanttScaleType>('weeks');
protected readonly columns = [
{ field: 'title' },
{ field: 'progress' },
{ field: 'owner', header: 'Owner' },
] as const;
protected readonly tasks = [
{ id: 1, title: 'Discovery', start: new Date(2026, 6, 6), end: new Date(2026, 6, 24), progress: 100, owner: 'Ada' },
{ id: 2, title: 'Build', start: new Date(2026, 6, 27), end: new Date(2026, 8, 4), progress: 40, owner: 'Grace' },
{ id: 3, title: 'Rollout', start: new Date(2026, 8, 7), end: new Date(2026, 8, 25), owner: 'Ada' },
];
}Work calendar, teams & export
workCalendar shades every off day (a four-day week here plus a holiday) and auto-scheduling rolls pushed starts onto working days, preserving working-day durations — a resource's own calendar overrides it per task. resourceId may hold an array of ids — the dialog edits assignments with a tag editor, bar labels join the names and showResourceWorkload renders the per-resource utilization band (overallocation in red). Three lazy export entry points: export-excel (exceljs, typed worksheet), export-pdf (jspdf, drawn vector chart) and export-image (dependency-free PNG).
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeGantt } from '@oge-ui/gantt';
import type { OgeGanttWorkCalendar } from '@oge-ui/gantt';
@Component({
selector: 'demo-root',
imports: [OgeGantt],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- workCalendar shades every off day (custom working week +
holidays) and makes auto-scheduling roll pushed starts onto working
days, preserving durations in working days. resourceId may hold an
ARRAY of ids — the dialog edits assignments with a tag editor and the
bar label joins the names. The export entry points load exceljs/jspdf
lazily: Excel writes the task tree as a typed worksheet, PDF draws the
chart as vector graphics. -->
<div class="mb-2 flex gap-2">
<button type="button" (click)="exportExcel(plan)">Export Excel</button>
<button type="button" (click)="exportPdf(plan)">Export PDF</button>
<button type="button" (click)="exportPng(plan)">Export PNG</button>
</div>
<oge-gantt
#plan
[tasks]="tasks"
[dependencies]="links"
[resources]="people"
[workCalendar]="calendar"
[showResourceWorkload]="true"
[autoScheduling]="true"
style="height: 400px"
/>
`,
})
export class Demo {
protected readonly calendar: OgeGanttWorkCalendar = {
workingDays: [1, 2, 3, 4], // four-day week
holidays: [new Date(2026, 7, 12)],
};
protected readonly people = [
{ id: 'ada', text: 'Ada', color: '#7c3aed' },
{
id: 'grace',
text: 'Grace',
color: '#0891b2',
// per-resource calendar: overrides workCalendar for Grace's tasks
calendar: { workingDays: [1, 2, 3, 4, 5] },
},
];
protected readonly tasks = [
{
id: 1,
title: 'Prototype',
start: new Date(2026, 7, 3),
end: new Date(2026, 7, 6),
progress: 80,
resourceId: ['ada', 'grace'], // multi-assignment
},
{
id: 2,
title: 'Field test',
start: new Date(2026, 7, 6),
end: new Date(2026, 7, 11),
resourceId: 'grace',
},
];
protected readonly links = [{ id: 1, predecessorId: 1, successorId: 2 }];
/** exceljs stays out of the initial bundle — loaded on first click. */
protected async exportExcel<T extends object, D extends object>(
gantt: OgeGantt<T, D>,
): Promise<void> {
const { exportGanttToExcel } = await import('@oge-ui/gantt/export-excel');
await exportGanttToExcel(gantt, { filename: 'plan.xlsx' });
}
/** jspdf loads lazily the same way. */
protected async exportPdf<T extends object, D extends object>(
gantt: OgeGantt<T, D>,
): Promise<void> {
const { exportGanttToPdf } = await import('@oge-ui/gantt/export-pdf');
await exportGanttToPdf(gantt, { filename: 'plan.pdf', title: 'Plan' });
}
/** PNG needs no third-party library at all — plain canvas drawing. */
protected async exportPng<T extends object, D extends object>(
gantt: OgeGantt<T, D>,
): Promise<void> {
const { exportGanttToPng } = await import('@oge-ui/gantt/export-image');
await exportGanttToPng(gantt, { filename: 'plan.png' });
}
}Task template
*ogeGanttTaskTemplate replaces the bar's title content and *ogeGanttTooltipTemplate the hover tooltip (default: title, dates + duration, progress, resources) — bar surface, gestures and keyboard semantics stay with the component.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeGantt, OgeGanttTaskTemplate, OgeGanttTooltipTemplate } from '@oge-ui/gantt';
@Component({
selector: 'demo-root',
imports: [OgeGantt, OgeGanttTaskTemplate, OgeGanttTooltipTemplate],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- *ogeGanttTaskTemplate replaces the bar's title content;
*ogeGanttTooltipTemplate replaces the hover tooltip (default: title,
dates + duration, progress, resources). Bar surface, gestures and
keyboard semantics stay with the component. -->
<oge-gantt [tasks]="tasks" style="height: 300px">
<ng-template ogeGanttTaskTemplate let-task>
<strong>{{ task.title }}</strong>
<span class="opacity-75"> · {{ task.progress }}%</span>
</ng-template>
<ng-template ogeGanttTooltipTemplate let-task>
<strong>{{ task.title }}</strong>
<em>{{ task.progress }}% complete</em>
</ng-template>
</oge-gantt>
`,
})
export class Demo {
protected readonly tasks = [
{ id: 1, title: 'Usability study', start: new Date(2026, 7, 3), end: new Date(2026, 7, 12), progress: 55, color: '#0f766e' },
{ id: 2, title: 'Findings report', start: new Date(2026, 7, 12), end: new Date(2026, 7, 17), progress: 10 },
];
}Configuration & i18n
Every user-facing string, aria labels included, lives in OgeGanttMessages — provide once with provideOgeGanttConfig() or override per instance with [messages]. locale drives every Intl date format; rowHeight and undoLimit are config-level.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeGantt, provideOgeGanttConfig } from '@oge-ui/gantt';
@Component({
selector: 'demo-root',
imports: [OgeGantt],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Every user-facing string, aria labels included, lives in
OgeGanttMessages — provide once with provideOgeGanttConfig() or override
per instance with [messages]. locale drives every Intl date format;
rowHeight and undoLimit are config-level too. -->
<oge-gantt [tasks]="tasks" locale="de" style="height: 300px" />
`,
})
export class Demo {
// App-wide (main.ts / route providers):
// provideOgeGanttConfig({
// locale: 'de',
// rowHeight: 32,
// messages: {
// toolbar: { ...germanToolbar },
// },
// })
protected readonly tasks = [
{ id: 1, title: 'Planung', start: new Date(2026, 7, 3), end: new Date(2026, 7, 10), progress: 25 },
];
}