General
Gantt
Usage#
import { WdsGanttComponent } from '@wds/angular/gantt';Examples#
Default#
resources (the rows) and events (the tasks) are properties. A resource with children is a collapsible group with a summary bar of its tasks; a task has start and end as ISO dates (end is inclusive), an optional progress and a color. Drag a bar to move it or its edges to resize it; the chart updates events and fires wds-event-change. The timeline scrolls without end and renders only what is visible; the arrows at the edge of a row jump to tasks out of view, and + / − zoom. off-days hatches weekends.
import { Component } from '@angular/core';
import { WdsGanttComponent } from '@wds/angular/gantt';
import { PROJECT_EVENTS, PROJECT_RESOURCES } from './gantt-data';
@Component({
selector: 'app-gantt-default-demo',
// The chart fills its container; its height sets how many rows are visible.
host: { class: 'block w-full' },
imports: [WdsGanttComponent],
template: `<wds-gantt class="h-[480px]" label="Website project" off-days [resources]="resources" [events]="events" />`,
})
export class GanttDefaultDemo {
protected readonly resources = PROJECT_RESOURCES;
protected readonly events = PROJECT_EVENTS;
}import '@wds/core/avatar';
import '@wds/core/badge';
import type { WdsGanttColumn, WdsGanttEvent, WdsGanttResource } from '@wds/core/gantt';
const pad = (n: number) => String(n).padStart(2, '0');
const base = new Date();
/** An ISO date `offset` days from today, so the examples always show the current period. */
export function day(offset: number): string {
const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + offset);
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** An ISO date moved by `days`. */
export function addDays(iso: string, days: number): string {
const [y, m, d] = iso.split('-').map(Number);
const date = new Date(y, m - 1, d + days);
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
/** Bar colors (any CSS color works). */
export const COLORS = {
blue: 'oklch(0.623 0.214 259.815)',
purple: 'oklch(0.627 0.265 303.9)',
violet: 'oklch(0.606 0.25 292.717)',
green: 'oklch(0.696 0.17 162.48)',
teal: 'oklch(0.704 0.14 182.503)',
sky: 'oklch(0.685 0.169 237.323)',
amber: 'oklch(0.769 0.188 70.08)',
rose: 'oklch(0.645 0.246 16.439)',
};
// --- Default: a website project
export const PROJECT_RESOURCES: WdsGanttResource[] = [
{
id: 'planning',
title: 'Planning',
children: [
{ id: 'brief', title: 'Project brief' },
{ id: 'scope', title: 'Scope review' },
],
},
{
id: 'design',
title: 'Design',
children: [
{ id: 'wireframes', title: 'Wireframes' },
{ id: 'visual-design', title: 'Visual design' },
],
},
{
id: 'build',
title: 'Build',
children: [
{ id: 'frontend', title: 'Frontend' },
{ id: 'backend', title: 'Backend' },
{ id: 'qa', title: 'QA pass' },
],
},
];
export const PROJECT_EVENTS: WdsGanttEvent[] = [
{ id: 'brief', title: 'Project brief', start: day(-20), end: day(-16), resourceId: 'brief', progress: 100, color: COLORS.blue },
{ id: 'scope', title: 'Scope review', start: day(-15), end: day(-11), resourceId: 'scope', progress: 100, color: COLORS.sky },
{ id: 'wireframes', title: 'Wireframes', start: day(-9), end: day(-4), resourceId: 'wireframes', progress: 100, color: COLORS.violet },
{ id: 'visual', title: 'Visual design', start: day(-3), end: day(2), resourceId: 'visual-design', progress: 40, color: COLORS.purple },
{ id: 'frontend', title: 'Frontend', start: day(0), end: day(12), resourceId: 'frontend', progress: 10, color: COLORS.green },
{ id: 'backend', title: 'Backend', start: day(2), end: day(14), resourceId: 'backend', color: COLORS.teal },
{ id: 'qa', title: 'QA pass', start: day(13), end: day(17), resourceId: 'qa', color: COLORS.amber },
];
/** Dependencies and planned windows added to the project (for the settings example). */
const PLAN: Record<string, Partial<WdsGanttEvent>> = {
scope: { dependencies: ['brief'] },
wireframes: { dependencies: ['scope'], baselineStart: day(-10), baselineEnd: day(-6) },
visual: { dependencies: ['wireframes'], baselineStart: day(-5), baselineEnd: day(1) },
frontend: { dependencies: ['visual'] },
qa: { dependencies: ['backend'] },
};
export const PROJECT_PLAN_EVENTS: WdsGanttEvent[] = PROJECT_EVENTS.map((e) => ({ ...e, ...PLAN[e.id] }));
// --- Roadmap: initiatives on the quarter scale
export const ROADMAP_RESOURCES: WdsGanttResource[] = [
{
id: 'platform',
title: 'Platform',
children: [
{ id: 'auth', title: 'Auth revamp' },
{ id: 'api', title: 'API v2' },
],
},
{
id: 'growth',
title: 'Growth',
children: [
{ id: 'onboarding', title: 'Onboarding flow' },
{ id: 'referral', title: 'Referral program' },
],
},
{ id: 'backlog', title: 'Backlog', children: [{ id: 'next', title: 'Next initiative' }] },
];
export const ROADMAP_EVENTS: WdsGanttEvent[] = [
{ id: 'r1', title: 'Auth revamp', start: day(-40), end: day(25), resourceId: 'auth', progress: 60, color: COLORS.blue },
{ id: 'r2', title: 'API v2', start: day(15), end: day(70), resourceId: 'api', color: COLORS.sky },
{ id: 'r3', title: 'Onboarding flow', start: day(-30), end: day(10), resourceId: 'onboarding', progress: 85, color: COLORS.green },
{ id: 'r4', title: 'Referral program', start: day(60), end: day(110), resourceId: 'referral', color: COLORS.teal },
];
// --- Team capacity: people grouped by squad, several assignments per person
export const TEAM_RESOURCES: WdsGanttResource[] = [
{
id: 'product',
title: 'Product squad',
children: [
{ id: 'ada', title: 'Ada Lovelace', color: COLORS.blue },
{ id: 'alan', title: 'Alan Turing', color: COLORS.sky },
],
},
{
id: 'design-squad',
title: 'Design squad',
children: [
{ id: 'grace', title: 'Grace Hopper', color: COLORS.violet },
{ id: 'linus', title: 'Linus Torvalds', color: COLORS.purple },
],
},
];
export const TEAM_EVENTS: WdsGanttEvent[] = [
{ id: 't1', title: 'Checkout', start: day(-3), end: day(1), resourceId: 'ada' },
{ id: 't2', title: 'Code review', start: day(0), end: day(3), resourceId: 'ada', color: COLORS.amber },
{ id: 't3', title: 'Billing API', start: day(-2), end: day(-1), resourceId: 'alan' },
{ id: 't4', title: 'Design tokens', start: day(-4), end: day(0), resourceId: 'grace' },
{ id: 't5', title: 'Icon set', start: day(2), end: day(5), resourceId: 'linus' },
];
/**
* Row label with an avatar (`renderResourceLabel`). It renders in the shadow DOM of the chart,
* so it is styled inline (Tailwind classes do not reach it).
*/
export function avatarLabel(resource: WdsGanttResource): string | Node {
if (resource.children?.length) return resource.title;
const label = document.createElement('span');
label.style.cssText = 'display:inline-flex;align-items:center;gap:8px;min-width:0';
// wds-avatar with initials; the row color reaches the fallback through the tokens it uses
const avatar = document.createElement('wds-avatar');
avatar.size = 'sm';
avatar.setAttribute('aria-hidden', 'true');
const color = resource.color ?? COLORS.blue;
avatar.style.setProperty('--wds-muted', `color-mix(in srgb, ${color} 22%, transparent)`);
avatar.style.setProperty('--wds-foreground', `color-mix(in srgb, ${color} 45%, currentColor)`);
avatar.textContent = resource.title
.split(' ')
.map((part) => part[0])
.join('')
.slice(0, 2);
const name = document.createElement('span');
name.style.cssText = 'overflow:hidden;text-overflow:ellipsis';
name.textContent = resource.title;
label.append(avatar, name);
return label;
}
// --- Status report: extra columns in the tree panel
export type Status = 'done' | 'on-track' | 'at-risk';
const STATUS_LABEL: Record<Status, string> = { done: 'Done', 'on-track': 'On track', 'at-risk': 'At risk' };
const STATUS_VARIANT: Record<Status, string> = { done: 'secondary', 'on-track': 'outline', 'at-risk': 'destructive' };
/** Content of a status cell (`render` of a column): a DOM node, here a `wds-badge`. */
export function statusBadge(status: Status): HTMLElement {
const el = document.createElement('wds-badge');
el.setAttribute('variant', STATUS_VARIANT[status]);
el.textContent = STATUS_LABEL[status];
return el;
}
export const STATUS_COLUMNS: WdsGanttColumn[] = [
{ key: 'owner', header: 'Owner', width: '5.5rem' },
{ key: 'status', header: 'Status', width: '6.5rem', render: (r) => statusBadge(r['status'] as Status) },
];
export const STATUS_RESOURCES: WdsGanttResource[] = [
{
id: 'launch',
title: 'Launch',
owner: 'Ana',
status: 'on-track',
children: [
{ id: 'brief-s', title: 'Brief', owner: 'Ana', status: 'done' },
{ id: 'content', title: 'Content', owner: 'Ben', status: 'on-track' },
{ id: 'campaign', title: 'Campaign', owner: 'Chloe', status: 'at-risk' },
{ id: 'release', title: 'Release', owner: 'David', status: 'on-track' },
],
},
];
export const STATUS_EVENTS: WdsGanttEvent[] = [
{ id: 's1', title: 'Brief', start: day(-14), end: day(-8), resourceId: 'brief-s', progress: 100, color: COLORS.blue },
{ id: 's2', title: 'Content', start: day(-7), end: day(5), resourceId: 'content', progress: 55, color: COLORS.green },
{ id: 's3', title: 'Campaign', start: day(-2), end: day(12), resourceId: 'campaign', progress: 10, color: COLORS.rose },
{ id: 's4', title: 'Release', start: day(14), end: day(14), resourceId: 'release', milestone: true, color: COLORS.amber },
];
// --- Baseline: planned against actual dates
export const BASELINE_RESOURCES: WdsGanttResource[] = [
{
id: 'implementation',
title: 'Implementation',
baselineStart: day(-8),
baselineEnd: day(4),
children: [
{ id: 'b-api', title: 'API integration' },
{ id: 'b-data', title: 'Data migration' },
{ id: 'b-security', title: 'Security hardening' },
],
},
{ id: 'launch-b', title: 'Launch', children: [{ id: 'b-handover', title: 'Handover' }] },
];
export const BASELINE_EVENTS: WdsGanttEvent[] = [
{ id: 'b1', title: 'API integration', start: day(-8), end: day(-3), resourceId: 'b-api', progress: 100, color: COLORS.violet, baselineStart: day(-8), baselineEnd: day(-5) },
{ id: 'b2', title: 'Data migration', start: day(-3), end: day(2), resourceId: 'b-data', progress: 70, color: COLORS.green, baselineStart: day(-3), baselineEnd: day(1) },
{ id: 'b3', title: 'Security hardening', start: day(1), end: day(6), resourceId: 'b-security', progress: 20, color: COLORS.teal, baselineStart: day(-1), baselineEnd: day(4) },
{ id: 'b4', title: 'Handover', start: day(8), end: day(8), resourceId: 'b-handover', milestone: true, color: COLORS.amber, baselineStart: day(6), baselineEnd: day(6) },
];
// --- Dependencies: a chained rollout plan
export const ROLLOUT_RESOURCES: WdsGanttResource[] = [
{
id: 'build-d',
title: 'Build',
children: [
{ id: 'd-frontend', title: 'Frontend' },
{ id: 'd-qa', title: 'QA pass' },
],
},
{
id: 'rollout',
title: 'Rollout',
children: [
{ id: 'd-staging', title: 'Staging deploy' },
{ id: 'd-signoff', title: 'Sign-off' },
{ id: 'd-live', title: 'Go live' },
],
},
];
export const ROLLOUT_EVENTS: WdsGanttEvent[] = [
{ id: 'd1', title: 'Frontend', start: day(-8), end: day(-4), resourceId: 'd-frontend', progress: 100, color: COLORS.violet },
{ id: 'd2', title: 'QA pass', start: day(-2), end: day(1), resourceId: 'd-qa', progress: 40, color: COLORS.purple, dependencies: ['d1'] },
{ id: 'd3', title: 'Staging deploy', start: day(2), end: day(3), resourceId: 'd-staging', color: COLORS.green, dependencies: ['d2'] },
{ id: 'd4', title: 'Sign-off', start: day(5), end: day(5), resourceId: 'd-signoff', milestone: true, color: COLORS.amber, dependencies: ['d3'] },
{ id: 'd5', title: 'Go live', start: day(6), end: day(7), resourceId: 'd-live', color: COLORS.teal, dependencies: ['d4'] },
];
// --- Day schedule: times as instants (UTC), shown in the chosen time zone
export const SHIFT_RESOURCES: WdsGanttResource[] = [
{
id: 'support',
title: 'Support rota',
children: [
{ id: 'emea', title: 'EMEA desk', color: COLORS.blue },
{ id: 'amer', title: 'Americas desk', color: COLORS.green },
{ id: 'apac', title: 'APAC desk', color: COLORS.amber },
],
},
];
const at = (offset: number, time: string) => `${day(offset)}T${time}:00Z`;
export const SHIFT_EVENTS: WdsGanttEvent[] = [
{ id: 'e1', title: 'Morning shift', start: at(0, '06:00'), end: at(0, '10:00'), resourceId: 'emea' },
{ id: 'e2', title: 'Release call', start: at(0, '11:00'), end: at(0, '11:45'), resourceId: 'emea', color: COLORS.rose },
{ id: 'e3', title: 'Afternoon shift', start: at(0, '12:00'), end: at(0, '16:00'), resourceId: 'emea' },
{ id: 'a1', title: 'Day shift', start: at(0, '13:00'), end: at(0, '21:00'), resourceId: 'amer' },
{ id: 'p1', title: 'Night shift', start: at(-1, '23:00'), end: at(0, '07:00'), resourceId: 'apac' },
{ id: 'p2', title: 'Handover', start: at(0, '07:00'), end: at(0, '07:00'), resourceId: 'apac', milestone: true },
];
export const TIME_ZONES = ['UTC', 'Europe/Warsaw', 'America/New_York', 'Asia/Tokyo'];
// --- Recurring: team rituals
export const RITUAL_RESOURCES: WdsGanttResource[] = [
{
id: 'rituals',
title: 'Team rituals',
children: [
{ id: 'standup', title: 'Standup', color: COLORS.sky },
{ id: 'review', title: 'Sprint review', color: COLORS.violet },
{ id: 'retro', title: 'Retrospective', color: COLORS.rose },
{ id: 'planning-r', title: 'Planning', color: COLORS.green },
],
},
];
export const RITUAL_EVENTS: WdsGanttEvent[] = [
{ id: 'standup', title: 'Standup', start: day(-28), end: day(-28), resourceId: 'standup', recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR' },
{ id: 'review', title: 'Sprint review', start: day(-25), end: day(-25), resourceId: 'review', recurrence: { freq: 'weekly', interval: 2 } },
{ id: 'retro', title: 'Retro', start: day(-24), end: day(-24), resourceId: 'retro', recurrence: 'FREQ=WEEKLY;INTERVAL=2' },
{ id: 'planning-r', title: 'Planning', start: day(-21), end: day(-20), resourceId: 'planning-r', recurrence: 'FREQ=WEEKLY;INTERVAL=2;COUNT=6' },
];
// --- Backlog: selection, menus, moving rows
export const BACKLOG_RESOURCES: WdsGanttResource[] = [
{
id: 'sprint-1',
title: 'Sprint 1',
children: [
{ id: 'login', title: 'Login' },
{ id: 'signup', title: 'Sign-up' },
],
},
{
id: 'sprint-2',
title: 'Sprint 2',
children: [
{ id: 'dashboard', title: 'Dashboard' },
{ id: 'reports', title: 'Reports' },
],
},
{ id: 'icebox', title: 'Icebox', children: [{ id: 'dark-mode', title: 'Dark mode' }] },
];
export const BACKLOG_EVENTS: WdsGanttEvent[] = [
{ id: 'k1', title: 'Login form', start: day(-6), end: day(-2), resourceId: 'login', progress: 100, color: COLORS.blue },
{ id: 'k2', title: 'Sign-up flow', start: day(-3), end: day(3), resourceId: 'signup', progress: 50, color: COLORS.sky },
{ id: 'k3', title: 'Dashboard', start: day(4), end: day(11), resourceId: 'dashboard', color: COLORS.green },
{ id: 'k4', title: 'Reports', start: day(8), end: day(15), resourceId: 'reports', color: COLORS.teal },
{ id: 'k5', title: 'Dark mode', start: day(18), end: day(24), resourceId: 'dark-mode', color: COLORS.violet },
];Product roadmap#
scale sets the period: day, week, month, quarter (here, with weeks in the header) or year; the scale menu in the bar changes it too. The toolbar slot adds items to the navigation bar: here a button that schedules the next initiative in the backlog row.
import { Component, signal } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WdsGanttComponent } from '@wds/angular/gantt';
import type { WdsGanttEvent } from '@wds/core/gantt';
import { COLORS, ROADMAP_EVENTS, ROADMAP_RESOURCES, addDays, day } from './gantt-data';
@Component({
selector: 'app-gantt-roadmap-demo',
host: { class: 'block w-full' },
imports: [WdsButtonComponent, WdsGanttComponent],
template: `
<wds-gantt
class="h-[440px]"
label="Product roadmap"
scale="quarter"
[resources]="resources"
[events]="events()"
(wdsEventChange)="events.set($event.events)"
>
<wds-button slot="toolbar" variant="outline" size="sm" (click)="schedule()">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14" /><path d="M12 5v14" /></svg>
Add to roadmap
</wds-button>
</wds-gantt>
`,
})
export class GanttRoadmapDemo {
protected readonly resources = ROADMAP_RESOURCES;
protected readonly events = signal<WdsGanttEvent[]>(ROADMAP_EVENTS);
/** Adds a 30-day initiative to the backlog row, after the last one. */
protected schedule(): void {
const backlog = this.events().filter((e) => e.resourceId === 'next');
const start = backlog.length ? addDays(backlog[backlog.length - 1].end, 1) : day(14);
const initiative: WdsGanttEvent = {
id: crypto.randomUUID(),
title: `Initiative ${backlog.length + 1}`,
start,
end: addDays(start, 29),
resourceId: 'next',
color: COLORS.violet,
};
this.events.update((events) => [...events, initiative]);
}
}import '@wds/core/avatar';
import '@wds/core/badge';
import type { WdsGanttColumn, WdsGanttEvent, WdsGanttResource } from '@wds/core/gantt';
const pad = (n: number) => String(n).padStart(2, '0');
const base = new Date();
/** An ISO date `offset` days from today, so the examples always show the current period. */
export function day(offset: number): string {
const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + offset);
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** An ISO date moved by `days`. */
export function addDays(iso: string, days: number): string {
const [y, m, d] = iso.split('-').map(Number);
const date = new Date(y, m - 1, d + days);
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
/** Bar colors (any CSS color works). */
export const COLORS = {
blue: 'oklch(0.623 0.214 259.815)',
purple: 'oklch(0.627 0.265 303.9)',
violet: 'oklch(0.606 0.25 292.717)',
green: 'oklch(0.696 0.17 162.48)',
teal: 'oklch(0.704 0.14 182.503)',
sky: 'oklch(0.685 0.169 237.323)',
amber: 'oklch(0.769 0.188 70.08)',
rose: 'oklch(0.645 0.246 16.439)',
};
// --- Default: a website project
export const PROJECT_RESOURCES: WdsGanttResource[] = [
{
id: 'planning',
title: 'Planning',
children: [
{ id: 'brief', title: 'Project brief' },
{ id: 'scope', title: 'Scope review' },
],
},
{
id: 'design',
title: 'Design',
children: [
{ id: 'wireframes', title: 'Wireframes' },
{ id: 'visual-design', title: 'Visual design' },
],
},
{
id: 'build',
title: 'Build',
children: [
{ id: 'frontend', title: 'Frontend' },
{ id: 'backend', title: 'Backend' },
{ id: 'qa', title: 'QA pass' },
],
},
];
export const PROJECT_EVENTS: WdsGanttEvent[] = [
{ id: 'brief', title: 'Project brief', start: day(-20), end: day(-16), resourceId: 'brief', progress: 100, color: COLORS.blue },
{ id: 'scope', title: 'Scope review', start: day(-15), end: day(-11), resourceId: 'scope', progress: 100, color: COLORS.sky },
{ id: 'wireframes', title: 'Wireframes', start: day(-9), end: day(-4), resourceId: 'wireframes', progress: 100, color: COLORS.violet },
{ id: 'visual', title: 'Visual design', start: day(-3), end: day(2), resourceId: 'visual-design', progress: 40, color: COLORS.purple },
{ id: 'frontend', title: 'Frontend', start: day(0), end: day(12), resourceId: 'frontend', progress: 10, color: COLORS.green },
{ id: 'backend', title: 'Backend', start: day(2), end: day(14), resourceId: 'backend', color: COLORS.teal },
{ id: 'qa', title: 'QA pass', start: day(13), end: day(17), resourceId: 'qa', color: COLORS.amber },
];
/** Dependencies and planned windows added to the project (for the settings example). */
const PLAN: Record<string, Partial<WdsGanttEvent>> = {
scope: { dependencies: ['brief'] },
wireframes: { dependencies: ['scope'], baselineStart: day(-10), baselineEnd: day(-6) },
visual: { dependencies: ['wireframes'], baselineStart: day(-5), baselineEnd: day(1) },
frontend: { dependencies: ['visual'] },
qa: { dependencies: ['backend'] },
};
export const PROJECT_PLAN_EVENTS: WdsGanttEvent[] = PROJECT_EVENTS.map((e) => ({ ...e, ...PLAN[e.id] }));
// --- Roadmap: initiatives on the quarter scale
export const ROADMAP_RESOURCES: WdsGanttResource[] = [
{
id: 'platform',
title: 'Platform',
children: [
{ id: 'auth', title: 'Auth revamp' },
{ id: 'api', title: 'API v2' },
],
},
{
id: 'growth',
title: 'Growth',
children: [
{ id: 'onboarding', title: 'Onboarding flow' },
{ id: 'referral', title: 'Referral program' },
],
},
{ id: 'backlog', title: 'Backlog', children: [{ id: 'next', title: 'Next initiative' }] },
];
export const ROADMAP_EVENTS: WdsGanttEvent[] = [
{ id: 'r1', title: 'Auth revamp', start: day(-40), end: day(25), resourceId: 'auth', progress: 60, color: COLORS.blue },
{ id: 'r2', title: 'API v2', start: day(15), end: day(70), resourceId: 'api', color: COLORS.sky },
{ id: 'r3', title: 'Onboarding flow', start: day(-30), end: day(10), resourceId: 'onboarding', progress: 85, color: COLORS.green },
{ id: 'r4', title: 'Referral program', start: day(60), end: day(110), resourceId: 'referral', color: COLORS.teal },
];
// --- Team capacity: people grouped by squad, several assignments per person
export const TEAM_RESOURCES: WdsGanttResource[] = [
{
id: 'product',
title: 'Product squad',
children: [
{ id: 'ada', title: 'Ada Lovelace', color: COLORS.blue },
{ id: 'alan', title: 'Alan Turing', color: COLORS.sky },
],
},
{
id: 'design-squad',
title: 'Design squad',
children: [
{ id: 'grace', title: 'Grace Hopper', color: COLORS.violet },
{ id: 'linus', title: 'Linus Torvalds', color: COLORS.purple },
],
},
];
export const TEAM_EVENTS: WdsGanttEvent[] = [
{ id: 't1', title: 'Checkout', start: day(-3), end: day(1), resourceId: 'ada' },
{ id: 't2', title: 'Code review', start: day(0), end: day(3), resourceId: 'ada', color: COLORS.amber },
{ id: 't3', title: 'Billing API', start: day(-2), end: day(-1), resourceId: 'alan' },
{ id: 't4', title: 'Design tokens', start: day(-4), end: day(0), resourceId: 'grace' },
{ id: 't5', title: 'Icon set', start: day(2), end: day(5), resourceId: 'linus' },
];
/**
* Row label with an avatar (`renderResourceLabel`). It renders in the shadow DOM of the chart,
* so it is styled inline (Tailwind classes do not reach it).
*/
export function avatarLabel(resource: WdsGanttResource): string | Node {
if (resource.children?.length) return resource.title;
const label = document.createElement('span');
label.style.cssText = 'display:inline-flex;align-items:center;gap:8px;min-width:0';
// wds-avatar with initials; the row color reaches the fallback through the tokens it uses
const avatar = document.createElement('wds-avatar');
avatar.size = 'sm';
avatar.setAttribute('aria-hidden', 'true');
const color = resource.color ?? COLORS.blue;
avatar.style.setProperty('--wds-muted', `color-mix(in srgb, ${color} 22%, transparent)`);
avatar.style.setProperty('--wds-foreground', `color-mix(in srgb, ${color} 45%, currentColor)`);
avatar.textContent = resource.title
.split(' ')
.map((part) => part[0])
.join('')
.slice(0, 2);
const name = document.createElement('span');
name.style.cssText = 'overflow:hidden;text-overflow:ellipsis';
name.textContent = resource.title;
label.append(avatar, name);
return label;
}
// --- Status report: extra columns in the tree panel
export type Status = 'done' | 'on-track' | 'at-risk';
const STATUS_LABEL: Record<Status, string> = { done: 'Done', 'on-track': 'On track', 'at-risk': 'At risk' };
const STATUS_VARIANT: Record<Status, string> = { done: 'secondary', 'on-track': 'outline', 'at-risk': 'destructive' };
/** Content of a status cell (`render` of a column): a DOM node, here a `wds-badge`. */
export function statusBadge(status: Status): HTMLElement {
const el = document.createElement('wds-badge');
el.setAttribute('variant', STATUS_VARIANT[status]);
el.textContent = STATUS_LABEL[status];
return el;
}
export const STATUS_COLUMNS: WdsGanttColumn[] = [
{ key: 'owner', header: 'Owner', width: '5.5rem' },
{ key: 'status', header: 'Status', width: '6.5rem', render: (r) => statusBadge(r['status'] as Status) },
];
export const STATUS_RESOURCES: WdsGanttResource[] = [
{
id: 'launch',
title: 'Launch',
owner: 'Ana',
status: 'on-track',
children: [
{ id: 'brief-s', title: 'Brief', owner: 'Ana', status: 'done' },
{ id: 'content', title: 'Content', owner: 'Ben', status: 'on-track' },
{ id: 'campaign', title: 'Campaign', owner: 'Chloe', status: 'at-risk' },
{ id: 'release', title: 'Release', owner: 'David', status: 'on-track' },
],
},
];
export const STATUS_EVENTS: WdsGanttEvent[] = [
{ id: 's1', title: 'Brief', start: day(-14), end: day(-8), resourceId: 'brief-s', progress: 100, color: COLORS.blue },
{ id: 's2', title: 'Content', start: day(-7), end: day(5), resourceId: 'content', progress: 55, color: COLORS.green },
{ id: 's3', title: 'Campaign', start: day(-2), end: day(12), resourceId: 'campaign', progress: 10, color: COLORS.rose },
{ id: 's4', title: 'Release', start: day(14), end: day(14), resourceId: 'release', milestone: true, color: COLORS.amber },
];
// --- Baseline: planned against actual dates
export const BASELINE_RESOURCES: WdsGanttResource[] = [
{
id: 'implementation',
title: 'Implementation',
baselineStart: day(-8),
baselineEnd: day(4),
children: [
{ id: 'b-api', title: 'API integration' },
{ id: 'b-data', title: 'Data migration' },
{ id: 'b-security', title: 'Security hardening' },
],
},
{ id: 'launch-b', title: 'Launch', children: [{ id: 'b-handover', title: 'Handover' }] },
];
export const BASELINE_EVENTS: WdsGanttEvent[] = [
{ id: 'b1', title: 'API integration', start: day(-8), end: day(-3), resourceId: 'b-api', progress: 100, color: COLORS.violet, baselineStart: day(-8), baselineEnd: day(-5) },
{ id: 'b2', title: 'Data migration', start: day(-3), end: day(2), resourceId: 'b-data', progress: 70, color: COLORS.green, baselineStart: day(-3), baselineEnd: day(1) },
{ id: 'b3', title: 'Security hardening', start: day(1), end: day(6), resourceId: 'b-security', progress: 20, color: COLORS.teal, baselineStart: day(-1), baselineEnd: day(4) },
{ id: 'b4', title: 'Handover', start: day(8), end: day(8), resourceId: 'b-handover', milestone: true, color: COLORS.amber, baselineStart: day(6), baselineEnd: day(6) },
];
// --- Dependencies: a chained rollout plan
export const ROLLOUT_RESOURCES: WdsGanttResource[] = [
{
id: 'build-d',
title: 'Build',
children: [
{ id: 'd-frontend', title: 'Frontend' },
{ id: 'd-qa', title: 'QA pass' },
],
},
{
id: 'rollout',
title: 'Rollout',
children: [
{ id: 'd-staging', title: 'Staging deploy' },
{ id: 'd-signoff', title: 'Sign-off' },
{ id: 'd-live', title: 'Go live' },
],
},
];
export const ROLLOUT_EVENTS: WdsGanttEvent[] = [
{ id: 'd1', title: 'Frontend', start: day(-8), end: day(-4), resourceId: 'd-frontend', progress: 100, color: COLORS.violet },
{ id: 'd2', title: 'QA pass', start: day(-2), end: day(1), resourceId: 'd-qa', progress: 40, color: COLORS.purple, dependencies: ['d1'] },
{ id: 'd3', title: 'Staging deploy', start: day(2), end: day(3), resourceId: 'd-staging', color: COLORS.green, dependencies: ['d2'] },
{ id: 'd4', title: 'Sign-off', start: day(5), end: day(5), resourceId: 'd-signoff', milestone: true, color: COLORS.amber, dependencies: ['d3'] },
{ id: 'd5', title: 'Go live', start: day(6), end: day(7), resourceId: 'd-live', color: COLORS.teal, dependencies: ['d4'] },
];
// --- Day schedule: times as instants (UTC), shown in the chosen time zone
export const SHIFT_RESOURCES: WdsGanttResource[] = [
{
id: 'support',
title: 'Support rota',
children: [
{ id: 'emea', title: 'EMEA desk', color: COLORS.blue },
{ id: 'amer', title: 'Americas desk', color: COLORS.green },
{ id: 'apac', title: 'APAC desk', color: COLORS.amber },
],
},
];
const at = (offset: number, time: string) => `${day(offset)}T${time}:00Z`;
export const SHIFT_EVENTS: WdsGanttEvent[] = [
{ id: 'e1', title: 'Morning shift', start: at(0, '06:00'), end: at(0, '10:00'), resourceId: 'emea' },
{ id: 'e2', title: 'Release call', start: at(0, '11:00'), end: at(0, '11:45'), resourceId: 'emea', color: COLORS.rose },
{ id: 'e3', title: 'Afternoon shift', start: at(0, '12:00'), end: at(0, '16:00'), resourceId: 'emea' },
{ id: 'a1', title: 'Day shift', start: at(0, '13:00'), end: at(0, '21:00'), resourceId: 'amer' },
{ id: 'p1', title: 'Night shift', start: at(-1, '23:00'), end: at(0, '07:00'), resourceId: 'apac' },
{ id: 'p2', title: 'Handover', start: at(0, '07:00'), end: at(0, '07:00'), resourceId: 'apac', milestone: true },
];
export const TIME_ZONES = ['UTC', 'Europe/Warsaw', 'America/New_York', 'Asia/Tokyo'];
// --- Recurring: team rituals
export const RITUAL_RESOURCES: WdsGanttResource[] = [
{
id: 'rituals',
title: 'Team rituals',
children: [
{ id: 'standup', title: 'Standup', color: COLORS.sky },
{ id: 'review', title: 'Sprint review', color: COLORS.violet },
{ id: 'retro', title: 'Retrospective', color: COLORS.rose },
{ id: 'planning-r', title: 'Planning', color: COLORS.green },
],
},
];
export const RITUAL_EVENTS: WdsGanttEvent[] = [
{ id: 'standup', title: 'Standup', start: day(-28), end: day(-28), resourceId: 'standup', recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR' },
{ id: 'review', title: 'Sprint review', start: day(-25), end: day(-25), resourceId: 'review', recurrence: { freq: 'weekly', interval: 2 } },
{ id: 'retro', title: 'Retro', start: day(-24), end: day(-24), resourceId: 'retro', recurrence: 'FREQ=WEEKLY;INTERVAL=2' },
{ id: 'planning-r', title: 'Planning', start: day(-21), end: day(-20), resourceId: 'planning-r', recurrence: 'FREQ=WEEKLY;INTERVAL=2;COUNT=6' },
];
// --- Backlog: selection, menus, moving rows
export const BACKLOG_RESOURCES: WdsGanttResource[] = [
{
id: 'sprint-1',
title: 'Sprint 1',
children: [
{ id: 'login', title: 'Login' },
{ id: 'signup', title: 'Sign-up' },
],
},
{
id: 'sprint-2',
title: 'Sprint 2',
children: [
{ id: 'dashboard', title: 'Dashboard' },
{ id: 'reports', title: 'Reports' },
],
},
{ id: 'icebox', title: 'Icebox', children: [{ id: 'dark-mode', title: 'Dark mode' }] },
];
export const BACKLOG_EVENTS: WdsGanttEvent[] = [
{ id: 'k1', title: 'Login form', start: day(-6), end: day(-2), resourceId: 'login', progress: 100, color: COLORS.blue },
{ id: 'k2', title: 'Sign-up flow', start: day(-3), end: day(3), resourceId: 'signup', progress: 50, color: COLORS.sky },
{ id: 'k3', title: 'Dashboard', start: day(4), end: day(11), resourceId: 'dashboard', color: COLORS.green },
{ id: 'k4', title: 'Reports', start: day(8), end: day(15), resourceId: 'reports', color: COLORS.teal },
{ id: 'k5', title: 'Dark mode', start: day(18), end: day(24), resourceId: 'dark-mode', color: COLORS.violet },
];Team capacity#
renderResourceLabel returns the row label as text or a DOM node (here an avatar with initials; it renders in shadow DOM, so style it inline). With drag-create, drawing a range on an empty part of a row fires wds-slot-select with the row and the dates; here it adds an assignment.
import { Component, signal } from '@angular/core';
import { WdsGanttComponent } from '@wds/angular/gantt';
import type { WdsGanttEvent, WdsGanttSlotSelectDetail } from '@wds/core/gantt';
import { TEAM_EVENTS, TEAM_RESOURCES, avatarLabel } from './gantt-data';
@Component({
selector: 'app-gantt-capacity-demo',
host: { class: 'block w-full' },
imports: [WdsGanttComponent],
template: `
<wds-gantt
class="h-[400px]"
label="Team capacity"
scale="week"
name-header="People"
off-days
drag-create
[resources]="resources"
[events]="events()"
[renderResourceLabel]="avatarLabel"
(wdsEventChange)="events.set($event.events)"
(wdsSlotSelect)="assign($event)"
/>
`,
})
export class GanttCapacityDemo {
protected readonly resources = TEAM_RESOURCES;
protected readonly events = signal<WdsGanttEvent[]>(TEAM_EVENTS);
protected readonly avatarLabel = avatarLabel;
/** A range drawn in a person's row becomes a new assignment. */
protected assign({ resourceId, start, end }: WdsGanttSlotSelectDetail): void {
this.events.update((events) => [...events, { id: crypto.randomUUID(), title: 'New assignment', start, end, resourceId }]);
}
}import '@wds/core/avatar';
import '@wds/core/badge';
import type { WdsGanttColumn, WdsGanttEvent, WdsGanttResource } from '@wds/core/gantt';
const pad = (n: number) => String(n).padStart(2, '0');
const base = new Date();
/** An ISO date `offset` days from today, so the examples always show the current period. */
export function day(offset: number): string {
const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + offset);
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** An ISO date moved by `days`. */
export function addDays(iso: string, days: number): string {
const [y, m, d] = iso.split('-').map(Number);
const date = new Date(y, m - 1, d + days);
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
/** Bar colors (any CSS color works). */
export const COLORS = {
blue: 'oklch(0.623 0.214 259.815)',
purple: 'oklch(0.627 0.265 303.9)',
violet: 'oklch(0.606 0.25 292.717)',
green: 'oklch(0.696 0.17 162.48)',
teal: 'oklch(0.704 0.14 182.503)',
sky: 'oklch(0.685 0.169 237.323)',
amber: 'oklch(0.769 0.188 70.08)',
rose: 'oklch(0.645 0.246 16.439)',
};
// --- Default: a website project
export const PROJECT_RESOURCES: WdsGanttResource[] = [
{
id: 'planning',
title: 'Planning',
children: [
{ id: 'brief', title: 'Project brief' },
{ id: 'scope', title: 'Scope review' },
],
},
{
id: 'design',
title: 'Design',
children: [
{ id: 'wireframes', title: 'Wireframes' },
{ id: 'visual-design', title: 'Visual design' },
],
},
{
id: 'build',
title: 'Build',
children: [
{ id: 'frontend', title: 'Frontend' },
{ id: 'backend', title: 'Backend' },
{ id: 'qa', title: 'QA pass' },
],
},
];
export const PROJECT_EVENTS: WdsGanttEvent[] = [
{ id: 'brief', title: 'Project brief', start: day(-20), end: day(-16), resourceId: 'brief', progress: 100, color: COLORS.blue },
{ id: 'scope', title: 'Scope review', start: day(-15), end: day(-11), resourceId: 'scope', progress: 100, color: COLORS.sky },
{ id: 'wireframes', title: 'Wireframes', start: day(-9), end: day(-4), resourceId: 'wireframes', progress: 100, color: COLORS.violet },
{ id: 'visual', title: 'Visual design', start: day(-3), end: day(2), resourceId: 'visual-design', progress: 40, color: COLORS.purple },
{ id: 'frontend', title: 'Frontend', start: day(0), end: day(12), resourceId: 'frontend', progress: 10, color: COLORS.green },
{ id: 'backend', title: 'Backend', start: day(2), end: day(14), resourceId: 'backend', color: COLORS.teal },
{ id: 'qa', title: 'QA pass', start: day(13), end: day(17), resourceId: 'qa', color: COLORS.amber },
];
/** Dependencies and planned windows added to the project (for the settings example). */
const PLAN: Record<string, Partial<WdsGanttEvent>> = {
scope: { dependencies: ['brief'] },
wireframes: { dependencies: ['scope'], baselineStart: day(-10), baselineEnd: day(-6) },
visual: { dependencies: ['wireframes'], baselineStart: day(-5), baselineEnd: day(1) },
frontend: { dependencies: ['visual'] },
qa: { dependencies: ['backend'] },
};
export const PROJECT_PLAN_EVENTS: WdsGanttEvent[] = PROJECT_EVENTS.map((e) => ({ ...e, ...PLAN[e.id] }));
// --- Roadmap: initiatives on the quarter scale
export const ROADMAP_RESOURCES: WdsGanttResource[] = [
{
id: 'platform',
title: 'Platform',
children: [
{ id: 'auth', title: 'Auth revamp' },
{ id: 'api', title: 'API v2' },
],
},
{
id: 'growth',
title: 'Growth',
children: [
{ id: 'onboarding', title: 'Onboarding flow' },
{ id: 'referral', title: 'Referral program' },
],
},
{ id: 'backlog', title: 'Backlog', children: [{ id: 'next', title: 'Next initiative' }] },
];
export const ROADMAP_EVENTS: WdsGanttEvent[] = [
{ id: 'r1', title: 'Auth revamp', start: day(-40), end: day(25), resourceId: 'auth', progress: 60, color: COLORS.blue },
{ id: 'r2', title: 'API v2', start: day(15), end: day(70), resourceId: 'api', color: COLORS.sky },
{ id: 'r3', title: 'Onboarding flow', start: day(-30), end: day(10), resourceId: 'onboarding', progress: 85, color: COLORS.green },
{ id: 'r4', title: 'Referral program', start: day(60), end: day(110), resourceId: 'referral', color: COLORS.teal },
];
// --- Team capacity: people grouped by squad, several assignments per person
export const TEAM_RESOURCES: WdsGanttResource[] = [
{
id: 'product',
title: 'Product squad',
children: [
{ id: 'ada', title: 'Ada Lovelace', color: COLORS.blue },
{ id: 'alan', title: 'Alan Turing', color: COLORS.sky },
],
},
{
id: 'design-squad',
title: 'Design squad',
children: [
{ id: 'grace', title: 'Grace Hopper', color: COLORS.violet },
{ id: 'linus', title: 'Linus Torvalds', color: COLORS.purple },
],
},
];
export const TEAM_EVENTS: WdsGanttEvent[] = [
{ id: 't1', title: 'Checkout', start: day(-3), end: day(1), resourceId: 'ada' },
{ id: 't2', title: 'Code review', start: day(0), end: day(3), resourceId: 'ada', color: COLORS.amber },
{ id: 't3', title: 'Billing API', start: day(-2), end: day(-1), resourceId: 'alan' },
{ id: 't4', title: 'Design tokens', start: day(-4), end: day(0), resourceId: 'grace' },
{ id: 't5', title: 'Icon set', start: day(2), end: day(5), resourceId: 'linus' },
];
/**
* Row label with an avatar (`renderResourceLabel`). It renders in the shadow DOM of the chart,
* so it is styled inline (Tailwind classes do not reach it).
*/
export function avatarLabel(resource: WdsGanttResource): string | Node {
if (resource.children?.length) return resource.title;
const label = document.createElement('span');
label.style.cssText = 'display:inline-flex;align-items:center;gap:8px;min-width:0';
// wds-avatar with initials; the row color reaches the fallback through the tokens it uses
const avatar = document.createElement('wds-avatar');
avatar.size = 'sm';
avatar.setAttribute('aria-hidden', 'true');
const color = resource.color ?? COLORS.blue;
avatar.style.setProperty('--wds-muted', `color-mix(in srgb, ${color} 22%, transparent)`);
avatar.style.setProperty('--wds-foreground', `color-mix(in srgb, ${color} 45%, currentColor)`);
avatar.textContent = resource.title
.split(' ')
.map((part) => part[0])
.join('')
.slice(0, 2);
const name = document.createElement('span');
name.style.cssText = 'overflow:hidden;text-overflow:ellipsis';
name.textContent = resource.title;
label.append(avatar, name);
return label;
}
// --- Status report: extra columns in the tree panel
export type Status = 'done' | 'on-track' | 'at-risk';
const STATUS_LABEL: Record<Status, string> = { done: 'Done', 'on-track': 'On track', 'at-risk': 'At risk' };
const STATUS_VARIANT: Record<Status, string> = { done: 'secondary', 'on-track': 'outline', 'at-risk': 'destructive' };
/** Content of a status cell (`render` of a column): a DOM node, here a `wds-badge`. */
export function statusBadge(status: Status): HTMLElement {
const el = document.createElement('wds-badge');
el.setAttribute('variant', STATUS_VARIANT[status]);
el.textContent = STATUS_LABEL[status];
return el;
}
export const STATUS_COLUMNS: WdsGanttColumn[] = [
{ key: 'owner', header: 'Owner', width: '5.5rem' },
{ key: 'status', header: 'Status', width: '6.5rem', render: (r) => statusBadge(r['status'] as Status) },
];
export const STATUS_RESOURCES: WdsGanttResource[] = [
{
id: 'launch',
title: 'Launch',
owner: 'Ana',
status: 'on-track',
children: [
{ id: 'brief-s', title: 'Brief', owner: 'Ana', status: 'done' },
{ id: 'content', title: 'Content', owner: 'Ben', status: 'on-track' },
{ id: 'campaign', title: 'Campaign', owner: 'Chloe', status: 'at-risk' },
{ id: 'release', title: 'Release', owner: 'David', status: 'on-track' },
],
},
];
export const STATUS_EVENTS: WdsGanttEvent[] = [
{ id: 's1', title: 'Brief', start: day(-14), end: day(-8), resourceId: 'brief-s', progress: 100, color: COLORS.blue },
{ id: 's2', title: 'Content', start: day(-7), end: day(5), resourceId: 'content', progress: 55, color: COLORS.green },
{ id: 's3', title: 'Campaign', start: day(-2), end: day(12), resourceId: 'campaign', progress: 10, color: COLORS.rose },
{ id: 's4', title: 'Release', start: day(14), end: day(14), resourceId: 'release', milestone: true, color: COLORS.amber },
];
// --- Baseline: planned against actual dates
export const BASELINE_RESOURCES: WdsGanttResource[] = [
{
id: 'implementation',
title: 'Implementation',
baselineStart: day(-8),
baselineEnd: day(4),
children: [
{ id: 'b-api', title: 'API integration' },
{ id: 'b-data', title: 'Data migration' },
{ id: 'b-security', title: 'Security hardening' },
],
},
{ id: 'launch-b', title: 'Launch', children: [{ id: 'b-handover', title: 'Handover' }] },
];
export const BASELINE_EVENTS: WdsGanttEvent[] = [
{ id: 'b1', title: 'API integration', start: day(-8), end: day(-3), resourceId: 'b-api', progress: 100, color: COLORS.violet, baselineStart: day(-8), baselineEnd: day(-5) },
{ id: 'b2', title: 'Data migration', start: day(-3), end: day(2), resourceId: 'b-data', progress: 70, color: COLORS.green, baselineStart: day(-3), baselineEnd: day(1) },
{ id: 'b3', title: 'Security hardening', start: day(1), end: day(6), resourceId: 'b-security', progress: 20, color: COLORS.teal, baselineStart: day(-1), baselineEnd: day(4) },
{ id: 'b4', title: 'Handover', start: day(8), end: day(8), resourceId: 'b-handover', milestone: true, color: COLORS.amber, baselineStart: day(6), baselineEnd: day(6) },
];
// --- Dependencies: a chained rollout plan
export const ROLLOUT_RESOURCES: WdsGanttResource[] = [
{
id: 'build-d',
title: 'Build',
children: [
{ id: 'd-frontend', title: 'Frontend' },
{ id: 'd-qa', title: 'QA pass' },
],
},
{
id: 'rollout',
title: 'Rollout',
children: [
{ id: 'd-staging', title: 'Staging deploy' },
{ id: 'd-signoff', title: 'Sign-off' },
{ id: 'd-live', title: 'Go live' },
],
},
];
export const ROLLOUT_EVENTS: WdsGanttEvent[] = [
{ id: 'd1', title: 'Frontend', start: day(-8), end: day(-4), resourceId: 'd-frontend', progress: 100, color: COLORS.violet },
{ id: 'd2', title: 'QA pass', start: day(-2), end: day(1), resourceId: 'd-qa', progress: 40, color: COLORS.purple, dependencies: ['d1'] },
{ id: 'd3', title: 'Staging deploy', start: day(2), end: day(3), resourceId: 'd-staging', color: COLORS.green, dependencies: ['d2'] },
{ id: 'd4', title: 'Sign-off', start: day(5), end: day(5), resourceId: 'd-signoff', milestone: true, color: COLORS.amber, dependencies: ['d3'] },
{ id: 'd5', title: 'Go live', start: day(6), end: day(7), resourceId: 'd-live', color: COLORS.teal, dependencies: ['d4'] },
];
// --- Day schedule: times as instants (UTC), shown in the chosen time zone
export const SHIFT_RESOURCES: WdsGanttResource[] = [
{
id: 'support',
title: 'Support rota',
children: [
{ id: 'emea', title: 'EMEA desk', color: COLORS.blue },
{ id: 'amer', title: 'Americas desk', color: COLORS.green },
{ id: 'apac', title: 'APAC desk', color: COLORS.amber },
],
},
];
const at = (offset: number, time: string) => `${day(offset)}T${time}:00Z`;
export const SHIFT_EVENTS: WdsGanttEvent[] = [
{ id: 'e1', title: 'Morning shift', start: at(0, '06:00'), end: at(0, '10:00'), resourceId: 'emea' },
{ id: 'e2', title: 'Release call', start: at(0, '11:00'), end: at(0, '11:45'), resourceId: 'emea', color: COLORS.rose },
{ id: 'e3', title: 'Afternoon shift', start: at(0, '12:00'), end: at(0, '16:00'), resourceId: 'emea' },
{ id: 'a1', title: 'Day shift', start: at(0, '13:00'), end: at(0, '21:00'), resourceId: 'amer' },
{ id: 'p1', title: 'Night shift', start: at(-1, '23:00'), end: at(0, '07:00'), resourceId: 'apac' },
{ id: 'p2', title: 'Handover', start: at(0, '07:00'), end: at(0, '07:00'), resourceId: 'apac', milestone: true },
];
export const TIME_ZONES = ['UTC', 'Europe/Warsaw', 'America/New_York', 'Asia/Tokyo'];
// --- Recurring: team rituals
export const RITUAL_RESOURCES: WdsGanttResource[] = [
{
id: 'rituals',
title: 'Team rituals',
children: [
{ id: 'standup', title: 'Standup', color: COLORS.sky },
{ id: 'review', title: 'Sprint review', color: COLORS.violet },
{ id: 'retro', title: 'Retrospective', color: COLORS.rose },
{ id: 'planning-r', title: 'Planning', color: COLORS.green },
],
},
];
export const RITUAL_EVENTS: WdsGanttEvent[] = [
{ id: 'standup', title: 'Standup', start: day(-28), end: day(-28), resourceId: 'standup', recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR' },
{ id: 'review', title: 'Sprint review', start: day(-25), end: day(-25), resourceId: 'review', recurrence: { freq: 'weekly', interval: 2 } },
{ id: 'retro', title: 'Retro', start: day(-24), end: day(-24), resourceId: 'retro', recurrence: 'FREQ=WEEKLY;INTERVAL=2' },
{ id: 'planning-r', title: 'Planning', start: day(-21), end: day(-20), resourceId: 'planning-r', recurrence: 'FREQ=WEEKLY;INTERVAL=2;COUNT=6' },
];
// --- Backlog: selection, menus, moving rows
export const BACKLOG_RESOURCES: WdsGanttResource[] = [
{
id: 'sprint-1',
title: 'Sprint 1',
children: [
{ id: 'login', title: 'Login' },
{ id: 'signup', title: 'Sign-up' },
],
},
{
id: 'sprint-2',
title: 'Sprint 2',
children: [
{ id: 'dashboard', title: 'Dashboard' },
{ id: 'reports', title: 'Reports' },
],
},
{ id: 'icebox', title: 'Icebox', children: [{ id: 'dark-mode', title: 'Dark mode' }] },
];
export const BACKLOG_EVENTS: WdsGanttEvent[] = [
{ id: 'k1', title: 'Login form', start: day(-6), end: day(-2), resourceId: 'login', progress: 100, color: COLORS.blue },
{ id: 'k2', title: 'Sign-up flow', start: day(-3), end: day(3), resourceId: 'signup', progress: 50, color: COLORS.sky },
{ id: 'k3', title: 'Dashboard', start: day(4), end: day(11), resourceId: 'dashboard', color: COLORS.green },
{ id: 'k4', title: 'Reports', start: day(8), end: day(15), resourceId: 'reports', color: COLORS.teal },
{ id: 'k5', title: 'Dark mode', start: day(18), end: day(24), resourceId: 'dark-mode', color: COLORS.violet },
];Status report#
columns adds columns to the tree panel after the name; a column reads resource[key] or returns text or a DOM node from render (here a wds-badge). readonly turns off moving, resizing and deleting (a task can also have readOnly). A task with milestone is a diamond. --wds-gantt-tree-width widens the tree panel.
import { Component } from '@angular/core';
import { WdsGanttComponent } from '@wds/angular/gantt';
import { STATUS_COLUMNS, STATUS_EVENTS, STATUS_RESOURCES } from './gantt-data';
@Component({
selector: 'app-gantt-status-demo',
host: { class: 'block w-full' },
imports: [WdsGanttComponent],
template: `
<wds-gantt
class="h-[320px] [--wds-gantt-tree-width:24rem]"
label="Launch status"
name-header="Task"
readonly
[columns]="columns"
[resources]="resources"
[events]="events"
/>
`,
})
export class GanttStatusDemo {
protected readonly columns = STATUS_COLUMNS;
protected readonly resources = STATUS_RESOURCES;
protected readonly events = STATUS_EVENTS;
}import '@wds/core/avatar';
import '@wds/core/badge';
import type { WdsGanttColumn, WdsGanttEvent, WdsGanttResource } from '@wds/core/gantt';
const pad = (n: number) => String(n).padStart(2, '0');
const base = new Date();
/** An ISO date `offset` days from today, so the examples always show the current period. */
export function day(offset: number): string {
const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + offset);
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** An ISO date moved by `days`. */
export function addDays(iso: string, days: number): string {
const [y, m, d] = iso.split('-').map(Number);
const date = new Date(y, m - 1, d + days);
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
/** Bar colors (any CSS color works). */
export const COLORS = {
blue: 'oklch(0.623 0.214 259.815)',
purple: 'oklch(0.627 0.265 303.9)',
violet: 'oklch(0.606 0.25 292.717)',
green: 'oklch(0.696 0.17 162.48)',
teal: 'oklch(0.704 0.14 182.503)',
sky: 'oklch(0.685 0.169 237.323)',
amber: 'oklch(0.769 0.188 70.08)',
rose: 'oklch(0.645 0.246 16.439)',
};
// --- Default: a website project
export const PROJECT_RESOURCES: WdsGanttResource[] = [
{
id: 'planning',
title: 'Planning',
children: [
{ id: 'brief', title: 'Project brief' },
{ id: 'scope', title: 'Scope review' },
],
},
{
id: 'design',
title: 'Design',
children: [
{ id: 'wireframes', title: 'Wireframes' },
{ id: 'visual-design', title: 'Visual design' },
],
},
{
id: 'build',
title: 'Build',
children: [
{ id: 'frontend', title: 'Frontend' },
{ id: 'backend', title: 'Backend' },
{ id: 'qa', title: 'QA pass' },
],
},
];
export const PROJECT_EVENTS: WdsGanttEvent[] = [
{ id: 'brief', title: 'Project brief', start: day(-20), end: day(-16), resourceId: 'brief', progress: 100, color: COLORS.blue },
{ id: 'scope', title: 'Scope review', start: day(-15), end: day(-11), resourceId: 'scope', progress: 100, color: COLORS.sky },
{ id: 'wireframes', title: 'Wireframes', start: day(-9), end: day(-4), resourceId: 'wireframes', progress: 100, color: COLORS.violet },
{ id: 'visual', title: 'Visual design', start: day(-3), end: day(2), resourceId: 'visual-design', progress: 40, color: COLORS.purple },
{ id: 'frontend', title: 'Frontend', start: day(0), end: day(12), resourceId: 'frontend', progress: 10, color: COLORS.green },
{ id: 'backend', title: 'Backend', start: day(2), end: day(14), resourceId: 'backend', color: COLORS.teal },
{ id: 'qa', title: 'QA pass', start: day(13), end: day(17), resourceId: 'qa', color: COLORS.amber },
];
/** Dependencies and planned windows added to the project (for the settings example). */
const PLAN: Record<string, Partial<WdsGanttEvent>> = {
scope: { dependencies: ['brief'] },
wireframes: { dependencies: ['scope'], baselineStart: day(-10), baselineEnd: day(-6) },
visual: { dependencies: ['wireframes'], baselineStart: day(-5), baselineEnd: day(1) },
frontend: { dependencies: ['visual'] },
qa: { dependencies: ['backend'] },
};
export const PROJECT_PLAN_EVENTS: WdsGanttEvent[] = PROJECT_EVENTS.map((e) => ({ ...e, ...PLAN[e.id] }));
// --- Roadmap: initiatives on the quarter scale
export const ROADMAP_RESOURCES: WdsGanttResource[] = [
{
id: 'platform',
title: 'Platform',
children: [
{ id: 'auth', title: 'Auth revamp' },
{ id: 'api', title: 'API v2' },
],
},
{
id: 'growth',
title: 'Growth',
children: [
{ id: 'onboarding', title: 'Onboarding flow' },
{ id: 'referral', title: 'Referral program' },
],
},
{ id: 'backlog', title: 'Backlog', children: [{ id: 'next', title: 'Next initiative' }] },
];
export const ROADMAP_EVENTS: WdsGanttEvent[] = [
{ id: 'r1', title: 'Auth revamp', start: day(-40), end: day(25), resourceId: 'auth', progress: 60, color: COLORS.blue },
{ id: 'r2', title: 'API v2', start: day(15), end: day(70), resourceId: 'api', color: COLORS.sky },
{ id: 'r3', title: 'Onboarding flow', start: day(-30), end: day(10), resourceId: 'onboarding', progress: 85, color: COLORS.green },
{ id: 'r4', title: 'Referral program', start: day(60), end: day(110), resourceId: 'referral', color: COLORS.teal },
];
// --- Team capacity: people grouped by squad, several assignments per person
export const TEAM_RESOURCES: WdsGanttResource[] = [
{
id: 'product',
title: 'Product squad',
children: [
{ id: 'ada', title: 'Ada Lovelace', color: COLORS.blue },
{ id: 'alan', title: 'Alan Turing', color: COLORS.sky },
],
},
{
id: 'design-squad',
title: 'Design squad',
children: [
{ id: 'grace', title: 'Grace Hopper', color: COLORS.violet },
{ id: 'linus', title: 'Linus Torvalds', color: COLORS.purple },
],
},
];
export const TEAM_EVENTS: WdsGanttEvent[] = [
{ id: 't1', title: 'Checkout', start: day(-3), end: day(1), resourceId: 'ada' },
{ id: 't2', title: 'Code review', start: day(0), end: day(3), resourceId: 'ada', color: COLORS.amber },
{ id: 't3', title: 'Billing API', start: day(-2), end: day(-1), resourceId: 'alan' },
{ id: 't4', title: 'Design tokens', start: day(-4), end: day(0), resourceId: 'grace' },
{ id: 't5', title: 'Icon set', start: day(2), end: day(5), resourceId: 'linus' },
];
/**
* Row label with an avatar (`renderResourceLabel`). It renders in the shadow DOM of the chart,
* so it is styled inline (Tailwind classes do not reach it).
*/
export function avatarLabel(resource: WdsGanttResource): string | Node {
if (resource.children?.length) return resource.title;
const label = document.createElement('span');
label.style.cssText = 'display:inline-flex;align-items:center;gap:8px;min-width:0';
// wds-avatar with initials; the row color reaches the fallback through the tokens it uses
const avatar = document.createElement('wds-avatar');
avatar.size = 'sm';
avatar.setAttribute('aria-hidden', 'true');
const color = resource.color ?? COLORS.blue;
avatar.style.setProperty('--wds-muted', `color-mix(in srgb, ${color} 22%, transparent)`);
avatar.style.setProperty('--wds-foreground', `color-mix(in srgb, ${color} 45%, currentColor)`);
avatar.textContent = resource.title
.split(' ')
.map((part) => part[0])
.join('')
.slice(0, 2);
const name = document.createElement('span');
name.style.cssText = 'overflow:hidden;text-overflow:ellipsis';
name.textContent = resource.title;
label.append(avatar, name);
return label;
}
// --- Status report: extra columns in the tree panel
export type Status = 'done' | 'on-track' | 'at-risk';
const STATUS_LABEL: Record<Status, string> = { done: 'Done', 'on-track': 'On track', 'at-risk': 'At risk' };
const STATUS_VARIANT: Record<Status, string> = { done: 'secondary', 'on-track': 'outline', 'at-risk': 'destructive' };
/** Content of a status cell (`render` of a column): a DOM node, here a `wds-badge`. */
export function statusBadge(status: Status): HTMLElement {
const el = document.createElement('wds-badge');
el.setAttribute('variant', STATUS_VARIANT[status]);
el.textContent = STATUS_LABEL[status];
return el;
}
export const STATUS_COLUMNS: WdsGanttColumn[] = [
{ key: 'owner', header: 'Owner', width: '5.5rem' },
{ key: 'status', header: 'Status', width: '6.5rem', render: (r) => statusBadge(r['status'] as Status) },
];
export const STATUS_RESOURCES: WdsGanttResource[] = [
{
id: 'launch',
title: 'Launch',
owner: 'Ana',
status: 'on-track',
children: [
{ id: 'brief-s', title: 'Brief', owner: 'Ana', status: 'done' },
{ id: 'content', title: 'Content', owner: 'Ben', status: 'on-track' },
{ id: 'campaign', title: 'Campaign', owner: 'Chloe', status: 'at-risk' },
{ id: 'release', title: 'Release', owner: 'David', status: 'on-track' },
],
},
];
export const STATUS_EVENTS: WdsGanttEvent[] = [
{ id: 's1', title: 'Brief', start: day(-14), end: day(-8), resourceId: 'brief-s', progress: 100, color: COLORS.blue },
{ id: 's2', title: 'Content', start: day(-7), end: day(5), resourceId: 'content', progress: 55, color: COLORS.green },
{ id: 's3', title: 'Campaign', start: day(-2), end: day(12), resourceId: 'campaign', progress: 10, color: COLORS.rose },
{ id: 's4', title: 'Release', start: day(14), end: day(14), resourceId: 'release', milestone: true, color: COLORS.amber },
];
// --- Baseline: planned against actual dates
export const BASELINE_RESOURCES: WdsGanttResource[] = [
{
id: 'implementation',
title: 'Implementation',
baselineStart: day(-8),
baselineEnd: day(4),
children: [
{ id: 'b-api', title: 'API integration' },
{ id: 'b-data', title: 'Data migration' },
{ id: 'b-security', title: 'Security hardening' },
],
},
{ id: 'launch-b', title: 'Launch', children: [{ id: 'b-handover', title: 'Handover' }] },
];
export const BASELINE_EVENTS: WdsGanttEvent[] = [
{ id: 'b1', title: 'API integration', start: day(-8), end: day(-3), resourceId: 'b-api', progress: 100, color: COLORS.violet, baselineStart: day(-8), baselineEnd: day(-5) },
{ id: 'b2', title: 'Data migration', start: day(-3), end: day(2), resourceId: 'b-data', progress: 70, color: COLORS.green, baselineStart: day(-3), baselineEnd: day(1) },
{ id: 'b3', title: 'Security hardening', start: day(1), end: day(6), resourceId: 'b-security', progress: 20, color: COLORS.teal, baselineStart: day(-1), baselineEnd: day(4) },
{ id: 'b4', title: 'Handover', start: day(8), end: day(8), resourceId: 'b-handover', milestone: true, color: COLORS.amber, baselineStart: day(6), baselineEnd: day(6) },
];
// --- Dependencies: a chained rollout plan
export const ROLLOUT_RESOURCES: WdsGanttResource[] = [
{
id: 'build-d',
title: 'Build',
children: [
{ id: 'd-frontend', title: 'Frontend' },
{ id: 'd-qa', title: 'QA pass' },
],
},
{
id: 'rollout',
title: 'Rollout',
children: [
{ id: 'd-staging', title: 'Staging deploy' },
{ id: 'd-signoff', title: 'Sign-off' },
{ id: 'd-live', title: 'Go live' },
],
},
];
export const ROLLOUT_EVENTS: WdsGanttEvent[] = [
{ id: 'd1', title: 'Frontend', start: day(-8), end: day(-4), resourceId: 'd-frontend', progress: 100, color: COLORS.violet },
{ id: 'd2', title: 'QA pass', start: day(-2), end: day(1), resourceId: 'd-qa', progress: 40, color: COLORS.purple, dependencies: ['d1'] },
{ id: 'd3', title: 'Staging deploy', start: day(2), end: day(3), resourceId: 'd-staging', color: COLORS.green, dependencies: ['d2'] },
{ id: 'd4', title: 'Sign-off', start: day(5), end: day(5), resourceId: 'd-signoff', milestone: true, color: COLORS.amber, dependencies: ['d3'] },
{ id: 'd5', title: 'Go live', start: day(6), end: day(7), resourceId: 'd-live', color: COLORS.teal, dependencies: ['d4'] },
];
// --- Day schedule: times as instants (UTC), shown in the chosen time zone
export const SHIFT_RESOURCES: WdsGanttResource[] = [
{
id: 'support',
title: 'Support rota',
children: [
{ id: 'emea', title: 'EMEA desk', color: COLORS.blue },
{ id: 'amer', title: 'Americas desk', color: COLORS.green },
{ id: 'apac', title: 'APAC desk', color: COLORS.amber },
],
},
];
const at = (offset: number, time: string) => `${day(offset)}T${time}:00Z`;
export const SHIFT_EVENTS: WdsGanttEvent[] = [
{ id: 'e1', title: 'Morning shift', start: at(0, '06:00'), end: at(0, '10:00'), resourceId: 'emea' },
{ id: 'e2', title: 'Release call', start: at(0, '11:00'), end: at(0, '11:45'), resourceId: 'emea', color: COLORS.rose },
{ id: 'e3', title: 'Afternoon shift', start: at(0, '12:00'), end: at(0, '16:00'), resourceId: 'emea' },
{ id: 'a1', title: 'Day shift', start: at(0, '13:00'), end: at(0, '21:00'), resourceId: 'amer' },
{ id: 'p1', title: 'Night shift', start: at(-1, '23:00'), end: at(0, '07:00'), resourceId: 'apac' },
{ id: 'p2', title: 'Handover', start: at(0, '07:00'), end: at(0, '07:00'), resourceId: 'apac', milestone: true },
];
export const TIME_ZONES = ['UTC', 'Europe/Warsaw', 'America/New_York', 'Asia/Tokyo'];
// --- Recurring: team rituals
export const RITUAL_RESOURCES: WdsGanttResource[] = [
{
id: 'rituals',
title: 'Team rituals',
children: [
{ id: 'standup', title: 'Standup', color: COLORS.sky },
{ id: 'review', title: 'Sprint review', color: COLORS.violet },
{ id: 'retro', title: 'Retrospective', color: COLORS.rose },
{ id: 'planning-r', title: 'Planning', color: COLORS.green },
],
},
];
export const RITUAL_EVENTS: WdsGanttEvent[] = [
{ id: 'standup', title: 'Standup', start: day(-28), end: day(-28), resourceId: 'standup', recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR' },
{ id: 'review', title: 'Sprint review', start: day(-25), end: day(-25), resourceId: 'review', recurrence: { freq: 'weekly', interval: 2 } },
{ id: 'retro', title: 'Retro', start: day(-24), end: day(-24), resourceId: 'retro', recurrence: 'FREQ=WEEKLY;INTERVAL=2' },
{ id: 'planning-r', title: 'Planning', start: day(-21), end: day(-20), resourceId: 'planning-r', recurrence: 'FREQ=WEEKLY;INTERVAL=2;COUNT=6' },
];
// --- Backlog: selection, menus, moving rows
export const BACKLOG_RESOURCES: WdsGanttResource[] = [
{
id: 'sprint-1',
title: 'Sprint 1',
children: [
{ id: 'login', title: 'Login' },
{ id: 'signup', title: 'Sign-up' },
],
},
{
id: 'sprint-2',
title: 'Sprint 2',
children: [
{ id: 'dashboard', title: 'Dashboard' },
{ id: 'reports', title: 'Reports' },
],
},
{ id: 'icebox', title: 'Icebox', children: [{ id: 'dark-mode', title: 'Dark mode' }] },
];
export const BACKLOG_EVENTS: WdsGanttEvent[] = [
{ id: 'k1', title: 'Login form', start: day(-6), end: day(-2), resourceId: 'login', progress: 100, color: COLORS.blue },
{ id: 'k2', title: 'Sign-up flow', start: day(-3), end: day(3), resourceId: 'signup', progress: 50, color: COLORS.sky },
{ id: 'k3', title: 'Dashboard', start: day(4), end: day(11), resourceId: 'dashboard', color: COLORS.green },
{ id: 'k4', title: 'Reports', start: day(8), end: day(15), resourceId: 'reports', color: COLORS.teal },
{ id: 'k5', title: 'Dark mode', start: day(18), end: day(24), resourceId: 'dark-mode', color: COLORS.violet },
];Baseline#
baselineStart and baselineEnd draw the planned window as an outline behind the actual bar; on a group resource they outline the plan of the whole group. A bar gets data-baseline-variance (early, late or on-time), and the variance is part of its accessible name. hide-baseline hides the plans.
import { Component } from '@angular/core';
import { WdsGanttComponent } from '@wds/angular/gantt';
import { BASELINE_EVENTS, BASELINE_RESOURCES } from './gantt-data';
@Component({
selector: 'app-gantt-baseline-demo',
host: { class: 'block w-full' },
imports: [WdsGanttComponent],
template: `<wds-gantt class="h-[340px]" label="Implementation plan" off-days [resources]="resources" [events]="events" />`,
})
export class GanttBaselineDemo {
protected readonly resources = BASELINE_RESOURCES;
protected readonly events = BASELINE_EVENTS;
}import '@wds/core/avatar';
import '@wds/core/badge';
import type { WdsGanttColumn, WdsGanttEvent, WdsGanttResource } from '@wds/core/gantt';
const pad = (n: number) => String(n).padStart(2, '0');
const base = new Date();
/** An ISO date `offset` days from today, so the examples always show the current period. */
export function day(offset: number): string {
const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + offset);
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** An ISO date moved by `days`. */
export function addDays(iso: string, days: number): string {
const [y, m, d] = iso.split('-').map(Number);
const date = new Date(y, m - 1, d + days);
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
/** Bar colors (any CSS color works). */
export const COLORS = {
blue: 'oklch(0.623 0.214 259.815)',
purple: 'oklch(0.627 0.265 303.9)',
violet: 'oklch(0.606 0.25 292.717)',
green: 'oklch(0.696 0.17 162.48)',
teal: 'oklch(0.704 0.14 182.503)',
sky: 'oklch(0.685 0.169 237.323)',
amber: 'oklch(0.769 0.188 70.08)',
rose: 'oklch(0.645 0.246 16.439)',
};
// --- Default: a website project
export const PROJECT_RESOURCES: WdsGanttResource[] = [
{
id: 'planning',
title: 'Planning',
children: [
{ id: 'brief', title: 'Project brief' },
{ id: 'scope', title: 'Scope review' },
],
},
{
id: 'design',
title: 'Design',
children: [
{ id: 'wireframes', title: 'Wireframes' },
{ id: 'visual-design', title: 'Visual design' },
],
},
{
id: 'build',
title: 'Build',
children: [
{ id: 'frontend', title: 'Frontend' },
{ id: 'backend', title: 'Backend' },
{ id: 'qa', title: 'QA pass' },
],
},
];
export const PROJECT_EVENTS: WdsGanttEvent[] = [
{ id: 'brief', title: 'Project brief', start: day(-20), end: day(-16), resourceId: 'brief', progress: 100, color: COLORS.blue },
{ id: 'scope', title: 'Scope review', start: day(-15), end: day(-11), resourceId: 'scope', progress: 100, color: COLORS.sky },
{ id: 'wireframes', title: 'Wireframes', start: day(-9), end: day(-4), resourceId: 'wireframes', progress: 100, color: COLORS.violet },
{ id: 'visual', title: 'Visual design', start: day(-3), end: day(2), resourceId: 'visual-design', progress: 40, color: COLORS.purple },
{ id: 'frontend', title: 'Frontend', start: day(0), end: day(12), resourceId: 'frontend', progress: 10, color: COLORS.green },
{ id: 'backend', title: 'Backend', start: day(2), end: day(14), resourceId: 'backend', color: COLORS.teal },
{ id: 'qa', title: 'QA pass', start: day(13), end: day(17), resourceId: 'qa', color: COLORS.amber },
];
/** Dependencies and planned windows added to the project (for the settings example). */
const PLAN: Record<string, Partial<WdsGanttEvent>> = {
scope: { dependencies: ['brief'] },
wireframes: { dependencies: ['scope'], baselineStart: day(-10), baselineEnd: day(-6) },
visual: { dependencies: ['wireframes'], baselineStart: day(-5), baselineEnd: day(1) },
frontend: { dependencies: ['visual'] },
qa: { dependencies: ['backend'] },
};
export const PROJECT_PLAN_EVENTS: WdsGanttEvent[] = PROJECT_EVENTS.map((e) => ({ ...e, ...PLAN[e.id] }));
// --- Roadmap: initiatives on the quarter scale
export const ROADMAP_RESOURCES: WdsGanttResource[] = [
{
id: 'platform',
title: 'Platform',
children: [
{ id: 'auth', title: 'Auth revamp' },
{ id: 'api', title: 'API v2' },
],
},
{
id: 'growth',
title: 'Growth',
children: [
{ id: 'onboarding', title: 'Onboarding flow' },
{ id: 'referral', title: 'Referral program' },
],
},
{ id: 'backlog', title: 'Backlog', children: [{ id: 'next', title: 'Next initiative' }] },
];
export const ROADMAP_EVENTS: WdsGanttEvent[] = [
{ id: 'r1', title: 'Auth revamp', start: day(-40), end: day(25), resourceId: 'auth', progress: 60, color: COLORS.blue },
{ id: 'r2', title: 'API v2', start: day(15), end: day(70), resourceId: 'api', color: COLORS.sky },
{ id: 'r3', title: 'Onboarding flow', start: day(-30), end: day(10), resourceId: 'onboarding', progress: 85, color: COLORS.green },
{ id: 'r4', title: 'Referral program', start: day(60), end: day(110), resourceId: 'referral', color: COLORS.teal },
];
// --- Team capacity: people grouped by squad, several assignments per person
export const TEAM_RESOURCES: WdsGanttResource[] = [
{
id: 'product',
title: 'Product squad',
children: [
{ id: 'ada', title: 'Ada Lovelace', color: COLORS.blue },
{ id: 'alan', title: 'Alan Turing', color: COLORS.sky },
],
},
{
id: 'design-squad',
title: 'Design squad',
children: [
{ id: 'grace', title: 'Grace Hopper', color: COLORS.violet },
{ id: 'linus', title: 'Linus Torvalds', color: COLORS.purple },
],
},
];
export const TEAM_EVENTS: WdsGanttEvent[] = [
{ id: 't1', title: 'Checkout', start: day(-3), end: day(1), resourceId: 'ada' },
{ id: 't2', title: 'Code review', start: day(0), end: day(3), resourceId: 'ada', color: COLORS.amber },
{ id: 't3', title: 'Billing API', start: day(-2), end: day(-1), resourceId: 'alan' },
{ id: 't4', title: 'Design tokens', start: day(-4), end: day(0), resourceId: 'grace' },
{ id: 't5', title: 'Icon set', start: day(2), end: day(5), resourceId: 'linus' },
];
/**
* Row label with an avatar (`renderResourceLabel`). It renders in the shadow DOM of the chart,
* so it is styled inline (Tailwind classes do not reach it).
*/
export function avatarLabel(resource: WdsGanttResource): string | Node {
if (resource.children?.length) return resource.title;
const label = document.createElement('span');
label.style.cssText = 'display:inline-flex;align-items:center;gap:8px;min-width:0';
// wds-avatar with initials; the row color reaches the fallback through the tokens it uses
const avatar = document.createElement('wds-avatar');
avatar.size = 'sm';
avatar.setAttribute('aria-hidden', 'true');
const color = resource.color ?? COLORS.blue;
avatar.style.setProperty('--wds-muted', `color-mix(in srgb, ${color} 22%, transparent)`);
avatar.style.setProperty('--wds-foreground', `color-mix(in srgb, ${color} 45%, currentColor)`);
avatar.textContent = resource.title
.split(' ')
.map((part) => part[0])
.join('')
.slice(0, 2);
const name = document.createElement('span');
name.style.cssText = 'overflow:hidden;text-overflow:ellipsis';
name.textContent = resource.title;
label.append(avatar, name);
return label;
}
// --- Status report: extra columns in the tree panel
export type Status = 'done' | 'on-track' | 'at-risk';
const STATUS_LABEL: Record<Status, string> = { done: 'Done', 'on-track': 'On track', 'at-risk': 'At risk' };
const STATUS_VARIANT: Record<Status, string> = { done: 'secondary', 'on-track': 'outline', 'at-risk': 'destructive' };
/** Content of a status cell (`render` of a column): a DOM node, here a `wds-badge`. */
export function statusBadge(status: Status): HTMLElement {
const el = document.createElement('wds-badge');
el.setAttribute('variant', STATUS_VARIANT[status]);
el.textContent = STATUS_LABEL[status];
return el;
}
export const STATUS_COLUMNS: WdsGanttColumn[] = [
{ key: 'owner', header: 'Owner', width: '5.5rem' },
{ key: 'status', header: 'Status', width: '6.5rem', render: (r) => statusBadge(r['status'] as Status) },
];
export const STATUS_RESOURCES: WdsGanttResource[] = [
{
id: 'launch',
title: 'Launch',
owner: 'Ana',
status: 'on-track',
children: [
{ id: 'brief-s', title: 'Brief', owner: 'Ana', status: 'done' },
{ id: 'content', title: 'Content', owner: 'Ben', status: 'on-track' },
{ id: 'campaign', title: 'Campaign', owner: 'Chloe', status: 'at-risk' },
{ id: 'release', title: 'Release', owner: 'David', status: 'on-track' },
],
},
];
export const STATUS_EVENTS: WdsGanttEvent[] = [
{ id: 's1', title: 'Brief', start: day(-14), end: day(-8), resourceId: 'brief-s', progress: 100, color: COLORS.blue },
{ id: 's2', title: 'Content', start: day(-7), end: day(5), resourceId: 'content', progress: 55, color: COLORS.green },
{ id: 's3', title: 'Campaign', start: day(-2), end: day(12), resourceId: 'campaign', progress: 10, color: COLORS.rose },
{ id: 's4', title: 'Release', start: day(14), end: day(14), resourceId: 'release', milestone: true, color: COLORS.amber },
];
// --- Baseline: planned against actual dates
export const BASELINE_RESOURCES: WdsGanttResource[] = [
{
id: 'implementation',
title: 'Implementation',
baselineStart: day(-8),
baselineEnd: day(4),
children: [
{ id: 'b-api', title: 'API integration' },
{ id: 'b-data', title: 'Data migration' },
{ id: 'b-security', title: 'Security hardening' },
],
},
{ id: 'launch-b', title: 'Launch', children: [{ id: 'b-handover', title: 'Handover' }] },
];
export const BASELINE_EVENTS: WdsGanttEvent[] = [
{ id: 'b1', title: 'API integration', start: day(-8), end: day(-3), resourceId: 'b-api', progress: 100, color: COLORS.violet, baselineStart: day(-8), baselineEnd: day(-5) },
{ id: 'b2', title: 'Data migration', start: day(-3), end: day(2), resourceId: 'b-data', progress: 70, color: COLORS.green, baselineStart: day(-3), baselineEnd: day(1) },
{ id: 'b3', title: 'Security hardening', start: day(1), end: day(6), resourceId: 'b-security', progress: 20, color: COLORS.teal, baselineStart: day(-1), baselineEnd: day(4) },
{ id: 'b4', title: 'Handover', start: day(8), end: day(8), resourceId: 'b-handover', milestone: true, color: COLORS.amber, baselineStart: day(6), baselineEnd: day(6) },
];
// --- Dependencies: a chained rollout plan
export const ROLLOUT_RESOURCES: WdsGanttResource[] = [
{
id: 'build-d',
title: 'Build',
children: [
{ id: 'd-frontend', title: 'Frontend' },
{ id: 'd-qa', title: 'QA pass' },
],
},
{
id: 'rollout',
title: 'Rollout',
children: [
{ id: 'd-staging', title: 'Staging deploy' },
{ id: 'd-signoff', title: 'Sign-off' },
{ id: 'd-live', title: 'Go live' },
],
},
];
export const ROLLOUT_EVENTS: WdsGanttEvent[] = [
{ id: 'd1', title: 'Frontend', start: day(-8), end: day(-4), resourceId: 'd-frontend', progress: 100, color: COLORS.violet },
{ id: 'd2', title: 'QA pass', start: day(-2), end: day(1), resourceId: 'd-qa', progress: 40, color: COLORS.purple, dependencies: ['d1'] },
{ id: 'd3', title: 'Staging deploy', start: day(2), end: day(3), resourceId: 'd-staging', color: COLORS.green, dependencies: ['d2'] },
{ id: 'd4', title: 'Sign-off', start: day(5), end: day(5), resourceId: 'd-signoff', milestone: true, color: COLORS.amber, dependencies: ['d3'] },
{ id: 'd5', title: 'Go live', start: day(6), end: day(7), resourceId: 'd-live', color: COLORS.teal, dependencies: ['d4'] },
];
// --- Day schedule: times as instants (UTC), shown in the chosen time zone
export const SHIFT_RESOURCES: WdsGanttResource[] = [
{
id: 'support',
title: 'Support rota',
children: [
{ id: 'emea', title: 'EMEA desk', color: COLORS.blue },
{ id: 'amer', title: 'Americas desk', color: COLORS.green },
{ id: 'apac', title: 'APAC desk', color: COLORS.amber },
],
},
];
const at = (offset: number, time: string) => `${day(offset)}T${time}:00Z`;
export const SHIFT_EVENTS: WdsGanttEvent[] = [
{ id: 'e1', title: 'Morning shift', start: at(0, '06:00'), end: at(0, '10:00'), resourceId: 'emea' },
{ id: 'e2', title: 'Release call', start: at(0, '11:00'), end: at(0, '11:45'), resourceId: 'emea', color: COLORS.rose },
{ id: 'e3', title: 'Afternoon shift', start: at(0, '12:00'), end: at(0, '16:00'), resourceId: 'emea' },
{ id: 'a1', title: 'Day shift', start: at(0, '13:00'), end: at(0, '21:00'), resourceId: 'amer' },
{ id: 'p1', title: 'Night shift', start: at(-1, '23:00'), end: at(0, '07:00'), resourceId: 'apac' },
{ id: 'p2', title: 'Handover', start: at(0, '07:00'), end: at(0, '07:00'), resourceId: 'apac', milestone: true },
];
export const TIME_ZONES = ['UTC', 'Europe/Warsaw', 'America/New_York', 'Asia/Tokyo'];
// --- Recurring: team rituals
export const RITUAL_RESOURCES: WdsGanttResource[] = [
{
id: 'rituals',
title: 'Team rituals',
children: [
{ id: 'standup', title: 'Standup', color: COLORS.sky },
{ id: 'review', title: 'Sprint review', color: COLORS.violet },
{ id: 'retro', title: 'Retrospective', color: COLORS.rose },
{ id: 'planning-r', title: 'Planning', color: COLORS.green },
],
},
];
export const RITUAL_EVENTS: WdsGanttEvent[] = [
{ id: 'standup', title: 'Standup', start: day(-28), end: day(-28), resourceId: 'standup', recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR' },
{ id: 'review', title: 'Sprint review', start: day(-25), end: day(-25), resourceId: 'review', recurrence: { freq: 'weekly', interval: 2 } },
{ id: 'retro', title: 'Retro', start: day(-24), end: day(-24), resourceId: 'retro', recurrence: 'FREQ=WEEKLY;INTERVAL=2' },
{ id: 'planning-r', title: 'Planning', start: day(-21), end: day(-20), resourceId: 'planning-r', recurrence: 'FREQ=WEEKLY;INTERVAL=2;COUNT=6' },
];
// --- Backlog: selection, menus, moving rows
export const BACKLOG_RESOURCES: WdsGanttResource[] = [
{
id: 'sprint-1',
title: 'Sprint 1',
children: [
{ id: 'login', title: 'Login' },
{ id: 'signup', title: 'Sign-up' },
],
},
{
id: 'sprint-2',
title: 'Sprint 2',
children: [
{ id: 'dashboard', title: 'Dashboard' },
{ id: 'reports', title: 'Reports' },
],
},
{ id: 'icebox', title: 'Icebox', children: [{ id: 'dark-mode', title: 'Dark mode' }] },
];
export const BACKLOG_EVENTS: WdsGanttEvent[] = [
{ id: 'k1', title: 'Login form', start: day(-6), end: day(-2), resourceId: 'login', progress: 100, color: COLORS.blue },
{ id: 'k2', title: 'Sign-up flow', start: day(-3), end: day(3), resourceId: 'signup', progress: 50, color: COLORS.sky },
{ id: 'k3', title: 'Dashboard', start: day(4), end: day(11), resourceId: 'dashboard', color: COLORS.green },
{ id: 'k4', title: 'Reports', start: day(8), end: day(15), resourceId: 'reports', color: COLORS.teal },
{ id: 'k5', title: 'Dark mode', start: day(18), end: day(24), resourceId: 'dark-mode', color: COLORS.violet },
];Dependencies and milestones#
dependencies lists the ids of the predecessors of a task: the chart draws a finish-to-start arrow from each of them (hide-dependencies hides the arrows). canDropEvent validates a move or resize before it is applied: here a task cannot start before its predecessors end or end after its successors start.
import { Component, signal } from '@angular/core';
import { WdsGanttComponent } from '@wds/angular/gantt';
import type { WdsGanttEvent } from '@wds/core/gantt';
import { ROLLOUT_EVENTS, ROLLOUT_RESOURCES } from './gantt-data';
@Component({
selector: 'app-gantt-dependencies-demo',
host: { class: 'block w-full' },
imports: [WdsGanttComponent],
template: `
<wds-gantt
class="h-[340px]"
label="Rollout plan"
off-days
[resources]="resources"
[events]="events()"
[canDropEvent]="canDrop"
(wdsEventChange)="events.set($event.events)"
/>
`,
})
export class GanttDependenciesDemo {
protected readonly resources = ROLLOUT_RESOURCES;
protected readonly events = signal<WdsGanttEvent[]>(ROLLOUT_EVENTS);
/** A task starts after its predecessors end and ends before its successors start (ISO dates compare as text). */
protected readonly canDrop = ({ event }: { event: WdsGanttEvent }): boolean => {
const events = this.events();
const predecessors = events.filter((e) => event.dependencies?.includes(e.id));
const successors = events.filter((e) => e.dependencies?.includes(event.id));
return predecessors.every((p) => p.end < event.start) && successors.every((s) => event.end < s.start);
};
}import '@wds/core/avatar';
import '@wds/core/badge';
import type { WdsGanttColumn, WdsGanttEvent, WdsGanttResource } from '@wds/core/gantt';
const pad = (n: number) => String(n).padStart(2, '0');
const base = new Date();
/** An ISO date `offset` days from today, so the examples always show the current period. */
export function day(offset: number): string {
const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + offset);
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** An ISO date moved by `days`. */
export function addDays(iso: string, days: number): string {
const [y, m, d] = iso.split('-').map(Number);
const date = new Date(y, m - 1, d + days);
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
/** Bar colors (any CSS color works). */
export const COLORS = {
blue: 'oklch(0.623 0.214 259.815)',
purple: 'oklch(0.627 0.265 303.9)',
violet: 'oklch(0.606 0.25 292.717)',
green: 'oklch(0.696 0.17 162.48)',
teal: 'oklch(0.704 0.14 182.503)',
sky: 'oklch(0.685 0.169 237.323)',
amber: 'oklch(0.769 0.188 70.08)',
rose: 'oklch(0.645 0.246 16.439)',
};
// --- Default: a website project
export const PROJECT_RESOURCES: WdsGanttResource[] = [
{
id: 'planning',
title: 'Planning',
children: [
{ id: 'brief', title: 'Project brief' },
{ id: 'scope', title: 'Scope review' },
],
},
{
id: 'design',
title: 'Design',
children: [
{ id: 'wireframes', title: 'Wireframes' },
{ id: 'visual-design', title: 'Visual design' },
],
},
{
id: 'build',
title: 'Build',
children: [
{ id: 'frontend', title: 'Frontend' },
{ id: 'backend', title: 'Backend' },
{ id: 'qa', title: 'QA pass' },
],
},
];
export const PROJECT_EVENTS: WdsGanttEvent[] = [
{ id: 'brief', title: 'Project brief', start: day(-20), end: day(-16), resourceId: 'brief', progress: 100, color: COLORS.blue },
{ id: 'scope', title: 'Scope review', start: day(-15), end: day(-11), resourceId: 'scope', progress: 100, color: COLORS.sky },
{ id: 'wireframes', title: 'Wireframes', start: day(-9), end: day(-4), resourceId: 'wireframes', progress: 100, color: COLORS.violet },
{ id: 'visual', title: 'Visual design', start: day(-3), end: day(2), resourceId: 'visual-design', progress: 40, color: COLORS.purple },
{ id: 'frontend', title: 'Frontend', start: day(0), end: day(12), resourceId: 'frontend', progress: 10, color: COLORS.green },
{ id: 'backend', title: 'Backend', start: day(2), end: day(14), resourceId: 'backend', color: COLORS.teal },
{ id: 'qa', title: 'QA pass', start: day(13), end: day(17), resourceId: 'qa', color: COLORS.amber },
];
/** Dependencies and planned windows added to the project (for the settings example). */
const PLAN: Record<string, Partial<WdsGanttEvent>> = {
scope: { dependencies: ['brief'] },
wireframes: { dependencies: ['scope'], baselineStart: day(-10), baselineEnd: day(-6) },
visual: { dependencies: ['wireframes'], baselineStart: day(-5), baselineEnd: day(1) },
frontend: { dependencies: ['visual'] },
qa: { dependencies: ['backend'] },
};
export const PROJECT_PLAN_EVENTS: WdsGanttEvent[] = PROJECT_EVENTS.map((e) => ({ ...e, ...PLAN[e.id] }));
// --- Roadmap: initiatives on the quarter scale
export const ROADMAP_RESOURCES: WdsGanttResource[] = [
{
id: 'platform',
title: 'Platform',
children: [
{ id: 'auth', title: 'Auth revamp' },
{ id: 'api', title: 'API v2' },
],
},
{
id: 'growth',
title: 'Growth',
children: [
{ id: 'onboarding', title: 'Onboarding flow' },
{ id: 'referral', title: 'Referral program' },
],
},
{ id: 'backlog', title: 'Backlog', children: [{ id: 'next', title: 'Next initiative' }] },
];
export const ROADMAP_EVENTS: WdsGanttEvent[] = [
{ id: 'r1', title: 'Auth revamp', start: day(-40), end: day(25), resourceId: 'auth', progress: 60, color: COLORS.blue },
{ id: 'r2', title: 'API v2', start: day(15), end: day(70), resourceId: 'api', color: COLORS.sky },
{ id: 'r3', title: 'Onboarding flow', start: day(-30), end: day(10), resourceId: 'onboarding', progress: 85, color: COLORS.green },
{ id: 'r4', title: 'Referral program', start: day(60), end: day(110), resourceId: 'referral', color: COLORS.teal },
];
// --- Team capacity: people grouped by squad, several assignments per person
export const TEAM_RESOURCES: WdsGanttResource[] = [
{
id: 'product',
title: 'Product squad',
children: [
{ id: 'ada', title: 'Ada Lovelace', color: COLORS.blue },
{ id: 'alan', title: 'Alan Turing', color: COLORS.sky },
],
},
{
id: 'design-squad',
title: 'Design squad',
children: [
{ id: 'grace', title: 'Grace Hopper', color: COLORS.violet },
{ id: 'linus', title: 'Linus Torvalds', color: COLORS.purple },
],
},
];
export const TEAM_EVENTS: WdsGanttEvent[] = [
{ id: 't1', title: 'Checkout', start: day(-3), end: day(1), resourceId: 'ada' },
{ id: 't2', title: 'Code review', start: day(0), end: day(3), resourceId: 'ada', color: COLORS.amber },
{ id: 't3', title: 'Billing API', start: day(-2), end: day(-1), resourceId: 'alan' },
{ id: 't4', title: 'Design tokens', start: day(-4), end: day(0), resourceId: 'grace' },
{ id: 't5', title: 'Icon set', start: day(2), end: day(5), resourceId: 'linus' },
];
/**
* Row label with an avatar (`renderResourceLabel`). It renders in the shadow DOM of the chart,
* so it is styled inline (Tailwind classes do not reach it).
*/
export function avatarLabel(resource: WdsGanttResource): string | Node {
if (resource.children?.length) return resource.title;
const label = document.createElement('span');
label.style.cssText = 'display:inline-flex;align-items:center;gap:8px;min-width:0';
// wds-avatar with initials; the row color reaches the fallback through the tokens it uses
const avatar = document.createElement('wds-avatar');
avatar.size = 'sm';
avatar.setAttribute('aria-hidden', 'true');
const color = resource.color ?? COLORS.blue;
avatar.style.setProperty('--wds-muted', `color-mix(in srgb, ${color} 22%, transparent)`);
avatar.style.setProperty('--wds-foreground', `color-mix(in srgb, ${color} 45%, currentColor)`);
avatar.textContent = resource.title
.split(' ')
.map((part) => part[0])
.join('')
.slice(0, 2);
const name = document.createElement('span');
name.style.cssText = 'overflow:hidden;text-overflow:ellipsis';
name.textContent = resource.title;
label.append(avatar, name);
return label;
}
// --- Status report: extra columns in the tree panel
export type Status = 'done' | 'on-track' | 'at-risk';
const STATUS_LABEL: Record<Status, string> = { done: 'Done', 'on-track': 'On track', 'at-risk': 'At risk' };
const STATUS_VARIANT: Record<Status, string> = { done: 'secondary', 'on-track': 'outline', 'at-risk': 'destructive' };
/** Content of a status cell (`render` of a column): a DOM node, here a `wds-badge`. */
export function statusBadge(status: Status): HTMLElement {
const el = document.createElement('wds-badge');
el.setAttribute('variant', STATUS_VARIANT[status]);
el.textContent = STATUS_LABEL[status];
return el;
}
export const STATUS_COLUMNS: WdsGanttColumn[] = [
{ key: 'owner', header: 'Owner', width: '5.5rem' },
{ key: 'status', header: 'Status', width: '6.5rem', render: (r) => statusBadge(r['status'] as Status) },
];
export const STATUS_RESOURCES: WdsGanttResource[] = [
{
id: 'launch',
title: 'Launch',
owner: 'Ana',
status: 'on-track',
children: [
{ id: 'brief-s', title: 'Brief', owner: 'Ana', status: 'done' },
{ id: 'content', title: 'Content', owner: 'Ben', status: 'on-track' },
{ id: 'campaign', title: 'Campaign', owner: 'Chloe', status: 'at-risk' },
{ id: 'release', title: 'Release', owner: 'David', status: 'on-track' },
],
},
];
export const STATUS_EVENTS: WdsGanttEvent[] = [
{ id: 's1', title: 'Brief', start: day(-14), end: day(-8), resourceId: 'brief-s', progress: 100, color: COLORS.blue },
{ id: 's2', title: 'Content', start: day(-7), end: day(5), resourceId: 'content', progress: 55, color: COLORS.green },
{ id: 's3', title: 'Campaign', start: day(-2), end: day(12), resourceId: 'campaign', progress: 10, color: COLORS.rose },
{ id: 's4', title: 'Release', start: day(14), end: day(14), resourceId: 'release', milestone: true, color: COLORS.amber },
];
// --- Baseline: planned against actual dates
export const BASELINE_RESOURCES: WdsGanttResource[] = [
{
id: 'implementation',
title: 'Implementation',
baselineStart: day(-8),
baselineEnd: day(4),
children: [
{ id: 'b-api', title: 'API integration' },
{ id: 'b-data', title: 'Data migration' },
{ id: 'b-security', title: 'Security hardening' },
],
},
{ id: 'launch-b', title: 'Launch', children: [{ id: 'b-handover', title: 'Handover' }] },
];
export const BASELINE_EVENTS: WdsGanttEvent[] = [
{ id: 'b1', title: 'API integration', start: day(-8), end: day(-3), resourceId: 'b-api', progress: 100, color: COLORS.violet, baselineStart: day(-8), baselineEnd: day(-5) },
{ id: 'b2', title: 'Data migration', start: day(-3), end: day(2), resourceId: 'b-data', progress: 70, color: COLORS.green, baselineStart: day(-3), baselineEnd: day(1) },
{ id: 'b3', title: 'Security hardening', start: day(1), end: day(6), resourceId: 'b-security', progress: 20, color: COLORS.teal, baselineStart: day(-1), baselineEnd: day(4) },
{ id: 'b4', title: 'Handover', start: day(8), end: day(8), resourceId: 'b-handover', milestone: true, color: COLORS.amber, baselineStart: day(6), baselineEnd: day(6) },
];
// --- Dependencies: a chained rollout plan
export const ROLLOUT_RESOURCES: WdsGanttResource[] = [
{
id: 'build-d',
title: 'Build',
children: [
{ id: 'd-frontend', title: 'Frontend' },
{ id: 'd-qa', title: 'QA pass' },
],
},
{
id: 'rollout',
title: 'Rollout',
children: [
{ id: 'd-staging', title: 'Staging deploy' },
{ id: 'd-signoff', title: 'Sign-off' },
{ id: 'd-live', title: 'Go live' },
],
},
];
export const ROLLOUT_EVENTS: WdsGanttEvent[] = [
{ id: 'd1', title: 'Frontend', start: day(-8), end: day(-4), resourceId: 'd-frontend', progress: 100, color: COLORS.violet },
{ id: 'd2', title: 'QA pass', start: day(-2), end: day(1), resourceId: 'd-qa', progress: 40, color: COLORS.purple, dependencies: ['d1'] },
{ id: 'd3', title: 'Staging deploy', start: day(2), end: day(3), resourceId: 'd-staging', color: COLORS.green, dependencies: ['d2'] },
{ id: 'd4', title: 'Sign-off', start: day(5), end: day(5), resourceId: 'd-signoff', milestone: true, color: COLORS.amber, dependencies: ['d3'] },
{ id: 'd5', title: 'Go live', start: day(6), end: day(7), resourceId: 'd-live', color: COLORS.teal, dependencies: ['d4'] },
];
// --- Day schedule: times as instants (UTC), shown in the chosen time zone
export const SHIFT_RESOURCES: WdsGanttResource[] = [
{
id: 'support',
title: 'Support rota',
children: [
{ id: 'emea', title: 'EMEA desk', color: COLORS.blue },
{ id: 'amer', title: 'Americas desk', color: COLORS.green },
{ id: 'apac', title: 'APAC desk', color: COLORS.amber },
],
},
];
const at = (offset: number, time: string) => `${day(offset)}T${time}:00Z`;
export const SHIFT_EVENTS: WdsGanttEvent[] = [
{ id: 'e1', title: 'Morning shift', start: at(0, '06:00'), end: at(0, '10:00'), resourceId: 'emea' },
{ id: 'e2', title: 'Release call', start: at(0, '11:00'), end: at(0, '11:45'), resourceId: 'emea', color: COLORS.rose },
{ id: 'e3', title: 'Afternoon shift', start: at(0, '12:00'), end: at(0, '16:00'), resourceId: 'emea' },
{ id: 'a1', title: 'Day shift', start: at(0, '13:00'), end: at(0, '21:00'), resourceId: 'amer' },
{ id: 'p1', title: 'Night shift', start: at(-1, '23:00'), end: at(0, '07:00'), resourceId: 'apac' },
{ id: 'p2', title: 'Handover', start: at(0, '07:00'), end: at(0, '07:00'), resourceId: 'apac', milestone: true },
];
export const TIME_ZONES = ['UTC', 'Europe/Warsaw', 'America/New_York', 'Asia/Tokyo'];
// --- Recurring: team rituals
export const RITUAL_RESOURCES: WdsGanttResource[] = [
{
id: 'rituals',
title: 'Team rituals',
children: [
{ id: 'standup', title: 'Standup', color: COLORS.sky },
{ id: 'review', title: 'Sprint review', color: COLORS.violet },
{ id: 'retro', title: 'Retrospective', color: COLORS.rose },
{ id: 'planning-r', title: 'Planning', color: COLORS.green },
],
},
];
export const RITUAL_EVENTS: WdsGanttEvent[] = [
{ id: 'standup', title: 'Standup', start: day(-28), end: day(-28), resourceId: 'standup', recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR' },
{ id: 'review', title: 'Sprint review', start: day(-25), end: day(-25), resourceId: 'review', recurrence: { freq: 'weekly', interval: 2 } },
{ id: 'retro', title: 'Retro', start: day(-24), end: day(-24), resourceId: 'retro', recurrence: 'FREQ=WEEKLY;INTERVAL=2' },
{ id: 'planning-r', title: 'Planning', start: day(-21), end: day(-20), resourceId: 'planning-r', recurrence: 'FREQ=WEEKLY;INTERVAL=2;COUNT=6' },
];
// --- Backlog: selection, menus, moving rows
export const BACKLOG_RESOURCES: WdsGanttResource[] = [
{
id: 'sprint-1',
title: 'Sprint 1',
children: [
{ id: 'login', title: 'Login' },
{ id: 'signup', title: 'Sign-up' },
],
},
{
id: 'sprint-2',
title: 'Sprint 2',
children: [
{ id: 'dashboard', title: 'Dashboard' },
{ id: 'reports', title: 'Reports' },
],
},
{ id: 'icebox', title: 'Icebox', children: [{ id: 'dark-mode', title: 'Dark mode' }] },
];
export const BACKLOG_EVENTS: WdsGanttEvent[] = [
{ id: 'k1', title: 'Login form', start: day(-6), end: day(-2), resourceId: 'login', progress: 100, color: COLORS.blue },
{ id: 'k2', title: 'Sign-up flow', start: day(-3), end: day(3), resourceId: 'signup', progress: 50, color: COLORS.sky },
{ id: 'k3', title: 'Dashboard', start: day(4), end: day(11), resourceId: 'dashboard', color: COLORS.green },
{ id: 'k4', title: 'Reports', start: day(8), end: day(15), resourceId: 'reports', color: COLORS.teal },
{ id: 'k5', title: 'Dark mode', start: day(18), end: day(24), resourceId: 'dark-mode', color: COLORS.violet },
];Day schedule and time zones#
2026-10-01T06:00:00Z) they are instants shown in time-zone, without one (2026-10-01T09:00) they are wall-clock times. The day scale shows hours and moves tasks in steps of snap minutes. Change the zone to see the same shifts in another time zone; after a move the chart writes dates back in the form they came in.
import { Component, signal } from '@angular/core';
import { WdsGanttComponent } from '@wds/angular/gantt';
import { WDS_SELECT } from '@wds/angular/select';
import type { WdsGanttEvent } from '@wds/core/gantt';
import { SHIFT_EVENTS, SHIFT_RESOURCES, TIME_ZONES } from './gantt-data';
@Component({
selector: 'app-gantt-day-demo',
host: { class: 'block w-full' },
imports: [WdsGanttComponent, ...WDS_SELECT],
template: `
<wds-gantt
class="h-[300px]"
label="Support rota"
scale="day"
snap="30"
[timeZone]="timeZone()"
[resources]="resources"
[events]="events()"
(wdsEventChange)="events.set($event.events)"
>
<wds-select slot="toolbar" class="w-48" size="sm" aria-label="Time zone" [value]="timeZone()" (wdsChange)="timeZone.set($event.value)">
@for (zone of zones; track zone) {
<wds-select-item [value]="zone">{{ zone }}</wds-select-item>
}
</wds-select>
</wds-gantt>
`,
})
export class GanttDayDemo {
protected readonly resources = SHIFT_RESOURCES;
protected readonly events = signal<WdsGanttEvent[]>(SHIFT_EVENTS);
protected readonly zones = TIME_ZONES;
protected readonly timeZone = signal('Europe/Warsaw');
}import '@wds/core/avatar';
import '@wds/core/badge';
import type { WdsGanttColumn, WdsGanttEvent, WdsGanttResource } from '@wds/core/gantt';
const pad = (n: number) => String(n).padStart(2, '0');
const base = new Date();
/** An ISO date `offset` days from today, so the examples always show the current period. */
export function day(offset: number): string {
const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + offset);
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** An ISO date moved by `days`. */
export function addDays(iso: string, days: number): string {
const [y, m, d] = iso.split('-').map(Number);
const date = new Date(y, m - 1, d + days);
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
/** Bar colors (any CSS color works). */
export const COLORS = {
blue: 'oklch(0.623 0.214 259.815)',
purple: 'oklch(0.627 0.265 303.9)',
violet: 'oklch(0.606 0.25 292.717)',
green: 'oklch(0.696 0.17 162.48)',
teal: 'oklch(0.704 0.14 182.503)',
sky: 'oklch(0.685 0.169 237.323)',
amber: 'oklch(0.769 0.188 70.08)',
rose: 'oklch(0.645 0.246 16.439)',
};
// --- Default: a website project
export const PROJECT_RESOURCES: WdsGanttResource[] = [
{
id: 'planning',
title: 'Planning',
children: [
{ id: 'brief', title: 'Project brief' },
{ id: 'scope', title: 'Scope review' },
],
},
{
id: 'design',
title: 'Design',
children: [
{ id: 'wireframes', title: 'Wireframes' },
{ id: 'visual-design', title: 'Visual design' },
],
},
{
id: 'build',
title: 'Build',
children: [
{ id: 'frontend', title: 'Frontend' },
{ id: 'backend', title: 'Backend' },
{ id: 'qa', title: 'QA pass' },
],
},
];
export const PROJECT_EVENTS: WdsGanttEvent[] = [
{ id: 'brief', title: 'Project brief', start: day(-20), end: day(-16), resourceId: 'brief', progress: 100, color: COLORS.blue },
{ id: 'scope', title: 'Scope review', start: day(-15), end: day(-11), resourceId: 'scope', progress: 100, color: COLORS.sky },
{ id: 'wireframes', title: 'Wireframes', start: day(-9), end: day(-4), resourceId: 'wireframes', progress: 100, color: COLORS.violet },
{ id: 'visual', title: 'Visual design', start: day(-3), end: day(2), resourceId: 'visual-design', progress: 40, color: COLORS.purple },
{ id: 'frontend', title: 'Frontend', start: day(0), end: day(12), resourceId: 'frontend', progress: 10, color: COLORS.green },
{ id: 'backend', title: 'Backend', start: day(2), end: day(14), resourceId: 'backend', color: COLORS.teal },
{ id: 'qa', title: 'QA pass', start: day(13), end: day(17), resourceId: 'qa', color: COLORS.amber },
];
/** Dependencies and planned windows added to the project (for the settings example). */
const PLAN: Record<string, Partial<WdsGanttEvent>> = {
scope: { dependencies: ['brief'] },
wireframes: { dependencies: ['scope'], baselineStart: day(-10), baselineEnd: day(-6) },
visual: { dependencies: ['wireframes'], baselineStart: day(-5), baselineEnd: day(1) },
frontend: { dependencies: ['visual'] },
qa: { dependencies: ['backend'] },
};
export const PROJECT_PLAN_EVENTS: WdsGanttEvent[] = PROJECT_EVENTS.map((e) => ({ ...e, ...PLAN[e.id] }));
// --- Roadmap: initiatives on the quarter scale
export const ROADMAP_RESOURCES: WdsGanttResource[] = [
{
id: 'platform',
title: 'Platform',
children: [
{ id: 'auth', title: 'Auth revamp' },
{ id: 'api', title: 'API v2' },
],
},
{
id: 'growth',
title: 'Growth',
children: [
{ id: 'onboarding', title: 'Onboarding flow' },
{ id: 'referral', title: 'Referral program' },
],
},
{ id: 'backlog', title: 'Backlog', children: [{ id: 'next', title: 'Next initiative' }] },
];
export const ROADMAP_EVENTS: WdsGanttEvent[] = [
{ id: 'r1', title: 'Auth revamp', start: day(-40), end: day(25), resourceId: 'auth', progress: 60, color: COLORS.blue },
{ id: 'r2', title: 'API v2', start: day(15), end: day(70), resourceId: 'api', color: COLORS.sky },
{ id: 'r3', title: 'Onboarding flow', start: day(-30), end: day(10), resourceId: 'onboarding', progress: 85, color: COLORS.green },
{ id: 'r4', title: 'Referral program', start: day(60), end: day(110), resourceId: 'referral', color: COLORS.teal },
];
// --- Team capacity: people grouped by squad, several assignments per person
export const TEAM_RESOURCES: WdsGanttResource[] = [
{
id: 'product',
title: 'Product squad',
children: [
{ id: 'ada', title: 'Ada Lovelace', color: COLORS.blue },
{ id: 'alan', title: 'Alan Turing', color: COLORS.sky },
],
},
{
id: 'design-squad',
title: 'Design squad',
children: [
{ id: 'grace', title: 'Grace Hopper', color: COLORS.violet },
{ id: 'linus', title: 'Linus Torvalds', color: COLORS.purple },
],
},
];
export const TEAM_EVENTS: WdsGanttEvent[] = [
{ id: 't1', title: 'Checkout', start: day(-3), end: day(1), resourceId: 'ada' },
{ id: 't2', title: 'Code review', start: day(0), end: day(3), resourceId: 'ada', color: COLORS.amber },
{ id: 't3', title: 'Billing API', start: day(-2), end: day(-1), resourceId: 'alan' },
{ id: 't4', title: 'Design tokens', start: day(-4), end: day(0), resourceId: 'grace' },
{ id: 't5', title: 'Icon set', start: day(2), end: day(5), resourceId: 'linus' },
];
/**
* Row label with an avatar (`renderResourceLabel`). It renders in the shadow DOM of the chart,
* so it is styled inline (Tailwind classes do not reach it).
*/
export function avatarLabel(resource: WdsGanttResource): string | Node {
if (resource.children?.length) return resource.title;
const label = document.createElement('span');
label.style.cssText = 'display:inline-flex;align-items:center;gap:8px;min-width:0';
// wds-avatar with initials; the row color reaches the fallback through the tokens it uses
const avatar = document.createElement('wds-avatar');
avatar.size = 'sm';
avatar.setAttribute('aria-hidden', 'true');
const color = resource.color ?? COLORS.blue;
avatar.style.setProperty('--wds-muted', `color-mix(in srgb, ${color} 22%, transparent)`);
avatar.style.setProperty('--wds-foreground', `color-mix(in srgb, ${color} 45%, currentColor)`);
avatar.textContent = resource.title
.split(' ')
.map((part) => part[0])
.join('')
.slice(0, 2);
const name = document.createElement('span');
name.style.cssText = 'overflow:hidden;text-overflow:ellipsis';
name.textContent = resource.title;
label.append(avatar, name);
return label;
}
// --- Status report: extra columns in the tree panel
export type Status = 'done' | 'on-track' | 'at-risk';
const STATUS_LABEL: Record<Status, string> = { done: 'Done', 'on-track': 'On track', 'at-risk': 'At risk' };
const STATUS_VARIANT: Record<Status, string> = { done: 'secondary', 'on-track': 'outline', 'at-risk': 'destructive' };
/** Content of a status cell (`render` of a column): a DOM node, here a `wds-badge`. */
export function statusBadge(status: Status): HTMLElement {
const el = document.createElement('wds-badge');
el.setAttribute('variant', STATUS_VARIANT[status]);
el.textContent = STATUS_LABEL[status];
return el;
}
export const STATUS_COLUMNS: WdsGanttColumn[] = [
{ key: 'owner', header: 'Owner', width: '5.5rem' },
{ key: 'status', header: 'Status', width: '6.5rem', render: (r) => statusBadge(r['status'] as Status) },
];
export const STATUS_RESOURCES: WdsGanttResource[] = [
{
id: 'launch',
title: 'Launch',
owner: 'Ana',
status: 'on-track',
children: [
{ id: 'brief-s', title: 'Brief', owner: 'Ana', status: 'done' },
{ id: 'content', title: 'Content', owner: 'Ben', status: 'on-track' },
{ id: 'campaign', title: 'Campaign', owner: 'Chloe', status: 'at-risk' },
{ id: 'release', title: 'Release', owner: 'David', status: 'on-track' },
],
},
];
export const STATUS_EVENTS: WdsGanttEvent[] = [
{ id: 's1', title: 'Brief', start: day(-14), end: day(-8), resourceId: 'brief-s', progress: 100, color: COLORS.blue },
{ id: 's2', title: 'Content', start: day(-7), end: day(5), resourceId: 'content', progress: 55, color: COLORS.green },
{ id: 's3', title: 'Campaign', start: day(-2), end: day(12), resourceId: 'campaign', progress: 10, color: COLORS.rose },
{ id: 's4', title: 'Release', start: day(14), end: day(14), resourceId: 'release', milestone: true, color: COLORS.amber },
];
// --- Baseline: planned against actual dates
export const BASELINE_RESOURCES: WdsGanttResource[] = [
{
id: 'implementation',
title: 'Implementation',
baselineStart: day(-8),
baselineEnd: day(4),
children: [
{ id: 'b-api', title: 'API integration' },
{ id: 'b-data', title: 'Data migration' },
{ id: 'b-security', title: 'Security hardening' },
],
},
{ id: 'launch-b', title: 'Launch', children: [{ id: 'b-handover', title: 'Handover' }] },
];
export const BASELINE_EVENTS: WdsGanttEvent[] = [
{ id: 'b1', title: 'API integration', start: day(-8), end: day(-3), resourceId: 'b-api', progress: 100, color: COLORS.violet, baselineStart: day(-8), baselineEnd: day(-5) },
{ id: 'b2', title: 'Data migration', start: day(-3), end: day(2), resourceId: 'b-data', progress: 70, color: COLORS.green, baselineStart: day(-3), baselineEnd: day(1) },
{ id: 'b3', title: 'Security hardening', start: day(1), end: day(6), resourceId: 'b-security', progress: 20, color: COLORS.teal, baselineStart: day(-1), baselineEnd: day(4) },
{ id: 'b4', title: 'Handover', start: day(8), end: day(8), resourceId: 'b-handover', milestone: true, color: COLORS.amber, baselineStart: day(6), baselineEnd: day(6) },
];
// --- Dependencies: a chained rollout plan
export const ROLLOUT_RESOURCES: WdsGanttResource[] = [
{
id: 'build-d',
title: 'Build',
children: [
{ id: 'd-frontend', title: 'Frontend' },
{ id: 'd-qa', title: 'QA pass' },
],
},
{
id: 'rollout',
title: 'Rollout',
children: [
{ id: 'd-staging', title: 'Staging deploy' },
{ id: 'd-signoff', title: 'Sign-off' },
{ id: 'd-live', title: 'Go live' },
],
},
];
export const ROLLOUT_EVENTS: WdsGanttEvent[] = [
{ id: 'd1', title: 'Frontend', start: day(-8), end: day(-4), resourceId: 'd-frontend', progress: 100, color: COLORS.violet },
{ id: 'd2', title: 'QA pass', start: day(-2), end: day(1), resourceId: 'd-qa', progress: 40, color: COLORS.purple, dependencies: ['d1'] },
{ id: 'd3', title: 'Staging deploy', start: day(2), end: day(3), resourceId: 'd-staging', color: COLORS.green, dependencies: ['d2'] },
{ id: 'd4', title: 'Sign-off', start: day(5), end: day(5), resourceId: 'd-signoff', milestone: true, color: COLORS.amber, dependencies: ['d3'] },
{ id: 'd5', title: 'Go live', start: day(6), end: day(7), resourceId: 'd-live', color: COLORS.teal, dependencies: ['d4'] },
];
// --- Day schedule: times as instants (UTC), shown in the chosen time zone
export const SHIFT_RESOURCES: WdsGanttResource[] = [
{
id: 'support',
title: 'Support rota',
children: [
{ id: 'emea', title: 'EMEA desk', color: COLORS.blue },
{ id: 'amer', title: 'Americas desk', color: COLORS.green },
{ id: 'apac', title: 'APAC desk', color: COLORS.amber },
],
},
];
const at = (offset: number, time: string) => `${day(offset)}T${time}:00Z`;
export const SHIFT_EVENTS: WdsGanttEvent[] = [
{ id: 'e1', title: 'Morning shift', start: at(0, '06:00'), end: at(0, '10:00'), resourceId: 'emea' },
{ id: 'e2', title: 'Release call', start: at(0, '11:00'), end: at(0, '11:45'), resourceId: 'emea', color: COLORS.rose },
{ id: 'e3', title: 'Afternoon shift', start: at(0, '12:00'), end: at(0, '16:00'), resourceId: 'emea' },
{ id: 'a1', title: 'Day shift', start: at(0, '13:00'), end: at(0, '21:00'), resourceId: 'amer' },
{ id: 'p1', title: 'Night shift', start: at(-1, '23:00'), end: at(0, '07:00'), resourceId: 'apac' },
{ id: 'p2', title: 'Handover', start: at(0, '07:00'), end: at(0, '07:00'), resourceId: 'apac', milestone: true },
];
export const TIME_ZONES = ['UTC', 'Europe/Warsaw', 'America/New_York', 'Asia/Tokyo'];
// --- Recurring: team rituals
export const RITUAL_RESOURCES: WdsGanttResource[] = [
{
id: 'rituals',
title: 'Team rituals',
children: [
{ id: 'standup', title: 'Standup', color: COLORS.sky },
{ id: 'review', title: 'Sprint review', color: COLORS.violet },
{ id: 'retro', title: 'Retrospective', color: COLORS.rose },
{ id: 'planning-r', title: 'Planning', color: COLORS.green },
],
},
];
export const RITUAL_EVENTS: WdsGanttEvent[] = [
{ id: 'standup', title: 'Standup', start: day(-28), end: day(-28), resourceId: 'standup', recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR' },
{ id: 'review', title: 'Sprint review', start: day(-25), end: day(-25), resourceId: 'review', recurrence: { freq: 'weekly', interval: 2 } },
{ id: 'retro', title: 'Retro', start: day(-24), end: day(-24), resourceId: 'retro', recurrence: 'FREQ=WEEKLY;INTERVAL=2' },
{ id: 'planning-r', title: 'Planning', start: day(-21), end: day(-20), resourceId: 'planning-r', recurrence: 'FREQ=WEEKLY;INTERVAL=2;COUNT=6' },
];
// --- Backlog: selection, menus, moving rows
export const BACKLOG_RESOURCES: WdsGanttResource[] = [
{
id: 'sprint-1',
title: 'Sprint 1',
children: [
{ id: 'login', title: 'Login' },
{ id: 'signup', title: 'Sign-up' },
],
},
{
id: 'sprint-2',
title: 'Sprint 2',
children: [
{ id: 'dashboard', title: 'Dashboard' },
{ id: 'reports', title: 'Reports' },
],
},
{ id: 'icebox', title: 'Icebox', children: [{ id: 'dark-mode', title: 'Dark mode' }] },
];
export const BACKLOG_EVENTS: WdsGanttEvent[] = [
{ id: 'k1', title: 'Login form', start: day(-6), end: day(-2), resourceId: 'login', progress: 100, color: COLORS.blue },
{ id: 'k2', title: 'Sign-up flow', start: day(-3), end: day(3), resourceId: 'signup', progress: 50, color: COLORS.sky },
{ id: 'k3', title: 'Dashboard', start: day(4), end: day(11), resourceId: 'dashboard', color: COLORS.green },
{ id: 'k4', title: 'Reports', start: day(8), end: day(15), resourceId: 'reports', color: COLORS.teal },
{ id: 'k5', title: 'Dark mode', start: day(18), end: day(24), resourceId: 'dark-mode', color: COLORS.violet },
];Recurring tasks#
recurrence repeats a task: a rule object ({ freq: 'weekly', interval: 2 }) or an RRULE string (FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR) with INTERVAL, COUNT, UNTIL, BYDAY, EXDATE and RDATE. Occurrences are expanded for the visible range; moving one moves the whole series. parseRRule, formatRRule and expandRecurrence are exported too.
import { Component } from '@angular/core';
import { WdsGanttComponent } from '@wds/angular/gantt';
import { RITUAL_EVENTS, RITUAL_RESOURCES } from './gantt-data';
@Component({
selector: 'app-gantt-recurring-demo',
host: { class: 'block w-full' },
imports: [WdsGanttComponent],
template: `<wds-gantt class="h-[300px]" label="Team rituals" scale="month" off-days [resources]="resources" [events]="events" />`,
})
export class GanttRecurringDemo {
protected readonly resources = RITUAL_RESOURCES;
protected readonly events = RITUAL_EVENTS;
}import '@wds/core/avatar';
import '@wds/core/badge';
import type { WdsGanttColumn, WdsGanttEvent, WdsGanttResource } from '@wds/core/gantt';
const pad = (n: number) => String(n).padStart(2, '0');
const base = new Date();
/** An ISO date `offset` days from today, so the examples always show the current period. */
export function day(offset: number): string {
const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + offset);
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** An ISO date moved by `days`. */
export function addDays(iso: string, days: number): string {
const [y, m, d] = iso.split('-').map(Number);
const date = new Date(y, m - 1, d + days);
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
/** Bar colors (any CSS color works). */
export const COLORS = {
blue: 'oklch(0.623 0.214 259.815)',
purple: 'oklch(0.627 0.265 303.9)',
violet: 'oklch(0.606 0.25 292.717)',
green: 'oklch(0.696 0.17 162.48)',
teal: 'oklch(0.704 0.14 182.503)',
sky: 'oklch(0.685 0.169 237.323)',
amber: 'oklch(0.769 0.188 70.08)',
rose: 'oklch(0.645 0.246 16.439)',
};
// --- Default: a website project
export const PROJECT_RESOURCES: WdsGanttResource[] = [
{
id: 'planning',
title: 'Planning',
children: [
{ id: 'brief', title: 'Project brief' },
{ id: 'scope', title: 'Scope review' },
],
},
{
id: 'design',
title: 'Design',
children: [
{ id: 'wireframes', title: 'Wireframes' },
{ id: 'visual-design', title: 'Visual design' },
],
},
{
id: 'build',
title: 'Build',
children: [
{ id: 'frontend', title: 'Frontend' },
{ id: 'backend', title: 'Backend' },
{ id: 'qa', title: 'QA pass' },
],
},
];
export const PROJECT_EVENTS: WdsGanttEvent[] = [
{ id: 'brief', title: 'Project brief', start: day(-20), end: day(-16), resourceId: 'brief', progress: 100, color: COLORS.blue },
{ id: 'scope', title: 'Scope review', start: day(-15), end: day(-11), resourceId: 'scope', progress: 100, color: COLORS.sky },
{ id: 'wireframes', title: 'Wireframes', start: day(-9), end: day(-4), resourceId: 'wireframes', progress: 100, color: COLORS.violet },
{ id: 'visual', title: 'Visual design', start: day(-3), end: day(2), resourceId: 'visual-design', progress: 40, color: COLORS.purple },
{ id: 'frontend', title: 'Frontend', start: day(0), end: day(12), resourceId: 'frontend', progress: 10, color: COLORS.green },
{ id: 'backend', title: 'Backend', start: day(2), end: day(14), resourceId: 'backend', color: COLORS.teal },
{ id: 'qa', title: 'QA pass', start: day(13), end: day(17), resourceId: 'qa', color: COLORS.amber },
];
/** Dependencies and planned windows added to the project (for the settings example). */
const PLAN: Record<string, Partial<WdsGanttEvent>> = {
scope: { dependencies: ['brief'] },
wireframes: { dependencies: ['scope'], baselineStart: day(-10), baselineEnd: day(-6) },
visual: { dependencies: ['wireframes'], baselineStart: day(-5), baselineEnd: day(1) },
frontend: { dependencies: ['visual'] },
qa: { dependencies: ['backend'] },
};
export const PROJECT_PLAN_EVENTS: WdsGanttEvent[] = PROJECT_EVENTS.map((e) => ({ ...e, ...PLAN[e.id] }));
// --- Roadmap: initiatives on the quarter scale
export const ROADMAP_RESOURCES: WdsGanttResource[] = [
{
id: 'platform',
title: 'Platform',
children: [
{ id: 'auth', title: 'Auth revamp' },
{ id: 'api', title: 'API v2' },
],
},
{
id: 'growth',
title: 'Growth',
children: [
{ id: 'onboarding', title: 'Onboarding flow' },
{ id: 'referral', title: 'Referral program' },
],
},
{ id: 'backlog', title: 'Backlog', children: [{ id: 'next', title: 'Next initiative' }] },
];
export const ROADMAP_EVENTS: WdsGanttEvent[] = [
{ id: 'r1', title: 'Auth revamp', start: day(-40), end: day(25), resourceId: 'auth', progress: 60, color: COLORS.blue },
{ id: 'r2', title: 'API v2', start: day(15), end: day(70), resourceId: 'api', color: COLORS.sky },
{ id: 'r3', title: 'Onboarding flow', start: day(-30), end: day(10), resourceId: 'onboarding', progress: 85, color: COLORS.green },
{ id: 'r4', title: 'Referral program', start: day(60), end: day(110), resourceId: 'referral', color: COLORS.teal },
];
// --- Team capacity: people grouped by squad, several assignments per person
export const TEAM_RESOURCES: WdsGanttResource[] = [
{
id: 'product',
title: 'Product squad',
children: [
{ id: 'ada', title: 'Ada Lovelace', color: COLORS.blue },
{ id: 'alan', title: 'Alan Turing', color: COLORS.sky },
],
},
{
id: 'design-squad',
title: 'Design squad',
children: [
{ id: 'grace', title: 'Grace Hopper', color: COLORS.violet },
{ id: 'linus', title: 'Linus Torvalds', color: COLORS.purple },
],
},
];
export const TEAM_EVENTS: WdsGanttEvent[] = [
{ id: 't1', title: 'Checkout', start: day(-3), end: day(1), resourceId: 'ada' },
{ id: 't2', title: 'Code review', start: day(0), end: day(3), resourceId: 'ada', color: COLORS.amber },
{ id: 't3', title: 'Billing API', start: day(-2), end: day(-1), resourceId: 'alan' },
{ id: 't4', title: 'Design tokens', start: day(-4), end: day(0), resourceId: 'grace' },
{ id: 't5', title: 'Icon set', start: day(2), end: day(5), resourceId: 'linus' },
];
/**
* Row label with an avatar (`renderResourceLabel`). It renders in the shadow DOM of the chart,
* so it is styled inline (Tailwind classes do not reach it).
*/
export function avatarLabel(resource: WdsGanttResource): string | Node {
if (resource.children?.length) return resource.title;
const label = document.createElement('span');
label.style.cssText = 'display:inline-flex;align-items:center;gap:8px;min-width:0';
// wds-avatar with initials; the row color reaches the fallback through the tokens it uses
const avatar = document.createElement('wds-avatar');
avatar.size = 'sm';
avatar.setAttribute('aria-hidden', 'true');
const color = resource.color ?? COLORS.blue;
avatar.style.setProperty('--wds-muted', `color-mix(in srgb, ${color} 22%, transparent)`);
avatar.style.setProperty('--wds-foreground', `color-mix(in srgb, ${color} 45%, currentColor)`);
avatar.textContent = resource.title
.split(' ')
.map((part) => part[0])
.join('')
.slice(0, 2);
const name = document.createElement('span');
name.style.cssText = 'overflow:hidden;text-overflow:ellipsis';
name.textContent = resource.title;
label.append(avatar, name);
return label;
}
// --- Status report: extra columns in the tree panel
export type Status = 'done' | 'on-track' | 'at-risk';
const STATUS_LABEL: Record<Status, string> = { done: 'Done', 'on-track': 'On track', 'at-risk': 'At risk' };
const STATUS_VARIANT: Record<Status, string> = { done: 'secondary', 'on-track': 'outline', 'at-risk': 'destructive' };
/** Content of a status cell (`render` of a column): a DOM node, here a `wds-badge`. */
export function statusBadge(status: Status): HTMLElement {
const el = document.createElement('wds-badge');
el.setAttribute('variant', STATUS_VARIANT[status]);
el.textContent = STATUS_LABEL[status];
return el;
}
export const STATUS_COLUMNS: WdsGanttColumn[] = [
{ key: 'owner', header: 'Owner', width: '5.5rem' },
{ key: 'status', header: 'Status', width: '6.5rem', render: (r) => statusBadge(r['status'] as Status) },
];
export const STATUS_RESOURCES: WdsGanttResource[] = [
{
id: 'launch',
title: 'Launch',
owner: 'Ana',
status: 'on-track',
children: [
{ id: 'brief-s', title: 'Brief', owner: 'Ana', status: 'done' },
{ id: 'content', title: 'Content', owner: 'Ben', status: 'on-track' },
{ id: 'campaign', title: 'Campaign', owner: 'Chloe', status: 'at-risk' },
{ id: 'release', title: 'Release', owner: 'David', status: 'on-track' },
],
},
];
export const STATUS_EVENTS: WdsGanttEvent[] = [
{ id: 's1', title: 'Brief', start: day(-14), end: day(-8), resourceId: 'brief-s', progress: 100, color: COLORS.blue },
{ id: 's2', title: 'Content', start: day(-7), end: day(5), resourceId: 'content', progress: 55, color: COLORS.green },
{ id: 's3', title: 'Campaign', start: day(-2), end: day(12), resourceId: 'campaign', progress: 10, color: COLORS.rose },
{ id: 's4', title: 'Release', start: day(14), end: day(14), resourceId: 'release', milestone: true, color: COLORS.amber },
];
// --- Baseline: planned against actual dates
export const BASELINE_RESOURCES: WdsGanttResource[] = [
{
id: 'implementation',
title: 'Implementation',
baselineStart: day(-8),
baselineEnd: day(4),
children: [
{ id: 'b-api', title: 'API integration' },
{ id: 'b-data', title: 'Data migration' },
{ id: 'b-security', title: 'Security hardening' },
],
},
{ id: 'launch-b', title: 'Launch', children: [{ id: 'b-handover', title: 'Handover' }] },
];
export const BASELINE_EVENTS: WdsGanttEvent[] = [
{ id: 'b1', title: 'API integration', start: day(-8), end: day(-3), resourceId: 'b-api', progress: 100, color: COLORS.violet, baselineStart: day(-8), baselineEnd: day(-5) },
{ id: 'b2', title: 'Data migration', start: day(-3), end: day(2), resourceId: 'b-data', progress: 70, color: COLORS.green, baselineStart: day(-3), baselineEnd: day(1) },
{ id: 'b3', title: 'Security hardening', start: day(1), end: day(6), resourceId: 'b-security', progress: 20, color: COLORS.teal, baselineStart: day(-1), baselineEnd: day(4) },
{ id: 'b4', title: 'Handover', start: day(8), end: day(8), resourceId: 'b-handover', milestone: true, color: COLORS.amber, baselineStart: day(6), baselineEnd: day(6) },
];
// --- Dependencies: a chained rollout plan
export const ROLLOUT_RESOURCES: WdsGanttResource[] = [
{
id: 'build-d',
title: 'Build',
children: [
{ id: 'd-frontend', title: 'Frontend' },
{ id: 'd-qa', title: 'QA pass' },
],
},
{
id: 'rollout',
title: 'Rollout',
children: [
{ id: 'd-staging', title: 'Staging deploy' },
{ id: 'd-signoff', title: 'Sign-off' },
{ id: 'd-live', title: 'Go live' },
],
},
];
export const ROLLOUT_EVENTS: WdsGanttEvent[] = [
{ id: 'd1', title: 'Frontend', start: day(-8), end: day(-4), resourceId: 'd-frontend', progress: 100, color: COLORS.violet },
{ id: 'd2', title: 'QA pass', start: day(-2), end: day(1), resourceId: 'd-qa', progress: 40, color: COLORS.purple, dependencies: ['d1'] },
{ id: 'd3', title: 'Staging deploy', start: day(2), end: day(3), resourceId: 'd-staging', color: COLORS.green, dependencies: ['d2'] },
{ id: 'd4', title: 'Sign-off', start: day(5), end: day(5), resourceId: 'd-signoff', milestone: true, color: COLORS.amber, dependencies: ['d3'] },
{ id: 'd5', title: 'Go live', start: day(6), end: day(7), resourceId: 'd-live', color: COLORS.teal, dependencies: ['d4'] },
];
// --- Day schedule: times as instants (UTC), shown in the chosen time zone
export const SHIFT_RESOURCES: WdsGanttResource[] = [
{
id: 'support',
title: 'Support rota',
children: [
{ id: 'emea', title: 'EMEA desk', color: COLORS.blue },
{ id: 'amer', title: 'Americas desk', color: COLORS.green },
{ id: 'apac', title: 'APAC desk', color: COLORS.amber },
],
},
];
const at = (offset: number, time: string) => `${day(offset)}T${time}:00Z`;
export const SHIFT_EVENTS: WdsGanttEvent[] = [
{ id: 'e1', title: 'Morning shift', start: at(0, '06:00'), end: at(0, '10:00'), resourceId: 'emea' },
{ id: 'e2', title: 'Release call', start: at(0, '11:00'), end: at(0, '11:45'), resourceId: 'emea', color: COLORS.rose },
{ id: 'e3', title: 'Afternoon shift', start: at(0, '12:00'), end: at(0, '16:00'), resourceId: 'emea' },
{ id: 'a1', title: 'Day shift', start: at(0, '13:00'), end: at(0, '21:00'), resourceId: 'amer' },
{ id: 'p1', title: 'Night shift', start: at(-1, '23:00'), end: at(0, '07:00'), resourceId: 'apac' },
{ id: 'p2', title: 'Handover', start: at(0, '07:00'), end: at(0, '07:00'), resourceId: 'apac', milestone: true },
];
export const TIME_ZONES = ['UTC', 'Europe/Warsaw', 'America/New_York', 'Asia/Tokyo'];
// --- Recurring: team rituals
export const RITUAL_RESOURCES: WdsGanttResource[] = [
{
id: 'rituals',
title: 'Team rituals',
children: [
{ id: 'standup', title: 'Standup', color: COLORS.sky },
{ id: 'review', title: 'Sprint review', color: COLORS.violet },
{ id: 'retro', title: 'Retrospective', color: COLORS.rose },
{ id: 'planning-r', title: 'Planning', color: COLORS.green },
],
},
];
export const RITUAL_EVENTS: WdsGanttEvent[] = [
{ id: 'standup', title: 'Standup', start: day(-28), end: day(-28), resourceId: 'standup', recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR' },
{ id: 'review', title: 'Sprint review', start: day(-25), end: day(-25), resourceId: 'review', recurrence: { freq: 'weekly', interval: 2 } },
{ id: 'retro', title: 'Retro', start: day(-24), end: day(-24), resourceId: 'retro', recurrence: 'FREQ=WEEKLY;INTERVAL=2' },
{ id: 'planning-r', title: 'Planning', start: day(-21), end: day(-20), resourceId: 'planning-r', recurrence: 'FREQ=WEEKLY;INTERVAL=2;COUNT=6' },
];
// --- Backlog: selection, menus, moving rows
export const BACKLOG_RESOURCES: WdsGanttResource[] = [
{
id: 'sprint-1',
title: 'Sprint 1',
children: [
{ id: 'login', title: 'Login' },
{ id: 'signup', title: 'Sign-up' },
],
},
{
id: 'sprint-2',
title: 'Sprint 2',
children: [
{ id: 'dashboard', title: 'Dashboard' },
{ id: 'reports', title: 'Reports' },
],
},
{ id: 'icebox', title: 'Icebox', children: [{ id: 'dark-mode', title: 'Dark mode' }] },
];
export const BACKLOG_EVENTS: WdsGanttEvent[] = [
{ id: 'k1', title: 'Login form', start: day(-6), end: day(-2), resourceId: 'login', progress: 100, color: COLORS.blue },
{ id: 'k2', title: 'Sign-up flow', start: day(-3), end: day(3), resourceId: 'signup', progress: 50, color: COLORS.sky },
{ id: 'k3', title: 'Dashboard', start: day(4), end: day(11), resourceId: 'dashboard', color: COLORS.green },
{ id: 'k4', title: 'Reports', start: day(8), end: day(15), resourceId: 'reports', color: COLORS.teal },
{ id: 'k5', title: 'Dark mode', start: day(18), end: day(24), resourceId: 'dark-mode', color: COLORS.violet },
];Selection, menus and moving rows#
selectable adds a checkbox to every row that selects its tasks (Ctrl or ⌘ and a click adds a single task) and fires wds-selection-change. Delete or Backspace deletes the selected tasks (wds-event-remove). eventActions adds a context menu to the bars (right click or Shift+F10) and resourceActions a menu to the rows; the chosen item comes in wds-event-action or wds-resource-action. reorderable lets rows be moved with the handle or Alt+↑/↓ (wds-resource-reorder).
Selected tasks: 0
import { Component, signal } from '@angular/core';
import { WdsGanttComponent } from '@wds/angular/gantt';
import type { WdsGanttAction, WdsGanttEvent, WdsGanttEventActionDetail, WdsGanttResource, WdsGanttResourceActionDetail } from '@wds/core/gantt';
import { BACKLOG_EVENTS, BACKLOG_RESOURCES, COLORS, addDays, day } from './gantt-data';
@Component({
selector: 'app-gantt-backlog-demo',
host: { class: 'flex w-full flex-col gap-3' },
imports: [WdsGanttComponent],
template: `
<wds-gantt
class="h-[400px]"
label="Backlog"
selectable
reorderable
[resources]="resources()"
[events]="events()"
[eventActions]="eventActions"
[resourceActions]="resourceActions"
(wdsEventChange)="events.set($event.events)"
(wdsEventRemove)="events.set($event.events)"
(wdsResourceReorder)="resources.set($event.resources)"
(wdsSelectionChange)="selected.set($event.selected)"
(wdsEventAction)="onEventAction($event)"
(wdsResourceAction)="onResourceAction($event)"
/>
<p class="m-0 text-sm text-muted-foreground">Selected tasks: {{ selected().length }}</p>
`,
})
export class GanttBacklogDemo {
protected readonly resources = signal<WdsGanttResource[]>(BACKLOG_RESOURCES);
protected readonly events = signal<WdsGanttEvent[]>(BACKLOG_EVENTS);
protected readonly selected = signal<string[]>([]);
protected readonly eventActions: WdsGanttAction[] = [
{ value: 'duplicate', label: 'Duplicate' },
{ separator: true },
{ value: 'delete', label: 'Delete', variant: 'destructive' },
];
protected readonly resourceActions: WdsGanttAction[] = [{ value: 'add', label: 'Add task' }];
protected onEventAction({ action, event }: WdsGanttEventActionDetail): void {
if (action === 'delete') this.events.update((list) => list.filter((e) => e.id !== event.id));
if (action === 'duplicate') {
this.events.update((list) => [...list, { ...event, id: crypto.randomUUID(), title: `${event.title} (copy)`, progress: 0 }]);
}
}
/** "Add task" on a group adds the task to its first row. */
protected onResourceAction({ resource }: WdsGanttResourceActionDetail): void {
const resourceId = resource.children?.[0]?.id ?? resource.id;
const start = day(0);
this.events.update((list) => [...list, { id: crypto.randomUUID(), title: 'New task', start, end: addDays(start, 4), resourceId, color: COLORS.amber }]);
}
}import '@wds/core/avatar';
import '@wds/core/badge';
import type { WdsGanttColumn, WdsGanttEvent, WdsGanttResource } from '@wds/core/gantt';
const pad = (n: number) => String(n).padStart(2, '0');
const base = new Date();
/** An ISO date `offset` days from today, so the examples always show the current period. */
export function day(offset: number): string {
const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + offset);
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** An ISO date moved by `days`. */
export function addDays(iso: string, days: number): string {
const [y, m, d] = iso.split('-').map(Number);
const date = new Date(y, m - 1, d + days);
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
/** Bar colors (any CSS color works). */
export const COLORS = {
blue: 'oklch(0.623 0.214 259.815)',
purple: 'oklch(0.627 0.265 303.9)',
violet: 'oklch(0.606 0.25 292.717)',
green: 'oklch(0.696 0.17 162.48)',
teal: 'oklch(0.704 0.14 182.503)',
sky: 'oklch(0.685 0.169 237.323)',
amber: 'oklch(0.769 0.188 70.08)',
rose: 'oklch(0.645 0.246 16.439)',
};
// --- Default: a website project
export const PROJECT_RESOURCES: WdsGanttResource[] = [
{
id: 'planning',
title: 'Planning',
children: [
{ id: 'brief', title: 'Project brief' },
{ id: 'scope', title: 'Scope review' },
],
},
{
id: 'design',
title: 'Design',
children: [
{ id: 'wireframes', title: 'Wireframes' },
{ id: 'visual-design', title: 'Visual design' },
],
},
{
id: 'build',
title: 'Build',
children: [
{ id: 'frontend', title: 'Frontend' },
{ id: 'backend', title: 'Backend' },
{ id: 'qa', title: 'QA pass' },
],
},
];
export const PROJECT_EVENTS: WdsGanttEvent[] = [
{ id: 'brief', title: 'Project brief', start: day(-20), end: day(-16), resourceId: 'brief', progress: 100, color: COLORS.blue },
{ id: 'scope', title: 'Scope review', start: day(-15), end: day(-11), resourceId: 'scope', progress: 100, color: COLORS.sky },
{ id: 'wireframes', title: 'Wireframes', start: day(-9), end: day(-4), resourceId: 'wireframes', progress: 100, color: COLORS.violet },
{ id: 'visual', title: 'Visual design', start: day(-3), end: day(2), resourceId: 'visual-design', progress: 40, color: COLORS.purple },
{ id: 'frontend', title: 'Frontend', start: day(0), end: day(12), resourceId: 'frontend', progress: 10, color: COLORS.green },
{ id: 'backend', title: 'Backend', start: day(2), end: day(14), resourceId: 'backend', color: COLORS.teal },
{ id: 'qa', title: 'QA pass', start: day(13), end: day(17), resourceId: 'qa', color: COLORS.amber },
];
/** Dependencies and planned windows added to the project (for the settings example). */
const PLAN: Record<string, Partial<WdsGanttEvent>> = {
scope: { dependencies: ['brief'] },
wireframes: { dependencies: ['scope'], baselineStart: day(-10), baselineEnd: day(-6) },
visual: { dependencies: ['wireframes'], baselineStart: day(-5), baselineEnd: day(1) },
frontend: { dependencies: ['visual'] },
qa: { dependencies: ['backend'] },
};
export const PROJECT_PLAN_EVENTS: WdsGanttEvent[] = PROJECT_EVENTS.map((e) => ({ ...e, ...PLAN[e.id] }));
// --- Roadmap: initiatives on the quarter scale
export const ROADMAP_RESOURCES: WdsGanttResource[] = [
{
id: 'platform',
title: 'Platform',
children: [
{ id: 'auth', title: 'Auth revamp' },
{ id: 'api', title: 'API v2' },
],
},
{
id: 'growth',
title: 'Growth',
children: [
{ id: 'onboarding', title: 'Onboarding flow' },
{ id: 'referral', title: 'Referral program' },
],
},
{ id: 'backlog', title: 'Backlog', children: [{ id: 'next', title: 'Next initiative' }] },
];
export const ROADMAP_EVENTS: WdsGanttEvent[] = [
{ id: 'r1', title: 'Auth revamp', start: day(-40), end: day(25), resourceId: 'auth', progress: 60, color: COLORS.blue },
{ id: 'r2', title: 'API v2', start: day(15), end: day(70), resourceId: 'api', color: COLORS.sky },
{ id: 'r3', title: 'Onboarding flow', start: day(-30), end: day(10), resourceId: 'onboarding', progress: 85, color: COLORS.green },
{ id: 'r4', title: 'Referral program', start: day(60), end: day(110), resourceId: 'referral', color: COLORS.teal },
];
// --- Team capacity: people grouped by squad, several assignments per person
export const TEAM_RESOURCES: WdsGanttResource[] = [
{
id: 'product',
title: 'Product squad',
children: [
{ id: 'ada', title: 'Ada Lovelace', color: COLORS.blue },
{ id: 'alan', title: 'Alan Turing', color: COLORS.sky },
],
},
{
id: 'design-squad',
title: 'Design squad',
children: [
{ id: 'grace', title: 'Grace Hopper', color: COLORS.violet },
{ id: 'linus', title: 'Linus Torvalds', color: COLORS.purple },
],
},
];
export const TEAM_EVENTS: WdsGanttEvent[] = [
{ id: 't1', title: 'Checkout', start: day(-3), end: day(1), resourceId: 'ada' },
{ id: 't2', title: 'Code review', start: day(0), end: day(3), resourceId: 'ada', color: COLORS.amber },
{ id: 't3', title: 'Billing API', start: day(-2), end: day(-1), resourceId: 'alan' },
{ id: 't4', title: 'Design tokens', start: day(-4), end: day(0), resourceId: 'grace' },
{ id: 't5', title: 'Icon set', start: day(2), end: day(5), resourceId: 'linus' },
];
/**
* Row label with an avatar (`renderResourceLabel`). It renders in the shadow DOM of the chart,
* so it is styled inline (Tailwind classes do not reach it).
*/
export function avatarLabel(resource: WdsGanttResource): string | Node {
if (resource.children?.length) return resource.title;
const label = document.createElement('span');
label.style.cssText = 'display:inline-flex;align-items:center;gap:8px;min-width:0';
// wds-avatar with initials; the row color reaches the fallback through the tokens it uses
const avatar = document.createElement('wds-avatar');
avatar.size = 'sm';
avatar.setAttribute('aria-hidden', 'true');
const color = resource.color ?? COLORS.blue;
avatar.style.setProperty('--wds-muted', `color-mix(in srgb, ${color} 22%, transparent)`);
avatar.style.setProperty('--wds-foreground', `color-mix(in srgb, ${color} 45%, currentColor)`);
avatar.textContent = resource.title
.split(' ')
.map((part) => part[0])
.join('')
.slice(0, 2);
const name = document.createElement('span');
name.style.cssText = 'overflow:hidden;text-overflow:ellipsis';
name.textContent = resource.title;
label.append(avatar, name);
return label;
}
// --- Status report: extra columns in the tree panel
export type Status = 'done' | 'on-track' | 'at-risk';
const STATUS_LABEL: Record<Status, string> = { done: 'Done', 'on-track': 'On track', 'at-risk': 'At risk' };
const STATUS_VARIANT: Record<Status, string> = { done: 'secondary', 'on-track': 'outline', 'at-risk': 'destructive' };
/** Content of a status cell (`render` of a column): a DOM node, here a `wds-badge`. */
export function statusBadge(status: Status): HTMLElement {
const el = document.createElement('wds-badge');
el.setAttribute('variant', STATUS_VARIANT[status]);
el.textContent = STATUS_LABEL[status];
return el;
}
export const STATUS_COLUMNS: WdsGanttColumn[] = [
{ key: 'owner', header: 'Owner', width: '5.5rem' },
{ key: 'status', header: 'Status', width: '6.5rem', render: (r) => statusBadge(r['status'] as Status) },
];
export const STATUS_RESOURCES: WdsGanttResource[] = [
{
id: 'launch',
title: 'Launch',
owner: 'Ana',
status: 'on-track',
children: [
{ id: 'brief-s', title: 'Brief', owner: 'Ana', status: 'done' },
{ id: 'content', title: 'Content', owner: 'Ben', status: 'on-track' },
{ id: 'campaign', title: 'Campaign', owner: 'Chloe', status: 'at-risk' },
{ id: 'release', title: 'Release', owner: 'David', status: 'on-track' },
],
},
];
export const STATUS_EVENTS: WdsGanttEvent[] = [
{ id: 's1', title: 'Brief', start: day(-14), end: day(-8), resourceId: 'brief-s', progress: 100, color: COLORS.blue },
{ id: 's2', title: 'Content', start: day(-7), end: day(5), resourceId: 'content', progress: 55, color: COLORS.green },
{ id: 's3', title: 'Campaign', start: day(-2), end: day(12), resourceId: 'campaign', progress: 10, color: COLORS.rose },
{ id: 's4', title: 'Release', start: day(14), end: day(14), resourceId: 'release', milestone: true, color: COLORS.amber },
];
// --- Baseline: planned against actual dates
export const BASELINE_RESOURCES: WdsGanttResource[] = [
{
id: 'implementation',
title: 'Implementation',
baselineStart: day(-8),
baselineEnd: day(4),
children: [
{ id: 'b-api', title: 'API integration' },
{ id: 'b-data', title: 'Data migration' },
{ id: 'b-security', title: 'Security hardening' },
],
},
{ id: 'launch-b', title: 'Launch', children: [{ id: 'b-handover', title: 'Handover' }] },
];
export const BASELINE_EVENTS: WdsGanttEvent[] = [
{ id: 'b1', title: 'API integration', start: day(-8), end: day(-3), resourceId: 'b-api', progress: 100, color: COLORS.violet, baselineStart: day(-8), baselineEnd: day(-5) },
{ id: 'b2', title: 'Data migration', start: day(-3), end: day(2), resourceId: 'b-data', progress: 70, color: COLORS.green, baselineStart: day(-3), baselineEnd: day(1) },
{ id: 'b3', title: 'Security hardening', start: day(1), end: day(6), resourceId: 'b-security', progress: 20, color: COLORS.teal, baselineStart: day(-1), baselineEnd: day(4) },
{ id: 'b4', title: 'Handover', start: day(8), end: day(8), resourceId: 'b-handover', milestone: true, color: COLORS.amber, baselineStart: day(6), baselineEnd: day(6) },
];
// --- Dependencies: a chained rollout plan
export const ROLLOUT_RESOURCES: WdsGanttResource[] = [
{
id: 'build-d',
title: 'Build',
children: [
{ id: 'd-frontend', title: 'Frontend' },
{ id: 'd-qa', title: 'QA pass' },
],
},
{
id: 'rollout',
title: 'Rollout',
children: [
{ id: 'd-staging', title: 'Staging deploy' },
{ id: 'd-signoff', title: 'Sign-off' },
{ id: 'd-live', title: 'Go live' },
],
},
];
export const ROLLOUT_EVENTS: WdsGanttEvent[] = [
{ id: 'd1', title: 'Frontend', start: day(-8), end: day(-4), resourceId: 'd-frontend', progress: 100, color: COLORS.violet },
{ id: 'd2', title: 'QA pass', start: day(-2), end: day(1), resourceId: 'd-qa', progress: 40, color: COLORS.purple, dependencies: ['d1'] },
{ id: 'd3', title: 'Staging deploy', start: day(2), end: day(3), resourceId: 'd-staging', color: COLORS.green, dependencies: ['d2'] },
{ id: 'd4', title: 'Sign-off', start: day(5), end: day(5), resourceId: 'd-signoff', milestone: true, color: COLORS.amber, dependencies: ['d3'] },
{ id: 'd5', title: 'Go live', start: day(6), end: day(7), resourceId: 'd-live', color: COLORS.teal, dependencies: ['d4'] },
];
// --- Day schedule: times as instants (UTC), shown in the chosen time zone
export const SHIFT_RESOURCES: WdsGanttResource[] = [
{
id: 'support',
title: 'Support rota',
children: [
{ id: 'emea', title: 'EMEA desk', color: COLORS.blue },
{ id: 'amer', title: 'Americas desk', color: COLORS.green },
{ id: 'apac', title: 'APAC desk', color: COLORS.amber },
],
},
];
const at = (offset: number, time: string) => `${day(offset)}T${time}:00Z`;
export const SHIFT_EVENTS: WdsGanttEvent[] = [
{ id: 'e1', title: 'Morning shift', start: at(0, '06:00'), end: at(0, '10:00'), resourceId: 'emea' },
{ id: 'e2', title: 'Release call', start: at(0, '11:00'), end: at(0, '11:45'), resourceId: 'emea', color: COLORS.rose },
{ id: 'e3', title: 'Afternoon shift', start: at(0, '12:00'), end: at(0, '16:00'), resourceId: 'emea' },
{ id: 'a1', title: 'Day shift', start: at(0, '13:00'), end: at(0, '21:00'), resourceId: 'amer' },
{ id: 'p1', title: 'Night shift', start: at(-1, '23:00'), end: at(0, '07:00'), resourceId: 'apac' },
{ id: 'p2', title: 'Handover', start: at(0, '07:00'), end: at(0, '07:00'), resourceId: 'apac', milestone: true },
];
export const TIME_ZONES = ['UTC', 'Europe/Warsaw', 'America/New_York', 'Asia/Tokyo'];
// --- Recurring: team rituals
export const RITUAL_RESOURCES: WdsGanttResource[] = [
{
id: 'rituals',
title: 'Team rituals',
children: [
{ id: 'standup', title: 'Standup', color: COLORS.sky },
{ id: 'review', title: 'Sprint review', color: COLORS.violet },
{ id: 'retro', title: 'Retrospective', color: COLORS.rose },
{ id: 'planning-r', title: 'Planning', color: COLORS.green },
],
},
];
export const RITUAL_EVENTS: WdsGanttEvent[] = [
{ id: 'standup', title: 'Standup', start: day(-28), end: day(-28), resourceId: 'standup', recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR' },
{ id: 'review', title: 'Sprint review', start: day(-25), end: day(-25), resourceId: 'review', recurrence: { freq: 'weekly', interval: 2 } },
{ id: 'retro', title: 'Retro', start: day(-24), end: day(-24), resourceId: 'retro', recurrence: 'FREQ=WEEKLY;INTERVAL=2' },
{ id: 'planning-r', title: 'Planning', start: day(-21), end: day(-20), resourceId: 'planning-r', recurrence: 'FREQ=WEEKLY;INTERVAL=2;COUNT=6' },
];
// --- Backlog: selection, menus, moving rows
export const BACKLOG_RESOURCES: WdsGanttResource[] = [
{
id: 'sprint-1',
title: 'Sprint 1',
children: [
{ id: 'login', title: 'Login' },
{ id: 'signup', title: 'Sign-up' },
],
},
{
id: 'sprint-2',
title: 'Sprint 2',
children: [
{ id: 'dashboard', title: 'Dashboard' },
{ id: 'reports', title: 'Reports' },
],
},
{ id: 'icebox', title: 'Icebox', children: [{ id: 'dark-mode', title: 'Dark mode' }] },
];
export const BACKLOG_EVENTS: WdsGanttEvent[] = [
{ id: 'k1', title: 'Login form', start: day(-6), end: day(-2), resourceId: 'login', progress: 100, color: COLORS.blue },
{ id: 'k2', title: 'Sign-up flow', start: day(-3), end: day(3), resourceId: 'signup', progress: 50, color: COLORS.sky },
{ id: 'k3', title: 'Dashboard', start: day(4), end: day(11), resourceId: 'dashboard', color: COLORS.green },
{ id: 'k4', title: 'Reports', start: day(8), end: day(15), resourceId: 'reports', color: COLORS.teal },
{ id: 'k5', title: 'Dark mode', start: day(18), end: day(24), resourceId: 'dark-mode', color: COLORS.violet },
];Settings#
hide-today, hide-summary, hide-dependencies, hide-baseline, hide-indicators, hide-zoom and hide-nav; bar-label places the titles inside or after the bars. Here a wds-dropdown in the toolbar switches them.
import { Component, signal } from '@angular/core';
import { WdsButtonComponent } from '@wds/angular/button';
import { WDS_DROPDOWN } from '@wds/angular/dropdown';
import { WdsGanttComponent } from '@wds/angular/gantt';
import type { WdsDropdownSelectDetail } from '@wds/core/dropdown';
import type { WdsGanttBarLabel } from '@wds/core/gantt';
import { PROJECT_PLAN_EVENTS, PROJECT_RESOURCES } from './gantt-data';
type Setting = 'offDays' | 'today' | 'summary' | 'dependencies' | 'baselines' | 'indicators' | 'zoom' | 'editable';
const OPTIONS: { key: Setting; label: string }[] = [
{ key: 'offDays', label: 'Off days' },
{ key: 'today', label: 'Today line' },
{ key: 'summary', label: 'Summary bars' },
{ key: 'dependencies', label: 'Dependencies' },
{ key: 'baselines', label: 'Baselines' },
{ key: 'indicators', label: 'Off-screen indicators' },
{ key: 'zoom', label: 'Zoom control' },
{ key: 'editable', label: 'Drag and resize' },
];
@Component({
selector: 'app-gantt-settings-demo',
host: { class: 'block w-full' },
imports: [WdsButtonComponent, WdsGanttComponent, ...WDS_DROPDOWN],
template: `
<wds-gantt
class="h-[480px]"
label="Project with settings"
[resources]="resources"
[events]="events"
[offDays]="settings().offDays"
[hideToday]="!settings().today"
[hideSummary]="!settings().summary"
[hideDependencies]="!settings().dependencies"
[hideBaseline]="!settings().baselines"
[hideIndicators]="!settings().indicators"
[hideZoom]="!settings().zoom"
[readonly]="!settings().editable"
[barLabel]="barLabel()"
>
<wds-dropdown slot="toolbar" align="end" (wdsSelect)="toggle($event)">
<wds-button slot="trigger" variant="outline" size="sm">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" /><circle cx="12" cy="12" r="3" /></svg>
Settings
</wds-button>
<wds-dropdown-label>Display</wds-dropdown-label>
@for (option of options; track option.key) {
<wds-dropdown-item type="checkbox" [value]="option.key" [checked]="settings()[option.key]">{{ option.label }}</wds-dropdown-item>
}
<wds-dropdown-separator />
<wds-dropdown-label>Bar labels</wds-dropdown-label>
<wds-dropdown-radio-group [value]="barLabel()" (wdsChange)="barLabel.set($any($event.value))">
<wds-dropdown-item type="radio" value="auto">Auto</wds-dropdown-item>
<wds-dropdown-item type="radio" value="inside">Inside</wds-dropdown-item>
<wds-dropdown-item type="radio" value="outside">Outside</wds-dropdown-item>
</wds-dropdown-radio-group>
</wds-dropdown>
</wds-gantt>
`,
})
export class GanttSettingsDemo {
protected readonly resources = PROJECT_RESOURCES;
protected readonly events = PROJECT_PLAN_EVENTS;
protected readonly options = OPTIONS;
protected readonly settings = signal<Record<Setting, boolean>>({
offDays: true,
today: true,
summary: true,
dependencies: true,
baselines: true,
indicators: true,
zoom: true,
editable: true,
});
protected readonly barLabel = signal<WdsGanttBarLabel>('auto');
/** A checkbox item toggles its setting (radio items are handled by the radio group). */
protected toggle({ value, checked }: WdsDropdownSelectDetail): void {
if (OPTIONS.some((o) => o.key === value)) this.settings.update((s) => ({ ...s, [value]: !!checked }));
}
}import '@wds/core/avatar';
import '@wds/core/badge';
import type { WdsGanttColumn, WdsGanttEvent, WdsGanttResource } from '@wds/core/gantt';
const pad = (n: number) => String(n).padStart(2, '0');
const base = new Date();
/** An ISO date `offset` days from today, so the examples always show the current period. */
export function day(offset: number): string {
const d = new Date(base.getFullYear(), base.getMonth(), base.getDate() + offset);
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/** An ISO date moved by `days`. */
export function addDays(iso: string, days: number): string {
const [y, m, d] = iso.split('-').map(Number);
const date = new Date(y, m - 1, d + days);
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
}
/** Bar colors (any CSS color works). */
export const COLORS = {
blue: 'oklch(0.623 0.214 259.815)',
purple: 'oklch(0.627 0.265 303.9)',
violet: 'oklch(0.606 0.25 292.717)',
green: 'oklch(0.696 0.17 162.48)',
teal: 'oklch(0.704 0.14 182.503)',
sky: 'oklch(0.685 0.169 237.323)',
amber: 'oklch(0.769 0.188 70.08)',
rose: 'oklch(0.645 0.246 16.439)',
};
// --- Default: a website project
export const PROJECT_RESOURCES: WdsGanttResource[] = [
{
id: 'planning',
title: 'Planning',
children: [
{ id: 'brief', title: 'Project brief' },
{ id: 'scope', title: 'Scope review' },
],
},
{
id: 'design',
title: 'Design',
children: [
{ id: 'wireframes', title: 'Wireframes' },
{ id: 'visual-design', title: 'Visual design' },
],
},
{
id: 'build',
title: 'Build',
children: [
{ id: 'frontend', title: 'Frontend' },
{ id: 'backend', title: 'Backend' },
{ id: 'qa', title: 'QA pass' },
],
},
];
export const PROJECT_EVENTS: WdsGanttEvent[] = [
{ id: 'brief', title: 'Project brief', start: day(-20), end: day(-16), resourceId: 'brief', progress: 100, color: COLORS.blue },
{ id: 'scope', title: 'Scope review', start: day(-15), end: day(-11), resourceId: 'scope', progress: 100, color: COLORS.sky },
{ id: 'wireframes', title: 'Wireframes', start: day(-9), end: day(-4), resourceId: 'wireframes', progress: 100, color: COLORS.violet },
{ id: 'visual', title: 'Visual design', start: day(-3), end: day(2), resourceId: 'visual-design', progress: 40, color: COLORS.purple },
{ id: 'frontend', title: 'Frontend', start: day(0), end: day(12), resourceId: 'frontend', progress: 10, color: COLORS.green },
{ id: 'backend', title: 'Backend', start: day(2), end: day(14), resourceId: 'backend', color: COLORS.teal },
{ id: 'qa', title: 'QA pass', start: day(13), end: day(17), resourceId: 'qa', color: COLORS.amber },
];
/** Dependencies and planned windows added to the project (for the settings example). */
const PLAN: Record<string, Partial<WdsGanttEvent>> = {
scope: { dependencies: ['brief'] },
wireframes: { dependencies: ['scope'], baselineStart: day(-10), baselineEnd: day(-6) },
visual: { dependencies: ['wireframes'], baselineStart: day(-5), baselineEnd: day(1) },
frontend: { dependencies: ['visual'] },
qa: { dependencies: ['backend'] },
};
export const PROJECT_PLAN_EVENTS: WdsGanttEvent[] = PROJECT_EVENTS.map((e) => ({ ...e, ...PLAN[e.id] }));
// --- Roadmap: initiatives on the quarter scale
export const ROADMAP_RESOURCES: WdsGanttResource[] = [
{
id: 'platform',
title: 'Platform',
children: [
{ id: 'auth', title: 'Auth revamp' },
{ id: 'api', title: 'API v2' },
],
},
{
id: 'growth',
title: 'Growth',
children: [
{ id: 'onboarding', title: 'Onboarding flow' },
{ id: 'referral', title: 'Referral program' },
],
},
{ id: 'backlog', title: 'Backlog', children: [{ id: 'next', title: 'Next initiative' }] },
];
export const ROADMAP_EVENTS: WdsGanttEvent[] = [
{ id: 'r1', title: 'Auth revamp', start: day(-40), end: day(25), resourceId: 'auth', progress: 60, color: COLORS.blue },
{ id: 'r2', title: 'API v2', start: day(15), end: day(70), resourceId: 'api', color: COLORS.sky },
{ id: 'r3', title: 'Onboarding flow', start: day(-30), end: day(10), resourceId: 'onboarding', progress: 85, color: COLORS.green },
{ id: 'r4', title: 'Referral program', start: day(60), end: day(110), resourceId: 'referral', color: COLORS.teal },
];
// --- Team capacity: people grouped by squad, several assignments per person
export const TEAM_RESOURCES: WdsGanttResource[] = [
{
id: 'product',
title: 'Product squad',
children: [
{ id: 'ada', title: 'Ada Lovelace', color: COLORS.blue },
{ id: 'alan', title: 'Alan Turing', color: COLORS.sky },
],
},
{
id: 'design-squad',
title: 'Design squad',
children: [
{ id: 'grace', title: 'Grace Hopper', color: COLORS.violet },
{ id: 'linus', title: 'Linus Torvalds', color: COLORS.purple },
],
},
];
export const TEAM_EVENTS: WdsGanttEvent[] = [
{ id: 't1', title: 'Checkout', start: day(-3), end: day(1), resourceId: 'ada' },
{ id: 't2', title: 'Code review', start: day(0), end: day(3), resourceId: 'ada', color: COLORS.amber },
{ id: 't3', title: 'Billing API', start: day(-2), end: day(-1), resourceId: 'alan' },
{ id: 't4', title: 'Design tokens', start: day(-4), end: day(0), resourceId: 'grace' },
{ id: 't5', title: 'Icon set', start: day(2), end: day(5), resourceId: 'linus' },
];
/**
* Row label with an avatar (`renderResourceLabel`). It renders in the shadow DOM of the chart,
* so it is styled inline (Tailwind classes do not reach it).
*/
export function avatarLabel(resource: WdsGanttResource): string | Node {
if (resource.children?.length) return resource.title;
const label = document.createElement('span');
label.style.cssText = 'display:inline-flex;align-items:center;gap:8px;min-width:0';
// wds-avatar with initials; the row color reaches the fallback through the tokens it uses
const avatar = document.createElement('wds-avatar');
avatar.size = 'sm';
avatar.setAttribute('aria-hidden', 'true');
const color = resource.color ?? COLORS.blue;
avatar.style.setProperty('--wds-muted', `color-mix(in srgb, ${color} 22%, transparent)`);
avatar.style.setProperty('--wds-foreground', `color-mix(in srgb, ${color} 45%, currentColor)`);
avatar.textContent = resource.title
.split(' ')
.map((part) => part[0])
.join('')
.slice(0, 2);
const name = document.createElement('span');
name.style.cssText = 'overflow:hidden;text-overflow:ellipsis';
name.textContent = resource.title;
label.append(avatar, name);
return label;
}
// --- Status report: extra columns in the tree panel
export type Status = 'done' | 'on-track' | 'at-risk';
const STATUS_LABEL: Record<Status, string> = { done: 'Done', 'on-track': 'On track', 'at-risk': 'At risk' };
const STATUS_VARIANT: Record<Status, string> = { done: 'secondary', 'on-track': 'outline', 'at-risk': 'destructive' };
/** Content of a status cell (`render` of a column): a DOM node, here a `wds-badge`. */
export function statusBadge(status: Status): HTMLElement {
const el = document.createElement('wds-badge');
el.setAttribute('variant', STATUS_VARIANT[status]);
el.textContent = STATUS_LABEL[status];
return el;
}
export const STATUS_COLUMNS: WdsGanttColumn[] = [
{ key: 'owner', header: 'Owner', width: '5.5rem' },
{ key: 'status', header: 'Status', width: '6.5rem', render: (r) => statusBadge(r['status'] as Status) },
];
export const STATUS_RESOURCES: WdsGanttResource[] = [
{
id: 'launch',
title: 'Launch',
owner: 'Ana',
status: 'on-track',
children: [
{ id: 'brief-s', title: 'Brief', owner: 'Ana', status: 'done' },
{ id: 'content', title: 'Content', owner: 'Ben', status: 'on-track' },
{ id: 'campaign', title: 'Campaign', owner: 'Chloe', status: 'at-risk' },
{ id: 'release', title: 'Release', owner: 'David', status: 'on-track' },
],
},
];
export const STATUS_EVENTS: WdsGanttEvent[] = [
{ id: 's1', title: 'Brief', start: day(-14), end: day(-8), resourceId: 'brief-s', progress: 100, color: COLORS.blue },
{ id: 's2', title: 'Content', start: day(-7), end: day(5), resourceId: 'content', progress: 55, color: COLORS.green },
{ id: 's3', title: 'Campaign', start: day(-2), end: day(12), resourceId: 'campaign', progress: 10, color: COLORS.rose },
{ id: 's4', title: 'Release', start: day(14), end: day(14), resourceId: 'release', milestone: true, color: COLORS.amber },
];
// --- Baseline: planned against actual dates
export const BASELINE_RESOURCES: WdsGanttResource[] = [
{
id: 'implementation',
title: 'Implementation',
baselineStart: day(-8),
baselineEnd: day(4),
children: [
{ id: 'b-api', title: 'API integration' },
{ id: 'b-data', title: 'Data migration' },
{ id: 'b-security', title: 'Security hardening' },
],
},
{ id: 'launch-b', title: 'Launch', children: [{ id: 'b-handover', title: 'Handover' }] },
];
export const BASELINE_EVENTS: WdsGanttEvent[] = [
{ id: 'b1', title: 'API integration', start: day(-8), end: day(-3), resourceId: 'b-api', progress: 100, color: COLORS.violet, baselineStart: day(-8), baselineEnd: day(-5) },
{ id: 'b2', title: 'Data migration', start: day(-3), end: day(2), resourceId: 'b-data', progress: 70, color: COLORS.green, baselineStart: day(-3), baselineEnd: day(1) },
{ id: 'b3', title: 'Security hardening', start: day(1), end: day(6), resourceId: 'b-security', progress: 20, color: COLORS.teal, baselineStart: day(-1), baselineEnd: day(4) },
{ id: 'b4', title: 'Handover', start: day(8), end: day(8), resourceId: 'b-handover', milestone: true, color: COLORS.amber, baselineStart: day(6), baselineEnd: day(6) },
];
// --- Dependencies: a chained rollout plan
export const ROLLOUT_RESOURCES: WdsGanttResource[] = [
{
id: 'build-d',
title: 'Build',
children: [
{ id: 'd-frontend', title: 'Frontend' },
{ id: 'd-qa', title: 'QA pass' },
],
},
{
id: 'rollout',
title: 'Rollout',
children: [
{ id: 'd-staging', title: 'Staging deploy' },
{ id: 'd-signoff', title: 'Sign-off' },
{ id: 'd-live', title: 'Go live' },
],
},
];
export const ROLLOUT_EVENTS: WdsGanttEvent[] = [
{ id: 'd1', title: 'Frontend', start: day(-8), end: day(-4), resourceId: 'd-frontend', progress: 100, color: COLORS.violet },
{ id: 'd2', title: 'QA pass', start: day(-2), end: day(1), resourceId: 'd-qa', progress: 40, color: COLORS.purple, dependencies: ['d1'] },
{ id: 'd3', title: 'Staging deploy', start: day(2), end: day(3), resourceId: 'd-staging', color: COLORS.green, dependencies: ['d2'] },
{ id: 'd4', title: 'Sign-off', start: day(5), end: day(5), resourceId: 'd-signoff', milestone: true, color: COLORS.amber, dependencies: ['d3'] },
{ id: 'd5', title: 'Go live', start: day(6), end: day(7), resourceId: 'd-live', color: COLORS.teal, dependencies: ['d4'] },
];
// --- Day schedule: times as instants (UTC), shown in the chosen time zone
export const SHIFT_RESOURCES: WdsGanttResource[] = [
{
id: 'support',
title: 'Support rota',
children: [
{ id: 'emea', title: 'EMEA desk', color: COLORS.blue },
{ id: 'amer', title: 'Americas desk', color: COLORS.green },
{ id: 'apac', title: 'APAC desk', color: COLORS.amber },
],
},
];
const at = (offset: number, time: string) => `${day(offset)}T${time}:00Z`;
export const SHIFT_EVENTS: WdsGanttEvent[] = [
{ id: 'e1', title: 'Morning shift', start: at(0, '06:00'), end: at(0, '10:00'), resourceId: 'emea' },
{ id: 'e2', title: 'Release call', start: at(0, '11:00'), end: at(0, '11:45'), resourceId: 'emea', color: COLORS.rose },
{ id: 'e3', title: 'Afternoon shift', start: at(0, '12:00'), end: at(0, '16:00'), resourceId: 'emea' },
{ id: 'a1', title: 'Day shift', start: at(0, '13:00'), end: at(0, '21:00'), resourceId: 'amer' },
{ id: 'p1', title: 'Night shift', start: at(-1, '23:00'), end: at(0, '07:00'), resourceId: 'apac' },
{ id: 'p2', title: 'Handover', start: at(0, '07:00'), end: at(0, '07:00'), resourceId: 'apac', milestone: true },
];
export const TIME_ZONES = ['UTC', 'Europe/Warsaw', 'America/New_York', 'Asia/Tokyo'];
// --- Recurring: team rituals
export const RITUAL_RESOURCES: WdsGanttResource[] = [
{
id: 'rituals',
title: 'Team rituals',
children: [
{ id: 'standup', title: 'Standup', color: COLORS.sky },
{ id: 'review', title: 'Sprint review', color: COLORS.violet },
{ id: 'retro', title: 'Retrospective', color: COLORS.rose },
{ id: 'planning-r', title: 'Planning', color: COLORS.green },
],
},
];
export const RITUAL_EVENTS: WdsGanttEvent[] = [
{ id: 'standup', title: 'Standup', start: day(-28), end: day(-28), resourceId: 'standup', recurrence: 'FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR' },
{ id: 'review', title: 'Sprint review', start: day(-25), end: day(-25), resourceId: 'review', recurrence: { freq: 'weekly', interval: 2 } },
{ id: 'retro', title: 'Retro', start: day(-24), end: day(-24), resourceId: 'retro', recurrence: 'FREQ=WEEKLY;INTERVAL=2' },
{ id: 'planning-r', title: 'Planning', start: day(-21), end: day(-20), resourceId: 'planning-r', recurrence: 'FREQ=WEEKLY;INTERVAL=2;COUNT=6' },
];
// --- Backlog: selection, menus, moving rows
export const BACKLOG_RESOURCES: WdsGanttResource[] = [
{
id: 'sprint-1',
title: 'Sprint 1',
children: [
{ id: 'login', title: 'Login' },
{ id: 'signup', title: 'Sign-up' },
],
},
{
id: 'sprint-2',
title: 'Sprint 2',
children: [
{ id: 'dashboard', title: 'Dashboard' },
{ id: 'reports', title: 'Reports' },
],
},
{ id: 'icebox', title: 'Icebox', children: [{ id: 'dark-mode', title: 'Dark mode' }] },
];
export const BACKLOG_EVENTS: WdsGanttEvent[] = [
{ id: 'k1', title: 'Login form', start: day(-6), end: day(-2), resourceId: 'login', progress: 100, color: COLORS.blue },
{ id: 'k2', title: 'Sign-up flow', start: day(-3), end: day(3), resourceId: 'signup', progress: 50, color: COLORS.sky },
{ id: 'k3', title: 'Dashboard', start: day(4), end: day(11), resourceId: 'dashboard', color: COLORS.green },
{ id: 'k4', title: 'Reports', start: day(8), end: day(15), resourceId: 'reports', color: COLORS.teal },
{ id: 'k5', title: 'Dark mode', start: day(18), end: day(24), resourceId: 'dark-mode', color: COLORS.violet },
];API References#
wds-gantt#
resources) next to a timeline with task bars (events), from a day
with hours to a year with months. Bars can be moved and resized with the pointer or the keyboard;
a group row shows a summary bar of its tasks. Tasks can have progress, a planned window (baseline),
dependencies drawn as arrows, a repeat rule, and a milestone shows as a diamond. The timeline scrolls
without end (within min/max) and renders only the visible rows and dates.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| scale | WdsGanttScale | 'month' | |
| date | string | '' | |
| zoom | number | 1 | |
| selected | string[] | ||
| collapsed | string[] | collapsed: true starts here). In HTML a space-separated list. | |
| readonly | boolean | false | |
| drag-create.dragCreate | boolean | false | wds-slot-select (such as to add a task there). |
| selectable | boolean | false | |
| reorderable | boolean | false | wds-resource-reorder). |
| off-days.offDays | boolean | false | holidays. |
| holidays | string[] | [] | off-days. In HTML a space-separated list. |
| time-zone.timeZone | string | '' | Europe/Warsaw). Default: the browser's. |
| snap | number | 15 | day scale, in minutes (other scales move by whole days). |
| min | string | '' | |
| max | string | '' | |
| bar-label.barLabel | WdsGanttBarLabel | 'auto' | auto). |
| hide-nav.hideNav | boolean | false | next(), prev(), today() and goTo()). |
| hide-today.hideToday | boolean | false | |
| hide-summary.hideSummary | boolean | false | |
| hide-dependencies.hideDependencies | boolean | false | |
| hide-baseline.hideBaseline | boolean | false | |
| hide-zoom.hideZoom | boolean | false | |
| hide-indicators.hideIndicators | boolean | false | |
| loading | boolean | false | aria-busy (such as while tasks load). |
| name-header.nameHeader | string | 'Resources' | |
| empty-text.emptyText | string | 'No tasks.' | |
| label | string | 'Gantt chart' | |
| locale | string | '' | en-US). Defaults to the nearest lang attribute. |
| first-day.firstDay | number | undefined |
Properties
| Property | Description |
|---|---|
| resourcesWdsGanttResource[] | WdsGanttResource[]); a row with children is a collapsible group. The chart updates it after a row is moved. |
| eventsWdsGanttEvent[] | WdsGanttEvent[]). The chart updates this array itself after a move, resize or delete. |
| columnsWdsGanttColumn[] | WdsGanttColumn[]). |
| eventActionsWdsGanttAction[] | wds-event-action. |
| resourceActionsWdsGanttAction[] | wds-resource-action. |
| renderEvent(event: WdsGanttEvent) => string | Node | undefined | wds-* element) instead of the title. |
| renderResourceLabel(resource: WdsGanttResource) => string | Node | undefined | |
| canDropEvent(change: Omit<WdsGanttEventChangeDetail, 'events'>) => boolean | undefined | false to reject it. |
| canSelectSlot(slot: WdsGanttSlotSelectDetail) => boolean | undefined | drag-create: return false to reject it (the preview turns red). |
| canReorderResource(reorder: Omit<WdsGanttResourceReorderDetail, 'resources'>) => boolean | undefined | false to reject it. |
Events
| Event | Description |
|---|---|
| wds-zoom-changeCustomEvent<WdsGanttZoomChangeDetail> | |
| wds-range-changeCustomEvent<WdsGanttRangeChangeDetail> | |
| wds-selection-changeCustomEvent<WdsGanttSelectionChangeDetail> | |
| wds-event-changeCustomEvent<WdsGanttEventChangeDetail> | canDropEvent accepted it). |
| wds-event-removeCustomEvent<WdsGanttEventRemoveDetail> | |
| wds-collapse-changeCustomEvent<WdsGanttCollapseChangeDetail> | |
| wds-slot-selectCustomEvent<WdsGanttSlotSelectDetail> | drag-create). |
| wds-resource-reorderCustomEvent<WdsGanttResourceReorderDetail> | reorderable). |
| wds-resource-actionCustomEvent<WdsGanttResourceActionDetail> | resourceActions) was chosen. |
| wds-event-actionCustomEvent<WdsGanttEventActionDetail> | eventActions) was chosen. |
Methods
| Method | Description |
|---|---|
| next() | |
| prev() | |
| today() | |
| goTo(date: string) | |
| scrollToEvent(id: string) | |
| zoomIn() | |
| zoomOut() |
Slots
| Slot | Description |
|---|---|
| toolbar | |
| empty | empty-text). |
CSS parts (::part)
| Part | Description |
|---|---|
| nav | |
| title | |
| scroller | |
| tree | role="treegrid"). |
| timeline | |
| zoom |
CSS custom properties
| Property | Description |
|---|---|
| --wds-gantt-tree-width | 15rem, 9rem in a narrow chart). |
| --wds-gantt-bar | --wds-chart-2). |
| --wds-gantt-now | --wds-destructive). |
Accessibility#
Screen reader#
region named by label. The tree panel is a treegrid: a header row with columnheader cells and a row per resource with aria-level, aria-posinset, aria-setsize, aria-rowindex (rows outside the view are not rendered) and, on a group, aria-expanded. The header with dates is hidden from screen readers, because every bar names its dates.
group named “timeline”. A bar is a button (aria-pressed when it is selected, aria-haspopup with eventActions) named by its title, dates (with times on date-time tasks), progress, “repeats” for an occurrence and the variance against the plan, such as “Data migration, Sep 28 – Oct 3, 2026, 70% complete, late against the plan”. A milestone is named “milestone” with its date. A group summary bar is an image named by the group, its dates and progress. Dependency arrows and plan outlines are decorative.
canDropEvent or canReorderResource. The period title in the navigation bar is a live region and loading sets aria-busy.
label, name-header and empty-text can be changed.
Keyboard support#
| Key | Function |
|---|---|
selectable: selects or clears its tasks (on the header row: all tasks). On a bar: selects the task. | |
snap minutes on the day scale). | |
reorderable: moves the focused row up or down among its siblings. | |
eventActions) or row (resourceActions). | |