api reference
Gantt API Reference
createGantt: the scheduling engine and the critical path, the four dependency links, milestones and summaries, and editable bars.
API reference › The Gantt module
All 29 pages Everything on one page → Developer guide →
The Gantt module
modules/gantt is a separate, opt-in project-planning module - its own bundle, imported only when you want it, changing nothing in the grid core. It turns a task list into a real schedule: a CPM (Critical Path Method) engine computes each task's early/late start and finish, its slack (total float), and the zero-float critical path, recomputing on every edit. computeSchedule(tasks, deps) is the pure engine; createGantt(opts) is a controller that holds the model, recomputes on setTasks/setDependencies/applyEdit, and emits schedule (or error). Dependencies are the four standard link types - LINK_TYPES is ['FS','SS','FF','SF'] - each with optional lag/lead. A milestone is a zero-duration task scheduled as a point; a summary task (any task named as another's parent) is derived from its children (start = earliest child, end = latest child, duration-weighted progress) and is not scheduled itself. Bad input never throws or loops: a dependency cycle is refused and reported with a code from SCHEDULE_ERROR, and findViolations flags any task placed earlier than its predecessors allow. toISODate converts an engine day-number back to a calendar date for display.
Touch. Task bars move and resize by a pointer gesture, so they work on phones and tablets as well as with a mouse. A mouse drag starts on movement; a touch or pen drag starts after a short press-and-hold on the bar (or on its right end, to resize), so an ordinary swipe still scrolls the timeline and the task table. Holding near the timeline's edge scrolls it, and the drop lands on the day under the finger. Escape or a second finger cancels the drag and restores the bar; the scheduling cascade, constraints and locks apply exactly as for a mouse drag. The two resize ends, the progress handle and a dependency-link connector work the same press-and-hold way and, on a coarse pointer, each gets an invisible hit area at least 24×24 CSS px without moving its drawn mark.
Feed it the rows you already have. fields names your own task properties for the scheduler - { id: 'taskId', start: 'startDate', name: 'jobName', duration: 'dur' }, each a field name or a reader (row) => value - so a task list arrives as it is rather than being renamed first. The vocabulary is id, name, start, end, duration, milestone, percentComplete, parent, baselineStart, baselineEnd, constraint and constraintDate; anything you leave unmapped reads its canonical name, so an existing plan is unaffected. rowKey reaches the scheduler too, so a row carrying taskId and no id is identified by it - a task's own id still wins where it has one. It is a read mapping: applyEdit and level() write the canonical property, so each says so plainly rather than writing to a field the schedule is not read from, and assignee/cost/actualCost belong to the resource and earned-value layers rather than to this list.
import { createGantt, computeSchedule } from '@toclocoinc/lattice-grid/modules/gantt';
const plan = createGantt({
tasks: [
{ id: 'design', duration: 5 },
{ id: 'build', duration: 6 },
{ id: 'launch', milestone: true },
],
dependencies: [
{ from: 'design', to: 'build', type: 'FS' },
{ from: 'build', to: 'launch', type: 'FS' },
],
});
plan.on('schedule', (s) => console.log(s.critical, s.projectDuration));
plan.applyEdit({ id: 'design', duration: 7 }); // recomputes; the critical path shifts
Effort-driven scheduling. A task's schedulingMode - 'fixedDuration' (the default), 'fixedEffort' or 'fixedUnits' - decides which of the three quantities in effort = duration × Σ units × hoursPerDay is held when another changes, set per task or as the createGantt default. On a fixedEffort task the duration is derived from the stated effort and the assigned resources' hours, so adding a resource shortens it; the duration is recomputed once, before the critical path, so the schedule, level(), the workload band and earned value all read the one number. reconcileEffort is the pure rule every edit funnels through; MODE 0/1/2 and EffortDriven round-trip through MSPDI.
const { createGantt, computeSchedule, reconcileEffort, resolveSchedulingMode, SCHEDULING_MODES } = await import('../packages/modules/gantt/index.js');
// SCHEDULING_MODES lists the three; a host spelling resolves to one of them.
const mode = resolveSchedulingMode('fixed-effort'); // 'fixedEffort'
if (!SCHEDULING_MODES.includes(mode)) throw new Error('unknown mode');
// A 40-hour fixed-effort task: a full-timer (8h/day) finishes it in 5 days.
const full = createGantt({
tasks: [{ id: 'A', schedulingMode: mode, effort: 40, assignments: [{ resource: 'ren', units: 1 }] }],
resources: [{ id: 'ren', calendar: { hoursPerDay: 8 } }],
hoursPerDay: 8,
});
const fullDays = full.compute().tasks.get('A').duration;
// A part-timer (4h/day) accrues the same 40 hours over twice as long - 10 days.
const part = createGantt({
tasks: [{ id: 'A', schedulingMode: mode, effort: 40, assignments: [{ resource: 'sam', units: 1 }] }],
resources: [{ id: 'sam', calendar: { hoursPerDay: 4 } }],
hoursPerDay: 8,
});
const partDays = part.compute().tasks.get('A').duration;
// Adding a second full unit to the full-timer's task halves its duration.
const added = reconcileEffort({ mode, duration: 5, units: 1, hoursPerDay: 8 }, { field: 'addResource', units: 1 });
// computeSchedule is the pure engine behind the controller.
const direct = computeSchedule(
[{ id: 'A', schedulingMode: mode, effort: 40, assignments: [{ resource: 'ren', units: 1 }] }],
[], { hoursPerDay: 8, resourceHours: new Map([['ren', 8]]) },
).tasks.get('A').duration;
full.destroy(); part.destroy();
return `full ${fullDays}, part ${partDays}, +resource ${added.duration}`;
Assigning resources. In the split view the assignee cell is a button that opens an assignment picker: a searchable, multi-select list of resources, each showing its load over the task's dates, a units percentage per assignment (100 by default) and a remove button. The load comes from the same computeResourceLoad as the workload band - previewAssignments(id, roster) runs it with that one task's roster swapped, so an over-allocating choice is announced (in a polite live region) before it is committed. Apply sends the whole roster as one applyEdit({ id, assignments }): the task reschedules once per its schedulingMode, the write-back follows the plan's columns and fields, and it is a single change event. The picker is a listbox you can drive from the keyboard (arrows, Enter, Escape, Tab stays inside). Add 'effort' and 'units' to the split view's columns for two optional columns that edit through the same applyEdit rule.
Single-assignee mode. A task states assignmentPicker: { max, units } to shape its own picker. max: 1 makes the roster single-assignee: choosing a second resource replaces the first (a larger max drops the oldest entry once the roster is full), and the list's aria-multiselectable reflects it. units shows the per-assignment percent field - the default since the picker shipped, so units: false renders rows without it. The roster is still committed as one applyEdit({ assignments }).
const { createGantt } = await import('../packages/modules/gantt/index.js');
const plan = createGantt({
projectStart: '2027-01-04',
resources: { Ann: 1, Bob: 1 },
tasks: [
{ id: 'bob-busy', name: 'Bob is busy', start: '2027-01-04', duration: 4, assignee: 'Bob' },
{ id: 'build', name: 'Build', start: '2027-01-04', duration: 4, schedulingMode: 'fixedEffort',
assignments: [{ resource: 'Ann', units: 1 }] },
],
});
let events = 0;
plan.on('schedule', () => { events += 1; });
// What the picker shows before anything is committed: the load with Bob added.
const roster = [{ resource: 'Ann', units: 1 }, { resource: 'Bob', units: 1 }];
const preview = plan.previewAssignments('build', roster);
const over = preview.overAllocations.find((o) => o.resource === 'Bob');
const warning = `over ${over.resource} ${over.load * 100}% of ${over.capacity * 100}%`;
// Apply: one edit, one change event; fixed-effort, so two people halve the duration.
plan.applyEdit({ id: 'build', assignments: roster });
const build = plan.schedule.tasks.get('build');
const out = `${warning}; duration ${build.duration}, effort ${build.effort}, events ${events}`;
plan.destroy();
return out;
const { createTestDom, TestEvent } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root, document } = createTestDom();
const gantt = createGantt({
projectStart: '2027-01-04',
resources: { Ann: 1, Bob: 1 },
tasks: [
{ id: 'build', name: 'Build', start: '2027-01-04', duration: 4,
assignmentPicker: { max: 1, units: true }, assignee: 'Ann' },
],
});
const view = gantt.mountSplit(root, { columns: ['name', 'assignee', 'units'] });
const NS = 'lat-gantt-split';
const click = (el) => el.dispatchEvent(new TestEvent('click', { bubbles: true }));
// Open the picker and choose Bob: max: 1 replaces Ann rather than adding to her.
click(root.querySelector('[data-picker-for="build"]'));
const option = (name) => [...document.querySelectorAll('[role="option"]')]
.find((o) => o.getAttribute('data-resource') === name);
click(option('Bob'));
const chosen = [...document.querySelectorAll('.' + NS + '__picker-row')]
.map((row) => row.getAttribute('data-resource'));
const units = document.querySelector('.' + NS + '__picker-units') ? 'shown' : 'hidden';
click(document.querySelector('.' + NS + '__picker-apply'));
const roster = gantt.taskById('build').assignments;
view.destroy();
return `chosen ${chosen.join('+')}; units ${units}; roster ${roster.map((a) => `${a.resource} ${a.units * 100}%`).join('+')}`;
A standalone resource-load band. createResourceView(container, opts) draws the workload band on its own - resources and bookings, with no task Gantt, no plan and no CPM - for a resource-availability view by department, portfolio or scenario. It reuses the split view's workload band rather than forking it, so day/week/month/quarter buckets with the scale header and zoom, hours/percent/cost units with utilisation thresholds, expand-to-bookings, the workload:click bucket event, its own grid-backed column definitions and width round-trip, and SVG export all come across unchanged. Bookings are { resource, start, end, units | hours, label, group } (the workload.external shape), and a resource expands into its bookings grouped by group - one summed sub-row per group; setBookings applies a changed set live, and the time-bucket columns never carry a grid control.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGrid } = await import('../packages/dom/src/index.js');
const { createResourceView } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const rv = createResourceView(root, {
createGrid,
resources: [{ id: 'Ann', name: 'Ann', capacity: 1 }, { id: 'Bob', name: 'Bob', capacity: 1 }],
bookings: [
{ resource: 'Ann', start: '2027-01-04', end: '2027-01-09', hours: 40, label: 'Project X', group: 'Portfolio A' },
{ resource: 'Bob', start: '2027-01-04', end: '2027-01-09', hours: 120, label: 'Project Y', group: 'Portfolio B' },
],
zoom: 'week',
unit: 'percent',
thresholds: [{ at: 1.0, colour: 'red' }],
});
const countRows = () => { let n = 0; rv.grid.rows.forEach(() => { n += 1; }); return n; };
const rows = countRows(); // Ann, Bob, totals
// Bob's 120h single week is over any capacity-1 week: its bucket cell flags.
// The buckets are grid cells, read through the bucket
// column's own cell spec - the class function the grid itself paints with.
const bucketIds = rv.grid.columns.visible().map((c) => c.id).filter((id) => /^\d{4}-\d{2}-\d{2}$/.test(id));
const cellClass = (key, colId) => {
const row = rv.grid.rows.byKey(key);
return rv.grid.columns.get(colId).cell.class({ row, value: rv.grid.rows.value(key, colId), data: row.data });
};
const over = bucketIds.some((id) => cellClass('Bob\u0000', id).includes('lat-gantt-split__wl-bucket--over')) ? 'yes' : 'no';
rv.setBookings([{ resource: 'Ann', start: '2027-01-04', end: '2027-01-09', hours: 40, group: 'Portfolio A' }]);
const after = countRows(); // Ann, totals - Bob's gone
const svg = rv.toSVG().includes('<svg') ? 'yes' : 'no';
const unit = rv.getUnit();
rv.destroy();
return `unit ${unit}; rows ${rows}; after ${after}; over ${over}; svg ${svg}`;
Grouping the resources by a field. groupBy groups the resource rows by one of the resources' own fields - 'department', a business unit, a division - or by a function of the resource. Each group draws a collapsible header row carrying the group's summed load and available hours, so the thresholds and the percent unit read the group's own utilisation; resources that name no group (and Unassigned) collect last. A sort on the left-hand columns keeps every resource under its group; toggleGroup(value) collapses or expands a group.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGrid } = await import('../packages/dom/src/index.js');
const { createResourceView } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const rv = createResourceView(root, {
createGrid,
groupBy: 'department',
resources: [
{ id: 'ann', name: 'Ann', capacity: 1, department: 'Engineering' },
{ id: 'bob', name: 'Bob', capacity: 1, department: 'Engineering' },
{ id: 'cat', name: 'Cat', capacity: 1, department: 'Design' },
],
bookings: [
{ resource: 'ann', start: '2027-01-04', end: '2027-01-09', hours: 40, label: 'Project X' },
{ resource: 'bob', start: '2027-01-04', end: '2027-01-09', hours: 30, label: 'Project Y' },
{ resource: 'cat', start: '2027-01-04', end: '2027-01-09', hours: 10, label: 'Project Z' },
],
zoom: 'week',
});
const labels = () => { const out = []; rv.grid.rows.forEach((r) => out.push(rv.grid.rows.text(r.key, 'resource'))); return out.join(', '); };
const shown = labels();
// The Engineering group header is a grid row; its bucket
// cells are grid cells whose values sum the group's resources: 40 + 30 = 70 h.
let engKey = null;
rv.grid.rows.forEach((r) => { if (rv.grid.rows.text(r.key, 'resource') === 'Engineering') engKey = r.key; });
const bucketIds = rv.grid.columns.visible().map((c) => c.id).filter((id) => /^\d{4}-\d{2}-\d{2}$/.test(id));
const engLoad = bucketIds.map((id) => rv.grid.rows.value(engKey, id)).filter((v) => v != null && v > 0).join('');
rv.toggleGroup('Engineering');
const collapsed = labels();
rv.destroy();
return `${shown} | Engineering ${engLoad} h | collapsed ${collapsed}`;
Plan-table defaults for the split view's grids. The task table and the band resource tables are plan tables, not data-entry grids, so mountSplit builds them with no cell-range selection and no fill handle (the task table keeps its row selection, keyboard navigation and editing); a band cell shows the focus outline only for keyboard focus. gridConfig is laid over those defaults and applies to every table, so gridConfig: { selection: { ranges: true, fillHandle: true } } restores both, and gridConfig.band is laid over it again for the workload and histogram band tables only.
const { createTestDom, resetTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGrid } = await import('../packages/dom/src/index.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const plan = { projectStart: '2027-01-04', tasks: [{ id: 'a', name: 'A', duration: 2, assignee: 'Ann' }] };
const probe = (gridConfig) => {
const { root } = createTestDom();
const gantt = createGantt(plan);
const view = gantt.mountSplit(root, { createGrid, workload: {}, gridConfig });
const sel = (g) => g.get('selection');
const out = { tasks: sel(view.grids.tasks), band: sel(view.grids.workload) };
view.destroy();
return out;
};
const plain = probe(undefined);
const host = probe({ selection: { ranges: true, fillHandle: true } });
const bandOnly = probe({ band: { selection: { ranges: true } } });
return `default ranges ${plain.band.ranges} fill ${plain.band.fillHandle} | host ranges ${host.band.ranges} fill ${host.tasks.fillHandle} | band-only tasks ${bandOnly.tasks.ranges} band ${bandOnly.band.ranges}`;
Selecting tasks,. The selection is the task table's row selection, extended to the timeline bars: a click selects a task (as a row or a bar), Ctrl/Cmd-click toggles it, Shift-click selects a range, and the arrow keys move it; the selected rows and bars carry a theme token. It is driven from code with select(ids), deselect(ids) and read with selected(), each firing one selectionChange with { selectedIds, added, removed, cause }. It survives redraws, zoom and filters, and a selected task that a collapse or a filter hides stays selected - selectedHidden() reports which. Expanding or collapsing a summary row (by chevron, keyboard or view.toggle) raises the vetoable beforeTaskExpand/beforeTaskCollapse and then taskExpand/taskCollapse with the new expandedIds; the workload band's resource rows raise resourceExpand/resourceCollapse.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const gantt = createGantt({
projectStart: '2027-01-04',
tasks: [
{ id: 'phase', name: 'Phase' },
{ id: 'A', name: 'A', parent: 'phase', duration: 2 },
{ id: 'B', name: 'B', parent: 'phase', duration: 2 },
{ id: 'C', name: 'C', parent: 'phase', duration: 2 },
],
});
const view = gantt.mountSplit(root, {});
const changes = [];
gantt.on('selectionChange', (e) => changes.push(e.cause));
gantt.select('A'); // one selectionChange, cause 'api'
gantt.select(['B', 'C']); // adds B and C in one more
const picked = gantt.selected().join('+');
view.toggle('phase'); // collapse: A, B, C hide but stay selected
const hidden = gantt.selectedHidden().length;
gantt.deselect(['B']); // remove B, leaving A and C
view.destroy();
return `selected ${picked} | hidden ${hidden} | after deselect ${gantt.selected().join('+')} | changes ${changes.length}`;
Copy, cut and paste. Ctrl/Cmd+C copies the selected tasks with their subtrees, Ctrl/Cmd+X cuts them, Ctrl/Cmd+V pastes below the focused row and Ctrl/Cmd+Shift+V pastes as its children. Each copy has a new id and keeps its assignments; links between two copied tasks are re-created and links to an outside task are dropped. From code: copyTasks(ids), cutTasks(ids) and pasteTasks({ below: id } | { childOf: id }). A paste is gated on the vetoable beforeTaskPaste (carrying the tasks about to be inserted; a veto raises taskPaste:cancelled) and is one undo step, so a cut then a paste is two, each reversible. The system clipboard also receives a tab-separated text form (name, start, end, duration, percent) for a spreadsheet, and tab-separated text pasted from outside (pasteTasks({ below: id, text }), or a browser paste event) creates tasks the same way.
const { createGantt } = await import('../packages/modules/gantt/index.js');
const gantt = createGantt({
projectStart: '2027-01-04',
tasks: [
{ id: 'X', name: 'Outside', duration: 2 },
{ id: 'S', name: 'Summary' },
{ id: 'S1', name: 'One', parent: 'S', duration: 2, assignee: 'Ann' },
{ id: 'S2', name: 'Two', parent: 'S', duration: 3 },
],
dependencies: [{ from: 'S1', to: 'S2' }, { from: 'X', to: 'S1' }],
});
gantt.on('beforeTaskPaste', (e) => { if (e.tasks.length > 10) e.preventDefault('too many'); });
gantt.copyTasks(['S']);
const ids = gantt.pasteTasks({ below: 'X' });
const [, s1, s2] = ids;
const deps = gantt.dependencies;
const kept = deps.some((d) => d.from === s1 && d.to === s2) ? 'link kept' : 'link lost';
const external = deps.some((d) => d.to === s1 && d.from === 'X') ? 'external kept' : 'external dropped';
gantt.undo();
gantt.cutTasks(['S']);
const left = gantt.schedule.order.length;
gantt.undo();
return `copies ${ids.length} | ${kept} | ${external} | cut left ${left} | undo restores ${gantt.schedule.order.length}`;
Task actions. addTask(task?, { above | below | child | successorOf | predecessorOf: id }) returns the new id; convertToMilestone(id) / convertToTask(id) turn a task into a zero-duration diamond and back; deleteTask(id) removes a task and its links. Each is one undo step and gated by a vetoable event - beforeTaskAdd, beforeMilestoneConvert and the existing beforeTaskDelete - whose veto raises taskAdd:cancelled, milestoneConvert:cancelled or taskDelete:cancelled with the reason. A new task takes a catalogue name ("New task"), a duration of 1 and its sibling's start (or the project start); a successor or predecessor is linked finish-to-start, and the WBS code follows the new position. The split view's contextMenu: true offers them as default items (add task above / below, add subtask, add successor, add predecessor, convert to milestone, delete), and a contextMenu(task, event, defaults) function receives those items as its third argument to keep, reorder, hide or extend. From the keyboard, Insert adds a task below the focused row, Shift+Insert adds a child and Delete deletes (vetoable). A read-only task refuses an interface-driven child, successor, predecessor, convert or delete on itself (reason: 'readOnly'); a plain API call still works.
const { createGantt } = await import('../packages/modules/gantt/index.js');
const gantt = createGantt({
projectStart: '2027-01-04',
tasks: [
{ id: 'A', name: 'Design', duration: 3 },
{ id: 'B', name: 'Build', duration: 2 },
{ id: 'C', name: 'Ship', duration: 1 },
],
});
const below = gantt.addTask({}, { below: 'A' });
const next = gantt.addTask({ name: 'Review' }, { successorOf: 'B' });
const linked = gantt.dependencies.some((d) => d.from === 'B' && d.to === next && d.type === 'FS');
gantt.convertToMilestone('C');
const milestone = `milestone duration ${gantt.taskById('C').duration}`;
gantt.convertToTask('C');
const stop = gantt.on('beforeTaskDelete', (e) => e.preventDefault('keep'));
gantt.deleteTask('A');
const kept = gantt.schedule.order.length;
stop();
gantt.undo(); // reverts the Review task and its link together
gantt.undo(); // reverts the conversion back to a task
gantt.undo(); // reverts the milestone conversion
gantt.undo(); // reverts the first add
return `added ${below} | successor ${linked ? 'linked FS' : 'unlinked'} | ${milestone} | vetoed delete kept ${kept} | can undo ${gantt.canUndo()}`;
Scheduling conflicts. After each recompute the controller derives a normalised conflict set - a dependency cycle, an unhonoured dated constraint, a hard lock a predecessor would move, or a task past its deadline - and fires schedulingConflict only when that set changed, carrying { conflicts, added, resolved }, each conflict a { kind, tasks, links, message }. Read it any time from gantt.schedulingConflicts. A host load (setTasks, rows.apply) seeds the set silently. The split view's issuesPanel: true lists the set in a collapsible panel with a count badge, keyboard operable; a row click scrolls to and highlights its tasks, and each row offers the fix actions safe for its kind - remove the offending link, set the constraint to as-soon-as-possible, unlock the task, clear the deadline - each one undo step through the controller's own gated path.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const gantt = createGantt({
projectStart: '2027-01-04',
tasks: [{ id: 'a', name: 'A', duration: 2 }, { id: 'b', name: 'B', duration: 2 }],
dependencies: [{ from: 'a', to: 'b', type: 'FS' }],
});
const view = gantt.mountSplit(root, { issuesPanel: true });
const seen = [];
gantt.on('schedulingConflict', (e) => seen.push(e));
// Close a loop a -> b -> a: one event names the cycle.
gantt.setDependencies([
{ from: 'a', to: 'b', type: 'FS' },
{ from: 'b', to: 'a', type: 'FS' },
]);
const added = `${seen[0].added[0].kind} | tasks ${[...seen[0].conflicts[0].tasks].sort().join(',')}`;
// Remove the offending link: the cycle is resolved.
gantt.deleteDependency({ from: 'b', to: 'a', type: 'FS' });
const resolved = seen[1].resolved[0].kind;
const panelRows = root.querySelectorAll('.lat-gantt-split__issues-row').length;
view.destroy();
return `added ${added} | resolved ${resolved} | panel ${panelRows}`;
Linked data bands. mountSplit(el, { dataBands: [...] }) stacks one or more collapsible bands under the gantt and the resource bands, each showing a different measure per task per timeline bucket - forecast cost per week, say - laid out like the workload band and scroll- and zoom-locked to the timeline. A band reads value(task, bucketStart, bucketEnd, ctx) per leaf task, or a series field on the task (task.cost = [{ date, value }] at any granularity), re-bucketed to the current zoom: interpolate: 'none' puts each point in its bucket, 'spread' spreads it over its days (a monthly figure over its month) and re-sums. Rows follow the task tree (collapsing in step with the gantt; rows: 'summaries', 'resources' or a host grouping function are the alternatives), summaries roll up by aggregate ('sum', 'avg', 'last', 'min', 'max' or a function), with a totals row and column (totals: { row, column }), a format ('currency', 'number', 'percent' or a function), thresholds (theme-token tones) and cellStyle(value, ctx). With createGrid injected the left-hand columns are a real grid (resize, sort, filter, state); the date (bucket) columns never carry a header control under any config. A moved or resized task onReschedule: 'shift' (default), 'stretch'es or 'keep's its series and fires dataBand:change with the new series to save; an editable band opens a cell editor on double-click (or Enter/F2), gated by the cancellable beforeDataBandEdit (dataBandEdit:cancelled when refused). The view's getDataBandState/setDataBandState (also under getState().dataBands), toggleDataBand(id) and getDataBandTable(id) (one column per bucket, for an Excel/CSV writer) round it out; toSVG and print carry every band.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGrid } = await import('../packages/dom/src/index.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const gantt = createGantt({
projectStart: '2027-01-04',
tasks: [
{ id: 'p', name: 'Phase' },
{ id: 'a', name: 'Design', parent: 'p', start: '2027-01-04', duration: 5,
cost: [{ date: '2027-01-04', value: 100 }, { date: '2027-01-11', value: 30 }] },
{ id: 'b', name: 'Build', parent: 'p', start: '2027-01-11', duration: 5, cost: [{ date: '2027-01-12', value: 200 }] },
{ id: 'c', name: 'Alpha', start: '2027-01-04', duration: 3, cost: [{ date: '2027-01-05', value: 60 }] },
],
});
const view = gantt.mountSplit(root, {
createGrid, zoom: 'week', weekStartDay: 1, locale: 'en-US',
dataBands: [{ id: 'cost', title: 'Forecast cost', series: 'cost', format: 'currency', editable: true }],
});
const table = () => view.getDataBandTable('cost');
const week = table().columns.findIndex((c) => c.id === '2027-01-04');
const total = table().rows.find((r) => r[0] === 'Total');
const phase = table().rows.find((r) => r[0] === 'Phase');
// Move Build a week later: its cost moves with it.
let moved = '';
gantt.on('dataBand:change', (e) => { moved = e.series[0].date; });
gantt.applyEdit({ id: 'b', start: gantt.schedule.tasks.get('b').es + 7 }, { event: { type: 'pointerup' } });
// A host veto keeps the bucket as it was.
gantt.on('beforeDataBandEdit', (e) => e.preventDefault('locked'));
// Each band is a real grid (view.grids.dataBands[id]); its bucket cells edit
// through the grid, gated by beforeDataBandEdit. A refused edit leaves $30.
const bandGrid = view.grids.dataBands.cost;
bandGrid.edit.start('a', '2027-01-11');
bandGrid.edit.stop(false, { colId: '2027-01-11', value: 75 });
const col11 = table().columns.findIndex((c) => c.id === '2027-01-11');
const after = table().rows.find((r) => r[0] === 'Design')[col11];
view.destroy();
return `week of 4 Jan $${total[week]} | phase ${phase[1]} | moved to ${moved} | vetoed $${after}`;
Task-bar pointer events and the progress handle …,. Every bar gesture raises its own events on the controller (gantt.on), with id, rawTask (the task as you supplied it), the scheduled task, the view and the originating DOM event. Dates are ISO dates and end is the scheduled finish boundary. The before-events are cancellable with preventDefault(reason) and pair with a :cancelled event; a gesture cannot wait, so a handler that returns a Promise cancels it as 'deferred'. Keyboard edits raise the same start events as pointer drags (and a resize / progress edit its end / drop too); none fire for your own setTasks or rows.apply. A leaf bar (in both mount and mountSplit) shows a progress handle at its percent-complete point on hover or focus - a 16px target on the bar's bottom edge; dragging it previews the fill live and commits one percentComplete edit through beforeProgressChange and the undo history, snapped to progressStep (default 5) and clamped to 0-100. Alt+Shift+Left/Right on a focused bar change it by one step (Shift+arrows keep resizing). Milestones have no handle and summaries are read-only. The Lattice event names:
| Lattice | When, and what it carries |
|---|---|
| beforeTaskDrag | A move drag passed the 3px threshold (or a keyboard move is about to apply); preventDefault(reason) cancels it before anything moves and fires taskDrag:cancelled. |
| taskDragStart | Once per move drag, before any position changes: { id, rawTask, task, start, end, pointer }. |
| taskDrag | At most once per animation frame: { from, to, deltaDays, valid, pointer, pushed } - to is the snapped landing, valid false for a lock or a constraint that would put the task elsewhere, pushed the successors that would move. |
| taskDrag:cancelled | A refused start (reason from the veto) or Escape ('escape'). |
| beforeTaskResizeStart | A resize is about to start; vetoable, the refusal fires taskResize:cancelled with phase: 'start'. Lattice's own beforeTaskResize is still the commit gate. |
| taskResizeStart / taskPartialResize / taskResizeEnd | { edge, from: { start, end, duration }, to, valid }; the partial event per frame, the end on release (before the commit). Escape fires taskResize:cancelled with phase: 'drag'. |
| beforePercentBarDrag / percentBarDragStart | The progress handle's drag start, vetoable through the before-event (percentBarDrag:cancelled). |
| percentBarDrag / percentBarDrop | { from, to } in percent, snapped to progressStep; the drop is raised on release and beforeProgressChange still gates the commit. |
| percentBarDrag:cancelled | A refused start or Escape. |
| taskClick | { part, date, event } - part is bar, progress, label, baseline, milestone or summary, date the day under the pointer. Never the release of a drag. Enter / Space on a focused bar raise it too. |
| taskDblClick | The same payload; preventDefault() claims it so a host opens its own editor. Two taskClick precede it. |
| taskMouseOver / taskMouseOut | Once per entry / exit of a bar, milestone or summary bar (not empty row space). Keyboard focus reaching or leaving a bar raises the same pair, with the focus event. |
| taskContextMenu | A right-click, or the Menu key / Shift+F10 on a focused row: { part, event }. preventDefault() claims it - the split view's built-in menu (when contextMenu is set) does not open, and the browser's own is suppressed either way, freeing a host to show its own. |
const { createTestDom, resetTestDom, TestEvent } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const gantt = createGantt({
projectStart: '2027-01-04',
tasks: [{ id: 'a', name: 'Alpha', duration: 6, percentComplete: 20 }],
});
gantt.mountSplit(root, { progressStep: 10 });
const seen = [];
for (const name of ['beforePercentBarDrag', 'percentBarDragStart', 'percentBarDrop', 'beforeTaskDrag', 'taskDragStart', 'taskClick']) {
gantt.on(name, (e) => seen.push(name === 'percentBarDrop' ? `${name}(${e.from}→${e.to})` : name));
}
// Each edit redraws the timeline, so the bar is looked up afresh every time.
const row = () => root.querySelector('.lat-gantt-split__barrow[data-task="a"]');
const key = (k, extra = {}) => row().dispatchEvent(new TestEvent('keydown', { key: k, bubbles: true, ...extra }));
// Alt+Shift+ArrowRight: one progressStep, through beforeProgressChange and the undo history.
key('ArrowRight', { shiftKey: true, altKey: true });
const pct = gantt.tasks[0].percentComplete;
// Enter on a focused bar is a click.
key('Enter');
// A host refuses moves at the drag's start; the keyboard move raises the same gate.
gantt.on('beforeTaskDrag', (e) => e.preventDefault('frozen'));
let why = null;
gantt.on('taskDrag:cancelled', (e) => { why = e.reason; });
key('ArrowRight');
const moved = gantt.tasks[0].start != null;
const handle = root.querySelector('.lat-gantt-split__progresshandle') !== null;
gantt.destroy();
resetTestDom();
return `${seen.join(' ')} | ${pct}% | ${why} moved ${moved} | handle ${handle}`;
Dependency-link pointer events and the task tooltip. A link's arrow raises its own click / double-click / context-menu / hover, each carrying { from, to, linkType, lag, view, event } - the link's type is named linkType, not type, because every Gantt event payload's type is the event's own name; lag is read back from gantt.dependencies since it is not drawn on the arrow itself (the visible one is a separate label). A double click, or Enter/Space on a focused link, is the cancellable linkDblClick: claiming it stops the built-in type/lag editor from opening (the pointer route runs no default of its own, the same shape taskDblClick already has). linkContextMenu is cancellable the same way as taskContextMenu - neither view draws a menu for a link, so claiming it only suppresses the browser's own. The task tooltip's own tooltipShow is vetoable - preventDefault() keeps the built-in card from showing at all, for a host that wants its own in its place - fired once per distinct task shown, and its pair tooltipHide notifies only when a shown one is actually dismissed. The Lattice event names:
| Lattice | When, and what it carries |
|---|---|
| linkClick | A single click on the link's arrow: { from, to, linkType, lag, view, event }. |
| linkDblClick | The same payload, plus preventDefault; two linkClick precede the pointer route. |
| linkContextMenu | The same payload, plus preventDefault. |
| linkHover | Once per entry of a link's arrow (moving onto its own arrowhead is not a re-entry). |
| tooltipShow | { id, rawTask, task, view, text, event }; preventDefault() vetoes the built-in card. |
| tooltipHide | The same context, without text; raised only when a tooltip was actually shown. |
const { createTestDom, resetTestDom, TestEvent } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const gantt = createGantt({
projectStart: '2027-01-04',
tasks: [{ id: 'a', name: 'A', duration: 2 }, { id: 'b', name: 'B', duration: 2 }],
dependencies: [{ from: 'a', to: 'b', type: 'FS', lag: 3 }],
});
const view = gantt.mountSplit(root, { tooltip: true });
const seen = [];
for (const name of ['taskContextMenu', 'linkClick', 'linkDblClick', 'linkContextMenu', 'linkHover', 'tooltipShow', 'tooltipHide']) {
gantt.on(name, () => seen.push(name));
}
let lag = null;
let linkType = null;
gantt.on('linkClick', (e) => { lag = e.lag; linkType = e.linkType; });
// The link's own click / double-click / context menu / hover.
const link = root.querySelector('.lat-gantt-split__linkgroup[data-from="a"][data-to="b"]');
link.dispatchEvent(new TestEvent('pointerover', { bubbles: true }));
link.dispatchEvent(new TestEvent('click', { bubbles: true }));
link.dispatchEvent(new TestEvent('dblclick', { bubbles: true, cancelable: true }));
link.dispatchEvent(new TestEvent('contextmenu', { bubbles: true, cancelable: true }));
// A right-click on the bar's row: taskContextMenu (no built-in menu configured here).
root.querySelector('.lat-gantt-split__barrow[data-task="a"]')
.dispatchEvent(new TestEvent('contextmenu', { bubbles: true, cancelable: true }));
// The task tooltip: shown, hidden on pointerleave, then vetoed.
const row = () => root.querySelector('.lat-gantt-split__barrow[data-task="a"]');
const bar = () => row().querySelector('.lat-gantt-split__bar');
const tip = () => root.querySelector('.lat-gantt-split__tooltip');
bar().dispatchEvent(new TestEvent('pointermove', { bubbles: true, clientX: 10, clientY: 10 }));
const shown = !tip().hasAttribute('hidden');
root.querySelector('.lat-gantt-split__time-body').dispatchEvent(new TestEvent('pointerleave', {}));
const hiddenAfterLeave = tip().hasAttribute('hidden');
gantt.on('tooltipShow', (e) => e.preventDefault('replaced'));
bar().dispatchEvent(new TestEvent('pointermove', { bubbles: true, clientX: 10, clientY: 10 }));
const stillHidden = tip().hasAttribute('hidden');
view.destroy();
resetTestDom();
return `${seen.join(' ')} | ${linkType} lag ${lag} | tooltip ${shown},${hiddenAfterLeave},${stillHidden}`;
Previewing a drag. While a bar is dragged or resized, both views redraw the dependency arrows into and out of it on every frame and shift the successors the scheduler will push. previewEdit({ id, start?, duration? }) is what they call: it dry-runs the edit and returns the schedule it would produce, without changing the model or emitting schedule, and returns null when the edit cannot be scheduled.
const { createGantt } = await import('../packages/modules/gantt/index.js');
const plan = createGantt({
projectStart: '2027-01-04',
tasks: [{ id: 'A', duration: 5 }, { id: 'B', duration: 4 }],
dependencies: [{ from: 'A', to: 'B', type: 'FS' }],
});
let events = 0;
plan.on('schedule', () => { events += 1; });
// Stretch A by two days: B would be pushed two days right.
const base = plan.schedule.tasks.get('B').es;
const preview = plan.previewEdit({ id: 'A', duration: 7 });
const out = `preview B +${preview.tasks.get('B').es - base} | live B +${plan.schedule.tasks.get('B').es - base} | events ${events}`;
plan.destroy();
return out;
Reordering and re-parenting rows,. In the split view, a drag handle in the name column drags a row above or below a sibling to reorder it, or onto another row to make it a child; by keyboard, Alt+Up/Alt+Down move a focused row among its current siblings, and Tab/Shift+Tab indent or outdent it. Every gesture funnels through moveRow(id, { parent, index }), the same gated, undoable choke point: the cancellable beforeRowMove fires first - a handler may refuse with preventDefault(reason), which fires rowMove:cancelled and leaves the plan untouched - and once allowed, the task's parent and its position among the plan's own task order are written through the live re-parent path as one undo step, then rowMove fires once with { id, oldParent, newParent, oldIndex, newIndex }, so a host can store its own sort key. The same drop also raises the beforeRowDrop/rowDrop/rowDrop:cancelled trio - rowDrop is rowMove plus position ('before'/'after'/'inside') and target (the row the drop anchored to); beforeRowDrop gates first, so a drop-specific refusal reports rowDrop:cancelled rather than rowMove:cancelled. A drop onto the task itself or onto one of its own descendants (which would cycle the tree) is refused before anything is gated; a move that changes neither parent nor position is a silent no-op.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const gantt = createGantt({
projectStart: '2027-01-04',
tasks: [
{ id: 'phase', name: 'Phase' },
{ id: 'A', name: 'A', parent: 'phase', duration: 2 },
{ id: 'B', name: 'B', parent: 'phase', duration: 2 },
{ id: 'C', name: 'C', parent: 'phase', duration: 2 },
],
});
const view = gantt.mountSplit(root, { keyboard: true });
const order = () => [...root.querySelectorAll('.lat-gantt-split__row')].map((r) => r.getAttribute('data-task'));
let moves = 0;
let drops = [];
gantt.on('rowMove', () => { moves += 1; });
gantt.on('rowDrop', (e) => { drops.push(`${e.position}/${e.target}`); });
// Alt+ArrowUp on C's row moves it up one, ahead of B - a plain sibling reorder,
// and the drop reports position 'before' the row it landed on.
const rowC = root.querySelector('.lat-gantt-split__row[data-task="C"]');
rowC.dispatchEvent({ type: 'keydown', target: rowC, bubbles: true, key: 'ArrowUp', altKey: true });
const afterAlt = order().join(',');
// Tab on B (now the last sibling) indents it under its new previous sibling, C
// - position 'inside' C.
const rowB = root.querySelector('.lat-gantt-split__row[data-task="B"]');
rowB.dispatchEvent({ type: 'keydown', target: rowB, bubbles: true, key: 'Tab' });
const bParent = gantt.schedule.tasks.get('B').parent;
// A vetoed drop changes nothing and fires rowDrop:cancelled instead of rowDrop.
gantt.on('beforeRowDrop', (e) => e.preventDefault('locked'));
let cancelled = false;
gantt.on('rowDrop:cancelled', () => { cancelled = true; });
gantt.moveRow('A', { parent: 'C' });
view.destroy();
return `order ${afterAlt} | B under ${bParent} | moves ${moves} | drops ${drops.join(' ')} | veto cancelled ${cancelled}`;
WBS outline codes. Every scheduled task carries a derived wbs string - '1', '1.1', '1.2', '2', … - from its position in the tree, read off gantt.schedule.tasks.get(id).wbs; it renumbers on every reorder, indent/outdent, add or delete, with nothing written onto the task itself, so renumbering never adds an undo step of its own. gantt.taskByWbs(code) resolves a task by its current code - the lookup a predecessor reference by WBS will use. The built-in kind: 'wbs' split-view column shows it, read-only and sortable under the same tree-aware sort as every other column. An MSPDI task's own <WBS>, when the source plan supplied one, is kept verbatim by importMSPDI rather than overridden by the derived number, and exportMSPDI writes it back.
const { createGantt, importMSPDI, exportMSPDI } = await import('../packages/modules/gantt/index.js');
const plan = createGantt({
tasks: [
{ id: 'p', name: 'Phase 1' },
{ id: 'a', name: 'A', parent: 'p', duration: 2 },
{ id: 'b', name: 'B', parent: 'p', duration: 2 },
{ id: 'q', name: 'Phase 2', duration: 1 },
],
});
const codes = ['p', 'a', 'b', 'q'].map((id) => plan.schedule.tasks.get(id).wbs).join(',');
// Moving b above a renumbers both - nothing stored, so nothing stale to fix.
plan.moveRow('b', { parent: 'p', index: 0 });
const moved = plan.schedule.tasks.get('b').wbs;
const lookup = plan.taskByWbs('1.2').id;
plan.destroy();
// A supplied, non-standard WBS (Project lets a plan carry a custom mask) is
// kept verbatim on import and written back unchanged on export.
const xml = '<Project xmlns="http://schemas.microsoft.com/project"><Tasks>' +
'<Task><UID>1</UID><Name>Alpha</Name><OutlineLevel>1</OutlineLevel><WBS>A</WBS></Task>' +
'<Task><UID>2</UID><Name>Beta</Name><OutlineLevel>1</OutlineLevel><WBS>B</WBS></Task>' +
'</Tasks></Project>';
const back = importMSPDI(xml);
const custom = back.tasks.map((t) => t.wbs).join(',');
const xmlOut = exportMSPDI({ tasks: back.tasks });
const roundTrips = xmlOut.includes('<WBS>A</WBS>') && xmlOut.includes('<WBS>B</WBS>');
return `${codes} | moved ${moved} | lookup ${lookup} | custom ${custom} | roundtrip ${roundTrips}`;
The task tooltip. Hovering a bar, or focusing it by keyboard, in either view, shows a card anchored to it - above the bar by default, flipping below or sideways to stay inside the view, dismissed on pointer leave, blur, Escape or scroll, and suppressed mid-drag. The default content is the task name as a title, then labelled Start/End/Duration/Complete rows (dates in the grid's own locale), plus Effort, Assigned, Cost and Slack when the task has them; a milestone shows a single Date row instead. tooltip: { fields } picks and orders the rows from 'start', 'end', 'duration', 'percentComplete', 'effort', 'resources', 'cost', 'slack'; tooltip: (task, ctx) => ... renders your own string (always text) or DOM node (attached as it is), with ctx.formatDate the same locale formatter the default rows use; tooltip: false turns it off. The hovered or focused bar's aria-description always matches the card's own text exactly, whichever of the three produced it.
const { createTestDom, resetTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const plan = createGantt({
tasks: [
{ id: 'design', name: 'Design', duration: 5, percentComplete: 40, cost: 1200 },
{ id: 'ship', name: 'Ship', milestone: true },
],
});
// `fields` picks and orders the rows; a milestone always shows just its Date.
plan.mount(root, { dateAxis: false, tooltip: { fields: ['start', 'percentComplete', 'cost'] } });
const hover = (sel) => {
const row = root.querySelector(sel);
const bar = row.querySelector('.lat-gantt__bar') || row.querySelector('.lat-gantt__milestone');
bar.dispatchEvent({ type: 'pointermove', target: bar, bubbles: true });
return row.getAttribute('aria-description').replace(/\n/g, ' / ');
};
const picked = hover('.lat-gantt__row[data-task="design"]');
const milestone = hover('.lat-gantt__row[data-task="ship"]');
plan.destroy();
// A host render function: a plain string is always text, never markup.
const { root: root2 } = createTestDom();
const custom = createGantt({ tasks: [{ id: 'a', name: 'Design', duration: 5 }] });
custom.mount(root2, { dateAxis: false, tooltip: (task, ctx) => `${task.name}: ${ctx.formatDate(task.es)}` });
const row2 = root2.querySelector('.lat-gantt__row[data-task="a"]');
const bar2 = row2.querySelector('.lat-gantt__bar');
bar2.dispatchEvent({ type: 'pointermove', target: bar2, bubbles: true });
const customText = row2.getAttribute('aria-description');
custom.destroy();
resetTestDom();
return `picked [${picked}] | milestone [${milestone}] | custom [${customText}]`;
Per-task bar colour. A task's colour field paints its bar, summary bar or milestone, and the bar's progress fill is the same hue darkened so the completed part still reads against it; the barColour(task) option on mount/mountSplit wins over the field. The colour is a data colour - written literally, not through a --lattice-* token - so it reads the same in dark mode, and it is re-applied on every redraw.
const { createTestDom, resetTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
// A task `colour` field paints its bar and its milestone; the progress fill is
// the same hue darkened.
const { root } = createTestDom();
const plan = createGantt({
tasks: [
{ id: 'design', name: 'Design', duration: 5, percentComplete: 40, colour: '#3366cc' },
{ id: 'ship', name: 'Ship', milestone: true, colour: '#cc3333' },
],
});
plan.mount(root, { dateAxis: false });
const designRow = root.querySelector('.lat-gantt__row[data-task="design"]');
const shipRow = root.querySelector('.lat-gantt__row[data-task="ship"]');
const fills = `${designRow.querySelector('.lat-gantt__bar').style.fill} ${designRow.querySelector('.lat-gantt__progress').style.fill} ${shipRow.querySelector('.lat-gantt__milestone').style.fill}`;
plan.destroy();
// `barColour(task)` wins over the field.
const { root: root2 } = createTestDom();
const plan2 = createGantt({ tasks: [{ id: 'a', duration: 3, colour: '#3366cc' }] });
plan2.mount(root2, { dateAxis: false, barColour: (task) => (task.id === 'a' ? '#00aa00' : null) });
const overridden = root2.querySelector('.lat-gantt__row[data-task="a"]').querySelector('.lat-gantt__bar').style.fill;
plan2.destroy();
resetTestDom();
return `${fills} | ${overridden}`;
Bar label position. The split view's barLabelPosition - 'inside', 'right' or 'auto' - says where a task or summary bar's own label sits. 'right' (the default) keeps the label just past the bar's right edge, the behaviour the view shipped with; 'inside' centres it on the bar and swaps it to a contrasting fill so it stays readable over the solid bar in light and dark themes alike - a per-task colour picks its own text colour (near-black on a light fill, white on a dark one) because the token contrast is only guaranteed against the theme's own fills, not an arbitrary data colour; 'auto' centres the label only when it fits, estimating its width the same way the axis labels are thinned, and falls back to the right otherwise. A label too wide to read inside its bar falls back to the right under either 'inside' or 'auto', so it is never cut off at the bar's edges. Milestones draw no bar and so no label.
const { createTestDom, resetTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const NS = 'lat-gantt-split';
const tasks = [
{ id: 'go', name: 'Go', duration: 8 },
{ id: 'long', name: 'A very long task name that will not fit inside a narrow bar', duration: 1 },
];
const px = (v) => Number(String(v || '0'));
// `inside` centres the label on the bar and marks it with the contrasting fill.
const { root } = createTestDom();
const inside = createGantt({ projectStart: '2027-01-04', tasks });
inside.mountSplit(root, { barLabelPosition: 'inside' });
const goRow = root.querySelector(`.${NS}__barrow[data-task="go"]`);
const goBar = goRow.querySelector(`.${NS}__bar`);
const goLabel = goRow.querySelector(`.${NS}__barlabel`);
const centred = goLabel.getAttribute('data-inside') === 'true' && goLabel.getAttribute('text-anchor') === 'middle'
&& Math.abs(px(goLabel.getAttribute('x')) - (px(goBar.getAttribute('x')) + px(goBar.getAttribute('width')) / 2)) < 2;
inside.destroy();
// `auto` keeps a short label inside and pushes a too-wide one to the right.
const { root: root2 } = createTestDom();
const auto = createGantt({ projectStart: '2027-01-04', tasks });
auto.mountSplit(root2, { barLabelPosition: 'auto' });
const goAutoRow = root2.querySelector(`.${NS}__barrow[data-task="go"]`);
const longAutoRow = root2.querySelector(`.${NS}__barrow[data-task="long"]`);
const autoInside = goAutoRow.querySelector(`.${NS}__barlabel`).getAttribute('data-inside') === 'true';
const autoRight = longAutoRow.querySelector(`.${NS}__barlabel`).getAttribute('data-inside') === null;
auto.destroy();
resetTestDom();
return `inside centred ${centred} | auto short ${autoInside} long ${autoRight}`;
| Function | What it does |
|---|---|
| createGantt({ tasks?, dependencies?, projectStart?, autoSchedule?, honourDates?, grid?, fields?, rowKey? }) | Create a controller over a task list and a dependency list. Computes the CPM schedule immediately and on every edit; on('schedule'|'error', fn) subscribes; applyEdit/setTasks/setDependencies mutate and recompute; grid is kept for the write-back binding. honourDates derives each link's lag from the stored task dates so a loaded plan recomputes to those dates. fields maps your own task property names onto the ones the scheduler reads, and rowKey identifies a task for both rows.apply and the schedule. |
| rows.apply(change) / rows.forEach(fn) / rows.count | The live consumer surface, the same keyed diff a grid, a board and a KPI panel accept, so one feed - or one tab strip - drives them all: apply({ add, update, remove }) upserts by rowKey and recomputes, forEach visits every task, and count is how many the plan holds. |
| computeSchedule(tasks, deps?, { projectStart? }) | The pure CPM engine: forward/backward passes over the leaf tasks honouring FS/SS/FF/SF + lag, slack/float and the zero-float critical path, with summaries derived and cycles refused. Returns { ok, tasks, critical, criticalPaths, projectDuration, ... } or { ok:false, error }. |
| findViolations(tasks, schedule) | The tasks whose user-placed start begins earlier than CPM allows (the manual-with-validation flag). Summaries, whose dates are derived, are skipped. |
| toISODate(day) | Format an engine day-number as an ISO calendar date (YYYY-MM-DD, UTC). |
| LINK_TYPES | The four dependency link types, in order: ['FS','SS','FF','SF']. |
| CONSTRAINT_TYPES | The eight scheduling constraint codes, in MS Project order: ['ASAP','MSO','MFO','ALAP','SNET','SNLT','FNET','FNLT']. A task's constraint takes any of them. |
| SCHEDULE_ERROR | The error codes the engine reports rather than throwing (cycle, duplicate-id, unknown-task, bad-duration, bad-link-type, unknown-parent, parent-cycle, …). |
| importMSPDI(xml, { hoursPerDay? }) | Import a Microsoft Project (MSPDI) .xml document into a { tasks, dependencies, resources, projectStart, calendar } model ready for createGantt: the task tree, typed dependencies with lag, constraints, baseline, %complete, resources with capacity and the resource assignments. |
| exportMSPDI(model, { hoursPerDay?, projectName? }) | Serialise a Gantt model (optionally a scheduled one) back to Microsoft Project (MSPDI) XML - the same fields, round-tripping with importMSPDI. The controller offers gantt.toMSPDI() as a shortcut over the current plan. |
| computeEarnedValue(tasks, schedule, { statusDate?, costField?, actualCostField? }) | Earned-value management (EVM) from the baseline and %complete at a status date: Planned Value (PV/BCWS), Earned Value (EV/BCWP), Actual Cost (AC/ACWP, from a per-task actualCost), plus Schedule Variance (EV−PV), Cost Variance (EV−AC), SPI (EV/PV) and CPI (EV/AC) - per task, rolled up to summaries and the project. Budget (BAC) is the task's cost, or its duration when no cost is given. The controller exposes gantt.earnedValue({ statusDate }) as the shortcut; the split view surfaces the metrics through kind: 'evm' columns. |
| getState() / setState(snapshot) | Serialise and restore the Gantt's own VIEWER state - which view is mounted ('plain'/'split'/null), zoom, the arrow/progress/baseline toggles, calendar/non-working shading, the plain view's swimlane grouping, the split view's left-panel width and its collapsed summary rows. Not the task data (already covered by setTasks/rows.apply). Versioned, JSON-safe, and setState tolerates an unknown, wrong-typed or newer-version snapshot without throwing, applying only what it recognises. |
const { computeSchedule, findViolations, createGantt, LINK_TYPES, SCHEDULE_ERROR, toISODate } = await import('../packages/modules/gantt/index.js');
const tasks = [
{ id: 'design', duration: 5, percentComplete: 100 },
{ id: 'build', duration: 6, percentComplete: 50 },
{ id: 'test', duration: 4 },
{ id: 'launch', milestone: true },
];
const deps = [
{ from: 'design', to: 'build' },
{ from: 'build', to: 'test' },
{ from: 'test', to: 'launch' },
];
const s = computeSchedule(tasks, deps);
const plan = createGantt({ tasks, dependencies: deps });
const cyc = computeSchedule([{ id: 'a', duration: 1 }, { id: 'b', duration: 1 }], [{ from: 'a', to: 'b' }, { from: 'b', to: 'a' }]);
return [plan.schedule.projectDuration, s.critical.join('-'), toISODate(0), LINK_TYPES.join(','), cyc.error.code === SCHEDULE_ERROR.CYCLE, findViolations(tasks, s).length].join(' | ');
Milestones and summary (WBS) tasks: a summary is derived from its children and a dependency may target it.
const { computeSchedule } = await import('../packages/modules/gantt/index.js');
const tasks = [
{ id: 'phase', name: 'Phase 1' },
{ id: 'a', duration: 3, parent: 'phase', percentComplete: 100 },
{ id: 'b', duration: 2, parent: 'phase', percentComplete: 0 },
{ id: 'ship', milestone: true },
];
const s = computeSchedule(tasks, [{ from: 'a', to: 'b' }, { from: 'phase', to: 'ship' }]);
const phase = s.tasks.get('phase');
return [phase.es, phase.ef, phase.percentComplete, phase.isSummary, s.tasks.get('ship').es].join(' | ');
Locked tasks and hard constraints. A dated constraint like MSO is reported as a conflict when a predecessor would force it later, but the date still floats to wherever the predecessor pushed it - a completed task's own placement is not safe from a late-running predecessor. locked: true (or the equivalent constraintMode: 'hard') is the stronger guarantee: the scheduler never moves the task, full stop. A violating predecessor link is instead left unhonoured and reported in schedule.conflicts as a 'LOCKED' conflict naming the exact from/to edge, which a mounted view draws as a conflict arrow; dragging the locked task itself is refused by default. The plan-wide lockWhen(task) predicate (on createGantt and computeSchedule) locks by rule - typically "100% complete" - without setting locked on every finished task by hand. The same rule is exported as isTaskLocked(task, lockWhen) for a host that needs to ask before offering a drag or an edit: it reads locked, constraintMode: 'hard' and the plan's lockWhen predicate off the raw task, and a predicate that throws locks nothing.
const { computeSchedule, createGantt } = await import('../packages/modules/gantt/index.js');
const deps = [{ from: 'design', to: 'build', type: 'FS' }];
// `design` now overruns into what was `build`'s placed window.
const locked = computeSchedule(
[{ id: 'design', duration: 10 }, { id: 'build', duration: 4, locked: true, start: 5 }],
deps,
);
const hard = computeSchedule(
[{ id: 'design', duration: 10 }, { id: 'build', duration: 4, constraintMode: 'hard', start: 5 }],
deps,
);
const byRule = computeSchedule(
[{ id: 'design', duration: 10 }, { id: 'build', duration: 4, percentComplete: 100, start: 5 }],
deps,
{ lockWhen: (t) => t.percentComplete === 100 },
);
const plan = createGantt({ tasks: [{ id: 'build', duration: 4, locked: true, start: 5 }] });
const refused = plan.applyEdit({ id: 'build', start: 20 }); // beforeTaskMove refuses a locked task by default
return [
locked.tasks.get('build').es, locked.conflicts[0].type, locked.conflicts[0].from,
hard.tasks.get('build').es, byRule.tasks.get('build').es, refused === undefined,
].join(' | ');
Read-only tasks. task.readOnly: true (or the plan-wide readOnlyWhen(task) option on createGantt) refuses every user-driven edit on the task - pointer and keyboard drag, resize, progress drag, a dependency create/delete touching it, and a task-table cell edit on it - before the gesture's own before* event ever fires: the paired :cancelled event fires with reason 'readOnly', but no host handler is ever asked to veto an attempt that was never going to happen. It is a weaker promise than locked in two ways: the scheduler still pushes a read-only task's dates when a predecessor moves (unless the task is also locked), and applyEdit/setDependencies/deleteDependency called directly from code - carrying no originating DOM event - still work, because readOnly governs interactive editing, not the API. A read-only row and bar carry a --readonly class (a muted progress handle, no drag cursor) and an accessible description, in both the plain and split view.
const { createGantt } = await import('../packages/modules/gantt/index.js');
const plan = createGantt({
tasks: [{ id: 'design', duration: 5 }, { id: 'build', duration: 4, readOnly: true }],
dependencies: [{ from: 'design', to: 'build', type: 'FS' }],
});
let beforeFired = false;
plan.on('beforeTaskMove', () => { beforeFired = true; });
let reason = null;
plan.on('taskMove:cancelled', (e) => { reason = e.reason; });
// A UI-driven edit (one carrying an originating event, as a gesture commit or a
// task-table cell edit does) refuses before beforeTaskMove ever fires.
const refused = plan.applyEdit({ id: 'build', start: 1 }, { event: {} });
const pushedStart = plan.schedule.tasks.get('build').es;
// The scheduler still pushes the read-only task when its predecessor moves.
plan.applyEdit({ id: 'design', duration: 8 });
const pushedAgain = plan.schedule.tasks.get('build').es;
// A bare applyEdit call from the host (no originating event) still works.
const apiEdit = plan.applyEdit({ id: 'build', duration: 2 });
return [
refused === undefined, beforeFired, reason,
pushedStart, pushedAgain, apiEdit.ok, plan.schedule.tasks.get('build').duration,
].join(' | ');
Extra task-table columns. The split view’s columns list also takes constraint (an editable select over the constraint types, with catalogue labels), constraintDate (an editable date), the read-only earlyStart, lateStart, lateFinish, baselineStart and baselineEnd dates, variance (finish variance in days, signed, a late value drawn in the warning token), note (editable text) and sequence (the 1-based row number, renumbered as the table is sorted or filtered). Each is an ordinary grid column definition, so a host overrides it by id, the dates honour dateFormat, every one exports with the table, and the editable ones commit through applyEdit - one undo step, the usual before* vetoes.
const { createTestDom, flushFrames } = await import('../packages/dom/src/renderer/testdom.js');
const { createGrid } = await import('../packages/dom/src/index.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom({ width: 1200, height: 300 });
const gantt = createGantt({
projectStart: '2027-01-04',
tasks: [
{ id: 'a', name: 'Design', duration: 3, baselineStart: '2027-01-04', baselineEnd: '2027-01-05' },
{ id: 'b', name: 'Build', duration: 2, note: 'after design' },
],
dependencies: [{ from: 'a', to: 'b' }],
});
const view = gantt.mountSplit(root, {
createGrid, editable: true,
columns: ['name', 'constraint', 'constraintDate', 'variance', 'sequence'],
});
flushFrames();
const grid = view.grids.tasks;
// A constraint and its date commit through applyEdit and reschedule the plan.
await grid.edit.setCells([{ key: 'b', colId: 'constraint', value: 'SNET' }], 'cell', { origin: 'user' });
await grid.edit.setCells([{ key: 'b', colId: 'constraintDate', value: '2027-01-11' }], 'cell', { origin: 'user' });
flushFrames();
await new Promise((r) => setTimeout(r, 5));
const out = [
gantt.tasks[1].constraint, gantt.tasks[1].constraintDate,
grid.rows.text('a', 'variance'), grid.rows.text('b', 'sequence'),
(gantt.undo(), gantt.tasks[1].constraintDate ?? 'none'), // one undo step reverts the date edit
].join(' | ');
view.destroy();
return out;
Honouring stored dates. A host migrating an existing plan already has every task's dates, but the links may carry a wrong or absent lag. With honourDates: true the controller derives each link's lag from its two endpoints' start/duration on setTasks/setDependencies, so the computed schedule reproduces the stored dates and nothing moves; the derived lags are reflected in gantt.dependencies.
const { createGantt, computeSchedule } = await import('../packages/modules/gantt/index.js');
// Stored dates, and a link whose lag is wrong - 5, where the dates imply 1.
const tasks = [
{ id: 'design', start: 0, duration: 5 },
{ id: 'build', start: 6, duration: 4 },
];
const deps = [{ from: 'design', to: 'build', type: 'FS', lag: 5 }];
// Without honourDates the CPM obeys the wrong lag and pushes build to day 10.
const plain = createGantt({ tasks, dependencies: deps });
const moved = plain.schedule.tasks.get('build').es;
plain.destroy();
// With honourDates the lag is re-derived from the dates, so build stays on 6.
const plan = createGantt({ honourDates: true, tasks, dependencies: deps });
const kept = plan.schedule.tasks.get('build').es;
const lag = plan.dependencies[0].lag;
plan.destroy();
// computeSchedule accepts the option directly and returns the derived deps.
const direct = computeSchedule(tasks, deps, { honourDates: true });
if (direct.dependencies[0].lag !== 1) throw new Error('lag not derived');
return `moved ${moved}, kept ${kept}, lag ${lag}`;
Undo and redo. Every user or API edit - a move, resize, delete, link add/remove/lag, assignment or units change, a workload-band hour edit, a baseline applied to the plan, a task add through rows.apply, even a whole level() run - is one undo step, because they all funnel through the same controller mutators the recorder sits on. undo() restores the exact prior plan (tasks, dependencies, assignments and work contours) and re-runs scheduling to the identical result; redo() re-applies it. canUndo()/canRedo() drive a toolbar, and a history event { canUndo, canRedo, label } fires on every change. In a mounted view Ctrl/Cmd+Z undoes and Ctrl/Cmd+Shift+Z (or Ctrl+Y) redoes, scoped to the focused Gantt - and a Ctrl+Z typed into the split table's open cell editor is left to the grid's own text undo, never hijacked. History is session only: kept while the page is open, cleared by setTasks/setState and on reload, and NOT part of getState/setState. The depth is historyDepth (default 100), and clearHistory() empties it.
const { createGantt } = await import('../packages/modules/gantt/index.js');
const plan = createGantt({
tasks: [{ id: 'design', duration: 5 }, { id: 'build', duration: 4 }],
dependencies: [{ from: 'design', to: 'build', type: 'FS' }],
});
plan.applyEdit({ id: 'design', start: 3 }); // one edit = one undo step
const moved = plan.schedule.tasks.get('build').es; // 8 - build (FS successor) followed
plan.undo(); // restore the exact prior plan + schedule
const undone = plan.schedule.tasks.get('build').es; // back to 5
plan.redo(); // and forward again
const redone = plan.schedule.tasks.get('build').es; // 8
// History is session only: clearing it (as setTasks/setState also do) empties both stacks.
plan.clearHistory();
return [moved, undone, redone, plan.canUndo(), plan.canRedo()].join(' | ');
Dirty tracking. gantt.changes() is the accumulated diff since the last commit() or load - { tasks: { added, updated, removed }, dependencies: { added, updated, removed }, assignments: { updated } } - and hasChanges is the boolean a Save button binds to. Both are computed fresh against a held committed baseline rather than an edit counter, so an add immediately removed nets to nothing and undoing back to the baseline clears hasChanges exactly as a fresh commit() would. The dirtyChange event { hasChanges } fires only the instant the flag flips, never on a second edit made while already dirty. commit() takes the plan as it stands now as the new baseline (leaving undo/redo untouched); setTasks/rows.apply reset the baseline to the load unless called with { keepChanges: true }, which keeps the load accumulating against whatever baseline was already held - the shape a live feed wants if its own upserts should still count as unsaved changes. mountSplit's dirtyMarks: true draws a small corner mark on every built-in task-table cell whose field is part of the diff, cleared the moment it is not.
const { createGantt } = await import('../packages/modules/gantt/index.js');
const plan = createGantt({ tasks: [{ id: 'a', name: 'A', duration: 5 }] });
const flips = [];
plan.on('dirtyChange', (e) => flips.push(e.hasChanges));
plan.applyEdit({ id: 'a', name: 'A renamed' });
const dirtyAfterEdit = plan.hasChanges; // true
const fields = Object.keys(plan.changes().tasks.updated[0].fields); // ['name']
plan.commit(); // the rename is now the baseline
const dirtyAfterCommit = plan.hasChanges; // false
// A live-feed upsert that should still count as an unsaved change joins the
// baseline already held rather than resetting to it.
plan.rows.apply({ add: [{ id: 'b', name: 'B', duration: 2 }] }, { keepChanges: true });
const added = plan.changes().tasks.added.length; // 1
// ...and removing the same row nets back to nothing, diffed fresh rather
// than counted.
plan.rows.apply({ remove: ['b'] }, { keepChanges: true });
const nets = plan.hasChanges; // false
plan.destroy();
return [dirtyAfterEdit, fields.join(','), dirtyAfterCommit, added, nets, flips.join(',')].join(' | ');
Resource management. Assign people (or machines) to tasks with assignee/assignees or explicit assignments: [{ resource, units }], and give each resource a capacity through the resources option. The controller then reports overAllocations - where one resource is booked beyond its capacity across concurrent tasks - and the full resourceLoad; the split view rings the over-booked avatars and outlines the clashing bars. gantt.level() auto-shifts the lower-priority tasks later to clear the over-allocation, honouring the critical path and the working-time calendar (pass { dryRun: true } to preview the moves).
Per-resource working calendars. A resource may carry its own calendar - workingDays (0=Sun…6=Sat), hoursPerDay (a flat number or a per-weekday map like { 1: 7.5, 5: 4 }), and holidays/leave (ISO dates, day-numbers or inclusive { from, to } ranges) - and a capacity in hours per day. Load, the workload band, the over-allocation marks and level() then measure that resource against ITS calendar: it has zero capacity on a day it does not work (so a booking there is wholly over-allocation), and level() never delays a task onto one of its non-working days. Anything the resource calendar omits is inherited from the plan calendar, and a resource with no calendar keeps the historical unitless capacity (1 = a full-time booking, a fraction of the plan's hoursPerDay), so a plan with no resource calendars is unchanged. The plan's full working day is the hoursPerDay option (default 8). Resource calendars round-trip through MSPDI as a Resource → CalendarUID → Calendar.
const { createGantt } = await import('../packages/modules/gantt/index.js');
const plan = createGantt({
projectStart: '2027-01-04', // a Monday
calendar: 'weekends',
// Pat works Mon/Tue/Wed at 7.5 h; the plan's full day is 8 h.
resources: [{ id: 'Pat', calendar: { workingDays: [1, 2, 3], hoursPerDay: 7.5 } }],
tasks: [{ id: 'T', duration: 5, assignee: 'Pat' }],
});
const load = plan.resources();
const over = load.overAllocations;
// Mon-Wed: over by the half hour Pat cannot cover (capacity 7.5/8 = 0.9375 u).
// Thu-Fri: Pat does not work, so capacity is 0 and the whole booking is over.
return [over.length, over[0].capacity, over[1].capacity, load.byResource.get('Pat').capacity].join(' | ');
Per-task and per-resource calendars. A task's calendar takes the plan calendar's shape (workdays, holidays or exceptions, hoursPerDay) or the id of an entry in the calendars: { id: spec } option; a resource's calendar likewise. The scheduler counts that task's durations and lags in its own working days, so five days on a seven-day calendar span five calendar days where the Monday-Friday plan spans seven. The split view shades each bar by its own calendar, the workload band and level() book the days that calendar works, schedule.tasks.get(id).calendar reports the resolved id ('plan' when the task follows the plan), and MSPDI export and import round-trip the calendars. An id that names no calendar refuses the schedule with an unknown-calendar entry in errors that names the task (or resource) and the id.
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { computeSchedule } = await import('../packages/modules/gantt/schedule.js');
const plan = createGantt({
projectStart: '2027-01-04', // a Monday
calendar: 'weekends',
calendars: { seven: { workdays: [0, 1, 2, 3, 4, 5, 6] } },
tasks: [
{ id: 'office', duration: 5 },
{ id: 'site', duration: 5, calendar: 'seven' },
],
});
const t = plan.schedule.tasks;
const span = (id) => t.get(id).ef - t.get(id).es;
const refused = computeSchedule([{ id: 'x', duration: 1, calendar: 'night-shift' }], [], { projectStart: '2027-01-04' });
return [span('office'), span('site'), t.get('site').calendar, t.get('office').calendar,
refused.errors[0].code + ' ' + refused.errors[0].calendar].join(' | ');
Resource avatars. A resource may carry an avatar image URL; the split view draws it as a round chip in the assignee column, the workload band's resource rows and the assignment picker, sized by the --lattice-gantt-avatar-size theme token (defaulting to the row height). The picture's alt text is the resource's name, and a resource without a picture - or whose image fails to load - keeps its initials glyph, so nothing shifts while a picture loads.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const PICTURE = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
const plan = createGantt({
projectStart: '2027-01-04',
// Ali carries an avatar (a data URL, so the example runs offline); Bo has
// none and keeps her initials.
resources: [
{ id: 'ann', name: 'Ali Mohsin', avatar: PICTURE },
{ id: 'bo', name: 'Bo Chen' },
],
tasks: [
{ id: 'a', name: 'Design', start: '2027-01-04', duration: 2, assignee: 'ann' },
{ id: 'b', name: 'Build', start: '2027-01-04', duration: 2, assignee: 'bo' },
],
});
const view = plan.mountSplit(root, { columns: ['name', 'assignee'], workload: true });
const NS = 'lat-gantt-split';
// The assignee column's first picture is Ali's, alt = her name; the band draws
// the same chip per resource, and Bo's row keeps her initials.
const assigneeAlt = root.querySelector('.' + NS + '__avatar-img').getAttribute('alt');
const band = [...root.querySelectorAll('.' + NS + '__wl-row')]
.map((row) => {
const img = row.querySelector('.' + NS + '__avatar-img');
const glyph = row.querySelector('.' + NS + '__avatar-glyph');
return img ? img.getAttribute('alt') : (glyph ? glyph.getAttribute('data-initials') : null);
})
.filter(Boolean);
view.destroy();
plan.destroy();
return `${assigneeAlt} | band ${band.join('+')}`;
Microsoft Project. importMSPDI reads a Project .xml (MSPDI) document into a model for createGantt, and exportMSPDI (or gantt.toMSPDI()) writes one back - tasks, typed dependencies with lag, constraints, baseline, %complete, resources (including per-resource working calendars) and assignments all round-trip.
const { createGantt, exportMSPDI, importMSPDI } = await import('../packages/modules/gantt/index.js');
const plan = createGantt({
tasks: [
{ id: '1', name: 'Design', duration: 5, assignee: 'Ada' },
{ id: '2', name: 'Build', duration: 3, assignee: 'Ada' },
],
});
const overBefore = plan.overAllocations.length; // Ada is double-booked
plan.level(); // shift one task later
const overAfter = plan.overAllocations.length; // now clear
const xml = exportMSPDI({ tasks: plan.tasks, dependencies: plan.dependencies, schedule: plan.schedule });
const back = importMSPDI(xml);
return [overBefore, overAfter, back.ok, back.tasks.length, xml.startsWith('<?xml')].join(' | ');
Earned-value analytics. With a captured baseline (captureBaseline) and per-task percentComplete, gantt.earnedValue({ statusDate }) (or the standalone computeEarnedValue(tasks, schedule, { statusDate })) reports EVM at a status date - Planned Value (PV/BCWS) from the baseline, Earned Value (EV/BCWP) from %complete, Actual Cost (AC/ACWP) from a per-task actualCost - and the derived Schedule Variance (EV−PV), Cost Variance (EV−AC), SPI (EV/PV) and CPI (EV/AC), per task and rolled up to summaries and the project. Give each task a cost for money-based EVM, or omit it for schedule-only EVM off the durations. The split view surfaces any metric through kind: 'evm' columns. This worked example reproduces the classic four-side fence: at the end of day 3, three sides are planned (PV 3000) but two are done (EV 2000) at a cost of 2500 (AC).
const { createGantt, computeEarnedValue } = await import('../packages/modules/gantt/index.js');
const tasks = [
{ id: 's1', name: 'Side 1', duration: 1, cost: 1000, percentComplete: 100, actualCost: 1250, baselineStart: 0, baselineEnd: 1 },
{ id: 's2', name: 'Side 2', duration: 1, cost: 1000, percentComplete: 100, actualCost: 1250, baselineStart: 1, baselineEnd: 2 },
{ id: 's3', name: 'Side 3', duration: 1, cost: 1000, percentComplete: 0, baselineStart: 2, baselineEnd: 3 },
{ id: 's4', name: 'Side 4', duration: 1, cost: 1000, percentComplete: 0, baselineStart: 3, baselineEnd: 4 },
];
const deps = [{ from: 's1', to: 's2' }, { from: 's2', to: 's3' }, { from: 's3', to: 's4' }];
const plan = createGantt({ tasks, dependencies: deps, projectStart: 0 });
const evm = plan.earnedValue({ statusDate: 3 }); // via the controller
const direct = computeEarnedValue(tasks, plan.schedule, { statusDate: 3 }); // or standalone
const p = evm.project;
return [p.pv, p.ev, p.ac, p.sv, p.cv, p.spi.toFixed(2), p.cpi.toFixed(2), direct.project.ev].join(' | ');
S-curve. gantt.sCurve({ statusDate, bucket }) turns the same earned-value arithmetic into a cumulative curve - one { date, pv, ev, ac } point per bucket from the project start to the finish, with the final PV equal to the total budget; gantt.mountSCurve(container, opts) draws that curve (three lines, a status-date marker, a legend and a hidden table) and hands back a handle whose refresh() redraws it and destroy() detaches it.
const { createGantt, computeSCurve } = await import('../packages/modules/gantt/index.js');
const tasks = [
{ id: 'a', duration: 2, cost: 100, percentComplete: 100 },
{ id: 'b', duration: 2, cost: 200, percentComplete: 50 },
{ id: 'c', duration: 2, cost: 300, percentComplete: 0 },
];
const plan = createGantt({ projectStart: 0, tasks });
const curve = plan.sCurve({ statusDate: 2, bucket: 'day' }); // via the controller
const direct = computeSCurve(tasks, plan.schedule, { statusDate: 2, bucket: 'day' }); // or standalone
const last = curve[curve.length - 1];
// Two day buckets; the last point lands on the finish with the whole 600 budget planned.
return [curve.length, last.pv, last.date, direct.length, direct[direct.length - 1].pv].join(' | ');
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom({ width: 640, height: 240 });
const plan = createGantt({
projectStart: 0,
tasks: [
{ id: 'a', duration: 3, cost: 100, percentComplete: 100, actualCost: 110 },
{ id: 'b', duration: 3, cost: 100, percentComplete: 50, actualCost: 40 },
],
});
const handle = plan.mountSCurve(root, { statusDate: 2, bucket: 'day', width: 400, height: 200 });
// The handle detaches the chart; the chart draws the three series and the status marker.
const series = [...root.querySelectorAll('.lat-gantt-scurve__series')].map((s) => s.getAttribute('data-series'));
const out = [typeof handle.destroy, typeof handle.refresh, series.join(','), root.querySelector('.lat-gantt-scurve__status') != null].join(' | ');
handle.destroy();
plan.destroy();
return out;
Resource-rate cost. A resource may carry an hourly rate, dated rates: [{ from, rate }] changes, and a type ('work' default, 'material' cost per assignment unit, 'cost' a flat amount per assignment). gantt.cost() (or the standalone computeCost(tasks, schedule, { resources })) prices every task as Σ assignment effort × the rate in force each day, plus the task's own fixed cost, rolled up to summaries and the project - a task with no explicit cost and no rated assignment costs nothing, keeping a plan with no rate data unaffected. earnedValue() reads this same rate-derived figure as its BAC when a task carries no explicit cost. Ann is $50/h through day 2, then $60/h from day 2 on; task "a" (days 0-1) costs 16h at the old rate, task "b" (day 0 only, since it has no dependency on "a") costs 8h at the same old rate too.
const { createGantt, computeCost } = await import('../packages/modules/gantt/index.js');
const tasks = [
{ id: 'a', duration: 2, assignee: 'Ann' },
{ id: 'b', duration: 1, assignee: 'Ann' },
];
const resources = [{ id: 'Ann', rate: 50, rates: [{ from: 2, rate: 60 }] }];
const plan = createGantt({ projectStart: 0, tasks, resources });
const cost = plan.cost(); // via the controller
const direct = computeCost(tasks, plan.schedule, { resources }); // or standalone
const evm = plan.earnedValue(); // EV's BAC falls back to this same figure
return [cost.byTask.get('a').cost, cost.byTask.get('b').cost, cost.project.cost, direct.project.cost, evm.byTask.get('a').bac].join(' | ');
Render the plan as an SVG timeline with mount(container, options) - bars on a time scale, dependency arrows with a per-link-type anchor and a lag/lead label, the critical path highlighted, a today line, optional non-working-day shading, milestones as diamonds, a progress bar-fill and configurable labels. The view redraws itself whenever the schedule recomputes; unmount() detaches it. All geometry is computed from the schedule, so it draws identically headless or in a browser.
import { createGantt } from '@toclocoinc/lattice-grid/modules/gantt';
const gantt = createGantt({ tasks, dependencies });
gantt.mount(document.querySelector('#plan'), {
today: '2026-03-06', // a day-number, ISO date or Date; draws the today line
nonWorking: 'weekends', // shade Saturdays and Sundays
label: 'percent', // bar label: 'name' | 'percent' | 'dates' | (task) => string
dateAxis: true, // axis ticks as calendar dates
zoom: 'week', // 'day' | 'week' | 'month' | 'quarter' | pixels-per-day; omit to fit width
scrollToToday: true, // scroll so the today line is in view
groupBy: 'assignee', // swimlanes by a task property or (task) => key
rowHeight: 26,
width: 'container', // the default: fill the container, and keep following it
markers: [ // vertical date markers
{ date: '2026-03-09', label: 'Freeze', colour: '#e2513b', dashed: true },
{ from: '2026-03-11', to: '2026-03-13', label: 'Review' },
],
});
Vertical date markers. A markers array on mount or mountSplit draws display-only annotations through the whole plot: each { date, label } is a full-height line with its label in the time header, a { from, to } entry shades the range between the two dates with an edge line at each end, and colour/dashed restyle the line. Dates are day-numbers, ISO date strings or Dates; in the plain view a string/Date is rebased through projectEpoch like today. Markers are redrawn at every zoom and never touch the schedule; a marker that cannot resolve is skipped with a warning, and the rest of the plan still draws.
const { createTestDom, resetTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
// The plain view: a dashed, coloured line at a date and a shaded range.
const { root } = createTestDom();
const plain = createGantt({ tasks: [{ id: 'a', duration: 8 }] });
plain.mount(root, {
markers: [
{ date: 2, label: 'Freeze', colour: '#e2513b', dashed: true },
{ from: 5, to: 7, label: 'Review' },
],
});
const line = root.querySelector('.lat-gantt__marker');
const plainOut = {
lines: root.querySelectorAll('.lat-gantt__marker').length,
labels: [...root.querySelectorAll('.lat-gantt__marker-label')].map((l) => l.textContent).join('+'),
ranges: root.querySelectorAll('.lat-gantt__marker-range').length,
dash: line.getAttribute('stroke-dasharray'),
colour: line.getAttribute('stroke'),
};
plain.destroy();
// The split view draws the same markers, labels in its own time header.
const { root: root2 } = createTestDom();
const split = createGantt({ tasks: [{ id: 'a', duration: 8 }] });
split.mountSplit(root2, {
markers: [{ date: 2, label: 'Freeze' }, { from: 5, to: 7, label: 'Review' }],
});
const splitOut = {
lines: root2.querySelectorAll('.lat-gantt-split__marker').length,
labels: [...root2.querySelectorAll('.lat-gantt-split__marker-label')].map((l) => l.textContent).join('+'),
ranges: root2.querySelectorAll('.lat-gantt-split__marker-range').length,
};
split.destroy();
resetTestDom();
return `plain ${plainOut.lines} lines, ${plainOut.labels}, ${plainOut.ranges} range, ${plainOut.dash} ${plainOut.colour}; split ${splitOut.lines} lines, ${splitOut.labels}, ${splitOut.ranges} range`;
Sizing. width defaults to 'container': the view measures the box it was mounted into and redraws itself whenever that box changes, so a plan in a tab, a drawer, an accordion, a responsive panel or a split pane fits without the host writing a ResizeObserver of its own. A container with no box - a hidden tab, or an element that has not been laid out yet - is not treated as a container of zero width: the view holds a 720px fallback and adopts the real width the moment there is one. Pass a number to take the decision yourself; a numeric width is honoured exactly, installs no observer, and keeps the eight-tick axis it always had - only a container-sized plot thins its tick labels to the width it was given, because only a container-sized plot can be somewhere it had not been before. zoom and a numeric width are mutually exclusive: zoom wins - it fixes the pixels-per-day and lets the plot scroll past the container - and passing both now warns rather than discarding the width in silence.
The time axis thins its own labels. A Gantt picks its tick
rhythm from the calendar - every day at zoom: 'day', every seven days at
'week', every month above that - and that rhythm knows nothing about how wide
a date is. Where a label is wider than the gap between two ticks, only every nth
label is drawn: the largest regular stride that leaves at least 6px of clear space between
neighbours, so the axis keeps an even rhythm a reader can count on rather than the uneven gaps a
greedy left-to-right fit would leave. The gridlines and the split view's week separators
are not thinned with the text - the fine rhythm is information and costs nothing to
read; it was only ever the text that collided. Both surfaces do it: mount's axis and
mountSplit's timeline header, which is where a week-zoom header used to read
Sun 05 Oct 2025 Sun 12 Oct 2025 Sun 19 Oct 2025 with each date painted across the
next. Widening the Gantt now shows more dates rather than the same overlap, and the thinning is
re-decided on every draw, so a resize or a zoom change re-fits the labels. An un-zoomed
axis is untouched: it already chooses how many ticks to draw from the plot's own width, so its
labels always fitted, and a host that passed an explicit width keeps exactly the
drawing it had.
The project anchor. A mount option, not a mountSplit one: the joined split view below takes neither projectEpoch nor a date-valued today, and its weekend shading is unanchored. The engine's time line is whole days since the Unix epoch, so a plan written as day offsets (0, 4, 9…) legitimately renders as January 1970 - day 0 is 1970-01-01, and the module cannot tell an offset from a real epoch day, so it cannot warn about it. projectEpoch says which calendar date plan day 0 stands for. It and today take an ISO date string, a Date or a day number. A Date is read as the calendar date its local wall clock shows, the way a date column reads one: new Date(2026, 2, 2) is 2 March in every time zone, and a Date that carries a time of day is the local day it falls on. Before 1104 the UTC instant was floored, which put local midnight a day early everywhere east of Greenwich. A Date is therefore decided by the reader's zone; a string is the same day on every machine - '2026-03-02' is 2 March in Sydney and in New York alike - which is the form to prefer for an anchor stored with the plan. Recognise your own case: if you worked around the old behaviour by passing new Date(Date.UTC(y, m, d)), a reader west of Greenwich now sees the previous day, because UTC midnight is still the evening before in New York - pass new Date(y, m, d) or the ISO string instead. It is display-only: axis ticks, bar labels, tooltips, screen-reader text and the built-in weekend shading move with it, and nothing the scheduler, getState, the CSV or the MSPDI export produces does - every es/ef you read back is still the number you supplied. A host-supplied nonWorking function keeps receiving raw plan days, since it was written against your day numbers. Use projectStart instead when you want the model itself to be on calendar dates.
// A relative plan: offsets in the data, real dates on the screen.
gantt.mount(el, {
projectEpoch: '2026-03-02', // plan day 0 is this Monday
nonWorking: 'weekends', // so days 5-6 are the first weekend
today: '2026-03-06', // converted into plan space through the anchor
});
gantt.view.scrollToToday(); // also callable on demand
gantt.applyEdit({ id: 'design', duration: 7 }); // the view redraws automatically
gantt.unmount();
Dependency arrows. A link is routed by geometry. When the successor's anchor is at or beyond the predecessor's, it goes straight down and then in - a plain zero-lag finish-to-start link, where the two anchors share an x, is a clean vertical. When the anchor is behind the predecessor's - a negative lag (a lead), or two overlapping tasks - "down then in" does not exist, so the link takes a deliberate detour: out on the predecessor's own side, along a lane between the two rows, down, and in. Either way the final segment runs in the direction the arrowhead points, which is what stops a link doubling back on itself. The anchors themselves differ per link type - FS finish→start, SS start→start, FF finish→finish, SF start→finish - and so does the side the arrow arrives on, so an FF or SF link comes in from the right of the successor's finish rather than running through the bar to get there.
Link shorthand. A dependency's type accepts the MS Project string form as well as the structured one: 'FS+2', 'SS-1'. It normalises to { type: 'FS', lag: 2 } on the way in, so gantt.dependencies, the scheduler, the lag label and the MSPDI export all see the one canonical form. A shorthand lag alongside an explicit lag that disagrees warns; the explicit field wins.
createGantt({ tasks, dependencies: [
{ from: 'design', to: 'build', type: 'FS+2' }, // same as { type: 'FS', lag: 2 }
{ from: 'build', to: 'test', type: 'SS-1' }, // a lead
] });
Bars are draggable: drag the body to move a task, drag the right edge to resize it. When a grid and a columns map are given, each drag writes the new dates back through the grid's public edit surface (grid.edit.setCells) and reconciles a reverted or conflicted write; autoSchedule: true cascades dependents. A task placed earlier than its predecessors allow is flagged (findViolations), not silently moved.
import { createGantt } from '@toclocoinc/lattice-grid/modules/gantt';
const gantt = createGantt({
tasks, dependencies, grid, // a Lattice grid over the same tasks
columns: { start: 'start', duration: 'duration' }, // task field -> grid column
autoSchedule: true,
});
gantt.mount(document.querySelector('#plan'), { editable: true });
// drag a bar -> gantt.applyEdit(..., { writeBack: true }) -> grid.edit.setCells(...)
Hovering a bar shows a tooltip with its dates, duration, % complete and slack. Tasks are flagged when they slip: overdue (incomplete and finishing before today) and at-risk (negative total float). Negative float needs a target: pass a deadline (a day-number) to createGantt and any task that cannot meet it gets negative slack and is drawn at-risk.
Export: gantt.toCSV() writes the scheduled tasks as CSV ({ dates: true } for ISO dates); when a grid is bound, the grid's own Excel/CSV export works too. gantt.view.toSVG() serialises the drawn chart to a standalone SVG string - the handoff for turning it into an image or PDF.
Accessibility: bars are focusable and carry an aria-label describing the task (name, dates, progress, slack, critical). With the keyboard, arrows move a focused task, Shift+arrows resize it, and L links two tasks (press it on the source, then on the successor) with a finish-to-start dependency; every edit is announced in a polite live region and focus follows the edited task. Set keyboard: false to opt out.
Two panes, two ways. There are two arrangements, and which one you want depends on whether the left pane is your grid or the Gantt's own. gantt.mountSplit(container, options) - the joined split view, whose options are listed with GanttController in the type reference - draws both panes itself as one row-aligned surface, and is what to reach for unless you specifically need your own grid beside the timeline. The arrangement described here is the other one: you create and place a normal Lattice grid over the same task rows, mount the timeline beside it with gantt.mount(), and the Gantt consumes the grid rather than building it.
Binding a grid you built yourself. It is two-way. A drag on the timeline writes the new dates back through the grid's public edit surface (grid.edit.setCells) whenever createGantt was given a grid and a columns map - without both, the drag moves the bar and writes nothing - and an edit made in the grid pane, from any editor, reflects on the timeline. Both panes must be given the same row height for the rows to line up, and view.linkVerticalScroll(el) mirrors vertical scroll between them. Neither is needed with mountSplit, which shares one scroll and one row height by construction.
<div class="split" style="display:grid;grid-template-columns:360px 1fr">
<div id="tasks"></div> <!-- the grid pane -->
<div id="plan"></div> <!-- the timeline pane -->
</div>
<script type="module">
import { createGrid } from '@toclocoinc/lattice-grid';
import { createGantt } from '@toclocoinc/lattice-grid/modules/gantt';
const grid = createGrid({
element: document.querySelector('#tasks'),
columns: [{ field: 'name' }, { field: 'start', type: 'number', edit: { enabled: true } },
{ field: 'duration', type: 'number', edit: { enabled: true } }],
rows: tasks, rowKey: 'id', edit: { enabled: true },
});
const gantt = createGantt({ tasks, dependencies, grid, columns: { start: 'start', duration: 'duration' } });
gantt.mount(document.querySelector('#plan'));
gantt.view.linkVerticalScroll(grid.element); // keep the two panes aligned
</script>
Split view: the resource histogram. mountSplit's workload option draws a table of hours; its sibling histogram option draws the SAME numbers as bars instead - one row per resource, bars per time bucket, a dashed capacity line, and a tooltip on hover or keyboard focus. Both bands can be mounted together, the histogram drawn below the workload table; either alone is fine too. true takes the defaults; an object overrides resources (which to draw, and in what order - omitted draws every resource, Unassigned last), unit ('hours', the default, or 'percent' of capacity - a resource with no capacity falls back to hours for its own row), stacked ('task' splits a bar into one coloured segment per contributing task; the default draws one solid bar), height and rowHeight.
A bar that exceeds its resource's capacity carries two parts: the normal fill up to capacity, and the over-allocation fill above it - present whether or not stacked is on, since the over-allocated portion is always one thing to see, never split per task. The bars are positioned with the SAME day-to-pixel mapping as the timeline above them, so a bucket's edges line up with the scale header's own columns exactly as the workload band's do. A visually-hidden table (resource, period, hours or percent, capacity, over-allocated) carries the identical figures for a screen reader, rebuilt on every redraw.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const gantt = createGantt({
projectStart: '2027-01-04',
resources: { Ann: 1 }, // one unit of capacity: an 8-hour day
tasks: [
{ id: 'design', name: 'Design', start: '2027-01-04', duration: 2, assignee: 'Ann' },
{ id: 'review', name: 'Review', start: '2027-01-05', duration: 3, assignee: 'Ann', work: 6 },
],
});
// Tuesday carries both tasks: design's 8h plus review's 2h is 10h against
// Ann's 8h capacity - over, and (stacked: 'task') splits it into two segments.
const view = gantt.mountSplit(root, { zoom: 'day', histogram: { stacked: 'task' } });
const NS = 'lat-gantt-split';
const tuesday = root.querySelector('.' + NS + '__hg-bargroup[data-resource="Ann"][data-bucket="2027-01-05"]');
const segments = tuesday.querySelectorAll('.' + NS + '__hg-bar').length;
const overAllocated = root.querySelectorAll('.' + NS + '__hg-bar--overalloc').length;
// One header row plus one row per bucket the timeline drew - the timeline now
// extends the domain to fill the 800px test pane, so there are 31 bucket rows.
const tableRows = root.querySelectorAll('tr').length;
view.destroy();
return [segments + ' segments', overAllocated + ' over-allocated', tableRows + ' table rows'].join('; ');
Split view: hours or percent. The workload band's own unit option - 'hours' (the default; an existing page is unchanged) or 'percent' of the resource's own available hours that bucket - reads booked ÷ available × 100, where available is the exact figure the over-capacity highlight already uses (the per-resource working calendar from included), never a second calculation; a bucket with a KNOWN zero available hours (a part-timer's non-working day, a week wholly on leave) reads the en-dash '-', never Infinity/NaN, and a resource with no known capacity at all falls back to hours, the same rule the histogram band already shipped. An <button aria-pressed> switch in the band's header toggles the unit live - the histogram band's, too, when one is mounted - and is reachable off the view directly as view.setWorkloadUnit(unit)/view.getWorkloadUnit(); toggling never itself changes the plan. Editing a bucket in percent mode writes HOURS: typing 50 books half the resource's available hours there, through the identical applyEdit path an hours edit already takes.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const gantt = createGantt({
projectStart: '2027-01-04',
resources: { Ann: 1 }, // one unit of capacity: an 8-hour day
tasks: [{ id: 'design', name: 'Design', start: '2027-01-04', duration: 1, assignee: 'Ann' }],
});
const view = gantt.mountSplit(root, { zoom: 'day', workload: { unit: 'percent' } });
const NS = 'lat-gantt-split';
// Re-queried fresh each time: a unit switch redraws the band onto new cells,
// so a reference held from before the switch would read stale content.
const cellText = () => root.querySelector('.' + NS + '__wl-timerow[data-resource="Ann"]')
.querySelector('.' + NS + '__wl-bucket[data-bucket="2027-01-04"]').textContent;
const before = cellText(); // 8h booked of Ann's 8h available Monday: 100%
view.setWorkloadUnit('hours'); // the same toggle the band's own switch drives
const after = cellText(); // the identical booking, read back as hours: 8
view.destroy();
return [before, after].join(' | ');
Split view: a band's maximum height. The workload and histogram bands used to keep one fixed height, so a long resource list squeezed the plan above them. workload.maxHeight (and histogram.maxHeight) changes what that height MEANS: instead of a fixed box, the band sizes itself to its rows - the header strip plus one row per resource (and the workload band's totals row and any expanded sub-rows) - capped at this many pixels. Past the ceiling the band scrolls vertically on its own, with its header pinned above the scrolling rows; under it the band shrinks to its content, so a two-resource plan gets a two-row band, not an empty box. Either way the plan keeps at least half the view whatever the ceiling (a ceiling that would not fit beside the plan is shrunk to what would), and the same is true in the grid-table mode. A band with no maxHeight keeps its fixed height, so an existing page is unchanged.
const { createTestDom, resetTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const NS = 'lat-gantt-split';
const flex = (root) => root.querySelector('.' + NS + '__wl').style.flex;
// Forty resources, one task each: content is (40 rows + totals + header) × 28px,
// so the 240px ceiling binds and the band scrolls past it.
const many = Array.from({ length: 40 }, (_, i) => ({ id: 't' + i, name: 'Task ' + i, start: '2027-01-04', duration: 1, assignee: 'r' + i }));
let { root } = createTestDom();
createGantt({ projectStart: '2027-01-04', tasks: many })
.mountSplit(root, { height: 800, zoom: 'day', workload: { maxHeight: 240 } });
const capped = flex(root);
// Two resources: content is (2 + totals + header) × 28 = 112px - under the
// ceiling, so the band shrinks to its rows instead of keeping a fixed height.
resetTestDom();
({ root } = createTestDom());
createGantt({ projectStart: '2027-01-04', tasks: many.slice(0, 2) })
.mountSplit(root, { height: 800, zoom: 'day', workload: { maxHeight: 240 } });
const sized = flex(root);
return capped + ' | ' + sized;
Split view: load from other projects. A resource's real load is not always this plan's own tasks. Two equivalent spellings book load that counts in the workload band, the histogram and the band's own over-allocation highlight without ever becoming a row or a bar on the timeline, and without ever touching the critical path, the project finish or a summary: a task carrying loadOnly: true books its own start/end/duration and assignments exactly as any task would, but the controller keeps it out of the schedule entirely; the workload.external option books { resource, start, end, units | hours, label, group } directly, with no task behind it at all. Either way the booking is listed under its resource's expanded sub-rows as another project's booking (drawn distinctly, never editable - there is no bar to drag and no contour to write back to).
const { createTestDom, resetTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const gantt = createGantt({
projectStart: '2027-02-01',
resources: { Ann: 1 },
tasks: [
{ id: 'design', name: 'Design', start: '2027-02-01', duration: 1, assignee: 'Ann' }, // 8h Monday
{ id: 'review', name: 'Review', start: '2027-02-02', duration: 1, assignee: 'Ann' }, // 8h Tuesday
// Another project's booking, carried as a task so it has its own dates,
// but flagged load-only: counted in the band, never a row or a bar.
{ id: 'other', name: 'Other launch', start: '2027-02-02', duration: 1, assignee: 'Ann', work: 4, loadOnly: true },
],
});
const view = gantt.mountSplit(root, {
zoom: 'day',
workload: {
// The non-task spelling of the same idea: no task at all behind it.
external: [{ resource: 'Ann', start: '2027-02-01', end: '2027-02-02', hours: 2, label: 'Phoenix support' }],
},
});
const NS = 'lat-gantt-split';
const bars = root.querySelectorAll('.' + NS + '__barrow').length; // design + review only
const total = root.querySelector('.' + NS + '__wl-row[data-resource="Ann"]')
.querySelector('.' + NS + '__wl-cell--total').textContent;
// 8 (design) + 8 (review) + 4 (other, load-only) + 2 (external) = 22
view.toggleResource('Ann');
const otherProjectRows = root.querySelectorAll('.' + NS + '__wl-row--loadonly').length; // "other" + the external entry
const finish = gantt.schedule.projectFinish; // unaffected by "other" - it never reaches the CPM engine
view.destroy();
resetTestDom();
return `${bars} bars; total ${total}; ${otherProjectRows} other-project rows; finish unaffected: ${finish === gantt.schedule.tasks.get('review').ef}`;
Split view: resizable columns and divider. Every left-table column header (__colhead) carries a draggable right border with a col-resize cursor; once it is focused (by pointer or Tab), the arrow keys step it 8px a press, 32px with Shift. A column's own minWidth/maxWidth clamp how far it goes, and resizable: false omits the border entirely. The table/timeline divider (gridWidth) is the same gesture - a thin strip between the two panels, focusable and keyed the same way. A settled drag or keystroke on either - never mid-drag - fires exactly one columnResize event: { key, width, widths }, the divider reporting key: 'grid' and widths carrying every column's current width plus the divider's own. The same widths are reachable any time off the view as view.getColumnWidths(), and round-trip through gantt.getState()/setState() - restoring a saved state calls view.setColumnWidths() for you.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root, document } = createTestDom();
const columns = [
{ key: 'name', title: 'Task', width: 0, kind: 'name' },
{ key: 'duration', title: 'Days', width: 60, kind: 'duration', minWidth: 50, maxWidth: 90 },
{ key: 'locked', title: 'Locked', width: 40, resizable: false },
];
const gantt = createGantt({ projectStart: '2027-01-04', tasks: [{ id: 'design', name: 'Design', duration: 5 }] });
const view = gantt.mountSplit(root, { gridWidth: 300, columns });
const NS = 'lat-gantt-split';
let events = 0;
let lastKey = null;
gantt.on('columnResize', (e) => { events += 1; lastKey = e.key; });
// Drag the "Days" column's border 50px right - past its own 90px maxWidth.
const handle = root.querySelector('.' + NS + '__colresize[data-resize-key="duration"]');
handle.dispatchEvent({ type: 'pointerdown', target: handle, clientX: 0, clientY: 0, button: 0, bubbles: true });
document.documentElement.dispatchEvent({ type: 'pointermove', target: document.documentElement, clientX: 50, clientY: 0 });
document.documentElement.dispatchEvent({ type: 'pointerup', target: document.documentElement, clientX: 50, clientY: 0 });
const daysWidth = root.querySelectorAll('.' + NS + '__colhead')[1].style.width;
// `resizable: false` grows no border to drag at all.
const lockedHandle = root.querySelector('.' + NS + '__colresize[data-resize-key="locked"]');
// Shift+ArrowRight on the focused divider steps it 32px.
const divider = root.querySelector('.' + NS + '__divider--resize');
divider.dispatchEvent({ type: 'keydown', target: divider, key: 'ArrowRight', shiftKey: true, bubbles: true });
// Persist the resize, remount fresh, and restore it.
const saved = gantt.getState();
gantt.mountSplit(root, { gridWidth: 300, columns });
gantt.setState(saved);
const restored = gantt.getState();
view.destroy();
return [events, lastKey, daysWidth, lockedHandle ? 'handle' : 'none', restored.gridWidth, restored.columnWidths.duration].join(' | ');
Split view: the plain table keeps the name column legible. In the hand-built table (no createGrid) every column keeps its declared or natural width, and the name column is never squeezed below its 160px floor - overridable per column with minWidth - to fit gridWidth; when the columns are wider than the pane the pane scrolls sideways (header and body together) instead of compressing them. A task name renders on one line with one trailing ellipsis, its full text in the cell's native title and accessible name, and every row stays rowHeight. Pass wrapNames: true to opt back into wrapping, which grows the row to fit.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const NS = 'lat-gantt-split';
const gantt = createGantt({ projectStart: '2027-01-04', tasks: [
{ id: 'a', name: 'Design the landing page with a long name that would wrap', duration: 5 },
] });
const view = gantt.mountSplit(root, { gridWidth: 300 });
const widths = view.getColumnWidths();
const nameText = root.querySelector('.' + NS + '__name-text');
const row = root.querySelector('.' + NS + '__row[data-task="a"]');
const oneLine = nameText.classList.contains(NS + '__name-text--nowrap');
const rowH = row.style.height;
// `wrapNames: true` opts back into wrapping, so the row grows to fit.
gantt.mountSplit(root, { gridWidth: 300, wrapNames: true });
const wrapped = root.querySelector('.' + NS + '__row[data-task="a"]').style.height !== rowH;
view.destroy();
return [widths.name, widths.start, oneLine ? 'one' : 'wrap', rowH, wrapped ? 'true' : 'false'].join('|');
Split view: the critical-path highlight. By default every zero-float task and its dependency arrows are drawn in the critical colour. showCritical: false on mountSplit turns that off entirely, and view.setShowCritical(bool) toggles it live - a redraw in place, no remount. criticalStyle: 'fill' (the default) recolours a critical bar; criticalStyle: 'outline' keeps the bar's own fill and adds only the critical outline, so the marking sits over per-task colours. A kind: 'slack' left-panel column shows each task's total float in whole working days, read-only.
const { createTestDom, resetTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const gantt = createGantt({
projectStart: '2027-01-04',
tasks: [
{ id: 'a', name: 'Design', duration: 3 },
{ id: 'b', name: 'Build', duration: 2 },
{ id: 'd', name: 'Docs', duration: 1 }, // 4 days of float
],
dependencies: [{ from: 'a', to: 'b', type: 'FS' }],
});
const NS = 'lat-gantt-split';
const barOf = (id) => {
const r = root.querySelector('.' + NS + '__barrow[data-task="' + id + '"]');
return r ? r.querySelector('.' + NS + '__bar') : null;
};
const view = gantt.mountSplit(root, { columns: ['name', 'slack'] });
const criticalOn = barOf('a').classList.contains(NS + '__bar--critical');
const slack = root.querySelector('.' + NS + '__grid-body')
.querySelector('.' + NS + '__row[data-task="d"]')
.querySelector('.' + NS + '__cell[data-col="1"]').textContent;
view.setShowCritical(false); // redraw, no remount
const toggledOff = !barOf('a').classList.contains(NS + '__bar--critical');
view.setShowCritical(true);
const toggledBack = barOf('a').classList.contains(NS + '__bar--critical');
const outline = gantt.mountSplit(root, { columns: ['name'], criticalStyle: 'outline' });
const outlineClass = barOf('a').classList.contains(NS + '__bar--critical-outline');
const keepsOwnFill = !barOf('a').classList.contains(NS + '__bar--critical');
outline.destroy();
resetTestDom();
return [criticalOn, slack, toggledOff, toggledBack, outlineClass, keepsOwnFill].join('|');
Split view: the tables as real grids. Inject your grid factory - mountSplit(el, { createGrid }), injected rather than imported so the Gantt bundle does not carry the grid - and the task table, the workload band’s resource table and the histogram band’s are mounted as real Lattice grids, configured with the normal column API: columns for the tasks, workload.columns and histogram.columns for the bands. Every built-in column (name, start, end, duration, progress, assignee, effort, units; a band’s resource and total) is an ordinary column: name it by id to show it, give an object with its id to override part of it (merged one level deep, so a reformatted start still reschedules the plan when edited), leave it out to remove it, and add any number of your own columns with the full grid spec. List order is display order; only name (tasks) and resource (bands) are required. Rows are drawn at the timeline’s own row heights and scroll with it; a grid sort or filter reorders the timeline (or the band’s plot) to match. Each table’s column order, widths, visibility, sort and filters round-trip through gantt.getState()/setState() under tables, and columnResize still fires. A band’s time buckets are grid columns too - one per timeline bucket, pinned under the timeline’s own scale - but every header control is switched off on them and a column definition wins over any restored state, so a bucket never carries a sort, filter, menu, drag, hide, resize or group control of any kind. The grids themselves are on view.grids.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGrid } = await import('../packages/dom/src/index.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const gantt = createGantt({ projectStart: '2027-01-04', tasks: [
{ id: 'design', name: 'Design', duration: 5, cost: 1200, assignee: 'Ann' },
{ id: 'build', name: 'Build', duration: 3, cost: 800, assignee: 'Bob', dependsOn: ['design'] },
] });
const view = gantt.mountSplit(root, {
createGrid,
columns: [
'name',
{ id: 'start', title: 'Begins', format: { type: 'date', pattern: 'dd/MM/yyyy' } }, // an overridden default
{ id: 'cost', field: 'cost', title: 'Cost', type: 'number' }, // a host column
],
workload: { columns: [{ id: 'resource', title: 'Who' }, 'total'] },
});
const grid = view.grids.tasks;
grid.sort.set([{ col: 'cost', dir: 'asc' }]); // the timeline follows the grid's order
const saved = gantt.getState(); // saved.tables.task carries the sort
const out = [
grid.columns.visible().map((c) => c.id).join(','),
grid.rows.text('design', 'start'),
view.rowGeometry().map((g) => g.id).join(','),
saved.tables.task.sort.map((s) => s.col + ':' + s.dir).join(','),
view.grids.workload.columns.visible().filter((c) => !/^\d{4}-\d{2}-\d{2}$/.test(c.id)).map((c) => c.title).join(','),
].join(' | ');
view.destroy();
return out;
Split view: the bands’ own columns. workload.columns and histogram.columns are independent of the task table. Besides the built-in resource and total, a column is { key, title, width, align, value(resource, ctx), render(resource, ctx), subRows }. resource is the resource’s own spec from resources (a role, a team, a rate …) plus its key and display name; ctx carries the band’s figures for the row (capacity, total, cost, over, overCount, unit, tasks). A host can compute anything there: a cross-project peak, the weeks over-allocated. value is what the column sorts and filters on; render, when given, draws the cell (a string is set as text, a node is mounted). subRows is the rule for the task sub-rows beneath an expanded resource: 'blank' (default) shows nothing, 'inherit' repeats the resource’s value, and a function (task, resource, ctx) supplies the sub-row’s own. The totals row is blank. The grid header sorts and resizes the columns; a settled resize fires columnResize under a namespaced key (band:role for the workload band, histogram:role for the histogram), and getColumnWidths()/setColumnWidths() round-trip those keys. The time buckets are grid columns too but never get header controls - each is inert to sort, filter, resize and move whatever the config. Needs createGrid, as the other grid columns do.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGrid } = await import('../packages/dom/src/index.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const gantt = createGantt({
projectStart: '2027-01-04',
resources: [{ id: 'ann', name: 'Ann', role: 'Designer', capacity: 1 }],
tasks: [{ id: 'a', name: 'Design', duration: 5, assignee: 'ann' }],
});
const view = gantt.mountSplit(root, {
createGrid,
workload: { columns: [
'resource',
{ key: 'role', title: 'Role', width: 90, value: (r) => r.role, subRows: 'inherit' },
{ key: 'peak', title: 'Peak', width: 70, align: 'right', value: (r, ctx) => ctx.total },
] },
});
const grid = view.grids.workload;
view.toggleResource('ann');
view.setColumnWidths({ 'band:role': 120 });
const out = [
grid.columns.visible().map((c) => c.id).filter((id) => !/^\d{4}-\d{2}-\d{2}$/.test(id)).join(','),
grid.rows.text('ann\u0000', 'role'),
grid.rows.text('ann\u0000a', 'role'),
grid.rows.text('ann\u0000a', 'peak'),
'band:role=' + view.getColumnWidths()['band:role'],
].join(' | ');
view.destroy();
return out;
Split view: the grid’s select and bounded-number editors. A host column can ask for one of the grid’s own editors with the plain editor shorthand instead of a full edit/lookup spec: editor: { type: 'select', options } gives the select editor - a dropdown of the options in the order given, committing the chosen option’s value - and editor: { type: 'number', min, max, step } gives the number editor, which clamps to the bounds and steps by step (a spinner is added, so the step is keyboard-operable). An editor makes the column editable unless editable/edit says otherwise, and it needs createGrid: the lightweight tables have no editors. A host column written with a pre-grid key and no field writes its field back under that id.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGrid, createEditor } = await import('../packages/dom/src/index.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root, document } = createTestDom();
const gantt = createGantt({ projectStart: '2027-01-04', tasks: [
{ id: 'design', name: 'Design', duration: 5, status: 'New', cost: 500 },
] });
const view = gantt.mountSplit(root, {
createGrid,
columns: [
'name',
{ key: 'status', editor: { type: 'select', options: ['New', 'In progress', 'Done'] } }, // a dropdown column
{ key: 'cost', editor: { type: 'number', min: 0, max: 1000, step: 100 } }, // a bounded number column
],
});
const grid = view.grids.tasks;
const options = grid.columns.get('status').lookup.options().map((o) => o.label).join(',');
// The number editor clamps a typed value to the bound, and steps by `step`.
const number = createEditor({
document, cell: document.createElement('div'), overlay: document.body,
column: grid.columns.get('cost'), value: 500,
});
number.input.value = '2500';
const clamped = number.commit().value;
// The select editor commits the chosen option to the task's field.
grid.edit.setCells([{ key: 'design', colId: 'status', value: 'In progress' }], 'cell', { origin: 'user' });
await new Promise((r) => setTimeout(r, 5));
const chosen = gantt.taskById('design').status;
view.destroy();
return [options, chosen, clamped].join(' | ');
Split view: workload thresholds, showUnassigned and the click event. The workload band's thresholds option colours a cell - and the histogram band's bar for the same bucket - by how full the bucket is: each entry is { at, className | colour }, at a fraction of the bucket's available hours (0.8 = 80% booked), and either a className added to the cell/bar or a colour applied inline (spelled colour or color). The highest threshold a bucket reaches wins, and it applies in every unit - hours, percent and cost - because the fraction is read off the hours figures, which is also why it still works in cost mode, where the band has no capacity line. showUnassigned: false hides the Unassigned row. Clicking a cell fires workload:click: { resource, bucketStart, bucketEnd, booked, available, tasks }, the hour figures always hours whatever the band is drawing.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const gantt = createGantt({
projectStart: '2027-01-04',
resources: { Ann: 1 }, // one unit of capacity: an 8-hour day
tasks: [
{ id: 'design', name: 'Design', start: '2027-01-04', duration: 1, assignee: 'Ann' },
{ id: 'loose', name: 'Loose', start: '2027-01-04', duration: 1 }, // no assignee
],
});
const view = gantt.mountSplit(root, {
zoom: 'day',
workload: { thresholds: [{ at: 0.5, colour: 'rgb(255, 0, 0)' }], showUnassigned: false },
});
const NS = 'lat-gantt-split';
// Monday: Ann books 8h of her 8h capacity - 100% meets the 0.5 threshold, so
// the configured colour lands on the cell (and would on the histogram bar).
const row = root.querySelector('.' + NS + '__wl-timerow[data-resource="Ann"]');
const cell = row.querySelector('.' + NS + '__wl-bucket[data-bucket="2027-01-04"]');
const coloured = cell.style.backgroundColor;
// The unassigned task's row is hidden.
const unassigned = root.querySelector('.' + NS + '__wl-timerow[data-resource=""]') ? 'shown' : 'hidden';
// A click on the cell reports the bucket's bookings.
let clicked = null;
gantt.on('workload:click', (e) => { clicked = e; });
cell.dispatchEvent({ type: 'click', target: cell, bubbles: true });
view.destroy();
return [coloured, unassigned, clicked.resource, clicked.bucketStart, clicked.booked, clicked.available, clicked.tasks.length, clicked.tasks[0].id].join(' | ');
Split view: the task table's own cell and row events. The task table became a real Lattice grid; this maps its own pointer and edit events onto the controller - cellClick/cellDblClick/cellContextMenu, rowClick/rowDblClick and the edit lifecycle beforeCellEditStart (vetoable) / cellEditStart / cellEditCommit / cellEditCancel - with id, rawTask, the scheduled task and the originating DOM event on every payload. A double-click, Enter or F2 on an editable cell with no editor already open raises beforeCellEditStart before the grid's own editor opens, for every one of those paths alike - Enter is also how a COMMIT is typed from inside an already-open editor, but that is told apart by whether an edit is already in progress, not by the key, so a commit is never mistaken for a fresh open attempt. A handler's preventDefault(reason) keeps the cell closed for that gesture - the grid's own cellDblClick is not raised either, since nothing reached the grid at all - and fires the paired cellEditStart:cancelled with that reason, the same before*/:cancelled contract every other gated Gantt event follows. Escape on an open editor raises cellEditCancel; Enter raises cellEditCommit with oldValue/newValue.
const { createTestDom, TestEvent, flushFrames } = await import('../packages/dom/src/renderer/testdom.js');
const { createGrid } = await import('../packages/dom/src/index.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const gantt = createGantt({
projectStart: '2027-01-04',
tasks: [{ id: 'a', name: 'Alpha' }, { id: 'b', name: 'Beta' }],
});
const view = gantt.mountSplit(root, { createGrid, editable: true, keyboard: true, columns: ['name'] });
flushFrames();
const seen = [];
for (const name of ['cellClick', 'rowClick', 'cellDblClick', 'beforeCellEditStart', 'cellEditStart', 'cellEditStart:cancelled', 'cellEditCommit', 'cellEditCancel']) {
gantt.on(name, (e) => seen.push([name, e.id, e.columnKey, e.reason]));
}
const cellOf = (key) => { const row = root.querySelector(`.lat-row[data-key="${key}"]`); return row ? row.querySelector('.lat-cell[data-col="name"]') : null; };
// A click raises cellClick, then rowClick for the row under it.
cellOf('a').dispatchEvent(new TestEvent('click'));
const [click, rowClick] = seen.splice(0);
// A host veto on beforeCellEditStart keeps row b's cell closed for the
// gesture: no editor opens, cellDblClick is not raised for a gesture the
// gate claimed before the grid's own handler ever saw it, and the paired
// cellEditStart:cancelled carries the veto's reason.
gantt.on('beforeCellEditStart', (e) => { if (e.id === 'b') e.preventDefault('locked'); });
cellOf('b').dispatchEvent(new TestEvent('dblclick'));
const vetoed = !cellOf('b').querySelector('input');
const vetoBatch = seen.splice(0);
const vetoSeen = vetoBatch.map((e) => e[0]).join(',');
const vetoReason = vetoBatch.find((e) => e[0] === 'cellEditStart:cancelled')[3];
// Row a's double-click raises beforeCellEditStart, then cellDblClick and
// cellEditStart, and the grid's own editor opens.
cellOf('a').dispatchEvent(new TestEvent('dblclick'));
const opened = !!cellOf('a').querySelector('input');
const openSeen = seen.splice(0).map((e) => e[0]).join(',');
// Enter commits - cellEditCommit carries the old and new value.
const input = cellOf('a').querySelector('input');
input.value = 'Alpha v2';
input.dispatchEvent(new TestEvent('keydown', { key: 'Enter', bubbles: true }));
await new Promise((r) => setTimeout(r, 5));
const newName = gantt.taskById('a').name;
seen.length = 0;
// Escape abandons an open edit - cellEditCancel, no write.
cellOf('a').dispatchEvent(new TestEvent('dblclick'));
cellOf('a').querySelector('input').dispatchEvent(new TestEvent('keydown', { key: 'Escape', bubbles: true }));
const cancelled = seen.some((e) => e[0] === 'cellEditCancel');
seen.length = 0;
// Row a has no editor open now: Enter on it is an OPEN attempt, not a
// commit, so it raises beforeCellEditStart then cellEditStart exactly like
// the double-click above, and the grid's own editor opens - a key reaching
// an open editor is the only case Enter is ever read as a commit instead.
cellOf('a').dispatchEvent(new TestEvent('keydown', { key: 'Enter', bubbles: true }));
const enterOpened = !!cellOf('a').querySelector('input');
const enterOpenSeen = seen.splice(0).map((e) => e[0]).join(',');
view.destroy();
return [
`click ${click[0]}+${rowClick[0]}`,
`veto ${vetoed} (${vetoSeen}) ${vetoReason}`,
`open ${opened} (${openSeen})`,
`commit ${newName}`,
`cancel ${cancelled}`,
`enterOpen ${enterOpened} (${enterOpenSeen})`,
].join(' | ');
Split view: workload cell and resource events. The workload band's bucket cells and the histogram band's bars raise the same pointer vocabulary on the controller (gantt.on), with resource, resourceName, the half-open bucketStart/bucketEnd, the hours booked/available, the percent of capacity (when a capacity is known), the over flag, the band's unit, the view, the originating DOM event and the contributing tasks ({ id, name, hours, units }) - hour figures always hours, whatever the band is drawing. A per-task sub-row cell adds id, rawTask (the task as you supplied it) and the scheduled task; a resource-name cell raises workloadResourceClick with just resource/resourceName. workloadCellDblClick and workloadCellContextMenu are claimable with preventDefault() (a host's own editor or menu), pairing with no :cancelled - nothing was refused, only claimed. Keyboard focus and Enter/Space on a histogram bar raise the same events as pointer ones, and none fire for the host's own setTasks/rows.apply. Hovering a cell shows a built-in tooltip listing the contributing tasks; set workload.tooltip: false to hide it or workload.tooltip: (payload) => node | string to replace it. The Lattice event names:
| Lattice | When, and what it carries |
|---|---|
| workloadCellClick | A click on a workload bucket cell or histogram bar: { resource, resourceName, bucketStart, bucketEnd, booked, available, percent, over, unit, tasks, id, rawTask, task }. |
| workloadCellDblClick | A double click; preventDefault() claims it so a host opens its own editor (and the band's own inline hours editor stays shut). Two workloadCellClick events precede it. |
| workloadCellContextMenu | A right click; preventDefault() suppresses the browser's own menu. |
| workloadCellHover / workloadCellHoverEnd | Once per entry / exit of a cell or bar (moving within the same cell is not a re-entry). Keyboard focus reaching or leaving a histogram bar raises the same pair. |
| workloadResourceClick | A click on a workload-band resource-name cell: { resource, resourceName }. |
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const gantt = createGantt({
projectStart: '2027-01-04',
resources: { Ann: 1 }, // one unit of capacity: an 8-hour day
tasks: [
{ id: 'design', name: 'Design', start: '2027-01-04', duration: 1, assignee: 'Ann' },
{ id: 'extra', name: 'Extra', start: '2027-01-04', duration: 1, assignee: 'Ann', work: 2 },
],
});
const view = gantt.mountSplit(root, { zoom: 'day', workload: true });
const NS = 'lat-gantt-split';
// Monday: Ann books 10h of her 8h capacity - over-allocated, 125%.
let clicked = null;
gantt.on('workloadCellClick', (e) => { clicked = e; });
const row = root.querySelector('.' + NS + '__wl-timerow[data-resource="Ann"]');
const cell = row.querySelector('.' + NS + '__wl-bucket[data-bucket="2027-01-04"]');
cell.dispatchEvent({ type: 'click', target: cell, bubbles: true });
// The hover tooltip lists the contributing tasks.
cell.dispatchEvent({ type: 'pointerover', target: cell, bubbles: true });
const tip = root.querySelector('.' + NS + '__wl-tooltip');
const tipHasExtra = tip && /Extra/.test(tip.textContent) ? 'Extra' : '';
view.destroy();
return [clicked.resource, clicked.percent, clicked.over, clicked.tasks.length, tipHasExtra].join(' | ');
Split view: scroll and zoom with no remount. Three methods drive the view off a dashboard's own controls - a row picker, a date search, a zoom slider - without tearing it down and rebuilding it. view.scrollToTask(id) scrolls the least each axis needs to bring a task's row and bar fully into view: vertically to the row itself, horizontally to its bar's start/finish (a milestone's single point), leaving an axis alone when the row/bar is already fully shown on it. view.scrollToDate(date) (a day-number, ISO string or Date) centres that date in the timeline's own viewport. view.setZoom(level) takes the same preset ('day'/'week'/'month'/'quarter') or raw pixels-per-day mountSplit's own zoom option does, and repaints in place: collapsed summaries, every column width and the date centred in the timeline before the call are all exactly as they were after it, since nothing here unmounts the view or loses the state the DOM itself does not carry. All three fire the same draw event any other repaint does.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom({ width: 300, height: 50 });
const gantt = createGantt({ projectStart: '2027-01-04', rowKey: 'id', tasks: [
{ id: 'proj', name: 'P' },
{ id: 'a', name: 'A', parent: 'proj', start: '2027-01-04', duration: 2 },
{ id: 'b', name: 'B', parent: 'proj', start: '2027-01-08', duration: 2 },
{ id: 'c', name: 'C', start: '2027-03-01', duration: 2 },
] });
const view = gantt.mountSplit(root, { zoom: 'day', rowHeight: 30, gridWidth: 200 });
view.toggle('proj'); // collapse the summary before any scroll/zoom
view.setColumnWidths({ name: 240 }); // and widen a column
const scroller = view.scroller;
view.scrollToTask('c'); // brings row c's row and bar into view
const taskScroll = scroller.scrollTop + ',' + scroller.scrollLeft;
view.scrollToDate('2027-01-20'); // centres that date in the timeline
const dateScroll = scroller.scrollLeft;
view.setZoom('week'); // no remount: both of the above still hold
const out = [
taskScroll,
dateScroll,
scroller.scrollLeft, // the centred date followed the rescale
[...view.collapsed].join(','), // still collapsed
view.getColumnWidths().name, // still widened
view.options.zoom,
].join(' | ');
view.destroy();
return out;
Split view: a built-in zoom control. The zoomControl option draws day / week / month / quarter / fit-to-plan buttons straight into the view’s own header - no host toolbar to build. Each button is a real <button> (so keyboard Enter/Space reaches it) carrying its catalogue label and an aria-pressed that tracks the level currently in effect; clicking one switches the zoom in place through the same setZoom/fitZoom a host would call, so collapsed summaries, every column width and the centred date all survive the change. fit picks the pixels-per-day that shows the whole plan and resets the scroll to its start. Pass an object instead of true to reorder or subset the buttons (levels) or to build the control detached and expose it as view.zoomControl for a host toolbar to place (position: 'toolbar', the default is 'header').
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom({ width: 300, height: 50 });
const gantt = createGantt({ projectStart: '2027-01-04', rowKey: 'id', tasks: [
{ id: 'proj', name: 'P' },
{ id: 'a', name: 'A', parent: 'proj', start: '2027-01-04', duration: 2 },
{ id: 'b', name: 'B', parent: 'proj', start: '2027-01-08', duration: 2 },
{ id: 'c', name: 'C', start: '2027-03-01', duration: 2 },
] });
// zoomControl: true draws the five buttons into the view's own header.
const view = gantt.mountSplit(root, { zoom: 'day', rowHeight: 30, gridWidth: 200, zoomControl: true });
const NS = 'lat-gantt-split';
const buttons = [...root.querySelectorAll('.' + NS + '__zoom-btn')];
const labels = buttons.map((b) => b.textContent).join('|');
const pressed = buttons.find((b) => b.getAttribute('aria-pressed') === 'true').getAttribute('data-zoom');
// Each is a real keyboard-operable
The object form. position: 'toolbar' builds the buttons detached from the view - nothing is drawn in the header - and hands them back as view.zoomControl for a host toolbar to place; levels keeps only the named buttons, in order. The detached control still tracks the view’s zoom, and its own buttons still drive the view, exactly as the header form does.
Pinch to zoom. On a touch screen a two-finger pinch over the split view’s timeline steps the zoom one level (quarter, month, week, day) through setZoom, firing one zoomChange with cause 'gesture' per level and keeping the date under the fingers in place; a two-finger drag scrolls, and pinchZoom: false turns it off.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom({ width: 300, height: 50 });
const gantt = createGantt({ projectStart: '2027-01-04', rowKey: 'id', tasks: [
{ id: 'a', name: 'A', start: '2027-01-04', duration: 2 },
{ id: 'b', name: 'B', start: '2027-01-08', duration: 2 },
] });
// position: 'toolbar' builds the buttons detached; levels keeps only these two.
const view = gantt.mountSplit(root, { zoom: 'day', gridWidth: 200, zoomControl: { position: 'toolbar', levels: ['month', 'fit'] } });
const NS = 'lat-gantt-split';
const control = view.zoomControl; // the host places this where its toolbar lives
const levels = [...control.querySelectorAll('.' + NS + '__zoom-btn')].map((b) => b.getAttribute('data-zoom')).join('|');
const inHeader = root.querySelector('.' + NS + '__zoom') ? 'header' : 'toolbar';
view.destroy();
return [levels, inHeader].join(' | ');
View and viewport events,,. The view reports its own lifecycle and viewport on the controller (gantt.on), so a host can persist a zoom, follow a scroll, or build an overlay that lives exactly as long as the view. zoomChange fires once each time the split view’s time scale changes - a host view.setZoom/view.fitZoom (cause: 'api') or a click on its built-in zoom control ('control') - carrying { from, to, cause, pxPerDay }; pxPerDay is the pixels-per-day the repaint actually drew with, so a host can reproduce the scale without asking twice. visibleRangeChange fires the ISO dates now under the timeline’s left and right edges, and scroll the settled { top, left } offsets, each coalesced to at most one per animation frame - a burst of horizontal scroll, a resize and a zoom settle into one report, not dozens - and a scroll that settles where it already was is silent. draw fires once after every completed paint. viewMount fires once from mount or mountSplit after the first paint, and viewDestroy once as that view is torn down (a remount, unmount() or destroy()), both carrying the view and its root element. All six are reports, not gates - there is no before* form - and none fire for your own setTasks/rows.apply. The Lattice event names:
| Lattice | When, and what it carries |
|---|---|
| zoomChange | Once per change of the split view’s time scale: { from, to, cause, pxPerDay }, the level before and after, what drove it, and the resolved pixels-per-day. |
| visibleRangeChange | The ISO { start, end } now under the timeline’s left and right edges, coalesced to once per frame and once after the first mount. |
| scroll | The settled { top, left } viewport offsets, coalesced to once per frame; silent when it settles where it already was. |
| draw | Once after every completed paint, coalesced across synchronous invalidations. |
| viewMount | Once from mount/mountSplit after the first paint: { view, element }. |
| viewDestroy | Once as the view is torn down, before its root is removed: { view, element }. |
const { createTestDom, resetTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom({ width: 300, height: 50 });
const gantt = createGantt({ projectStart: '2027-01-04', rowKey: 'id', tasks: [
{ id: 'proj', name: 'P' },
{ id: 'a', name: 'A', parent: 'proj', start: '2027-01-04', duration: 2 },
{ id: 'b', name: 'B', parent: 'proj', start: '2027-01-08', duration: 2 },
{ id: 'c', name: 'C', start: '2027-03-01', duration: 2 },
] });
const heard = { viewMount: 0, zoomChange: [], viewDestroy: 0 };
gantt.on('viewMount', () => { heard.viewMount += 1; });
gantt.on('zoomChange', (e) => { heard.zoomChange.push(`${e.to}:${Math.round(e.pxPerDay)}`); });
gantt.on('viewDestroy', () => { heard.viewDestroy += 1; });
const view = gantt.mountSplit(root, { zoom: 'day', rowHeight: 30, gridWidth: 200 });
const mounted = heard.viewMount; // one, synchronously from mountSplit
// The scroll event reports the settled offsets once per frame; the scroller
// exposes the same pair synchronously, so a burst of three scrolls reads 0,300.
view.scroller.setScroll(0, 100);
view.scroller.setScroll(0, 200);
view.scroller.setScroll(0, 300);
const scrolled = `${view.scroller.scrollTop},${view.scroller.scrollLeft}`;
view.setZoom('week'); // one zoomChange, with the resolved pxPerDay
const zoomed = heard.zoomChange.join(';');
view.destroy(); // one viewDestroy, root still attached
resetTestDom();
return `mount ${mounted} | scroll ${scrolled} | zoom ${zoomed} | destroy ${heard.viewDestroy}`;
Split view: the band label column sizes itself. The workload and histogram bands’ resource-name column no longer follows the task table’s first column - a narrow first column (a 52px WBS column, say) used to truncate every resource name to an ellipsis. It sizes itself instead: its own default width, or workload.labelWidth/histogram.labelWidth in pixels - and always carries the full name as a native title. totals: false drops the totals column and row entirely, rather than leaving an empty cell to take the rest of the grid width.
const { createTestDom } = await import('../packages/dom/src/renderer/testdom.js');
const { createGantt } = await import('../packages/modules/gantt/index.js');
const { root } = createTestDom();
const gantt = createGantt({
projectStart: '2027-01-04',
resources: { Ann: 1 },
tasks: [{ id: 'design', name: 'Design', start: '2027-01-04', duration: 2, assignee: 'Ann' }],
});
// A narrow first task column; the bands below are sized on their own, not by it.
const view = gantt.mountSplit(root, {
zoom: 'day',
gridWidth: 400,
columns: [
{ key: 'progress', title: '%', width: 52, kind: 'progress' },
{ key: 'name', title: 'Task', width: 0, kind: 'name' },
],
workload: { labelWidth: 200, totals: false },
histogram: { labelWidth: 160 },
});
const NS = 'lat-gantt-split';
const wlName = root.querySelector('.' + NS + '__wl-row').querySelector('.' + NS + '__wl-cell--name');
const width = wlName.style.width;
const title = wlName.querySelector('.' + NS + '__wl-name-text').getAttribute('title');
const totals = root.querySelector('.' + NS + '__wl-cell--total') ? 'present' : 'none';
const hgName = root.querySelector('.' + NS + '__hg-row').querySelector('.' + NS + '__hg-cell--name');
view.destroy();
return [width, title, totals, hgName.style.width].join(' | ');
Live data. The Gantt exposes the same consumer surface as the grid - gantt.rows.apply({ add, update, remove }), keyed by its rowKey (default id) - so a Data Router attaches to it exactly as it does a grid, a board or a chart. One arriving stream can hydrate and update a whole screen, the Gantt included; each change recomputes the schedule and redraws.
import { createDataRouter } from '@toclocoinc/lattice-grid/modules/data-router';
const router = createDataRouter({ key: 'kind', rowKey: 'id' });
router.attach(grid, 'order').attach(gantt, 'task'); // one feed drives both
router.load(snapshot); // task rows land in the Gantt
router.apply(deltas); // updates recompute the schedule
Type reference
Generated from the type declarations, so it always matches the release. Each surface lists its properties, its methods and the events it raises as three tables; an option or value type lists its members once.
The gantt module
GanttNamedCalendar
A named working calendar, the value of an entry in the `calendars` option: the plan calendar's shape, with the resource-calendar and exception spellings accepted. A member left out is inherited from the plan calendar.
| Property | Type | Description |
|---|---|---|
| workdays | number[] | The working weekdays, 0=Sunday … 6=Saturday; defaults to the plan's. (optional) |
| workingDays | number[] | The same as `workdays`, in the resource-calendar spelling. (optional) |
| holidays | Array<string | number | Date | { from: string | number | Date; to: string | number | Date }> | Days off: ISO dates, `Date`s, day-numbers or inclusive `{ from, to }` ranges; defaults to the plan's. (optional) |
| exceptions | Array<string | number | Date | { from: string | number | Date; to: string | number | Date }> | The same as `holidays`, named as exceptions to the working week. (optional) |
| hoursPerDay | number | Record<number, number> | The working hours in a day: a flat number or a per-weekday map; defaults to the plan's `hoursPerDay`. (optional) |
GanttResolvedCalendar
One resolved calendar in a schedule: the working time a task was scheduled under.
| Property | Type | Description |
|---|---|---|
| id | string | The calendar id the tasks report in `calendar`. |
| workdays | number[] | The working weekdays, 0=Sunday … 6=Saturday. |
| holidays | number[] | The days off, as day-numbers. |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| isWorking | (day: number): boolean | day: number | boolean | Whether a calendar day-number is a working day under this calendar. |
GanttZoomControlOptions
The split view's built-in zoom control: a row of day / week / month / quarter / fit-to-plan buttons drawn in the view's own header. `position` picks where they go; `levels` subsets or reorders them.
| Property | Type | Description |
|---|---|---|
| position | 'header' | 'toolbar' | `'header'` (default) draws the buttons in the split view's own header; `'toolbar'` builds them detached and exposes them as `view.zoomControl` for a host toolbar to place. (optional) |
| levels | GanttZoomLevel[] | Which levels to draw, in order; defaults to all five (`day`, `week`, `month`, `quarter`, `fit`). Unknown names are ignored. (optional) |
GanttTooltipContext
What a host's own `tooltip` render function is given: the same scheduled task `label`'s own function gets, plus a `{ t, formatDate, dateAxis, rawTask }` context - `t` the resolved translator, `formatDate` the SAME locale-formatted-date helper the default card's Start/End/Date rows use, `dateAxis` the view's own option, and `rawTask` the host's original task object (for a custom field the scheduled shape does not carry).
| Property | Type | Description |
|---|---|---|
| dateAxis | boolean | Whether the view draws a date axis (the view's own `dateAxis` option); when false, days are plain numbers. |
| rawTask | GanttTask | null | The host's original task object for this bar, for custom fields the scheduled shape does not carry; null if it cannot be found. |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| t | (key: string, params?: Record<string, unknown>) => string | key: stringparams?: Record<string, unknown> | => string | The Gantt's message function: resolves a catalogue key (such as `gantt.tip.start`) in the active locale, with `{name}` parameters filled in. |
| formatDate | (day: number) => string | day: number | => string | Formats a schedule day number as a date in the grid/plan locale, the way the built-in card shows Start and End (e.g. "May 1, 2026"). |
GanttMarker
A vertical date marker: a labelled line drawn through the whole plot at a date, or - given `from`/`to` - a shaded range between two dates. Display-only: it annotates the timeline and never changes the schedule. Pass the `markers` option to `mount` or `mountSplit`.
| Property | Type | Description |
|---|---|---|
| date | number | string | Date | Where the line is drawn: a day-number (plan space), an ISO date string or a `Date`. In the plain view a string/`Date` is rebased through `projectEpoch`. Ignored when `from`/`to` are given. (optional) |
| from | number | string | Date | The range's start, in the same forms as `date`; with `to`, shades from here to there. (optional) |
| to | number | string | Date | The range's end, in the same forms as `date`; with `from`, shades from there to here. (optional) |
| label | string | The label drawn in the time header at the marker's line (centred over a range). (optional) |
| colour | string | The marker's colour (any CSS colour); defaults to the theme's muted foreground token. (optional) |
| dashed | boolean | Draw the line(s) dashed rather than solid. (optional) |
GanttWorkloadExternal
One load-only booking from another project, named on the `workload.external` option rather than as a task: its hours count in the resource workload band, the histogram and the band's own over-allocation highlight, and it is listed as an "other project" sub-row under its resource, but it is never scheduled, never drawn as a row or a bar, and never touches the critical path, the project finish or a summary - there is no task for any of those to see. The equivalent per-task spelling is a task's own `loadOnly: true` flag.
| Property | Type | Description |
|---|---|---|
| resource | string | The resource the booking is against, matching a name on `resources` or a task's `assignee`. |
| start | number | string | Date | The booking's start, in the same forms as a task's `start`. |
| end | number | string | Date | The booking's end (exclusive), in the same forms as a task's `end`. |
| units | number | The booking's load as a units multiplier (1 = full-time), spread over its working days at `hoursPerDay`; ignored when `hours` is given. (optional) |
| hours | number | The booking's total hours, spread evenly over its working days; takes precedence over `units`. (optional) |
| label | string | The sub-row's label; defaults to a generic "other project" name. (optional) |
| group | string | An arbitrary tag carried through to the sub-row, for the host's own display or sorting. (optional) |
GanttTask
A task in a Gantt plan. Give a `duration` or a `start`+`end` (a day-number, ISO date string or `Date`; one is derived from the other). `milestone: true` (or `duration: 0`) is a zero-duration point. `parent` nests a task under a summary, whose window and progress are DERIVED from its children. `baselineStart`/`baselineEnd` (host-stored) drive planned-vs-actual variance; `constraint` pins or pulls the task; `assignee` and `height` feed the split view's grid panel.
| Property | Type | Description |
|---|---|---|
| id | string | number | The task's identity, used by dependencies, edits and `rows.apply`. Stringified; a duplicate id fails the schedule with `duplicate-id`. |
| name | string | The task's label in the table and on its bar. Defaults to the id. (optional) |
| colour | string | The task's own bar colour, drawn on its bar, summary bar or milestone with a matching darker progress fill. A CSS colour - `#rrggbb`, `rgb()`/`rgba()`, or a named colour; `barColour` on `mount`/`mountSplit` wins over this field. `color` is accepted as the American spelling. (optional) |
| calendar | GanttCalendarRef | The task's own working calendar: a spec in the plan calendar's shape, or the id of an entry in the `calendars` option. Its duration and lags count that calendar's working days, its bar is shaded by it, and the workload band and `level()` book the days it works. A task without one follows the plan calendar. (optional) |
| start | number | string | Date | Where the task is placed - a day-number, an ISO date or a `Date`. It is a floor, not a pin: the forward pass never starts the task earlier, but a predecessor may push it later. Use a `constraint` to pin it. (optional) |
| end | number | string | Date | The task's finish, in the same forms as `start`. Given with `start` and no `duration`, the duration becomes `end − start`. (optional) |
| duration | number | How long the task takes, in working days (the plan's time unit). Negative fails the schedule with `bad-duration`; a leaf with no duration and no start/end pair is unscheduled: listed, but given no dates or bar until it has one), while a summary's is ignored because its window comes from its children. (optional) |
| percentComplete | number | Progress, 0-100, drawn as the filled part of the bar and used as the earned-value multiplier. A summary's is the duration-weighted mean of its descendant leaves; anything unparseable reads as null. (optional) |
| milestone | boolean | Marks a zero-duration point: the task is scheduled as an instant (start equals finish) and drawn as a diamond. `duration: 0` does the same. (optional) |
| loadOnly | boolean | Marks a load-only booking from another project: its own `start`/`end`/`duration` and assignments still count in the resource workload band, the histogram and the band's own over-allocation highlight, listed as an "other project" sub-row under its resource, but the task is kept out of the CPM engine entirely - never scheduled, never drawn as a row or a bar, and never part of a summary, the critical path or the project finish. Not editable; the equivalent non-task spelling is the `workload.external` option. (optional) |
| parent | string | number | The id of the summary task this one sits under. A summary is never scheduled in its own right - its window, progress and criticality are derived from its children - and a parent chain that loops fails with `parent-cycle`. (optional) |
| baselineStart | number | string | Date | The planned start the task is measured against, in the same forms as `start`. With a baseline the schedule reports `startVariance` (actual − planned; positive is a slip). (optional) |
| baselineEnd | number | string | Date | The planned finish. With both baseline dates the schedule reports `finishVariance` and `durationVariance` too. (optional) |
| baseline | { start?: number | string | Date; end?: number | string | Date } | The planned window as one object, read when `baselineStart`/`baselineEnd` are absent. (optional) |
| constraint | GanttConstraintType | Pins, floors or ceilings the task. `must-start-on`/`must-finish-on` place it on `constraintDate`; `start-no-earlier-than`/`finish-no-earlier-than` only delay it; `start-no-later-than`/`finish-no-later-than` are a ceiling the schedule reports in `schedule.conflicts` rather than obeying; `as-late-as-possible` pulls it into its late window; `as-soon-as-possible` is the explicit spelling of the default. Each code (`MSO`, `SNET`, …) is accepted too. (optional) |
| constraintDate | number | string | Date | The date the constraint applies to - a day-number, ISO date or `Date`. Unused by `as-late-as-possible` and `as-soon-as-possible`. (optional) |
| deadline | number | string | Date | A per-task deadline - a day-number, ISO date or `Date`. It is NOT a constraint: the scheduler never moves the task because of it. A finish after the deadline sets the record's `late` flag and drives the task's total float negative (the at-risk signal), and the view draws a deadline marker on the task's row at that date. (optional) |
| locked | boolean | Hard-locks the task: the scheduler never moves it, no matter what a predecessor does. The task is pinned at its own `start`, or - when it has none - at the date of an `MSO`/`MFO` constraint, if it carries one; a locked task with neither has nothing to lock to and keeps floating with its dependencies. A dependency that would otherwise push it later is left unhonoured and reported in `schedule.conflicts` (as a `'LOCKED'` conflict naming the violated link) instead, and the drawn dependency arrow is marked a conflict. A drag on the task itself is refused by default. See also the plan-level `lockWhen` (on `createGantt` and `computeSchedule`), which locks a task by rule - e.g. 100% complete - without setting this on every task. (optional) |
| constraintMode | 'hard' | An alternative spelling of `locked: true`, read the same way; any other value is ignored. (optional) |
| inactive | boolean | Takes the task out of the schedule's arithmetic while keeping it listed and drawn, muted: it is skipped by the CPM passes, the project finish, the critical path, summary rollups, the workload band, cost and earned value. A link through it is bridged rather than broken - `A -> B(inactive) -> C` schedules `C` from `A` directly - and it keeps its OWN dates/duration rather than losing them, so its bar still draws where it was last placed. Round-trips through MSPDI as `<Active>` (`0` is inactive). Never drawn with the unscheduled marker at once - inactive wins when a task is both. (optional) |
| readOnly | boolean | Refuses every user-driven edit on the task: pointer and keyboard drag, resize, progress drag, a dependency create/delete touching it, and a task-table cell edit on it - each refused before its own `before*` event fires, with the paired `:cancelled` event carrying reason `'readOnly'`. Unlike `locked`, this says nothing about the scheduler (a predecessor still pushes the task, unless it is also locked) and nothing about the host's own code - `applyEdit`, `setDependencies` and `deleteDependency` called directly still work. See also the plan-level `readOnlyWhen` (on `createGantt`), which refuses by rule without setting this on every task. The row and bar carry a `--readonly` class and an accessible description. (optional) |
| assignee | string | string[] | Who is booked on the task: one name or a list. Each name is a full-time booking (units 1) for resource load, over-allocation and the split view's avatars. Ignored when `assignments` is present. (optional) |
| assignees | string[] | An alternative spelling of `assignee`, read when that is absent. (optional) |
| owner | string | A third spelling of `assignee`, read when neither `assignee` nor `assignees` is present. (optional) |
| assignments | Array<{ resource?: string; name?: string; id?: string; units?: number }> | Explicit resource assignments with fractional units: `units` is a multiplier where 1 is a full-time booking. Use this when a task books a resource at less (or more) than 100%; a bare `assignee` is `units: 1`. (optional) |
| assignmentPicker | GanttAssignmentPicker | How the split view's assignment picker behaves for this task. `max: 1` limits the roster to one resource - choosing a second replaces the first - and `units: true` shows the units (percent) field per assignment. Both are read when the picker opens; the roster is still committed as one `applyEdit({ assignments })`. (optional) |
| work | number | Array<{ date: number | string | Date; hours: number }> | The task's effort, in one of two forms/1282). A **number** is the task's TOTAL hours; the workload band divides it between the assignments in proportion to their units and spreads each share evenly over the working days the task spans. (`hours` is accepted as the same field under its other common name.) An **array** is an explicit per-day contour - what a planner types into a workload cell - and states each day's hours itself: the task's total is the sum of the entries, nothing is spread, and the contour is authoritative for the span, so `applyEdit` derives the task's `start` and `duration` from its first and last day. An EMPTY array means "no hours booked", which is how clearing every bucket is expressed without reviving the even spread. A bar move re-times the contour onto the new days unchanged; a resize stretches it across the new span at the same daily levels. `date` is an ISO date, a `Date` or a plan day-number; the module writes ISO dates back. (optional) |
| schedulingMode | GanttSchedulingMode 'fixedDuration' | 'fixedEffort' | 'fixedUnits' | The task's scheduling mode; defaults to the plan's `schedulingMode`, then `'fixedDuration'`. Under `fixedEffort` the duration is derived from the effort and the assigned resources' hours; under `fixedUnits`/`fixedDuration` the stated duration is kept. (optional) |
| effortDriven | boolean | Whether adding a resource keeps the effort by splitting the units. Only meaningful on a `fixedDuration` task; defaults to the plan's `effortDriven`. (optional) |
| effort | number | The task's total effort (work) in hours, the `W` in `effort = duration × Σ units × hours per day`. A scalar `work`/`hours` is read as the same value; a `work` CONTOUR array contributes its summed hours. Required for a `fixedEffort` task to drive its duration. (optional) |
| priority | number | Leveling priority: a higher value is delayed last (default 0). (optional) |
| height | number | An explicit row height (px) for the split view; applied to both panels. (optional) |
| cost | number | The budgeted cost (BAC) for earned-value analysis and the task's own fixed cost for `gantt.cost()`. When omitted, `earnedValue()`'s BAC falls back to the rate-derived cost from the task's resource assignments when any resolved to a rate, else to the task's duration, giving schedule-only EVM. (optional) |
| actualCost | number | The actual cost incurred (ACWP) for earned-value analysis. Left out, the task's cost variance/CPI are `null`. (optional) |
| wbs | string | A WBS outline code to honour verbatim, rather than deriving one from the task's position in the tree - what `importMSPDI` sets from a source plan's own `<WBS>` so a non-standard code round-trips through `exportMSPDI` unchanged. Prefixed onto this task's own children exactly as a derived code would be. Omit it (the normal case) to have the task's code derived fresh on every compute from `1`, `1.1`, `1.2`, `2`, … - see `GanttScheduledTask.wbs`. (optional) |
GanttAssignmentPicker
The assignment picker's per-task behaviour. A task states this to shape the picker the split view's assignee cell opens for it; the roster it produces is committed as one `applyEdit({ assignments })`.
| Property | Type | Description |
|---|---|---|
| max | number | The most resources the picker holds; `1` makes it single-assignee. (optional) |
| units | boolean | Whether each assignment shows its units (percent) field (default true). (optional) |
GanttResourceCalendar
A single resource's working calendar. Anything it leaves out is inherited from the plan calendar, so a resource that states only `holidays` keeps the plan's working week.
| Property | Type | Description |
|---|---|---|
| workingDays | number[] | The resource's working weekdays, 0=Sunday … 6=Saturday; defaults to the plan's. (optional) |
| hoursPerDay | number | Record<number, number> | The resource's working hours in a day: a flat number, or a per-weekday map (`{ 1: 7.5, 5: 4 }`). Defaults to the plan's `hoursPerDay`. This sizes the resource's own capacity, not the task's effort (which stays in plan days). (optional) |
| holidays | Array<string | number | Date | { from: string | number | Date; to: string | number | Date }> | Leave and holidays: ISO date strings, `Date`s, plan day-numbers, or inclusive `{ from, to }` ranges. A day here is non-working for the resource. (optional) |
GanttResourceRateChange
A resource's dated rate change: from `from` onward the rate in force is `rate`, replacing whatever was in force before it. `from` is a day-number, ISO date or `Date`.
| Property | Type | Description |
|---|---|---|
| from | number | string | Date | The day the new rate takes effect, inclusive - a day-number, ISO date or `Date`. |
| rate | number | The rate in force from `from` onward, replacing whatever was in force before it. |
GanttDependency
A typed dependency between two tasks (by id), with optional lag/lead. `type` defaults to `'FS'`; either endpoint may be a leaf or a summary. `type` also accepts the MS Project string shorthand - `'FS+2'`, `'SS-1'`. It is normalised to the structured form on the way in, so `gantt.dependencies` always reads back `{ type, lag }` and there is no second internal representation. Giving both a shorthand lag and a conflicting `lag` field warns; the explicit field wins.
| Property | Type | Description |
|---|---|---|
| from | string | number | The predecessor task's id. A link naming a summary is expanded to its descendant leaves before scheduling. |
| to | string | number | The successor task's id. |
| type | GanttLinkType | `${GanttLinkType}${'+' | '-'}${number}` | Which ends the link ties together - finish-to-start (the default), start-to-start, finish-to-finish or start-to-finish - optionally with the MS Project lag shorthand, `'FS+2'` or `'SS-1'`. It is normalised on the way in, so `gantt.dependencies` always reads back as `{ type, lag }`. (optional) |
| lag | number | A signed offset on the link in working days; a negative value is a lead. Given alongside a shorthand lag in `type`, this field wins and the mismatch is warned about. (optional) |
GanttScheduledTask
The computed CPM values for one task (a leaf is scheduled, a summary derived).
| Property | Type | Description |
|---|---|---|
| id | string | The task's id, as a string. |
| name | string | The task's name, defaulting to its id. |
| colour | string | null | The task's own bar colour, or null when it states none. |
| duration | number | null | The task's length in working days. A summary's is its derived window, `ef − es`. Null on an unscheduled task. |
| es | number | null | Early start: the earliest day the task can begin once every predecessor and its own placement floor are honoured. Null on an unscheduled task. |
| ef | number | null | Early finish, `es + duration` (mapped back to calendar days when a working-time calendar is in use). Null on an unscheduled task. |
| calendar | string | The id of the calendar the task was scheduled under: its own calendar's id, `'plan'` when it follows the plan calendar. Present only on a plan that uses the `calendars` option or a task `calendar`. (optional) |
| ls | number | null | Late start: the latest the task can begin without pushing the project finish (or the deadline) out. Null on an unscheduled task. |
| lf | number | null | Late finish, `ls + duration`. A task pinned by a constraint has `lf` equal to its `ef`, so it has no float. Null on an unscheduled task. |
| totalFloat | number | null | Slack in working days, `ls − es`. Zero means critical; a deadline earlier than the natural finish drives it negative, which is the at-risk signal. Null on an unscheduled task. |
| unscheduled | true | True when the task is unscheduled: a leaf with no duration and no start/end pair to derive one from, or a summary whose leaves are all such. It is kept and listed, with null dates, duration and float, but draws no bar and is left out of the critical path, the project finish, summaries, workload, cost and earned value; its links stay dormant until a duration or date is given. Absent on a scheduled task. (optional) |
| inactive | true | True when the task is inactive: kept and listed, drawn muted, with its OWN `es`/`ef`/`duration` (not computed by the CPM passes), null `ls`/`lf`/ `totalFloat` (it has no float - it is not part of the network) and never critical. Out of the project finish, the critical path, summary rollups, the workload band, cost and earned value; a link through it is bridged so a predecessor still reaches its successor. Absent on an active task. (optional) |
| critical | boolean | True when the total float is zero or negative. A summary is critical when any child is; an as-late-as-possible task is always marked critical. |
| percentComplete | number | null | The task's progress, or null when it states none. A summary's is the duration-weighted mean of its descendant leaves, and null when every one of them is a milestone. |
| parent | string | null | The id of this task's summary, or null at the top level. |
| wbs | string | The task's WBS outline code - `1`, `1.1`, `1.2`, `2`, … - derived from its position among its plan siblings and recomputed fresh every time the schedule recomputes, so a reorder, an indent / outdent, an add or a delete always reads the current number rather than a stale one. A task whose own `wbs` field supplied a code (an MSPDI import that carried one) reads that verbatim instead, and its own children are still numbered under it. Look a task up by its code with `gantt.taskByWbs(code)`. |
| schedulingMode | GanttSchedulingMode 'fixedDuration' | 'fixedEffort' | 'fixedUnits' | The scheduling mode the task was computed under, present only when the task takes part in effort-driven scheduling. A plain fixed-duration plan carries no such field. (optional) |
| effortDriven | boolean | Whether the task is effort-driven; present only on effort-scheduled tasks. (optional) |
| effort | number | null | The task's effort in hours, `duration × Σ units × hours per day`. Present only on effort-scheduled tasks. (optional) |
| units | number | The task's total assigned units; present only on effort-scheduled tasks. (optional) |
| isSummary | boolean | True when the task has children, and so was derived from them rather than scheduled. |
| isMilestone | boolean | True when the task's duration is zero - a point in the plan. |
| locked | boolean | True when the task is hard-locked - by its own `locked`, `constraintMode: 'hard'`, or the plan's `lockWhen` - so the engine fixed it at its own placement regardless of any predecessor. Always false on a summary. |
| children | string[] | A summary's direct children, by id, in input order. Empty for a leaf. |
| baselineStart | number | null | The planned (baseline) window, present only when the task carries a baseline. (optional) |
| baselineEnd | number | null | The planned finish day-number, or null when only a baseline start was given. (optional) |
| startVariance | number | null | Variance vs the baseline (actual − planned, day-numbers); a positive value is a slip. (optional) |
| finishVariance | number | null | `ef − baselineEnd` in days; positive means finishing later than planned. Null without a baseline finish. (optional) |
| durationVariance | number | null | How much longer the task runs than its baseline window, in days. Null unless both baseline dates were given. (optional) |
| deadline | number | The task's deadline as a calendar day-number, present only when the task states one. It anchors only the late dates - never the early ones - so a deadline never moves the bar. (optional) |
| late | boolean | True when the task's early finish runs past its deadline. Its total float is then negative by construction. Present only on a task that has a deadline, so a plain plan's records are unchanged. (optional) |
GanttConflict
An unhonourable scheduling constraint, reported rather than obeyed.
| Property | Type | Description |
|---|---|---|
| id | string | The task whose constraint could not be honoured (the locked task itself, for `'LOCKED'`). |
| type | string | The constraint that was refused, as its normalised code (`MSO`, `MFO`, `SNLT` or `FNLT`), or `'LOCKED'` when a hard-locked task's predecessor would have pushed it later. |
| date | number | The date the constraint asked for, as a day-number, snapped to a working day. For `'LOCKED'`, the task's own fixed start. |
| scheduled | number | The start (for `MSO`/`SNLT`) or finish (for `MFO`/`FNLT`) the engine actually scheduled instead. It never places a task before its predecessors, and never silently moves it to satisfy a start/finish-no-later-than ceiling. For `'LOCKED'`, the start the violating predecessor would have forced, had the task not been locked. |
| at | number | The same date as `date`; kept so hosts written before 1.88 keep working. |
| earliestFeasible | number | The earliest START the predecessors allow (for every constraint type); kept from before 1.88. |
| from | string | The predecessor whose link forced the push; present only on a `'LOCKED'` conflict. (optional) |
| to | string | The locked task itself (same as `id`); present only on a `'LOCKED'` conflict. (optional) |
GanttSchedule
A CPM schedule result: per-task dates/float and the critical path, or an error.
| Property | Type | Description |
|---|---|---|
| ok | boolean | Whether the schedule computed. False leaves every other field absent except `error`, and the controller keeps its previous schedule. |
| error | GanttScheduleError | Why the schedule was refused - the same object the `error` event carries. (optional) |
| tasks | Map<string, GanttScheduledTask> | Every task's computed values, keyed by id - leaves scheduled, summaries derived. (optional) |
| order | string[] | Every task id in input order, which is the order a table or WBS tree walks. (optional) |
| critical | string[] | The ids of the leaf tasks with no float, in input order. (optional) |
| criticalPaths | string[][] | Each zero-float chain through the network as its own list of ids, so a plan with several critical routes shows all of them. (optional) |
| projectStart | number | The day the plan is anchored to - the `projectStart` option, or the calendar's first working day when one is set. When no explicit `projectStart` is given, it is derived from the earliest task start so the timeline opens on the tasks. (optional) |
| projectFinish | number | The latest early finish across every task, as a calendar day-number. (optional) |
| projectDuration | number | `projectFinish − projectStart` in days - calendar days when a working-time calendar stretched the plan, not the sum of the durations. (optional) |
| conflicts | GanttConflict[] | Constraints a predecessor made infeasible (empty when all are satisfied). (optional) |
| calendar | boolean | Whether a working-time calendar was applied. (optional) |
| errors | GanttScheduleError[] | Every reason the schedule was refused; `error` is the first. An unknown calendar id on a task or resource is listed here by name, one entry each. (optional) |
| calendars | Map<string, GanttResolvedCalendar> | The calendars the plan's tasks were scheduled under, keyed by calendar id. Present only when a task follows a calendar other than the plan's. (optional) |
| unscheduled | string[] | The ids of the unscheduled tasks, in input order: leaves with no duration or dates, and summaries of only such leaves. Empty when every task is scheduled. (optional) |
| dependencies | GanttDependency[] | The dependencies with their lags derived from the stored task dates, present only when the schedule was computed with `honourDates`. (optional) |
| overAllocations | GanttOverAllocation[] | The resource over-allocations for this schedule. (optional) |
| resourceLoad | GanttResourceLoad | The full resource-load report for this schedule. (optional) |
GanttResourceSegment
One contiguous load segment for a resource: how many units are booked over a span.
| Property | Type | Description |
|---|---|---|
| start | number | The day the segment begins (inclusive), as a calendar day-number. |
| end | number | The day the segment ends (exclusive). A task that finishes as another starts does not double-count the boundary. |
| load | number | The units booked across the whole segment - the sum of the covering tasks' assignment units, where 1 is one full-time booking. |
| taskIds | string[] | The tasks active during the segment, which is what makes a heavy stretch explainable. |
GanttOverAllocation
A resource booked beyond its capacity across concurrent tasks.
| Property | Type | Description |
|---|---|---|
| resource | string | The over-booked resource's name. |
| capacity | number | The resource's capacity in units - from the `resources` option, or `defaultCapacity` (1) when it names none. |
| start | number | The day the over-allocation begins, as a calendar day-number. |
| end | number | The day it ends (exclusive). |
| load | number | The units booked over that stretch - strictly greater than `capacity`, which is what makes it an over-allocation. |
| taskIds | string[] | The tasks competing for the resource over that stretch. |
GanttResourceLoad
The per-resource load and the over-allocations across a schedule.
| Property | Type | Description |
|---|---|---|
| ok | boolean | Whether the load could be computed. False - with empty lists - when there is no successful schedule to read. |
| resources | Array<{ resource: string; capacity: number; peak: number; segments: GanttResourceSegment[] }> | One entry per resource that anything is booked on, sorted by name, each with its capacity, peak load and load segments. A resource named only in the capacities, with no booking, does not appear. |
| overAllocations | GanttOverAllocation[] | Every stretch where a resource is booked beyond its capacity, earliest first. Empty when the plan fits. |
| byResource | Map<string, { capacity: number; peak: number; segments: GanttResourceSegment[] }> | The same entries as `resources`, keyed by resource name for a direct lookup. |
GanttLevelResult
The result of resource leveling: the shifted tasks and what moved.
| Property | Type | Description |
|---|---|---|
| ok | boolean | Whether leveling ran. False only when the plan would not schedule, in which case `error` says why. |
| resolved | boolean | Whether every over-allocation was cleared. False when only pinned tasks were left to move, or the iteration cap was hit - the partial result is still returned. (optional) |
| tasks | GanttTask[] | The tasks with their new starts. These are copies; the controller adopts them unless the call was a dry run. (optional) |
| schedule | GanttSchedule | The schedule computed from the levelled tasks. (optional) |
| moves | Array<{ id: string; from: number; to: number; delay: number }> | What actually moved: the task, its start before leveling, its start after, and the delay in days. Unmoved tasks are not listed. (optional) |
| remaining | GanttOverAllocation[] | The over-allocations leveling could not clear. Absent when it resolved everything. (optional) |
| error | { code: string; message: string } | Why the plan would not schedule - the same codes `GanttSchedule.error` uses. (optional) |
GanttViolation
A placement violation flagged by `findViolations`.
| Property | Type | Description |
|---|---|---|
| id | string | The task placed earlier than its predecessors allow. |
| placedStart | number | Where the plan puts the task - the `start` on the raw task, as a day-number. |
| earliestStart | number | The earliest start CPM allows, given the dependencies and the calendar. |
| by | number | How many days early the placement is, `earliestStart − placedStart`. |
GanttEarnedValueTotals
The earned-value figures themselves, without the task they belong to. The project total is exactly this and no more, so it has its own type rather than claiming to be a row with five fields it has never carried.
| Property | Type | Description |
|---|---|---|
| hasBaseline | boolean | Whether a baseline (not the fallback scheduled window) drove PV. |
| hasActualCost | boolean | Whether any actual cost fed AC (else AC/CV/CPI are null). |
| bac | number | Budget at completion (the task's cost, or its duration when no cost). |
| pv | number | Planned Value (BCWS): budgeted cost of the work scheduled by the status date. |
| ev | number | Earned Value (BCWP): budgeted cost of the work performed (BAC × %complete). |
| ac | number | null | Actual Cost (ACWP): what the work performed actually cost, or null. |
| sv | number | Schedule Variance (EV − PV); positive is ahead of schedule. |
| cv | number | null | Cost Variance (EV − AC); positive is under budget; null without AC. |
| spi | number | null | Schedule Performance Index (EV / PV); null when PV is zero. |
| cpi | number | null | Cost Performance Index (EV / AC); null without AC or when AC is zero. |
GanttEarnedValueRow
One task's earned-value figures.
| Property | Type | Description |
|---|---|---|
| id | string | The task the row is for. |
| name | string | The task's name. |
| isSummary | boolean | True for a summary row, whose figures are the sums of its descendant leaves. |
| isMilestone | boolean | True for a zero-duration task. |
| percentComplete | number | null | The task's progress, reported exactly as the task states it, or null when it states none - which earns nothing. The earned-value multiplier clamps it to 0-100 first. |
GanttEarnedValue
The earned-value result at a status date.
| Property | Type | Description |
|---|---|---|
| ok | boolean | Whether the metrics could be computed. False when there is no successful schedule to measure against. |
| error | { code: string; message: string } | Why the metrics were refused - `NO_SCHEDULE` when the plan has not scheduled. (optional) |
| statusDate | number | The status date the metrics were evaluated at (day-number). (optional) |
| byTask | Map<string, GanttEarnedValueRow> | Every task keyed by id (leaf, summary and derived). (optional) |
| rows | GanttEarnedValueRow[] | The same rows in schedule order. (optional) |
| project | GanttEarnedValueTotals | The project total, rolled up as money sums of the leaves. The figures only: a total belongs to no task, so it carries no id, name or progress. (optional) |
GanttCostRow
One task's resource-rate cost figures.
| Property | Type | Description |
|---|---|---|
| id | string | The task the row is for. |
| name | string | The task's name. |
| isSummary | boolean | True for a summary row, whose figures are the sums of its descendant leaves. |
| isMilestone | boolean | True for a zero-duration task. |
| cost | number | The task's total cost: `rateCost + fixedCost`. |
| rateCost | number | The rate-derived component: `Σ` a `'work'` resource's assignment effort times the rate in force each day, a `'material'` resource's units times its rate, and a `'cost'` resource's flat amount per assignment. |
| fixedCost | number | The task's own explicit `cost` field (0 when it states none). |
| hasRate | boolean | Whether any assignment anywhere under this task resolved to a rate at all. `false` on a plan with no resource rate data, which is what keeps `earnedValue()`'s BAC fallback from ever substituting for a genuinely unrated task. |
GanttCostResult
The resource-rate cost result for a scheduled plan.
| Property | Type | Description |
|---|---|---|
| ok | boolean | Whether the cost could be computed. False when there is no successful schedule. |
| error | { code: string; message: string } | Why the cost was refused - `NO_SCHEDULE` when the plan has not scheduled. (optional) |
| byTask | Map<string, GanttCostRow> | Every task keyed by id (leaf, summary and derived). (optional) |
| rows | GanttCostRow[] | The same rows in schedule order. (optional) |
| project | { cost: number; rateCost: number; fixedCost: number; hasRate: boolean } | The project total, rolled up as a money sum of the leaves. (optional) |
GanttSCurvePoint
One cumulative S-curve point: the value of each curve as of a bucket date.
| Property | Type | Description |
|---|---|---|
| date | number | The bucket END as a day-number - the cumulative value "as of" this date. The final point is the project finish, where `pv` equals the total budget. |
| pv | number | Planned Value: the budget planned to be done by this date (linear spread). |
| ev | number | Earned Value: the budget actually earned by this date (from %complete). |
| ac | number | Actual Cost: what the work performed cost, all recognised at the status date. |
GanttResourceViewInit
The options for {@link createResourceView}. The display options (`unit`, `thresholds`, `columns`, `labelWidth`, `showUnassigned`, `decimals`, `totals`, `hoursPerDay`) mean exactly what they do on `mountSplit`'s `workload` option - the standalone band reuses it.
| Property | Type | Description |
|---|---|---|
| resources | GanttResourceSpec | The resources (`{ id, name, capacity, calendar, avatar, rate }`), the same shape `createGantt`'s `resources` takes. (optional) |
| bookings | GanttWorkloadExternal[] | The bookings to draw - a load with no task and no CPM behind it. See {@link GanttWorkloadExternal}. (optional) |
| unit | 'hours' | 'percent' | 'cost' | The band's display unit: hours (default), percent of available, or cost. (optional) |
| thresholds | Array<{ at: number; className?: string; colour?: string; color?: string }> | Utilisation thresholds colouring each bucket by how full it is; `{ at, className | colour }`, ascending. (optional) |
| columns | Array<string | GanttGridColumn | GanttBandColumn> | The band's own resource-table columns; needs `createGrid`. (optional) |
| labelWidth | number | The resource-name column width in pixels; defaults to the band's own label width. (optional) |
| showUnassigned | boolean | Draw the Unassigned row (bookings naming no resource); default true. (optional) |
| groupBy | string | ((resource: Record<string, unknown>) => unknown) | Group the resources by one of their fields (e.g. `'department'`, `'bu'`) or by a function of the resource spec: each group is a collapsible header row carrying the group's summed load and available hours (so thresholds and the percent unit read the group's utilisation), with its resources beneath it. Resources naming no group, and Unassigned, collect last. (optional) |
| decimals | number | Maximum decimal places in a cell, trailing zeros dropped; default 1. (optional) |
| totals | boolean | Draw the totals row and totals column; default true. (optional) |
| hoursPerDay | number | Hours a full-time (`units: 1`) resource works in a working day; default 8. (optional) |
| zoom | 'day' | 'week' | 'month' | 'quarter' | number | The bucket zoom: `'day'`/`'week'`/`'month'`/`'quarter'`, or a pixels-per-day number. (optional) |
| today | number | string | Date | A today marker on the scale. (optional) |
| weekStartDay | number | The week's first weekday (0=Sunday…6=Saturday), driving week buckets and the header. (optional) |
| calendar | GanttCalendar | null | The working-time calendar the buckets' available hours are measured against. (optional) |
| gridConfig | Record<string, unknown> | Extra grid config for the band's resource table, exactly `mountSplit`'s `gridConfig`; the time-bucket columns are grid columns too but inert to it, so it can never give them header controls. (optional) |
| gridWidth | number | The band's left (resource) panel width in pixels. (optional) |
| rowHeight | number | A band row's height in pixels. (optional) |
| height | number | The view's height in pixels. (optional) |
| histogram | boolean | object | A resource histogram band drawn below the workload band. (optional) |
| messages | Record<string, unknown> | A message-catalogue override for the band's own strings. (optional) |
| fields | Record<string, string> | The resource/booking field-name mapping. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| createGrid | (container: unknown, options: unknown) => Grid | container: unknownoptions: unknown | => Grid | The host grid factory that backs the band's grid-mode table; without it the band draws as plain DOM rows. (optional) |
GanttResourceView
A standalone resource-load band ({@link createResourceView}), the handle it returns.
Properties
| Property | Type | Description |
|---|---|---|
| gantt | Gantt | The underlying Gantt controller, for events and advanced access. |
| view | GanttSplitView | The underlying split view in its resource-only mode. |
| grid | Grid | null | The band's grid-mode table, or `null` without a grid factory. |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| setBookings | (bookings: GanttWorkloadExternal[]): void | bookings: GanttWorkloadExternal[] | void | Apply a new set of bookings and redraw - the live add / update / remove. |
| setZoom | (level: 'day' | 'week' | 'month' | 'quarter' | number): void | level: 'day' | 'week' | 'month' | 'quarter' | number | void | Switch the bucket zoom (`'day'`/`'week'`/`'month'`/`'quarter'` or pixels-per-day). |
| setUnit | (unit: 'hours' | 'percent' | 'cost'): void | unit: 'hours' | 'percent' | 'cost' | void | Switch the display unit (hours, percent of available, or cost). |
| getUnit | (): 'hours' | 'percent' | 'cost' | - | 'hours' | 'percent' | 'cost' | The current display unit. |
| toggleResource | (resource: string, opts?: { event?: unknown }): void | resource: stringopts?: { event?: unknown } | void | Expand or collapse a resource into its bookings (`''` for Unassigned). |
| toggleGroup | (group: string | null): void | group: string | null | void | Collapse or expand a `groupBy` group (`null` or `''` for the "no group" group). |
| getColumnWidths | (): Record<string, number> | - | Record<string, number> | Every band column's current width, by key (`band:resource` etc). |
| setColumnWidths | (widths: Record<string, number>): void | widths: Record<string, number> | void | Restore band column widths from {@link getColumnWidths}. |
| toSVG | (opts?: object): string | opts?: object | string | Export the band as an SVG string. |
| on | (event: string, fn: (payload: unknown) => void): void | event: stringfn: (payload: unknown) => void | void | Subscribe to a controller event - `workload:click` is the bucket-click event. |
| off | (event: string, fn: (payload: unknown) => void): void | event: stringfn: (payload: unknown) => void | void | Unsubscribe a handler added with {@link on}. |
| destroy | (): void | - | void | Tear the view down and release the controller. |
Events
This surface raises events, but the names are not declared: on() accepts any string. They cannot be listed here until the declaration names them.
GanttScheduleError
Why a schedule was refused. The payload of the `error` event - the very object the failed {@link GanttSchedule} carries, handed straight to the handler.
| Property | Type | Description |
|---|---|---|
| code | string | What was wrong: `cycle`, `duplicate-id`, `bad-duration`, `unknown-task`, `unknown-parent`, `parent-cycle`, `self-dependency`, `bad-link-type`, `dep-across-hierarchy` or `unknown-calendar`. |
| message | string | The failure in one English sentence, naming the task or link it is about. |
| cycle | string[] | The ids that form the cycle, on a `cycle`. (optional) |
| id | string | The task the failure is about, where one task is to blame. (optional) |
| from | string | A rejected dependency's predecessor, on `dep-across-hierarchy`. (optional) |
| to | string | A rejected dependency's successor, on `dep-across-hierarchy`. (optional) |
| calendar | string | The calendar id nothing defines, on `unknown-calendar`. (optional) |
| owner | 'task' | 'resource' | Whether the `task` or the `resource` named that calendar, on `unknown-calendar`. (optional) |
GanttBeforeEvent
What every cancellable Gantt event carries on top of its own context. The controller's bus mirrors the grid core's `emitBefore` contract exactly, so a host writes the same handler shape against a Gantt as against a grid: a handler refuses the action by calling `preventDefault(reason?)`, by returning `false`, or by throwing, and may be `async` - every thenable return is awaited before the decision, so a confirm dialog or a server check can hold the write. Veto wins. On a veto the matching `<action>:cancelled` fires with the reason and the model is untouched; an action re-validated after an await and no longer applicable is cancelled as `'stale'`. Only the user-initiated paths are gated. The live/router `rows.apply` path is remote truth and raises none of these.
| Property | Type | Description |
|---|---|---|
| type | string | Which event this is - `beforeTaskMove`, `beforeTaskDelete` and the rest. |
| defaultPrevented | boolean | True once a handler has refused the action. |
| reason | string | null | The reason given to `preventDefault`, or null while nothing has refused it. |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| preventDefault | (reason?: string): void | reason?: string | void | Refuse the action; the optional reason is carried on the `<action>:cancelled` event. |
GanttTaskEditEvent
An edit about to be applied to one task: the payload of `beforeTaskEdit`, `beforeTaskMove`, `beforeTaskResize`, `beforeProgressChange` and `beforeMilestoneMove`. One payload for the five, because they are one choke point - `applyEdit` classifies the patch and names the event, so which of the five fires says what kind of edit it is and the context below says what the edit is.
| Property | Type | Description |
|---|---|---|
| id | string | The task being edited, by id. |
| task | GanttTask | The task as it stands before the edit, as a shallow copy. |
| patch | { id: string | number; start?: number; end?: number; duration?: number; percentComplete?: number; work?: unknown } | The edit itself: only the fields it changes. |
| origin | string | Always `user`: only the user-initiated path is gated. |
| from | number | string | Date | Where the task starts now, from the plan, or its computed early start when it holds no start. (optional) |
| to | number | string | Date | Where the patch would move it to; absent unless the patch sets `start`. (optional) |
| duration | number | The duration the patch asks for; absent unless the patch sets one. (optional) |
| value | number | The progress the patch asks for; present only on a `beforeProgressChange`. (optional) |
| oldValue | number | The progress before it; present only on a `beforeProgressChange`. (optional) |
GanttTaskEditCancelledEvent
An edit that was refused: the payload of `taskEdit:cancelled`, `taskMove:cancelled`, `taskResize:cancelled`, `progressChange:cancelled` and `milestoneMove:cancelled`. The same context the before-event carried, plus the reason; a notification, so it carries no `preventDefault`.
| Property | Type | Description |
|---|---|---|
| id | string | The task that was not edited. |
| task | GanttTask | The task, unchanged. |
| patch | { id: string | number; start?: number; end?: number; duration?: number; percentComplete?: number; work?: unknown } | The edit that was not applied. |
| origin | string | Always `user`. |
| from | number | string | Date | Where the task starts, still. (optional) |
| to | number | string | Date | Where it would have gone. (optional) |
| duration | number | The duration that was asked for. (optional) |
| value | number | The progress that was asked for. (optional) |
| oldValue | number | The progress before it. (optional) |
| reason | string | The reason given to `preventDefault`, `'prevented'` when none was, or `'stale'` when the task had gone by the time a handler settled. |
| phase | 'start' | 'drag' | On a resize gesture's own cancellation: `'start'` when `beforeTaskResizeStart` refused it, `'drag'` when Escape abandoned it; absent for a refused commit. (optional) |
GanttDependencyCreateEvent
Links about to be created: the payload of `beforeDependencyCreate`. A pure removal or reorder adds no link and is not gated at all.
| Property | Type | Description |
|---|---|---|
| added | GanttDependency[] | Only the links this call adds, normalised. |
| dependencies | GanttDependency[] | The whole list the call would leave behind. |
| origin | string | Always `user`. |
GanttDependencyCreateCancelledEvent
Links that were not created: the payload of `dependencyCreate:cancelled`. A notification, so it carries no `preventDefault`.
| Property | Type | Description |
|---|---|---|
| added | GanttDependency[] | The links that were not added. |
| dependencies | GanttDependency[] | The list that was not adopted; the controller kept the one it had. |
| origin | string | Always `user`. |
| reason | string | The reason given to `preventDefault`, or `'prevented'` when none was. |
GanttDependencyDeleteEvent
One link about to be removed: the payload of `beforeDependencyDelete` - the selected-link Delete key and the link editor's own delete action both gate on this.
| Property | Type | Description |
|---|---|---|
| removed | GanttDependency[] | The link(s) that match the removal - ordinarily one. |
| dependencies | GanttDependency[] | The whole list the call would leave behind. |
| origin | string | Always `user`. |
GanttDependencyDeleteCancelledEvent
A link that was not removed: the payload of `dependencyDelete:cancelled`. A notification, so it carries no `preventDefault`.
| Property | Type | Description |
|---|---|---|
| removed | GanttDependency[] | The link(s) that stayed. |
| dependencies | GanttDependency[] | The list that was not adopted; the controller kept the one it had. |
| origin | string | Always `user`. |
| reason | string | The reason given to `preventDefault`, `'prevented'` when none was, or `'stale'` when the link had already gone. |
GanttDependenciesChangedEvent
Links that changed: the payload of `dependencies`, fired once a `setDependencies` call or a `deleteDependency` call actually changes the list - after `beforeDependencyCreate`/`beforeDependencyDelete` have already let it through, so a host that only wants to know what changed does not have to diff the whole list on every `schedule` event. A lag-only edit of an existing link (same `from`/`to`/`type`) is neither an add nor a remove and does not fire this.
| Property | Type | Description |
|---|---|---|
| added | GanttDependency[] | The links that are new in this call. |
| removed | GanttDependency[] | The links that dropped out in this call. |
| dependencies | GanttDependency[] | The whole list, as it now stands. |
| origin | string | Always `user`. |
GanttTaskDeleteEvent
A task about to be deleted, with its incident links: the payload of `beforeTaskDelete`.
| Property | Type | Description |
|---|---|---|
| id | string | The task being deleted, by id. |
| task | GanttTask | The task itself, as a shallow copy - the only chance a handler has to read it. |
| origin | string | Always `user`: the live/router `rows.apply` remove is never gated. |
GanttTaskDeleteCancelledEvent
A task that was not deleted: the payload of `taskDelete:cancelled`. A notification, so it carries no `preventDefault`.
| Property | Type | Description |
|---|---|---|
| id | string | The task that stayed. |
| task | GanttTask | The task itself. |
| origin | string | Always `user`. |
| reason | string | The reason given to `preventDefault`, `'prevented'` when none was, or `'stale'`. |
GanttTaskAddEvent
A task about to be added: the payload of `beforeTaskAdd`. Raised by `addTask`, Insert / Shift+Insert and the default menu's add items.
| Property | Type | Description |
|---|---|---|
| id | string | The id the new task will take. |
| task | GanttTask | The task as it will be inserted, defaults filled in; a copy a handler may read but not change. |
| target | { above?: string; below?: string; child?: string; successorOf?: string; predecessorOf?: string } | Where it goes: the one key given, with the anchor task's id; empty when appending. |
| dependency | GanttDependency | null | The finish-to-start link created with it (a successor or predecessor), else `null`. |
| origin | string | Always `user`. |
GanttTaskAddCancelledEvent
A task that was not added: the payload of `taskAdd:cancelled`. A notification, so it carries no `preventDefault`.
| Property | Type | Description |
|---|---|---|
| id | string | The id the task would have taken. |
| task | GanttTask | The task that was not inserted. |
| target | { above?: string; below?: string; child?: string; successorOf?: string; predecessorOf?: string } | Where it would have gone. |
| dependency | GanttDependency | null | The link that was not created, else `null`. |
| origin | string | Always `user`. |
| reason | string | The reason given to `preventDefault`, `'prevented'` when none was, `'readOnly'` for a read-only anchor, or `'stale'`. |
GanttMilestoneConvertEvent
A task about to become a milestone or an ordinary task again: the payload of `beforeMilestoneConvert`.
| Property | Type | Description |
|---|---|---|
| id | string | The task being converted. |
| task | GanttTask | The task as it stands before, as a shallow copy. |
| to | 'milestone' | 'task' | The kind it becomes. |
| origin | string | Always `user`. |
GanttMilestoneConvertCancelledEvent
A conversion that did not happen: the payload of `milestoneConvert:cancelled`. A notification, so it carries no `preventDefault`.
| Property | Type | Description |
|---|---|---|
| id | string | The task that stayed as it was. |
| task | GanttTask | The task itself. |
| to | 'milestone' | 'task' | The kind it would have become. |
| origin | string | Always `user`. |
| reason | string | The reason given to `preventDefault`, `'prevented'` when none was, `'summary'` for a summary, `'readOnly'` for a read-only task, or `'stale'`. |
GanttTaskPasteEvent
Tasks about to be pasted: the payload of `beforeTaskPaste`. Raised by `pasteTasks` and the Ctrl/Cmd+V key.
| Property | Type | Description |
|---|---|---|
| tasks | GanttTask[] | The new tasks about to be inserted, with their new ids, in plan order; copies a handler may read but not change. |
| dependencies | GanttDependency[] | The links about to be re-created between the new tasks. |
| target | { below: string | null } | { childOf: string } | Where they land: below a task, or as the children of one (`below` is `null` when appending at the end). |
| source | 'tasks' | 'text' | Whether the tasks came from the Gantt clipboard (`tasks`) or from tab-separated text (`text`). |
| origin | string | Always `user`. |
GanttTaskPasteCancelledEvent
Tasks that were not pasted: the payload of `taskPaste:cancelled`. A notification, so it carries no `preventDefault`.
| Property | Type | Description |
|---|---|---|
| tasks | GanttTask[] | The new tasks that were not inserted. |
| dependencies | GanttDependency[] | The links that were not re-created. |
| target | { below: string | null } | { childOf: string } | Where they would have landed. |
| source | 'tasks' | 'text' | Whether the tasks came from the Gantt clipboard or from text. |
| origin | string | Always `user`. |
| reason | string | The reason given to `preventDefault`, `'prevented'` when none was, or `'stale'`. |
GanttRowMoveEvent
A row about to move or re-parent: the payload of `beforeRowMove`. A name-column drag handle, a drop onto another row, Alt+Up/Down (reorder among siblings) and Tab/Shift+Tab (indent/outdent) all gate on this.
| Property | Type | Description |
|---|---|---|
| id | string | The row being moved, by id. |
| oldParent | string | null | The task's parent before the move, or `null` at the root. |
| newParent | string | null | The parent it would land under once the move applies, or `null` for the root. |
| oldIndex | number | Its 0-based index among its old parent's children, before the move. |
| newIndex | number | Its 0-based index among its new parent's children, once the move applies. |
| origin | string | Always `user`. |
| event | unknown | The originating DOM event (a drop or a keypress), or `null`. |
GanttRowMovedEvent
A row that moved or was re-parented: the payload of `rowMove`. A notification - it carries no `preventDefault`; the veto was `beforeRowMove`'s job. Fired once per gesture: a drag, a single Alt+Up/Down step or a single Tab/Shift+Tab is one `rowMove` and one undo step, by the live re-parent path.
| Property | Type | Description |
|---|---|---|
| id | string | The row that moved, by id. |
| oldParent | string | null | Its parent before the move, or `null` at the root. |
| newParent | string | null | Its parent now, or `null` at the root. |
| oldIndex | number | Its 0-based index among its old parent's children, before the move. |
| newIndex | number | Its 0-based index among its new parent's children, now. |
| origin | string | Always `user`. |
| event | unknown | The originating DOM event (a drop or a keypress), or `null`. |
GanttRowMoveCancelledEvent
A row that was not moved: the payload of `rowMove:cancelled`. A notification, so it carries no `preventDefault`.
| Property | Type | Description |
|---|---|---|
| id | string | The row that stayed put, by id. |
| oldParent | string | null | Its parent, unchanged. |
| newParent | string | null | The parent it would have moved under. |
| oldIndex | number | Its index, unchanged. |
| newIndex | number | The index it would have landed at. |
| origin | string | Always `user`. |
| event | unknown | The originating DOM event, or `null`. |
| reason | string | The reason given to `preventDefault`, `'prevented'` when none was, or `'stale'`. |
GanttDataBandPoint
One point of a data band series: a dated value at any granularity.
| Property | Type | Description |
|---|---|---|
| date | string | Date | number | The point's date: an ISO date string, a `Date` or a day number. |
| value | number | The value at that date. |
| end | string | Date | number | An explicit exclusive end for `interpolate: 'spread'`; omitted, a point spreads to the next point's date. (optional) |
GanttDataBandContext
The context a data band's `value` function, `format` function and `cellStyle` receive.
| Property | Type | Description |
|---|---|---|
| band | string | The band's id. |
| id | string | The task id (`value` only). (optional) |
| task | Record<string, unknown> | null | The host task (`value` and `cellStyle` on a task row). (optional) |
| rec | GanttTask | The scheduled record (`value` only). (optional) |
| start | number | The bucket's first day as a day number (`value` only). (optional) |
| end | number | The bucket's exclusive end as a day number (`value` only). (optional) |
| zoom | string | null | The zoom level the buckets were cut at (`value` only). (optional) |
| bucketStart | string | The bucket's start as an ISO date (`cellStyle` only). (optional) |
| bucketEnd | string | The bucket's exclusive end as an ISO date (`cellStyle` only). (optional) |
| isTotal | boolean | Whether the cell is on the totals row (`cellStyle` only). (optional) |
| isSummary | boolean | Whether the cell is on a summary row (`cellStyle` only). (optional) |
GanttDataBandThreshold
One threshold of a data band: the highest `at` a cell's value reaches styles it.
| Property | Type | Description |
|---|---|---|
| at | number | The value from which this threshold applies. |
| tone | 'ok' | 'info' | 'warn' | 'danger' | A theme-token tone: `'ok'`, `'info'`, `'warn'` or `'danger'` (dark-mode safe). (optional) |
| className | string | A class added to the cell. (optional) |
| colour | string | A background colour of the host's own. (optional) |
GanttDataBand
One linked data band under the split view: a measure per row per timeline bucket - forecast cost per task per week, say - laid out like the workload band and scroll- and zoom-locked to the timeline. The bucket cells carry no header controls under any configuration; the left-hand columns are a real grid when `createGrid` is injected.
| Property | Type | Description |
|---|---|---|
| id | string | The band's id: names it in events, state and `getDataBandTable`. |
| title | string | The band's title, shown in its header strip. (optional) |
| rows | 'tasks' | 'summaries' | 'resources' | ((tasks: Array<Record<string, unknown>>, ctx: { band: string }) => Array<{ key: string; label?: string; tasks: string[] }>) | Which rows: the task tree (`'tasks'`, default, collapsing in step with the gantt), `'summaries'` only, `'resources'`, or a host grouping `fn(tasks) => [{ key, label, tasks: ids }]`. (optional) |
| series | string | The task field carrying a `[{ date, value }]` series, re-bucketed to the zoom; used instead of `value`. (optional) |
| interpolate | 'none' | 'spread' | How a series fills the days between its points: `'none'` (default) or `'spread'` (spread over its days, re-summed per bucket). (optional) |
| aggregate | 'sum' | 'avg' | 'last' | 'min' | 'max' | ((values: number[], ctx: object) => number | null) | How children roll up to summaries, the totals row and column: `'sum'` (default), `'avg'`, `'last'`, `'min'`, `'max'` or a function. (optional) |
| format | 'number' | 'currency' | 'percent' | { type: 'number' | 'currency' | 'percent'; currency?: string; decimals?: number } | ((value: number, ctx: object) => string) | How a value displays: `'number'` (default), `'currency'`, `'percent'`, `{ type, currency, decimals }` or a function. (optional) |
| currency | string | The ISO currency code for `format: 'currency'`; default `'USD'`. (optional) |
| decimals | number | Fraction digits; defaults by format. (optional) |
| totals | boolean | { row?: boolean; column?: boolean } | The totals row and column: `true` (default) both, `false` neither, or `{ row, column }`. (optional) |
| height | number | The band's height in pixels; default 160. (optional) |
| rowHeight | number | A band row's height in pixels; default 26. (optional) |
| columns | Array<string | GanttGridColumn | GanttBandColumn> | The left-hand columns: `'name'`, `'total'`, overrides of them, grid columns and {@link GanttBandColumn}-style `{ value(row, ctx), render(row, ctx) }` columns. (optional) |
| thresholds | GanttDataBandThreshold[] | Ascending thresholds that style a bucket cell by its value. (optional) |
| editable | boolean | Double-click (or Enter/F2) a task cell to type its value into the `series`, through `beforeDataBandEdit`; series bands only. (optional) |
| onReschedule | 'shift' | 'stretch' | 'keep' | What a moved or resized task does to its series: `'shift'` (default), `'stretch'` or `'keep'`. (optional) |
| collapsed | boolean | Start collapsed to the header strip. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| value | (task: Record<string, unknown>, bucketStart: string, bucketEnd: string, ctx: GanttDataBandContext) => number | null | undefined | task: Record<string, unknown>bucketStart: stringbucketEnd: stringctx: GanttDataBandContext | => number | null | undefined | A leaf task's value for one bucket: `(task, bucketStart, bucketEnd, ctx)`, dates as ISO strings, end exclusive. (optional) |
| cellStyle | (value: number | null, ctx: GanttDataBandContext) => string | Record<string, string | number> | null | undefined | value: number | nullctx: GanttDataBandContext | => string | Record<string, string | number> | null | undefined | A cell's own style: a class name, or `{ className, ...css }`. (optional) |
GanttDataBandChangeEvent
The payload of `dataBand:change`: a task's series was rewritten for the host to save.
| Property | Type | Description |
|---|---|---|
| band | string | The band's id. |
| id | string | The task id. |
| task | Record<string, unknown> | The task, carrying the new series. |
| field | string | The series field. |
| series | GanttDataBandPoint[] | The new series. |
| previous | GanttDataBandPoint[] | The series before the change. |
| reason | 'shift' | 'stretch' | 'edit' | Why: `'shift'` or `'stretch'` after a reschedule, `'edit'` after a cell edit. |
| view | GanttSplitView | The split view. |
| event | unknown | The originating DOM event, or `null`. |
GanttDataBandEditEvent
The payload of `beforeDataBandEdit`: a typed value about to be written into one bucket.
| Property | Type | Description |
|---|---|---|
| band | string | The band's id. |
| id | string | The task id. |
| task | Record<string, unknown> | The task before the edit. |
| field | string | The series field. |
| bucketStart | string | The bucket's start, as an ISO date. |
| bucketEnd | string | The bucket's exclusive end, as an ISO date. |
| value | number | null | The typed value, or `null` to clear the bucket. |
| oldValue | number | null | The bucket's value before the edit, or `null`. |
| series | GanttDataBandPoint[] | The series once the edit applies. |
| previous | GanttDataBandPoint[] | The series now. |
| view | GanttSplitView | The split view. |
| event | unknown | The originating DOM event, or `null`. |
GanttDataBandEditCancelledEvent
The payload of `dataBandEdit:cancelled`: the refused edit and why.
| Property | Type | Description |
|---|---|---|
| band | string | The band's id. |
| id | string | The task id. |
| task | Record<string, unknown> | The task, unchanged. |
| field | string | The series field. |
| bucketStart | string | The bucket's start, as an ISO date. |
| bucketEnd | string | The bucket's exclusive end, as an ISO date. |
| value | number | null | The value that was refused. |
| oldValue | number | null | The bucket's value, unchanged. |
| series | GanttDataBandPoint[] | The series the edit would have written. |
| previous | GanttDataBandPoint[] | The series, unchanged. |
| view | GanttSplitView | The split view. |
| event | unknown | The originating DOM event, or `null`. |
| reason | string | The reason given to `preventDefault`, `'prevented'` when none was, or `'stale'`. |
GanttRowDropContext
The context a dropped row carries: the shared shape of `beforeRowDrop`, `rowDrop` and `rowDrop:cancelled`. It is `rowMove`'s payload plus where the row landed - `position` against `target`.
| Property | Type | Description |
|---|---|---|
| id | string | The row being dropped, by id. |
| rawTask | GanttTask | null | The task as the host supplied it (a shallow copy), or `null` when it is not a host task. |
| task | GanttScheduledTask | null | The task as the scheduler placed it before the drop, or `null` without a schedule. |
| view | unknown | The view the drop happened in, or `null` for a headless `moveRow`. |
| oldParent | string | null | Its parent before the drop, or `null` at the root. |
| newParent | string | null | The parent it lands under once the drop applies, or `null` for the root. |
| oldIndex | number | Its 0-based index among its old parent's children, before the drop. |
| newIndex | number | Its 0-based index among its new parent's children, once the drop applies. |
| position | 'before' | 'after' | 'inside' | Where it lands relative to `target`: before or after it as a sibling, or inside it as a child. |
| target | string | null | The row the drop anchors to - the row under the pointer, or the keyboard sibling/parent - or `null`. |
| origin | string | Always `user`. |
| event | unknown | The originating DOM event (a drop or a keypress), or `null`. |
GanttRowDropCancelledEvent
A row that was not dropped: the payload of `rowDrop:cancelled`. A notification, so it carries no `preventDefault`.
| Property | Type | Description |
|---|---|---|
| reason | string | The reason given to `preventDefault`, `'prevented'` when none was, or `'stale'`. |
GanttEventPayloads
What a handler receives, per Gantt event.
| Property | Type | Description |
|---|---|---|
| schedule | GanttSchedule | The recomputed schedule, exactly as `gantt.schedule` now reads. |
| error | GanttScheduleError | Why the recompute failed. |
| beforeTaskEdit | GanttTaskEditEvent | The edit about to be applied, with `preventDefault` to stop it. |
| beforeTaskMove | GanttTaskEditEvent | The move about to be applied, with `preventDefault` to stop it. |
| beforeTaskResize | GanttTaskEditEvent | The resize about to be applied, with `preventDefault` to stop it. |
| beforeProgressChange | GanttTaskEditEvent | The progress about to be written, with `preventDefault` to stop it. |
| beforeMilestoneMove | GanttTaskEditEvent | The milestone move about to be applied, with `preventDefault` to stop it. |
| beforeDependencyCreate | GanttDependencyCreateEvent | The links about to be created, with `preventDefault` to stop them. |
| beforeTaskDelete | GanttTaskDeleteEvent | The task about to be deleted, with `preventDefault` to stop it. |
| beforeDependencyDelete | GanttDependencyDeleteEvent | The link about to be removed, with `preventDefault` to stop it. |
| beforeRowMove | GanttRowMoveEvent | The row about to move or re-parent, with `preventDefault` to stop it. |
| beforeRowDrop | GanttRowDropEvent | The row about to drop in a new position, with `preventDefault` to stop it. |
| taskEdit:cancelled | GanttTaskEditCancelledEvent | The edit that was not applied, and why. |
| taskMove:cancelled | GanttTaskEditCancelledEvent | The move that was not applied, and why. |
| taskResize:cancelled | GanttTaskEditCancelledEvent | The resize that was not applied, and why. |
| progressChange:cancelled | GanttTaskEditCancelledEvent | The progress change that was not written, and why. |
| milestoneMove:cancelled | GanttTaskEditCancelledEvent | The milestone move that was not applied, and why. |
| dependencyCreate:cancelled | GanttDependencyCreateCancelledEvent | The links that were not created, and why. |
| taskDelete:cancelled | GanttTaskDeleteCancelledEvent | The task that was not deleted, and why. |
| beforeTaskPaste | GanttTaskPasteEvent | The tasks about to be pasted, with `preventDefault` to stop them. |
| taskPaste:cancelled | GanttTaskPasteCancelledEvent | The paste that was not applied, and why. |
| beforeTaskAdd | GanttTaskAddEvent | The task about to be added, with `preventDefault` to stop it. |
| taskAdd:cancelled | GanttTaskAddCancelledEvent | The add that was not applied, and why. |
| beforeMilestoneConvert | GanttMilestoneConvertEvent | The conversion about to happen, with `preventDefault` to stop it. |
| milestoneConvert:cancelled | GanttMilestoneConvertCancelledEvent | The conversion that was not applied, and why. |
| dependencyDelete:cancelled | GanttDependencyDeleteCancelledEvent | The link that was not removed, and why. |
| rowMove:cancelled | GanttRowMoveCancelledEvent | The move that was not applied, and why. |
| rowDrop:cancelled | GanttRowDropCancelledEvent | The drop that was not applied, and why. |
| dependencies | GanttDependenciesChangedEvent | The links that were added and/or removed by the change that just applied. |
| history | GanttHistoryEvent | The undo/redo timeline's new state. |
| columnResize | GanttColumnResizeEvent | The settled column/divider width, and a snapshot of every width. |
| workload:click | GanttWorkloadClickEvent | The clicked workload bucket's resource, span and bookings. |
| workloadCellClick | GanttWorkloadCellEvent | The clicked workload cell or histogram bar's resource, span and bookings. |
| workloadCellDblClick | GanttWorkloadCellDblClickEvent | The double-clicked workload cell or histogram bar, with `preventDefault` to claim it. |
| workloadCellContextMenu | GanttWorkloadCellContextMenuEvent | The right-clicked workload cell or histogram bar, with `preventDefault` to suppress the menu. |
| workloadCellHover | GanttWorkloadCellEvent | The workload cell or histogram bar the pointer or focus entered. |
| workloadCellHoverEnd | GanttWorkloadCellEvent | The workload cell or histogram bar the pointer or focus left. |
| workloadResourceClick | GanttWorkloadResourceClickEvent | The clicked workload-band resource-name cell's resource. |
| dataBand:change | GanttDataBandChangeEvent | The task, band, field and the rewritten series, with why it was rewritten. |
| beforeDataBandEdit | GanttDataBandEditEvent | The data band cell edit about to apply, with `preventDefault` to stop it. |
| dataBandEdit:cancelled | GanttDataBandEditCancelledEvent | The data band cell edit that was refused, with the reason. |
| cell:click | GanttCellClickEvent | The clicked task-table cell's task, column key and cell element. |
| cellClick | GanttCellPointerEvent | The clicked task-table cell's id, task, column key, value and element. |
| cellDblClick | GanttCellDblClickEvent | The double-clicked task-table cell. |
| cellContextMenu | GanttCellContextMenuEvent | The task-table cell whose context menu was requested. |
| beforeCellEditStart | GanttBeforeCellEditStartEvent | The task-table cell about to open its editor, with `preventDefault` to keep it closed. |
| cellEditStart | GanttCellEditStartEvent | The task-table cell whose editor opened. |
| cellEditStart:cancelled | GanttCellEditStartCancelledEvent | The open attempt that was refused, and why. |
| cellEditCommit | GanttCellEditCommitEvent | The task-table cell edit that committed, with the old and new value. |
| cellEditCancel | GanttCellEditCancelEvent | The task-table cell edit that was abandoned. |
| rowClick | GanttRowClickEvent | The clicked task-table row. |
| rowDblClick | GanttRowDblClickEvent | The double-clicked task-table row. |
| task:filtered | GanttTaskFilteredEvent | The id of the filtered-out task `scrollToTask` was asked for. |
| draw | GanttDrawEvent | The completed paint's view, root element, drawn axis range and resolved zoom. |
| beforeTaskExpand | GanttTaskToggleEvent | The summary row about to expand, with `preventDefault` to stop it. |
| beforeTaskCollapse | GanttTaskToggleEvent | The summary row about to collapse, with `preventDefault` to stop it. |
| taskExpand | GanttTaskToggledEvent | The summary row that expanded, and the expanded set that resulted. |
| taskCollapse | GanttTaskToggledEvent | The summary row that collapsed, and the expanded set that resulted. |
| taskExpand:cancelled | GanttTaskToggleCancelledEvent | The expand that was refused, and why. |
| taskCollapse:cancelled | GanttTaskToggleCancelledEvent | The collapse that was refused, and why. |
| resourceExpand | GanttResourceToggleEvent | The workload-band resource that expanded, and the expanded resource set. |
| resourceCollapse | GanttResourceToggleEvent | The workload-band resource that collapsed, and the expanded resource set. |
| selectionChange | GanttSelectionChangeEvent | The selection now, what it gained and lost, and what caused the change. |
| rowMove | GanttRowMovedEvent | The row that moved or was re-parented, and where it landed. |
| rowDrop | GanttRowDroppedEvent | The row that dropped into a new position, and where it landed. |
| schedulingConflict | GanttSchedulingConflictEvent | The whole scheduling-conflict set, plus what this recompute added and resolved. |
| taskAdd | GanttTaskChangeEvent | The task that was added, its scheduled record and cause. |
| taskRemove | GanttTaskChangeEvent | The task that was removed, the record it had and its cause. |
| taskUpdate | GanttTaskChangeEvent | The task that changed, a field-level diff, its record and cause. |
| dependencyAdd | GanttDependencyChangeEvent | The dependency link that was added and its cause. |
| dependencyRemove | GanttDependencyChangeEvent | The dependency link that was removed and its cause. |
| dependencyUpdate | GanttDependencyUpdateEvent | The dependency link that changed in place, with a field-level diff. |
| assignmentChange | GanttAssignmentChangeEvent | The task whose roster changed, with the old and new assignments. |
| datesChanged | GanttDatesChangedEvent | The date-only projection of a committed change. |
| change | GanttChangeBatchEvent | Every individual data-change event one committed action produced. |
| ready | GanttReadyEvent | The first successful schedule's result and the mounted view, if any. |
| beforeTaskDrag | GanttBeforeTaskDragEvent | The move drag about to start, with `preventDefault` to stop it. |
| taskDragStart | GanttTaskDragStartEvent | The move drag that started, from where. |
| taskDrag | GanttTaskDragEvent | One move drag frame: from, to, validity and the successors it would push. |
| taskDrag:cancelled | GanttTaskDragCancelledEvent | The move drag that was refused or abandoned, and why. |
| beforeTaskResizeStart | GanttBeforeTaskResizeStartEvent | The resize about to start, with `preventDefault` to stop it. |
| taskResizeStart | GanttTaskResizeStartEvent | The resize that started, from which edge and span. |
| taskPartialResize | GanttTaskResizeEvent | One resize frame: from, to and validity. |
| taskResizeEnd | GanttTaskResizeEvent | The released resize: from, to and validity. |
| beforePercentBarDrag | GanttBeforePercentBarDragEvent | The progress drag about to start, with `preventDefault` to stop it. |
| percentBarDragStart | GanttPercentBarDragStartEvent | The progress drag that started, from which percentage. |
| percentBarDrag | GanttPercentBarDragEvent | One progress drag frame, in snapped percent. |
| percentBarDrop | GanttPercentBarDragEvent | The released progress drag, in snapped percent. |
| percentBarDrag:cancelled | GanttPercentBarDragCancelledEvent | The progress drag that was refused or abandoned, and why. |
| taskClick | GanttTaskClickEvent | The clicked task, part and day. |
| taskDblClick | GanttTaskDblClickEvent | The double-clicked task, part and day, with `preventDefault` to claim it. |
| taskMouseOver | GanttTaskMouseEvent | The task the pointer or focus entered, and which part. |
| taskMouseOut | GanttTaskMouseEvent | The task the pointer or focus left, and which part. |
| taskContextMenu | GanttTaskContextMenuEvent | The task context-menu request, with `preventDefault` to claim it. |
| linkClick | GanttLinkClickEvent | The clicked link's identity. |
| linkDblClick | GanttLinkDblClickEvent | The double-clicked (or Enter/Space-activated) link's identity, with `preventDefault` to claim it. |
| linkContextMenu | GanttLinkContextMenuEvent | The link context-menu request, with `preventDefault` to claim it. |
| linkHover | GanttLinkHoverEvent | The hovered link's identity. |
| tooltipShow | GanttTooltipShowEvent | The tooltip about to be shown, with `preventDefault` to veto it. |
| tooltipHide | GanttTooltipHideEvent | The task whose tooltip was hidden. |
| zoomChange | GanttZoomChangeEvent | The split view's zoom before and after, and what drove the change. |
| visibleRangeChange | GanttVisibleRangeChangeEvent | The dates now under the split view's timeline edges, and the resolved zoom. |
| scroll | GanttScrollEvent | The split view's settled scroll offsets. |
| viewMount | GanttViewMountEvent | The view that mounted and painted its first frame. |
| viewDestroy | GanttViewDestroyEvent | The view that was torn down. |
| dirtyChange | GanttDirtyChangeEvent | Whether the plan now differs from the committed baseline. |
GanttDirtyChangeEvent
The payload of `dirtyChange`: fires only when `hasChanges` flips.
| Property | Type | Description |
|---|---|---|
| hasChanges | boolean | Whether the plan now differs from the committed baseline. |
GanttDirtyTaskUpdate
One task's changed fields against the committed baseline, inside a {@link GanttChanges} diff.
| Property | Type | Description |
|---|---|---|
| id | string | The task's id, the schedule's key. |
| fields | Record<string, GanttFieldChange> | Each changed field, keyed by name, old and new. |
GanttDirtyTasks
The task side of a {@link GanttChanges} diff.
| Property | Type | Description |
|---|---|---|
| added | GanttTask[] | Tasks present now that were absent from the committed baseline. |
| updated | GanttDirtyTaskUpdate[] | Tasks present in both, with at least one changed field. |
| removed | string[] | Ids in the committed baseline no longer in the plan. |
GanttDirtyDependencyUpdate
One dependency's changed fields against the committed baseline, inside a {@link GanttChanges} diff.
| Property | Type | Description |
|---|---|---|
| id | string | The link's identity key (its `from`/`to`/`type`). |
| fields | Record<string, GanttFieldChange> | Each changed field, keyed by name, old and new. |
GanttDirtyDependencies
The dependency side of a {@link GanttChanges} diff.
| Property | Type | Description |
|---|---|---|
| added | GanttDependency[] | Links present now that were absent from the committed baseline. |
| updated | GanttDirtyDependencyUpdate[] | Links present in both, with at least one changed field (a lag edit). |
| removed | string[] | Identity keys in the committed baseline no longer in the plan. |
GanttDirtyAssignmentUpdate
One task's resource roster change against the committed baseline, inside a {@link GanttChanges} diff.
| Property | Type | Description |
|---|---|---|
| id | string | The task's id. |
| from | unknown | The roster at the committed baseline. |
| to | unknown | The roster now. |
GanttChanges
The plan changes accumulated since the last `commit()` or load, as `gantt.changes()` returns it: diffed fresh against the committed baseline on every call, so an add immediately removed, or an edit undone back to the baseline, nets to nothing rather than being read off a running total.
| Property | Type | Description |
|---|---|---|
| tasks | GanttDirtyTasks | The task-level diff. |
| dependencies | GanttDirtyDependencies | The dependency-level diff. |
| assignments | { updated: GanttDirtyAssignmentUpdate[] } | The tasks whose resource roster changed, mirroring `assignmentChange`. |
GanttFieldChange
One field's before and after value in a committed-change diff.
| Property | Type | Description |
|---|---|---|
| from | unknown | The value before the change (`undefined` on an add). |
| to | unknown | The value after the change (`undefined` on a remove). |
GanttTaskChangeEvent
The payload of `taskAdd`, `taskRemove` and `taskUpdate`, raised once per task a committed change added, removed or altered. The scheduled-date fields of `changes` come from the schedule, so a successor the scheduler moved reports its date change even though its raw row did not.
| Property | Type | Description |
|---|---|---|
| id | string | The task's id, the schedule's key. |
| rawTask | GanttTask | The task exactly as the host supplied it (a shallow copy). |
| task | GanttScheduledTask | null | The scheduled task record, or `null` for a removed task. |
| changes | Record<string, GanttFieldChange> | Each changed field, keyed by name, old and new. |
| cause | GanttChangeCause 'user' | 'pushed' | 'api' | 'undo' | 'redo' | What caused the change: `'user'`/`'api'` for the edited task, `'pushed'` for a successor the scheduler moved, `'undo'`/`'redo'` on a history replay. |
| event | unknown | The originating DOM event, when a gesture drove the change; else `null`. |
| view | unknown | The mounted view the change is reachable from, or `null` when none is mounted. |
GanttAssignmentChangeEvent
The payload of `assignmentChange`, raised when a committed change altered a task's resource roster.
| Property | Type | Description |
|---|---|---|
| id | string | The task whose roster changed. |
| from | unknown[] | The roster before the change. |
| to | unknown[] | The roster after the change. |
| cause | GanttChangeCause 'user' | 'pushed' | 'api' | 'undo' | 'redo' | What caused the change. |
| event | unknown | The originating DOM event, when a gesture drove the change; else `null`. |
| view | unknown | The mounted view the change is reachable from, or `null` when none is mounted. |
GanttDependencyChangeEvent
The payload of `dependencyAdd` and `dependencyRemove`, raised once per link a committed change added or removed.
| Property | Type | Description |
|---|---|---|
| id | string | The link's identity key (`from``to``type`). |
| dependency | GanttDependency | The link itself, normalised. |
| cause | GanttChangeCause 'user' | 'pushed' | 'api' | 'undo' | 'redo' | What caused the change. |
| event | unknown | The originating DOM event, when a gesture drove the change; else `null`. |
| view | unknown | The mounted view the change is reachable from, or `null` when none is mounted. |
GanttDependencyUpdateEvent
The payload of `dependencyUpdate`, raised when a committed change altered a link in place (a lag or other non-identity edit; a change to `from`/`to`/`type` is a remove plus an add, not an update).
| Property | Type | Description |
|---|---|---|
| id | string | The link's identity key. |
| dependency | GanttDependency | The link as it now stands. |
| changes | Record<string, GanttFieldChange> | Each changed field, keyed by name, old and new. |
| cause | GanttChangeCause 'user' | 'pushed' | 'api' | 'undo' | 'redo' | What caused the change. |
| event | unknown | The originating DOM event, when a gesture drove the change; else `null`. |
| view | unknown | The mounted view the change is reachable from, or `null` when none is mounted. |
GanttDateChange
One task's scheduled dates in a {@link GanttDatesChangedEvent}.
| Property | Type | Description |
|---|---|---|
| id | string | The task's id. |
| start | number | The scheduled early start, as a calendar day-number. |
| end | number | The scheduled early finish, as a calendar day-number. |
| duration | number | The scheduled duration, in working days. |
| cause | GanttChangeCause 'user' | 'pushed' | 'api' | 'undo' | 'redo' | What caused the date to move: `'user'`/`'api'` for the edited task, `'pushed'` for a successor, `'undo'`/`'redo'` on a history replay. |
GanttDatesChangedEvent
The payload of `datesChanged`: the date-only projection of one committed change. `changes` lists every task whose scheduled dates moved - as `{ id, start, end, duration }` per task, carried under `changes` so the event shape stays an object like its siblings. Not fired for the host's own `setTasks`/`rows.apply` loads.
| Property | Type | Description |
|---|---|---|
| changes | GanttDateChange[] | The date move for every task whose scheduled dates changed. |
| cause | GanttChangeCause 'user' | 'pushed' | 'api' | 'undo' | 'redo' | The action's own cause. |
| event | unknown | The originating DOM event, when a gesture drove the change; else `null`. |
| view | unknown | The mounted view the change is reachable from, or `null` when none is mounted. |
GanttChangeEntry
One entry inside a {@link GanttChangeBatchEvent}: a typed data-change event with a `type` discriminant.
| Property | Type | Description |
|---|---|---|
| type | GanttChangeKind 'taskAdd' | 'taskRemove' | 'taskUpdate' | 'dependencyAdd' | 'dependencyRemove' | 'dependencyUpdate' | 'assignmentChange' | Which event this entry is. |
| id | string | The task id, or a link's identity key. |
| cause | GanttChangeCause 'user' | 'pushed' | 'api' | 'undo' | 'redo' | The entry's cause. |
| rawTask | GanttTask | The raw task, on a task entry. (optional) |
| task | GanttScheduledTask | null | The scheduled record, on a task entry (`null` for a remove). (optional) |
| dependency | GanttDependency | The link, on a dependency entry. (optional) |
| changes | Record<string, GanttFieldChange> | The field-level diff, on a task/dependency update. (optional) |
| from | unknown[] | The roster before, on an `assignmentChange` entry. (optional) |
| to | unknown[] | The roster after, on an `assignmentChange` entry. (optional) |
GanttChangeBatchEvent
The payload of `change`: the ONE batch per committed action, carrying every individual data-change event it produced as an array, so a host that wants the whole action at once has it without stitching the individual events back together.
| Property | Type | Description |
|---|---|---|
| changes | GanttChangeEntry[] | Every individual data-change event this action produced. |
| cause | GanttChangeCause 'user' | 'pushed' | 'api' | 'undo' | 'redo' | The action's own cause. |
| event | unknown | The originating DOM event, when a gesture drove the action; else `null`. |
| view | unknown | The mounted view the change is reachable from, or `null` when none is mounted. |
GanttCommitPrimary
The task a gesture acted on, named at the head of a `lattice:gantt-commit` detail.
| Property | Type | Description |
|---|---|---|
| id | string | The primary task's id. |
| kind | GanttCommitChangeKind 'add' | 'delete' | 'move' | 'resize' | 'progress' | 'cell-edit' | 'link' | 'row-move' | The kind of the primary task's change. |
GanttCommitChange
One entry of a `lattice:gantt-commit` `changes` array: the same shape a `lattice:gantt-change` carries, gathered into the one commit.
| Property | Type | Description |
|---|---|---|
| id | string | The task id, or a link's identity key. |
| kind | GanttCommitChangeKind 'add' | 'delete' | 'move' | 'resize' | 'progress' | 'cell-edit' | 'link' | 'row-move' | What kind of change it is. |
| task | GanttTask | null | The raw task, or `null` for a link change. |
| changes | Record<string, GanttFieldChange> | GanttDependency | The field-level diff (dates already ISO) or, for a link, the link itself. |
GanttCommitDetail
The `detail` of a `lattice:gantt-commit` DOM event: ONE per user gesture or API transaction, carrying the whole edit at once - `changes` lists every task the action changed (the primary task, the successors it pushed, and the summaries whose dates moved), so `hx-trigger="lattice:gantt-commit"` posts it in one request instead of a burst of `lattice:gantt-change` events.
| Property | Type | Description |
|---|---|---|
| cause | GanttChangeCause 'user' | 'pushed' | 'api' | 'undo' | 'redo' | What caused the action: `'user'`/`'api'` for a gesture/transaction, `'undo'`/`'redo'` on a history replay. |
| primary | GanttCommitPrimary | The task the gesture acted on. |
| changes | GanttCommitChange[] | Every change the action produced, in the per-task `lattice:gantt-change` shape. |
GanttReadyEvent
The payload of `ready`: the first successful schedule.
| Property | Type | Description |
|---|---|---|
| schedule | GanttSchedule | The first schedule the controller computed. |
| view | unknown | The mounted view, or `null` when none is mounted yet. |
GanttTaskPointerContext
What every task-bar pointer event carries … 0001992): the task by id, as the host supplied it and as the scheduler placed it, the view it happened in, and the DOM event that caused it. Dates in these payloads are ISO dates (`YYYY-MM-DD`) on a calendar axis and plan day numbers under `dateAxis: false`; `end` is the scheduled finish boundary, the same `ef` the schedule carries. None of these fire for a host's own `setTasks` or `rows.apply` loads.
| Property | Type | Description |
|---|---|---|
| id | string | The task, by id. |
| rawTask | GanttTask | null | The task as the host supplied it (a shallow copy), or `null` when it is not a host task. |
| task | GanttScheduledTask | null | The task as the scheduler placed it before this gesture, or `null` without a schedule. |
| view | unknown | The view the gesture happened in. |
| event | unknown | The originating DOM event (pointer, mouse, focus or key), or `null` where there is none. |
GanttCellPointerContext
The context a task-table cell event carries: the shared shape of `cellClick`, `cellDblClick`, `cellContextMenu` and `beforeCellEditStart`. The table became a real Lattice grid; this maps its own cell events onto the controller with the task-centric payload every Gantt event carries, plus the column and value under the cell.
| Property | Type | Description |
|---|---|---|
| columnKey | string | null | The column's key: the host column's `key`/`id`, or a built-in column's id, or `null` for a row's empty tail. |
| value | unknown | The cell's current value. |
| element | unknown | The cell element, or `null` for a keyboard context-menu with no element to anchor to. |
GanttCellEditStartEvent
A task-table cell's editor opened: the payload of `cellEditStart`, once `beforeCellEditStart` allowed it (or for a programmatic `grid.edit.start`/the auto-advance to the next cell after a commit, neither of which raises the `before` gate).
| Property | Type | Description |
|---|---|---|
| columnKey | string | The column being edited. |
GanttCellEditStartCancelledEvent
An open attempt that did not open an editor: the payload of `cellEditStart:cancelled`, fired when a `beforeCellEditStart` handler calls `preventDefault(reason)` or defers (a reason of `'deferred'`). The same context the gate carried, plus why.
| Property | Type | Description |
|---|---|---|
| reason | string | Why the cell's editor did not open. |
GanttCellEditCommitEvent
A task-table cell's edit committed: the payload of `cellEditCommit`.
| Property | Type | Description |
|---|---|---|
| columnKey | string | null | The column that was edited. |
| oldValue | unknown | The value before the commit. |
| newValue | unknown | The value after the commit. |
GanttCellEditCancelEvent
A task-table cell's edit was abandoned: the payload of `cellEditCancel` - Escape, or a commit a `beforeEdit` handler vetoed.
| Property | Type | Description |
|---|---|---|
| columnKey | string | null | The column that was being edited, or `null`. |
GanttPointerPosition
A pointer position in client (viewport) pixels; `null` on a keyboard path.
| Property | Type | Description |
|---|---|---|
| x | number | The client x. |
| y | number | The client y. |
GanttTaskDragStartEvent
A move drag that started: the payload of `taskDragStart`, raised once when the pointer passes the drag threshold (never on a click), or when a keyboard move starts, before anything moved.
| Property | Type | Description |
|---|---|---|
| start | number | string | Where the task starts now. |
| end | number | string | Where the task finishes now. |
| pointer | GanttPointerPosition | null | The pointer position, or `null` for a keyboard move. |
GanttBeforeTaskDragEvent
A move drag about to start: the payload of `beforeTaskDrag`. `preventDefault(reason)` cancels the drag before anything moves and fires `taskDrag:cancelled`. A gesture cannot wait, so a handler that defers (returns a Promise) cancels it as `'deferred'`.
| Property | Type | Description |
|---|---|---|
| phase | 'start' | Always `'start'`: this gate is the drag's start, not its commit (that is `beforeTaskMove`). |
GanttTaskDragCancelledEvent
A move drag that was refused or abandoned: the payload of `taskDrag:cancelled` - the start context plus why: the veto's reason, `'prevented'`, `'deferred'`, or `'escape'` when Escape abandoned it.
| Property | Type | Description |
|---|---|---|
| reason | string | Why the drag did not happen. |
GanttTaskSpan
A `{ start, end }` position in payload date form.
| Property | Type | Description |
|---|---|---|
| start | number | string | The start. |
| end | number | string | The finish. |
GanttTaskSpanWithDuration
A `{ start, end, duration }` span in payload date form.
| Property | Type | Description |
|---|---|---|
| duration | number | The duration in days. |
GanttTaskDragEvent
One frame of a move drag: the payload of `taskDrag`, at most once per animation frame. `to` is where the scheduler would land the task (snapped to whole days, constraints applied); `valid` is false when the commit would be refused - a hard lock, or a constraint that would put the task somewhere other than where it was dropped.
| Property | Type | Description |
|---|---|---|
| from | GanttTaskSpan | Where the task was when the drag started. |
| to | GanttTaskSpan | Where it would land if released now. |
| deltaDays | number | The whole days dragged. |
| valid | boolean | Whether the commit would be accepted. |
| pointer | GanttPointerPosition | null | The pointer position. |
| pushed | Array<{ id: string; to: GanttTaskSpan }> | The successors the scheduler would move, and where to. |
GanttTaskResizeStartEvent
A resize that started: the payload of `taskResizeStart`, once per drag past the threshold or keyboard resize.
| Property | Type | Description |
|---|---|---|
| edge | 'end' | 'start' | Which edge is being dragged: always `'end'` today, a bar resizes from its finish. |
| from | GanttTaskSpanWithDuration | The task's span when the resize started. |
| pointer | GanttPointerPosition | null | The pointer position, or `null` for a keyboard resize. |
GanttBeforeTaskResizeStartEvent
A resize about to start: the payload of `beforeTaskResizeStart`. `preventDefault(reason)` cancels it and fires `taskResize:cancelled` with `phase: 'start'`.
| Property | Type | Description |
|---|---|---|
| phase | 'start' | Always `'start'`: this gate is the resize's start, not its commit (that is `beforeTaskResize`). |
GanttTaskResizeEvent
A resize frame or its release: the payload of `taskPartialResize` (at most once per animation frame) and `taskResizeEnd` (once, on release, before the commit's own gate).
| Property | Type | Description |
|---|---|---|
| edge | 'end' | 'start' | Which edge is being dragged. |
| from | GanttTaskSpanWithDuration | The span when the resize started. |
| to | GanttTaskSpanWithDuration | The span the scheduler would give it now. |
| valid | boolean | Whether the commit would be accepted (not locked, not moved by a constraint). |
| pointer | GanttPointerPosition | null | The pointer position, or `null` for a keyboard resize. |
GanttPercentBarDragStartEvent
A progress-handle drag that started: the payload of `percentBarDragStart`.
| Property | Type | Description |
|---|---|---|
| from | number | The percent complete when the drag started. |
| pointer | GanttPointerPosition | null | The pointer position, or `null` for a keyboard edit. |
GanttBeforePercentBarDragEvent
A progress-handle drag about to start: the payload of `beforePercentBarDrag`. `preventDefault(reason)` cancels it and fires `percentBarDrag:cancelled`.
| Property | Type | Description |
|---|---|---|
| to | number | The percentage the gesture starts from (the same as `from`). |
GanttPercentBarDragEvent
A progress-handle frame or its release: the payload of `percentBarDrag` and `percentBarDrop`. Percentages are snapped to the view's `progressStep` and clamped to 0-100. The drop is raised before the commit, which still goes through `beforeProgressChange`.
| Property | Type | Description |
|---|---|---|
| from | number | The percent complete when the drag started. |
| to | number | The snapped percent complete under the pointer now. |
| pointer | GanttPointerPosition | null | The pointer position, or `null` for a keyboard edit. |
GanttPercentBarDragCancelledEvent
A progress-handle drag that was refused or abandoned: the payload of `percentBarDrag:cancelled`.
| Property | Type | Description |
|---|---|---|
| reason | string | The veto's reason, `'prevented'`, `'deferred'`, or `'escape'`. |
GanttTaskClickEvent
A click on a task: the payload of `taskClick`. Not raised for the release of a drag. Enter or Space on a focused bar raise it too, with the task's own start as `date`.
| Property | Type | Description |
|---|---|---|
| part | 'bar' | 'progress' | 'label' | 'baseline' | 'milestone' | 'summary' | Which part of the task's drawing was clicked. |
| date | number | string | null | The day under the pointer, in payload date form. |
GanttTaskDblClickEvent
A double click on a task: the payload of `taskDblClick`. Two `taskClick` events precede it (one per click of the pair). `preventDefault()` claims it - the view runs no default of its own and the browser's default (selecting text) is prevented - so a host opens its own editor instead.
| Property | Type | Description |
|---|---|---|
| part | 'bar' | 'progress' | 'label' | 'baseline' | 'milestone' | 'summary' | Which part of the task's drawing was double-clicked. |
| date | number | string | null | The day under the pointer, in payload date form. |
GanttTaskMouseEvent
The pointer entered or left a task: the payload of `taskMouseOver` and `taskMouseOut`, once per entry and exit of a bar, milestone or summary bar - moving between parts of the same task is not a re-entry, and empty row space is not a task. Keyboard focus reaching or leaving a bar raises the same pair, with the focus event as `event`.
| Property | Type | Description |
|---|---|---|
| part | 'bar' | 'progress' | 'label' | 'baseline' | 'milestone' | 'summary' | The part entered (on a focus, the task's own kind). |
GanttTaskContextMenuEvent
A context-menu request on a task: the payload of `taskContextMenu`, raised on a right-click or the Menu key / Shift+F10 on a focused row. `preventDefault()` claims it - the split view's built-in menu (when `contextMenu` is configured) is not opened, and the browser's own is suppressed either way - so a host shows its own instead.
| Property | Type | Description |
|---|---|---|
| part | 'bar' | 'progress' | 'label' | 'baseline' | 'milestone' | 'summary' | Which part of the task's drawing the request landed on. |
GanttLinkPointerContext
What every dependency-link pointer event carries: the link's identity - predecessor id, successor id, the normalised type and the lag in working days (0 when none, read back from `gantt.dependencies` since it is not drawn on the link itself) - the view it happened in, and the originating DOM event.
| Property | Type | Description |
|---|---|---|
| from | string | The predecessor task's id. |
| to | string | The successor task's id. |
| linkType | string | The link's type, normalised to `'FS'`/`'SS'`/`'FF'`/`'SF'`. Named `linkType`, not `type`: every Gantt event payload's `type` is the EVENT's own name (`'linkClick'`, `'linkHover'`, …), so the link's own type is carried under a different key to avoid colliding with it. |
| lag | number | The link's lag in working days; 0 when none. |
| view | unknown | The view the gesture happened in. |
| event | unknown | The originating DOM event (pointer or key), or `null` where there is none. |
GanttTooltipShowEvent
A tooltip about to be shown for a task: the payload of `tooltipShow`, raised once per distinct task shown (not on every pointer frame a view re-resolves the same hovered task's card on). `preventDefault()` vetoes it - the built-in card is not shown - so a host's own tooltip replaces it.
| Property | Type | Description |
|---|---|---|
| text | string | The tooltip's plain-text content, the same text an `aria-describedby` reader gets. |
GanttSchedulingConflict
One normalised scheduling conflict: a single problem with the current plan, named in host terms rather than the engine's. It is the shape `gantt.schedulingConflicts` reports and each `schedulingConflict` event carries, distinct from {@link GanttConflict}, which is the engine's raw per-edge constraint breach.
| Property | Type | Description |
|---|---|---|
| kind | string | The kind of problem: `'cycle'`, `'constraint'`, `'lock'` or `'deadline'`. |
| tasks | string[] | The task ids the conflict involves - the cycle's members, or the single task for a constraint, lock or deadline. |
| links | Array<{ from: string; to: string }> | The dependency links the conflict involves - the edges that close a cycle, or the one offending predecessor of a lock; empty for a constraint or deadline. |
| message | string | A ready-to-show message from the catalogue, already localised. |
GanttSchedulingConflictEvent
The payload of `schedulingConflict`, fired after a recompute only when the conflict set changed (compared by a stable key). It carries the whole current set plus the delta, so a host never has to diff the set itself. A host load (`setTasks`, `rows.apply`) seeds the set silently and fires nothing.
| Property | Type | Description |
|---|---|---|
| conflicts | GanttSchedulingConflict[] | Every conflict in the plan now, cycle-first then constraint, lock and deadline. |
| added | GanttSchedulingConflict[] | The conflicts new in this recompute. |
| resolved | GanttSchedulingConflict[] | The conflicts that cleared in this recompute. |
GanttTaskToggleEvent
A summary row about to expand or collapse: the payload of `beforeTaskExpand` and `beforeTaskCollapse`. A chevron click, a keyboard activation and the `view.toggle` API all gate on this; `preventDefault(reason)` leaves the tree as it was and fires the matching `taskExpand:cancelled`/`taskCollapse:cancelled`.
| Property | Type | Description |
|---|---|---|
| id | string | The summary task being expanded or collapsed, by id. |
| expandedIds | string[] | The summary task ids that would be expanded once this toggle applies. |
| rawTask | GanttTask | null | The task as the host supplied it, or `null` when it is not a host task. |
| task | GanttTask | null | The scheduled task, or `null` when there is no successful schedule. |
| view | GanttSplitView | The split view the row belongs to. |
| event | unknown | The originating DOM event, or `null` for the `view.toggle` API path. |
GanttTaskToggledEvent
A summary row that expanded or collapsed: the payload of `taskExpand` and `taskCollapse`. A notification - it carries no `preventDefault`; the veto was the before-event's job.
| Property | Type | Description |
|---|---|---|
| id | string | The summary task that was expanded or collapsed, by id. |
| expandedIds | string[] | The summary task ids now expanded. |
| rawTask | GanttTask | null | The task as the host supplied it, or `null` when it is not a host task. |
| task | GanttTask | null | The scheduled task, or `null` when there is no successful schedule. |
| view | GanttSplitView | The split view the row belongs to. |
| event | unknown | The originating DOM event, or `null` for the `view.toggle` API path. |
GanttTaskToggleCancelledEvent
A task tree toggle that was refused: the payload of `taskExpand:cancelled` and `taskCollapse:cancelled`. The context the before-event carried, plus the reason.
| Property | Type | Description |
|---|---|---|
| id | string | The summary task that was not toggled, by id. |
| expandedIds | string[] | The expanded set that would have resulted, had it not been refused. |
| rawTask | GanttTask | null | The task as the host supplied it, or `null`. |
| task | GanttTask | null | The scheduled task, or `null`. |
| view | GanttSplitView | The split view the row belongs to. |
| event | unknown | The originating DOM event, or `null` for the `view.toggle` API path. |
| reason | string | The reason given to `preventDefault`, or `'prevented'` when none was. |
GanttResourceToggleEvent
A workload-band resource row whose per-task sub-rows were shown or hidden: the payload of `resourceExpand` and `resourceCollapse`. A notification - a display-only band toggle, not a gate.
| Property | Type | Description |
|---|---|---|
| resource | string | The resource whose sub-rows were toggled (`''` for the Unassigned row). |
| expandedResources | string[] | The resource keys now expanded in the band. |
| view | GanttSplitView | The split view the band belongs to. |
| event | unknown | The originating DOM event, or `null` for the `view.toggleResource` API path. |
GanttSelectionChangeEvent
The task selection after a change: the payload of `selectionChange`, fired once per change - a click, a Ctrl/Cmd or Shift click, an arrow key, or `select`/`deselect`. A selected task that is collapsed away or filtered out stays in `selectedIds`; `gantt.selectedHidden()` reports which are off-screen.
| Property | Type | Description |
|---|---|---|
| selectedIds | string[] | Every selected task id, in selection order. |
| added | string[] | The ids this change added. |
| removed | string[] | The ids this change removed. |
| cause | 'pointer' | 'keyboard' | 'api' | What drove the change: a pointer gesture, a keyboard one, or the API. |
| event | unknown | The originating DOM event, or `null` for the API and grid-folded paths. |
GanttTaskFilteredEvent
The payload of the `task:filtered` event, fired when `scrollToTask` is asked for a task the task table's filter is hiding. The filter is the user's, so it is left exactly as it was.
| Property | Type | Description |
|---|---|---|
| id | string | The id of the task that is hidden by the filter. |
GanttColumnResizeEvent
The payload of the `columnResize` event, fired once a split-view column-border or table/timeline-divider drag or keyboard resize settles - never mid-drag, so one gesture is exactly one event.
| Property | Type | Description |
|---|---|---|
| key | string | The column `key` that was resized, or `'grid'` for the table/timeline divider. |
| width | number | The settled width, in pixels, after the column's/divider's own `minWidth`/`maxWidth` clamp. |
| widths | Record<string, number> | Every left-panel column's current rendered width, keyed by its own `key`, plus the divider's own width under `'grid'` - a full snapshot a host can persist directly, e.g. alongside `gantt.getState()`'s own `columnWidths` (which carries the same shape, minus `grid`). |
GanttWorkloadClickEvent
The payload of the `workload:click` event, fired when a workload-band bucket cell is clicked: the resource, the half-open bucket span, the hours booked and available in it, and the tasks that booked there. `booked`/`available` are always HOURS - whatever unit the band is drawing in.
| Property | Type | Description |
|---|---|---|
| resource | string | null | The resource the cell belongs to, or `null` for the Unassigned row. |
| bucketStart | string | The bucket's start, as an ISO date (`YYYY-MM-DD`). |
| bucketEnd | string | The bucket's end (exclusive), as an ISO date (`YYYY-MM-DD`). |
| booked | number | The hours booked in this bucket (0 when empty). |
| available | number | null | The resource's available hours in this bucket, or `null` when no capacity is known. |
| tasks | Array<{ id: string; name: string; hours: number }> | The tasks that booked in this bucket, with the hours each contributed. |
GanttWorkloadCellEvent
The payload of the workload-band cell events: fired on a click, double-click, context menu, hover or hover-end of a workload-band bucket cell or a histogram bar. It carries the resource and its display name, the half-open bucket span, the hours booked and available, the percent-of-capacity (when a capacity is known), the over-allocation flag, the unit the band is drawing in, and the tasks that booked there. `booked`/`available`/`task.hours` are always HOURS - whatever the unit. A per-task sub-row cell adds the task's `id`/`rawTask`/`task`; an aggregate resource cell leaves those `null`. None of these fire for the host's own `setTasks`/`rows.apply` loads.
| Property | Type | Description |
|---|---|---|
| resource | string | null | The resource the cell belongs to, or `null` for the Unassigned row. |
| resourceName | string | The resource's display name, or "Unassigned" for `resource: null`. |
| bucketStart | string | The bucket's start, as an ISO date (`YYYY-MM-DD`). |
| bucketEnd | string | The bucket's end (exclusive), as an ISO date (`YYYY-MM-DD`). |
| booked | number | The hours booked in this bucket (0 when empty). |
| available | number | null | The resource's available hours in this bucket, or `null` when no capacity is known. |
| percent | number | null | The booking as a percentage of capacity, or `null` when no capacity is known. |
| over | boolean | Whether the booking exceeds the resource's available hours in this bucket. |
| unit | 'hours' | 'percent' | 'cost' | The unit the band is drawing in: `'hours'`, `'percent'` or `'cost'`. |
| tasks | Array<{ id: string; name: string; hours: number; units: number | null }> | The tasks that booked in this bucket, with the hours and this assignment's units each contributed. |
| id | string | null | The task this sub-row belongs to, or `null` on an aggregate resource cell. |
| rawTask | GanttTask | null | The task as the host supplied it (a shallow copy), or `null` when the cell names no task or the booking is load-only. |
| task | GanttScheduledTask | null | The task as the scheduler placed it, or `null` without a schedule or a task. |
| view | unknown | The view the cell belongs to. |
| event | unknown | The originating DOM event (pointer, mouse, focus or key), or `null` where there is none. |
GanttWorkloadResourceClickEvent
A click on a workload-band resource-name cell: the payload of `workloadResourceClick`, fired for an aggregate resource row (never a per-task sub-row or the totals row).
| Property | Type | Description |
|---|---|---|
| resource | string | null | The resource, or `null` for the Unassigned row. |
| resourceName | string | The resource's display name, or "Unassigned" for `resource: null`. |
| view | unknown | The view the cell belongs to. |
| event | unknown | The originating DOM event, or `null` where there is none. |
GanttCellClickEvent
The payload of the `cell:click` event, fired when a cell of the split view's task table is clicked: the task the row belongs to, the column's key, and the cell element that was clicked. The key is the grid column id - for a host column that is the `key`/`id` it was given, and for a built-in column its own id (`name`, `start`, …).
| Property | Type | Description |
|---|---|---|
| task | GanttTask | null | The raw task the row belongs to, as a shallow copy; null when no task carries the row's id. |
| columnKey | string | The column's key: the host column's `key`/`id`, or a built-in column's id. |
| element | unknown | The cell element that was clicked. |
GanttHistoryEvent
The payload of the `history` event, fired whenever the session undo/redo timeline changes - an edit recorded, an undo, a redo, or a clear. A host wires its undo and redo controls off `canUndo`/`canRedo` and can show `label` in a toast ("Undone: move").
| Property | Type | Description |
|---|---|---|
| canUndo | boolean | Whether there is now an edit that `undo()` would reverse. |
| canRedo | boolean | Whether there is now an edit that `redo()` would re-apply. |
| label | string | null | A short label for the action that triggered this event - e.g. `'move'`, `'resize'`, `'link'`, `'delete'`, `'level'`, `'add'` - or `null` after a clear. English and stable (host-facing data, not an announced string). |
GanttDrawEvent
The payload of the `draw` event, fired once after every completed paint of the timeline and bands - the initial paint, a data change, a zoom change, a band toggle, or a column resize. Several synchronous invalidations in one turn are coalesced into a single event, and it never fires before the DOM has been updated. This is the supported replacement for wrapping `view.draw`.
| Property | Type | Description |
|---|---|---|
| view | unknown | The view that just painted - the plain `mount` view, or the `mountSplit` split view. |
| element | unknown | The view's root element, already reflecting the completed paint. |
| range | { start: number; end: number } | The axis range the paint drew, in plan-day numbers - `start` and `end` are the same numbers the scale maps onto the timeline. |
| zoom | string | null | The resolved zoom level (`'day'`, `'week'`, `'month'`, `'quarter'`, or `'custom'` for a numeric pixels-per-day), or `null` for a fit-to-width plain view. |
GanttZoomChangeEvent
The payload of the `zoomChange` event, fired once each time the split view's time scale changes - a host `view.setZoom`/ `view.fitZoom` (`cause: 'api'`) or a click on the view's own zoom control (`cause: 'control'`) - carrying the level before and after and the resolved pixels-per-day. A report, not a gate.
| Property | Type | Description |
|---|---|---|
| view | unknown | The view whose zoom changed - the `mountSplit` split view. |
| from | GanttZoomValue | The zoom level before the change. |
| to | GanttZoomValue | The zoom level after the change. |
| cause | GanttZoomCause 'api' | 'control' | 'gesture' | What drove the change: a host call (`'api'`), the built-in zoom control (`'control'`), or a future pinch/wheel gesture (`'gesture'`). |
| pxPerDay | number | The pixels-per-day the view resolved for the new level, exactly what the repaint drew with. |
GanttVisibleRangeChangeEvent
The payload of the `visibleRangeChange` event, fired with the ISO dates now under the split view's timeline left and right edges and the resolved zoom, coalesced to at most one per animation frame across a burst of horizontal scroll, resize and zoom work, and once after the view first mounts. A report, not a gate.
| Property | Type | Description |
|---|---|---|
| view | unknown | The view whose visible range changed - the `mountSplit` split view. |
| start | string | The ISO date (`YYYY-MM-DD`) under the timeline's left edge. |
| end | string | The ISO date (`YYYY-MM-DD`) under the timeline's right edge. |
| zoom | string | The resolved zoom level (`'day'`, `'week'`, `'month'`, `'quarter'`, or `'custom'` for a numeric pixels-per-day). |
GanttScrollEvent
The payload of the `scroll` event, fired by the split view at most once per animation frame while its viewport scrolls, with the offsets read from the settled position. A scroll that settles where it already was is silent. A report, not a gate.
| Property | Type | Description |
|---|---|---|
| view | unknown | The view whose viewport scrolled - the `mountSplit` split view. |
| top | number | The viewport's vertical offset, in content pixels. |
| left | number | The viewport's horizontal offset, in content pixels. |
GanttViewMountEvent
The payload of the `viewMount` event, fired once by the plain `mount` view or the `mountSplit` split view after its first paint. A report, not a gate.
| Property | Type | Description |
|---|---|---|
| view | unknown | The view that mounted - the plain `mount` view or the `mountSplit` split view. |
| element | unknown | The view's root element, already attached and painted. |
GanttViewDestroyEvent
The payload of the `viewDestroy` event, fired once by a mounted view as it is torn down - a remount, `unmount()` or `destroy()` - before its root is removed, so `element` is still readable. A report, not a gate.
| Property | Type | Description |
|---|---|---|
| view | unknown | The view being torn down - the plain `mount` view or the `mountSplit` split view. |
| element | unknown | The view's root element, still attached at the moment the event fires. |
GanttColumnEditor
The `editor` shorthand on a split-view grid column: ask for one of the grid's own editors with the split view's plain keys, rather than spelling out the full `edit`/`lookup` spec. `{ type: 'select', options }` is the select editor - a dropdown of the options in the order given, committing the chosen option's value - and `{ type: 'number', min, max, step }` is the number editor, which clamps to the bounds and steps up/down by `step` (a spinner is added, so the step is keyboard-operable). An `editor` makes the column editable unless `editable`/`edit` says otherwise. It needs the view's `createGrid`: the lightweight tables have no editors.
| Property | Type | Description |
|---|---|---|
| type | 'select' | 'number' | The grid editor to use: `'select'` for a dropdown of options, `'number'` for a bounded number. |
| options | Option[] | ReadonlyArray<string | number> | (() => Option[] | Promise<Option[]>) | The select editor's options, in the order the dropdown shows them: an array of `{ id, label }` options, of bare strings or numbers (each becomes its own value and label), or a function returning either (synchronously or as a promise). (optional) |
| min | number | The lowest value the number editor accepts; lower typed values clamp to it. (optional) |
| max | number | The highest value the number editor accepts; higher typed values clamp to it. (optional) |
| step | number | The increment the number editor steps by; a spinner is added for it. (optional) |
GanttGridColumn
A split-view table column when the tables are real grids: the full grid column spec. An `id` naming a built-in column (`name`, `start`, `end`, `duration`, `progress`, `assignee`, `effort`, `units`; a band's `resource`/`total`) overrides that column, merged over it one level deep so the built-in behaviour it does not replace is kept.
| Property | Type | Description |
|---|---|---|
| key | string | The pre-grid column key, read as `id`. (optional) |
| editField | string | The task field an edit in this column writes, when it is not `field`. (optional) |
| editor | GanttColumnEditor | The grid editor shorthand for this column (select or bounded number). (optional) |
| dateFormat | string | ((p: { value: unknown; locale?: string }) => string) | How this column's date is formatted, overriding the view's `dateFormat`: a token pattern (`'dd/MM/yyyy'`) or a function, as on the view's `dateFormat`. The column's `format` still wins when both are given. (optional) |
GanttBandResource
The resource a {@link GanttBandColumn} function is handed: its own spec from `resources`, with its key and display name.
| Property | Type | Description |
|---|---|---|
| key | string | The resource key (`''` for the Unassigned row). |
| name | string | The resource's display name. |
| [field: string] | unknown | Anything else the host wrote against the resource in `resources`. |
GanttBandColumnContext
What a {@link GanttBandColumn} function is handed besides the resource: the band's figures for the row.
| Property | Type | Description |
|---|---|---|
| band | 'workload' | 'histogram' | Which band the column is in. |
| unit | 'hours' | 'percent' | 'cost' | The band's current unit. |
| capacity | number | null | The resource's capacity in units, or `null` when unknown. |
| total | number | The resource's booked hours over the whole range. |
| cost | number | null | The resource's cost over the whole range, or `null` when it has no rate. |
| available | number | null | The resource's available hours over the whole range, or `null` when unknown. |
| cells | Array<number | null> | The resource's booked hours per bucket. |
| over | boolean[] | Whether each bucket is over the resource's capacity. |
| overCount | number | How many buckets are over capacity. |
| tasks | object[] | The resource's task sub-rows. |
| task | object | null | The task sub-row being drawn, or `null` on a resource row. |
GanttBandColumn
A band's own column, independent of the task table. Its width is namespaced in `getColumnWidths`/`columnResize` as `band:<key>` (workload) or `histogram:<key>` (histogram).
| Property | Type | Description |
|---|---|---|
| key | string | The column's key, which names it in the width snapshot. |
| width | number | The column's width in pixels. (optional) |
| align | Align 'start' | 'center' | 'end' | 'left' | 'right' | 'centre' | The cell's horizontal alignment. (optional) |
| subRows | 'blank' | 'inherit' | ((task: object, resource: GanttBandResource, ctx: GanttBandColumnContext) => unknown) | What a task sub-row shows: `'blank'` (default), `'inherit'` (the resource's value) or a function giving its own. (optional) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| value | ((resource: GanttBandResource, ctx: GanttBandColumnContext) => unknown) | ColumnValueSpec | (resource: GanttBandResource, ctx: GanttBandColumnContext) => unknown | | ColumnValueSpec | The column's value for a resource row: what the column sorts and filters on, and shows when it has no `render`. (optional) |
| render | (resource: GanttBandResource, ctx: GanttBandColumnContext) => string | number | null | unknown | resource: GanttBandResourcectx: GanttBandColumnContext | => string | number | null | unknown | Draw the cell for a resource row: a string is set as text, a node is mounted. (optional) |
GanttSplitView
The view `mountSplit` returns: the task grid, timeline and (when asked for) the resource workload/histogram bands, joined in one scroll surface. Only the methods a host drives directly off the handle are typed here - everything else (what it draws) is reached through the controller.
| Property | Type | Description |
|---|---|---|
| element | unknown | The view's root element. (read-only) |
| options | object | The resolved split-view options this view was (re)built with. (read-only) |
| scroller | unknown | The single vertical scroller (grid + timeline). (read-only) |
| zoomControl | unknown | The built-in zoom control's button group, for a host that mounted it with `zoomControl: { position: 'toolbar' }` to place in a toolbar of its own; `null` when the option is off. The control keeps tracking the view's zoom while detached. (read-only) |
| collapsed | Set<string> | A copy of the collapsed summary task ids. (read-only) |
| expandedResources | Set<string> | A copy of the workload band's expanded resource keys. (read-only) |
| collapsedWorkloadGroups | string[] | The `workload.groupBy` group values currently collapsed. (read-only) |
| grids | { tasks: Grid | null; workload: Grid | null; histogram: Grid | null } | The real grids the tables are mounted as; each `null` when that table is not a grid. (read-only) |
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| rowGeometry | (): Array<{ id: string; top: number; height: number }> | - | Array<{ id: string; top: number; height: number }> | The last computed row geometry: one `{ id, top, height }` per visible row. |
| scrollToDate | (date: number | string | Date): void | date: number | string | Date | void | Align the timeline's horizontal scroll on a date, centering it in the timeline's own viewport, with no remount. A no-op before anything has drawn, or for a date that fails to parse. |
| setZoom | (level: GanttZoom | number): void | level: GanttZoom | number | void | Change the time-scale zoom with no remount: the collapsed summaries, every column width and the date centred in the timeline before the call are all unchanged by it - only the pixels-per-day scale moves. Takes the same preset or raw pixels-per-day `mountSplit`'s own `zoom` option does; an unrecognised value falls back to `'day'`, silently, the same as that option does. |
| fitZoom | (): void | - | void | Zoom so the whole plan fits the timeline's own width: the pixels-per-day that maps the plan's full domain onto the time body's viewport, applied through the same in-place repaint `setZoom` uses - collapsed summaries, column widths and the date are all unchanged, and the horizontal scroll resets to the start, since a plan that fits has nothing to scroll to. |
| visibleRange | (): { start: string; end: string } | null | - | { start: string; end: string } | null | The dates currently under the timeline's viewport edges: `{ start, end }` ISO dates (`YYYY-MM-DD`) under the left and right edges, agreeing with the last `visibleRangeChange` event; `null` before the view has drawn. |
| toggle | (id: string): void | id: string | void | Toggle a summary row's collapsed state and redraw. |
| setShowCritical | (show: boolean): void | show: boolean | void | Turn the critical-path highlight on or off and redraw in place, without a remount. The same switch `showCritical` sets at `mountSplit` time. |
| scrollToTask | (id: string | number): boolean | id: string | number | boolean | Bring a task's row and bar into view and 0001976), expanding any collapsed summary that hides it and scrolling the least each axis needs - vertically to the row, horizontally to its bar's start/finish - with no remount. An axis already showing the row/bar is left alone. A task the task table's filter hides is not revealed and the filter is not changed: it returns `false` and fires `task:filtered` with `{ id }`. Also `false` for an unknown id. |
| toggleResource | (resource: string): void | resource: string | void | Expand or collapse a resource's per-task sub-rows in the workload band, and redraw. |
| toggleWorkloadGroup | (group: string | null): void | group: string | null | void | Collapse or expand one `workload.groupBy` group of the workload band, and redraw. `null` or `''` names the "no group" group. |
| setWorkloadBookings | (bookings: GanttWorkloadExternal[]): void | bookings: GanttWorkloadExternal[] | void | Replace the workload band's `external` bookings and redraw, the live add / update / remove the standalone resource view applies. |
| getWorkloadUnit | (): 'hours' | 'percent' | 'cost' | - | 'hours' | 'percent' | 'cost' | The unit the workload band (and, when mounted, the histogram band) is currently drawing in,. |
| setWorkloadUnit | (unit: 'hours' | 'percent' | 'cost'): void | unit: 'hours' | 'percent' | 'cost' | void | Switch the workload and histogram bands to hours, percent-of-capacity or cost,, redraw, and announce the change in the live region. Round-trips through the controller's `getState`/`setState`. A no-op when neither band is mounted. |
| getColumnWidths | (): Record<string, number> | - | Record<string, number> | Every left-panel column's current rendered width, keyed by column `key` - every column, not merely a resized one, so restoring this exact snapshot reproduces the layout on screen. Round-trips through the controller's `getState`/`setState`. |
| setColumnWidths | (widths: Record<string, number>): void | widths: Record<string, number> | void | Restore column widths from {@link getColumnWidths}. Each is clamped to that column's own `minWidth`/`maxWidth` and skipped for an unknown key or a `resizable: false` column, then the view redraws. |
| getTableStates | (): { task: GridState | null; workload: GridState | null; histogram: GridState | null } | null | - | { task: GridState | null; workload: GridState | null; histogram: GridState | null } | null | Each real-grid table's own view state - column order, widths, visibility, pin, sort and filters - when `createGrid` was injected; `null` otherwise. Round-trips through the controller's `getState`/`setState` under `tables`. |
| setTableStates | (states: { task?: GridState | null; workload?: GridState | null; histogram?: GridState | null }): void | states: { task?: GridState | null; workload?: GridState | null; histogram?: GridState | null } | void | Restore table states from {@link getTableStates} and redraw. |
| getIssuesPanelState | (): { open: boolean } | null | - | { open: boolean } | null | The issues panel's open/collapsed flag, or `null` when no panel is mounted. Round-trips through the controller's `getState`/`setState` under `issuesPanel`. |
| setIssuesPanelState | (state: { open?: boolean }): void | state: { open?: boolean } | void | Restore the issues panel's open/collapsed flag from {@link getIssuesPanelState}. |
| getDataBandState | (): Record<string, { collapsed: boolean; table: GridState | null }> | null | - | Record<string, { collapsed: boolean; table: GridState | null }> | null | Each data band's collapsed flag and grid state, by band id, or `null` when no data band is mounted. Round-trips through the controller's `getState`/`setState` under `dataBands`. |
| setDataBandState | (state: Record<string, { collapsed?: boolean; table?: GridState | null }>): void | state: Record<string, { collapsed?: boolean; table?: GridState | null }> | void | Restore {@link getDataBandState}'s output and redraw. |
| toggleDataBand | (id: string, collapsed?: boolean): boolean | id: stringcollapsed?: boolean | boolean | Collapse a data band to its header strip or open it; omit `collapsed` to toggle. Returns whether it is now collapsed. |
| getDataBandTable | (id: string): { id: string; title: string; columns: Array<{ id: string; title: string }>; rows: Array<Array<string | number | null>> } | null | id: string | { id: string; title: string; columns: Array<{ id: string; title: string }>; rows: Array<Array<string | number | null>> } | null | A data band as a plain table - left-hand texts then one column per bucket with the raw values - for an Excel/CSV writer; `null` for an unknown id. |
| toSVG | (svgOpts?: { range?: { from?: number | string | Date; to?: number | string | Date } }): string | svgOpts?: { range?: { from?: number | string | Date; to?: number | string | Date } } | string | Serialise this split view - the left panel, the timeline and whichever bands are mounted - to a standalone SVG string, drawn with a fixed palette independent of the mounted theme. `''` before anything has been drawn. |
| toPNG | (pngOpts?: { range?: { from?: number | string | Date; to?: number | string | Date }; scale?: number; background?: string; }): Promise<Blob | null> | pngOpts?: { range?: { from?: number | string | Date; to?: number | string | Date }; scale?: number; background?: string; } | Promise<Blob | null> | This split view as a PNG: `toSVG` rasterised through a canvas. Makes no network request. `null` when nothing is drawn or there is no canvas to rasterise with. |
| (printOpts?: { paper?: 'A4' | 'Letter' | 'A3'; orientation?: 'landscape' | 'portrait'; fitToWidth?: boolean; scale?: number; title?: string | false; header?: string | ((page: number, pages: number) => string) | false; footer?: string | ((page: number, pages: number) => string) | false; }): boolean | printOpts?: { paper?: 'A4' | 'Letter' | 'A3'; orientation?: 'landscape' | 'portrait'; fitToWidth?: boolean; scale?: number; title?: string | false; header?: string | ((page: number, pages: number) => string) | false; footer?: string | ((page: number, pages: number) => string) | false; } | boolean | Print this split view - the table, the timeline, and the workload and histogram bands when mounted - paged and scaled: the table header and timeline header repeat on every page and no row is split across a page boundary. Opens the browser's own print path over a standalone document; "PDF" is whatever the browser's print dialog offers ("Save as PDF") - no server, no dependency. Always prints in a fixed, print-safe light palette, independent of the mounted theme. | |
| destroy | (): void | - | void | Detach the view. The host still owns the container. |
Gantt
A headless Gantt controller: holds the model, recomputes on edits, emits changes.
Properties
| Property | Type | Description |
|---|---|---|
| tasks | GanttTask[] | The current tasks, as fresh shallow copies - mutating them changes nothing; call `applyEdit` or `setTasks`. (read-only) |
| dependencies | GanttDependency[] | The current links, as fresh copies, always in the normalised `{ from, to, type, lag }` form. (read-only) |
| schedule | GanttSchedule | null | The latest schedule result. It keeps the last successful one when a recompute fails, so a cycle does not blank the view. (read-only) |
| critical | string[] | The critical task ids from the latest schedule; empty when the last compute failed. (read-only) |
| conflicts | GanttConflict[] | Constraints the latest schedule could not honour (empty when all are satisfied). (read-only) |
| schedulingConflicts | GanttSchedulingConflict[] | The normalised scheduling-conflict set: one entry per cycle, unhonoured constraint, blocked lock or missed deadline, the shape the `schedulingConflict` event and the issues panel consume. A copy; empty when the plan is clean. (read-only) |
| autoSchedule | boolean | Whether the controller was asked to cascade an edit down the dependency chain rather than only recomputing. (read-only) |
| grid | unknown | The grid this controller is bound to, or null for a standalone plan. (read-only) |
| weekStartDay | number | The resolved week-start weekday (0-6, Sunday=0;: the plan's own `weekStartDay` option, else the bound grid's locale default (Monday for en-GB and most locales, Sunday for en-US), else Sunday. (read-only) |
| overAllocations | GanttOverAllocation[] | The over-allocations from the latest schedule. (read-only) |
| resourceLoad | GanttResourceLoad | null | The latest resource-load report, or null before a successful schedule. (read-only) |
| resourceNames | Map<string, string> | The display name for each resource key: a `resources` entry `{ id, name }` is keyed by its `id` - what assignments refer to - but labelled by its `name`, so the views show the human name while every lookup stays keyed on the id. A resource with no distinct `name` maps its key to itself. (read-only) |
| title | string | null | The plan's own title, from `createGantt({ title })`; `null` when none was given. (read-only) |
| rows | { /** * A load resets `changes()`/`hasChanges` to this plan * unless `{ keepChanges: true }` keeps accumulating against the * baseline already held. */ apply(change: { add?: GanttTask[]; update?: GanttTask[]; remove?: Array<string | GanttTask> }, loadOpts?: { keepChanges?: boolean }): { added: GanttTask[]; updated: GanttTask[]; removed: string[]; }; } | The live consumer surface, mirroring `grid.rows.apply`, so a Data Router can drive the Gantt like any other view. Keyed by the controller's rowKey. (read-only) |
| hasChanges | boolean | Whether the plan differs from the committed baseline - the last `commit()`, or the last `setTasks`/`rows.apply` load that did not pass `{ keepChanges: true }`. (read-only) |
| view | unknown | The mounted view, or null. (read-only) |
Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
| setTasks | (tasks: GanttTask[], setOpts?: { keepChanges?: boolean }): GanttSchedule | tasks: GanttTask[]setOpts?: { keepChanges?: boolean } | GanttSchedule | Replace the whole task list (copied in) and recompute, returning the new schedule. Ignored with a warning after `destroy()`. A load resets `changes()`/`hasChanges` to this plan unless `{ keepChanges: true }` keeps accumulating against the baseline already held. |
| setDependencies | (deps: GanttDependency[], editOpts?: { event?: unknown }): GanttSchedule | deps: GanttDependency[]editOpts?: { event?: unknown } | GanttSchedule | Replace the link list and recompute. Adding a link is gated on `beforeDependencyCreate`, so this returns undefined on a veto, or a promise when a handler defers; a pure removal or reorder applies straight away. `editOpts.event` is the originating DOM event when a gesture drove the change; it rides on the change events. |
| deleteDependency | (link: { from: string | number; to: string | number; type?: string }, editOpts?: { event?: unknown }): GanttSchedule | undefined | link: { from: string | number; to: string | number; type?: string }editOpts?: { event?: unknown } | GanttSchedule | undefined | Remove one dependency link, by its `from`/`to`/`type` identity - the selected-link Delete key and the link editor's own delete action both call this. Gated on `beforeDependencyDelete`, so this returns undefined on a veto or when no link matches, or a promise when a handler defers. `editOpts.event` is the originating DOM event, carried on the change events. |
| previewEdit | (patch: { id: string | number; start?: number; duration?: number }): GanttSchedule | null | patch: { id: string | number; start?: number; duration?: number } | GanttSchedule | null | Dry-run a start or duration edit and return the schedule it would produce, without changing the model, emitting `schedule` or writing back. Views call it on every drag frame so dependency arrows follow the bar; returns `null` when the edit cannot be scheduled. |
| applyEdit | (patch: { id: string | number; start?: number; end?: number; duration?: number; percentComplete?: number; work?: number | Array<{ date: number | string | Date; hours: number }>; effort?: number; units?: number; addResource?: string | { resource?: string; name?: string; id?: string; units?: number }; removeResource?: string | { resource?: string; name?: string; id?: string }; assignments?: Array<{ resource: string; units?: number }> }, editOpts?: { writeBack?: boolean; event?: unknown }): GanttSchedule | patch: { id: string | number; start?: number; end?: number; duration?: number; percentComplete?: number; work?: number | Array<{ date: number | string | Date; hours: number }>; effort?: number; units?: number; addResource?: string | { resource?: string; name?: string; id?: string; units?: number }; removeResource?: string | { resource?: string; name?: string; id?: string }; assignments?: Array<{ resource: string; units?: number }> }editOpts?: { writeBack?: boolean; event?: unknown } | GanttSchedule | Apply one task edit and recompute - the single gated choke point every drag, keypress, table cell and workload cell commits through. A `work` ARRAY is the task's per-day contour. Given without an explicit `start`/`end`/`duration` it SETS the span: the task starts on the contour's first day and runs through its last, so booking hours beyond the bar extends it and clearing an edge bucket pulls it back. Conversely, a `start` or `duration` in the patch re-times an existing contour rather than discarding it - a move keeps its shape, a resize stretches it across the new span at the same daily levels. `assignments` replaces the task's whole roster in ONE edit - what the split view's assignment picker commits - so several adds, removes and units changes are a single change event. It reschedules as a `units` edit to the roster's total under the task's `schedulingMode` (`units` is 1 for 100%); on a fixed-duration, effort-driven task a changed set of resources splits the held total as `addResource` does. An empty roster clears the assignments and leaves the duration alone. |
| moveRow | (taskId: string | number, target?: { parent?: string | number | null; index?: number; position?: 'before' | 'after' | 'inside'; target?: string | number | null; event?: unknown }): GanttSchedule | undefined | Promise<GanttSchedule | undefined> | taskId: string | numbertarget?: { parent?: string | number | null; index?: number; position?: 'before' | 'after' | 'inside'; target?: string | number | null; event?: unknown } | GanttSchedule | undefined | Promise<GanttSchedule | undefined> | Move or re-parent one row: the choke point a name-column drag handle, a drop onto another row, Alt+Up/Down and Tab/Shift+Tab all funnel through. Gated on the cancellable `beforeRowMove` event; once allowed, the task's parent and position among its new siblings are written through the live re-parent path as one undo step, and `rowMove` fires. |
| compute | (): GanttSchedule | - | GanttSchedule | Recompute the schedule now and return it. On success it emits `schedule` and refreshes the resource load; on a cycle or bad input it emits `error` and leaves the previous schedule in place. |
| taskByWbs | (code: string): GanttTask | null | code: string | GanttTask | null | The raw task carrying a given WBS outline code, or `null` when none does, or before any successful schedule. Reads the latest schedule's derived codes, so it reflects the plan's current order rather than a code a since-moved task used to carry. |
| findViolations | (): GanttViolation[] | - | GanttViolation[] | The tasks placed earlier than CPM allows - the "manual with validation" flag. Empty when every placement is feasible, and when there is no successful schedule. |
| resources | (loadOpts?: { resources?: GanttResourceSpec; defaultCapacity?: number }): GanttResourceLoad | loadOpts?: { resources?: GanttResourceSpec; defaultCapacity?: number } | GanttResourceLoad | Compute the resource load and over-allocations on demand, optionally overriding the capacities for this call. |
| previewAssignments | (taskId: string | number, roster: Array<{ resource: string; units?: number }>): GanttResourceLoad | taskId: string | numberroster: Array<{ resource: string; units?: number }> | GanttResourceLoad | The resource load as it WOULD be if a task carried the given roster: the same load calculation over the current schedule with that one task's assignments swapped, so a picker can warn of an over-allocation before anything is committed. The model is not touched and nothing is emitted. |
| cost | (costOpts?: { costField?: string }): GanttCostResult | costOpts?: { costField?: string } | GanttCostResult | Compute resource-rate cost for the current plan: a resource's hourly `rate` (its dated `rates` changes honoured day by day), a `'material'` resource's cost per unit, or a `'cost'` resource's flat amount per assignment, plus each task's own fixed `cost` field - per task, rolled up to summaries and the project. |
| level | (levelOpts?: { dryRun?: boolean; writeBack?: boolean; priorityField?: string; maxIterations?: number; resources?: GanttResourceSpec; defaultCapacity?: number; }): GanttLevelResult | levelOpts?: { dryRun?: boolean; writeBack?: boolean; priorityField?: string; maxIterations?: number; resources?: GanttResourceSpec; defaultCapacity?: number; } | GanttLevelResult | Resolve resource over-allocation by shifting tasks later - resource leveling. Honours the CPM dependencies and the working-time calendar. Mutates the model unless `{ dryRun: true }`; with `{ writeBack: true }` and a bound grid the moved tasks are pushed through the grid's edit surface. |
| toCSV | (csvOpts?: { dates?: boolean }): string | csvOpts?: { dates?: boolean } | string | Export the scheduled tasks as CSV; `{ dates: true }` writes ISO dates. |
| toMSPDI | (xmlOpts?: { hoursPerDay?: number; projectName?: string }): string | xmlOpts?: { hoursPerDay?: number; projectName?: string } | string | Export the current plan as Microsoft Project (MSPDI) XML: tasks, dependencies, constraints, baseline, resources and assignments, plus the working-time calendar, serialised with the computed schedule. |
| toSVG | (svgOpts?: { range?: { from?: number | string | Date; to?: number | string | Date } }): string | svgOpts?: { range?: { from?: number | string | Date; to?: number | string | Date } } | string | Serialise the mounted view to a standalone SVG string: the plain view's chart, or the split view's table, timeline and whichever bands are mounted. `''` when nothing is mounted or drawn. |
| toPNG | (pngOpts?: { range?: { from?: number | string | Date; to?: number | string | Date }; scale?: number; background?: string; }): Promise<Blob | null> | pngOpts?: { range?: { from?: number | string | Date; to?: number | string | Date }; scale?: number; background?: string; } | Promise<Blob | null> | The mounted view as a PNG. `null` when nothing is mounted or drawn, or there is no canvas to rasterise with. |
| (printOpts?: { paper?: 'A4' | 'Letter' | 'A3'; orientation?: 'landscape' | 'portrait'; fitToWidth?: boolean; scale?: number; title?: string | false; header?: string | ((page: number, pages: number) => string) | false; footer?: string | ((page: number, pages: number) => string) | false; }): boolean | printOpts?: { paper?: 'A4' | 'Letter' | 'A3'; orientation?: 'landscape' | 'portrait'; fitToWidth?: boolean; scale?: number; title?: string | false; header?: string | ((page: number, pages: number) => string) | false; footer?: string | ((page: number, pages: number) => string) | false; } | boolean | Print the mounted SPLIT view - paged and scaled, with the table header and timeline header repeating on every page - through the browser's own print path; "PDF" is the browser's own print-to- PDF, no server and no dependency. Needs a split view (`mountSplit`): the plain view has no table or bands to print, and this warns once and returns `false` without one. | |
| on | (event: GanttEventName, fn: (payload: GanttEventPayloads[GanttEventName]) => void): () => void | event: GanttEventNamefn: (payload: GanttEventPayloads[GanttEventName]) => void | () => void | Register an event listener; returns a function that unsubscribes. What each event carries is {@link GanttEventPayloads}; the listener is declared with the widest of them, so narrow on the name inside it. |
| off | (event: GanttEventName, fn: (payload: GanttEventPayloads[GanttEventName]) => void): void | event: GanttEventNamefn: (payload: GanttEventPayloads[GanttEventName]) => void | void | Remove a listener registered with `on`. |
| undo | (): boolean | - | boolean | Undo the most recent plan edit, restoring the exact prior plan - tasks, dependencies, assignments and work contours - and re-running scheduling to the identical result. A coalesced drag and a whole `level()` run each undo as one step. Emits `schedule` (views redraw) and `history`. Returns `false` when there is nothing to undo. History is session only: it is cleared by `setTasks`/`setState` and lost on reload, and is NOT part of `getState`/`setState`. |
| redo | (): boolean | - | boolean | Redo the most recently undone edit. Returns `false` when there is nothing to redo; the redo branch is discarded the moment a new edit is made. |
| canUndo | (): boolean | - | boolean | Whether there is an edit to undo. |
| canRedo | (): boolean | - | boolean | Whether there is an edit to redo. |
| clearHistory | (): void | - | void | Forget the whole undo/redo timeline and emit `history`. History is also cleared by `setTasks`/`setState` and lost on reload; this is the explicit reset after loading a fresh plan through `rows.apply`. |
| changes | (): GanttChanges | - | GanttChanges | The plan changes accumulated since the last `commit()` or load: diffed fresh against the committed baseline on every call, so an add immediately removed, or an edit undone back to the baseline, nets to nothing. |
| commit | (): void | - | void | Mark the plan clean: the tasks, dependencies and assignments as they stand now become the new baseline `changes()` diffs against, and `hasChanges` drops to `false`, firing `dirtyChange` when it was `true`. Leaves the undo/redo timeline untouched. |
| select | (ids: string | string[]): string[] | ids: string | string[] - one task id or several | string[] the selection now, in selection order | Select one or more tasks by id. Adds to the current selection rather than replacing it (the inverse of {@link deselect}), with the last id becoming the range anchor a later Shift gesture grows from. Fires one `selectionChange` with `cause: 'api'` when the selection actually changes, and the mounted view reflects it onto the grid rows and the timeline bars. Unknown or already-selected ids are harmless. |
| deselect | (ids: string | string[]): string[] | ids: string | string[] - one task id or several | string[] the selection now, in selection order | Deselect one or more tasks by id: removes them, leaving the rest - call `gantt.deselect(gantt.selected())` to clear. Fires one `selectionChange` with `cause: 'api'` when the selection changes. |
| selected | (): string[] | - | string[] | The selected task ids, in selection order. The whole selection, including tasks the current view has collapsed away or filtered out - they stay selected (see {@link selectedHidden}). |
| selectedHidden | (): string[] | - | string[] | The selected task ids that are not currently visible: still selected, but collapsed under a summary row or hidden by a filter. Empty when no view has drawn yet. |
| copyTasks | (ids: Array<string | number>): string | null | ids: Array<string | number> - the task ids to copy | string | null the text form, or `null` when nothing was copied | Copy tasks and their subtrees to the Gantt clipboard: every field with assignments, and the links between two copied tasks (a link to an outside task is dropped on paste). The tab-separated text form (name, start, end, duration, percent) is also written to the system clipboard where the browser allows it. Ctrl/Cmd+C does the same on the selection. |
| cutTasks | (ids: Array<string | number>): string | null | ids: Array<string | number> - the task ids to cut | string | null the text form, or `null` when nothing was cut | Cut tasks and their subtrees: copy, then remove them and their links as one undo step. Refused when `beforeTaskDelete` is vetoed. Ctrl/Cmd+X does the same on the selection. |
| pasteTasks | (target?: { below?: string | number; childOf?: string | number; text?: string }): string[] | null | Promise<string[] | null> | target?: { below?: string | number; childOf?: string | number; text?: string } | string[] | null | Promise<string[] | null> the new ids (empty on a veto), `null` when there is nothing to paste, or a Promise of either when a handler deferred | Paste the Gantt clipboard as new tasks with new ids, below a task or as its children, as one undo step, gated on `beforeTaskPaste`. With no target it pastes below the last selected task, else at the end of the plan. `text` pastes tab-separated rows (name, start, end, duration, percent) instead. Ctrl/Cmd+V pastes below the focused row, Ctrl/Cmd+Shift+V as its children. |
| addTask | (task?: Partial<GanttTask>, target?: { above?: string | number; below?: string | number; child?: string | number; successorOf?: string | number; predecessorOf?: string | number; event?: unknown; interactive?: boolean }): string | null | Promise<string | null> | task?: Partial<GanttTask>target?: { above?: string | number; below?: string | number; child?: string | number; successorOf?: string | number; predecessorOf?: string | number; event?: unknown; interactive?: boolean } | string | null | Promise<string | null> the new id, `null` on a veto or an unknown anchor, or a Promise of either when a handler deferred | Add a task above, below, as a child of, as a successor of or as a predecessor of another, as one undo step, gated on the vetoable `beforeTaskAdd`. A successor or predecessor is linked finish-to-start. Omitted fields default: a catalogue name, a duration of 1, and the anchor's start (a successor is placed by its link). The new task is selected. Insert adds below the focused row, Shift+Insert as its child. An interface-driven call (`event` or `interactive`) on a read-only anchor is refused for child, successor and predecessor. |
| convertToMilestone | (id: string | number, opts?: { event?: unknown; interactive?: boolean }): GanttSchedule | undefined | Promise<GanttSchedule | undefined> | id: string | number - the task idopts?: { event?: unknown; interactive?: boolean } | GanttSchedule | undefined | Promise<GanttSchedule | undefined> the recomputed schedule, `undefined` on a veto, or a Promise of either when a handler deferred | Convert a task to a milestone - duration 0, drawn as a diamond - as one undo step, gated on the vetoable `beforeMilestoneConvert`. A summary is refused (`reason: 'summary'`). |
| convertToTask | (id: string | number, opts?: { duration?: number; event?: unknown; interactive?: boolean }): GanttSchedule | undefined | Promise<GanttSchedule | undefined> | id: string | number - the task idopts?: { duration?: number; event?: unknown; interactive?: boolean } | GanttSchedule | undefined | Promise<GanttSchedule | undefined> the recomputed schedule, `undefined` on a veto, or a Promise of either when a handler deferred | Convert a milestone back to an ordinary task of `duration` days (default 1) as one undo step, gated on `beforeMilestoneConvert`. |
| deleteTask | (id: string | number, opts?: { event?: unknown; interactive?: boolean }): GanttSchedule | undefined | Promise<GanttSchedule | undefined> | id: string | number - the task idopts?: { event?: unknown; interactive?: boolean } | GanttSchedule | undefined | Promise<GanttSchedule | undefined> the recomputed schedule, `undefined` on a veto, or a Promise of either when a handler deferred | Delete one task and every link touching it, as one undo step, gated on the vetoable `beforeTaskDelete`; the Delete key and the default menu's "Delete task" call it. A read-only task refuses an interface-driven call (`event` or `interactive`) with `reason: 'readOnly'`. |
| scrollToTask | (id: string | number): boolean | id: string | number | boolean | Scroll a task's row into view in the mounted split view; the same as `scrollToTask` on the view `mountSplit` returned. A task the task table's filter hides is not revealed: it returns `false` and fires `task:filtered` with `{ id }`. |
| mount | (container: unknown, options?: { /** * The plot width. `'container'` (the default) measures the element it was * mounted into and keeps following it, so a plan in a tab, drawer, * accordion or split pane fits without the host writing a * `ResizeObserver`; a container with no box yet holds a * 720px fallback rather than drawing at zero. A number is honoured * exactly and installs no observer. Ignored under `zoom`, which warns. */ width?: number | 'container'; rowHeight?: number; labelWidth?: number; rowLabels?: boolean; showArrows?: boolean; showCritical?: boolean; showProgress?: boolean; /** * Whether hovering a row or bar highlights that task's whole dependency * chain - itself, every predecessor and every successor - across the rows, * bars and links (default true). `false` turns the highlight off. The * split view's `mountSplit` version also tells the three roles apart with * theme-token colours and names the chain in the hovered bar's accessible * description. */ hoverChain?: boolean; dateAxis?: boolean; /** * The today line, as a plan day-number or a calendar date. A date is * converted into plan space through `projectEpoch`, so * "put the line on the real today" is expressible for a relative plan. */ today?: number | string | Date; /** * Vertical date markers: one labelled full-height line * per entry at its `date`, or a shaded range for a `{ from, to }` entry. * Drawn in both the plain and split views, redrawn at every zoom. See * {@link GanttMarker}. */ markers?: GanttMarker[]; /** * The calendar date plan day 0 stands for. * * Display-only: axis ticks, bar labels, tooltips, screen-reader text and * the built-in `'weekends'` shading move with it; the schedule, `getState` * and the CSV/MSPDI exports do not. Without it, the engine's contract makes * day 0 the Unix epoch, which is why a plan written as day offsets renders * as January 1970. A host-supplied `nonWorking` function still receives raw * plan days. */ projectEpoch?: number | string | Date | null; nonWorking?: 'weekends' | ((day: number) => boolean); label?: GanttBarLabel | ((task: GanttScheduledTask) => string); /** * A per-task bar colour, drawn on a task's bar, summary * bar or milestone with a matching darker progress fill. Return a CSS * colour for a task, or null/undefined to fall back to that task's own * `colour` field, then to the theme token. It is a data colour, written * literally rather than through a `--lattice-*` token, so it reads the * same in light and dark themes. */ barColour?: (task: GanttScheduledTask) => string | null | undefined; /** Whether bars can be dragged to move/resize (default true). */ editable?: boolean; /** Pixels from a bar's right edge that begin a resize rather than a move. */ resizeZone?: number; /** Time-scale zoom: a level, or raw pixels-per-day. Omit to fit the width. */ zoom?: GanttZoom | number; /** Scroll so the today line is in view after drawing. */ scrollToToday?: boolean; /** * The task tooltip: a positioned card on hover and * keyboard focus, anchored to the bar. Default true. See {@link GanttTooltip}. */ tooltip?: GanttTooltip; /** Group tasks into swimlanes by a task property name or `fn(task)`. */ groupBy?: string | ((task: GanttTask) => unknown); /** Keyboard editing + focusable bars + ARIA announcements (default true). */ keyboard?: boolean; /** Days a keyboard arrow moves/resizes a task (default 1). */ moveStep?: number; /** The percent a progress-handle drag snaps to, and an Alt+Shift+arrow progress edit steps by; clamped results stay within 0-100. Default `5`. */ progressStep?: number; }): unknown | container: unknownoptions?: { /** * The plot width. `'container'` (the default) measures the element it was * mounted into and keeps following it, so a plan in a tab, drawer, * accordion or split pane fits without the host writing a * `ResizeObserver`; a container with no box yet holds a * 720px fallback rather than drawing at zero. A number is honoured * exactly and installs no observer. Ignored under `zoom`, which warns. */ width?: number | 'container'; rowHeight?: number; labelWidth?: number; rowLabels?: boolean; showArrows?: boolean; showCritical?: boolean; showProgress?: boolean; /** * Whether hovering a row or bar highlights that task's whole dependency * chain - itself, every predecessor and every successor - across the rows, * bars and links (default true). `false` turns the highlight off. The * split view's `mountSplit` version also tells the three roles apart with * theme-token colours and names the chain in the hovered bar's accessible * description. */ hoverChain?: boolean; dateAxis?: boolean; /** * The today line, as a plan day-number or a calendar date. A date is * converted into plan space through `projectEpoch`, so * "put the line on the real today" is expressible for a relative plan. */ today?: number | string | Date; /** * Vertical date markers: one labelled full-height line * per entry at its `date`, or a shaded range for a `{ from, to }` entry. * Drawn in both the plain and split views, redrawn at every zoom. See * {@link GanttMarker}. */ markers?: GanttMarker[]; /** * The calendar date plan day 0 stands for. * * Display-only: axis ticks, bar labels, tooltips, screen-reader text and * the built-in `'weekends'` shading move with it; the schedule, `getState` * and the CSV/MSPDI exports do not. Without it, the engine's contract makes * day 0 the Unix epoch, which is why a plan written as day offsets renders * as January 1970. A host-supplied `nonWorking` function still receives raw * plan days. */ projectEpoch?: number | string | Date | null; nonWorking?: 'weekends' | ((day: number) => boolean); label?: GanttBarLabel | ((task: GanttScheduledTask) => string); /** * A per-task bar colour, drawn on a task's bar, summary * bar or milestone with a matching darker progress fill. Return a CSS * colour for a task, or null/undefined to fall back to that task's own * `colour` field, then to the theme token. It is a data colour, written * literally rather than through a `--lattice-*` token, so it reads the * same in light and dark themes. */ barColour?: (task: GanttScheduledTask) => string | null | undefined; /** Whether bars can be dragged to move/resize (default true). */ editable?: boolean; /** Pixels from a bar's right edge that begin a resize rather than a move. */ resizeZone?: number; /** Time-scale zoom: a level, or raw pixels-per-day. Omit to fit the width. */ zoom?: GanttZoom | number; /** Scroll so the today line is in view after drawing. */ scrollToToday?: boolean; /** * The task tooltip: a positioned card on hover and * keyboard focus, anchored to the bar. Default true. See {@link GanttTooltip}. */ tooltip?: GanttTooltip; /** Group tasks into swimlanes by a task property name or `fn(task)`. */ groupBy?: string | ((task: GanttTask) => unknown); /** Keyboard editing + focusable bars + ARIA announcements (default true). */ keyboard?: boolean; /** Days a keyboard arrow moves/resizes a task (default 1). */ moveStep?: number; /** The percent a progress-handle drag snaps to, and an Alt+Shift+arrow progress edit steps by; clamped results stay within 0-100. Default `5`. */ progressStep?: number; } | unknown | Render the plan into a container as an SVG timeline (bars, dependency arrows, critical-path highlight, today line, non-working shading, milestones, progress). The view redraws when the schedule recomputes. |
| mountSplit | (container: unknown, options?: { /** * The view's height in pixels. A number sets the root * to exactly that many pixels and the rows scroll inside. Omitted (or * `null`), the view fills its host container when the container has a * definite height - the rows scroll inside and the view follows the * container as it resizes - and otherwise sizes to its rows up to a * sensible maximum. */ height?: number | null; rowHeight?: number; headerHeight?: number; gridWidth?: number; indent?: number; /** * Whether a task name in the plain (no `createGrid`) table wraps onto * further lines, growing its row to fit, instead of staying on one line * with a trailing ellipsis. Off by default - the full * name is still available on hover (native `title`) and to a screen * reader (accessible name). */ wrapNames?: boolean; zoom?: GanttZoom | number; /** Whether a two-finger pinch over the timeline steps the zoom one level; the cause reads `'gesture'`. Default true; `false` leaves the page's own touch handling alone. */ pinchZoom?: boolean; /** * The built-in zoom control: a row of day / week / * month / quarter / fit-to-plan buttons, each a real keyboard-operable * `<button aria-pressed>` labelled from the gantt catalogue, drawn in * the view's own header. `true` takes the defaults; an object sets * `position` and `levels`. See {@link GanttZoomControlOptions}. Falsy * draws none. */ zoomControl?: boolean | GanttZoomControlOptions; today?: number; /** * Vertical date markers: one labelled full-height line * per entry at its `date`, or a shaded range for a `{ from, to }` entry, * drawn over the timeline and labelled in the time header at every zoom. * See {@link GanttMarker}. */ markers?: GanttMarker[]; nonWorking?: 'weekends' | ((day: number) => boolean); calendar?: GanttCalendar | null; showArrows?: boolean; showProgress?: boolean; showBaseline?: boolean; /** * Whether hovering a row or bar highlights that task's whole dependency * chain: itself, every predecessor and every successor, * across the left panel's rows, the timeline's bars and the links between * them (default true). The highlight distinguishes the three roles with * theme tokens - the hovered task in the accent colour, predecessors in * the success colour, successors in the info colour - and the hovered * bar's accessible description names the chain (`N predecessors, M * successors`), so the highlight explains itself. `false` turns the * highlight off entirely. */ hoverChain?: boolean; /** * Whether to mark the zero-float critical path: critical * bars and links drawn in the critical colour. Default true; `false` marks * nothing. Toggle it live with {@link GanttSplitView.setShowCritical}. */ showCritical?: boolean; /** * How a critical bar is marked. `'fill'` (the default) * redraws it in the critical fill; `'outline'` keeps the bar’s own fill * colour and only adds the critical outline, so the marking sits over * per-task colours. */ criticalStyle?: GanttCriticalStyle; barLabel?: GanttBarLabel | ((task: GanttScheduledTask) => string); /** Where a bar’s own label is drawn: `inside` centres it on the bar, `right` just past its edge, `auto` inside only when it fits; a label too wide for its bar falls back to the right under `inside` or `auto`. */ barLabelPosition?: GanttBarLabelPosition; /** The percent a progress-handle drag snaps to, and an Alt+Shift+arrow progress edit steps by; clamped results stay within 0-100. Default `5`. */ progressStep?: number; /** * A per-task bar colour, drawn on a task’s bar, summary * bar or milestone with a matching darker progress fill. Return a CSS * colour for a task, or null/undefined to fall back to that task’s own * `colour` field, then to the theme token. It is a data colour, written * literally rather than through a `--lattice-*` token, so it reads the * same in light and dark themes. */ barColour?: (task: GanttScheduledTask) => string | null | undefined; /** * The locale the built-in `start` and `end` columns format their dates in *. Falls back to `gridConfig.locale`, then the bound * grid’s own locale, then the page’s. `en-GB` makes the columns read * `dd/mm/yyyy`. Overridden by `dateFormat`. */ locale?: string; /** * The weekday the week starts on (0-6, Sunday=0;: * drives the header's week band, the week zoom level's column starts * and the workload/histogram week buckets, all through one resolver so * they can't disagree. Left unset, it falls back to `locale`'s own * default (Monday for en-GB and most locales, Sunday for en-US) via * `Intl.Locale`'s `weekInfo`, else a small table, else Sunday. */ weekStartDay?: number; /** * How the built-in `start` and `end` columns format their dates *: a token pattern (`'dd/MM/yyyy'`) or a function. The * function receives the grid’s format params - `value` is the stored ISO * date, with the grid’s `locale` alongside. Overrides the * locale-derived default; a per-column `dateFormat` overrides this. */ dateFormat?: string | ((p: { value: unknown; locale?: string }) => string); /** * Surface earned-value metrics in `kind: 'evm'` columns. * `true` computes EVM at the today line (or the project finish); an object * overrides the status date and the cost field names. */ evm?: boolean | { statusDate?: number | string | Date; costField?: string; actualCostField?: string }; /** * The S-curve status-date progress line: a vertical line * at the status date that jogs, at each task row, to that task’s actual * progress point (start + %complete × duration). `true` uses the today line * (or the project finish); an object sets the status date explicitly. The * same status date drives the `plannedPercentComplete` column. */ progressLine?: boolean | { statusDate?: number | string | Date }; /** * Whether the plan can be edited: pointer drags on the timeline and the * left panel’s inline cell editors (default true). Same meaning and * default as `mount`'s. */ editable?: boolean; /** * Keyboard editing + focusable bars + ARIA announcements on the timeline * (default true). Same meaning and default as `mount`'s. */ keyboard?: boolean; /** * Pixels from a bar’s right edge that begin a resize rather than a move * (default 6). Same meaning and default as `mount`'s. */ resizeZone?: number; /** * The task tooltip: the SAME positioned card `mount` * draws, anchored to the hovered or keyboard-focused bar. Default true. * See {@link GanttTooltip}. */ tooltip?: GanttTooltip; /** * A `{ t(key, params) }` resolver for the view’s own text - the live * region’s edit announcements. Omit it and a gantt bound to a grid borrows * that grid’s catalogue; a standalone plan falls back to English. */ messages?: { t: (key: string, params?: Record<string, unknown>) => string }; /** * The columns of the left panel. `kind` decides what the cell shows and what a * double-click edits: `'name'` the WBS tree (edits the name), `'assignee'` * the avatars, `'progress'` the % ring (edits `percentComplete`), `'evm'` * an earned-value `metric`, and `'start'`/`'end'`/`'duration'` the * scheduled window - an ISO date, an ISO date, and a whole number of days, * each of which edits the plan through the same path a bar drag takes *. `'effort'` (hours) and `'units'` (a percentage, 100 = * one full-time resource) are the optional effort-driven columns *; each edits through `applyEdit`, so the task * reschedules per its `schedulingMode`. `'slack'` shows * the task’s total float in whole working days, read-only. `'unscheduled'` * is a read-only boolean column that marks the tasks with * no dates or duration yet; on a real grid table it filters like any * boolean column. `'wbs'` is a read-only column * showing each task's derived outline code (`1`, `1.1`, `1.2`, `2`, …), * sortable on a real grid table under the same tree-aware sort * as every other column. A bare string names a built-in * column, so `['name', 'duration', 'effort', 'units']` is enough. On an * editable plan the assignee cell opens the assignment picker. A column * with no `kind` shows the raw task’s `key` and edits it only with * `editable: true`. * * Every column header carries a draggable right border with a * col-resize cursor: a pointer drag, or arrow keys * once the border itself is focused (8px a press, 32px with Shift). * `minWidth`/`maxWidth` clamp how far it goes; `resizable: false` omits * the border entirely. The table/timeline divider (`gridWidth`) is the * same gesture; a settled drag or keystroke on either fires exactly one * `columnResize`, and every width - the divider’s included, under the * key `'grid'` - round-trips through `getState`/`setState`. */ columns?: Array<'name' | 'start' | 'end' | 'duration' | 'assignee' | 'progress' | 'effort' | 'units' | 'slack' | 'deadline' | 'unscheduled' | 'earlyStart' | 'lateStart' | 'lateFinish' | 'baselineStart' | 'baselineEnd' | 'variance' | 'note' | 'sequence' | 'constraint' | 'constraintDate' | 'wbs' | 'predecessors' | 'successors' | { key: string; title?: string; width?: number; kind?: 'name' | 'assignee' | 'progress' | 'evm' | 'plannedPercentComplete' | 'start' | 'end' | 'duration' | 'effort' | 'units' | 'slack' | 'deadline' | 'unscheduled' | 'earlyStart' | 'lateStart' | 'lateFinish' | 'baselineStart' | 'baselineEnd' | 'variance' | 'note' | 'sequence' | 'constraint' | 'constraintDate' | 'wbs' | 'predecessors' | 'successors' | 'number'; metric?: 'bac' | 'pv' | 'ev' | 'ac' | 'sv' | 'cv' | 'spi' | 'cpi'; digits?: number; editable?: boolean; editField?: string; dateFormat?: string | ((p: { value: unknown; locale?: string }) => string); /** The narrowest this column can be dragged/keyed to; default 160 for `kind: 'name'`, 40 otherwise. */ minWidth?: number; /** The widest this column can be dragged/keyed to; default unbounded. */ maxWidth?: number; /** `false` omits the resize border on this column’s header; default true. */ resizable?: boolean; render?: (task: GanttScheduledTask, ctx: { rawTask: GanttTask; depth: number }) => unknown; } | GanttGridColumn>; /** * The host’s grid factory, injected rather than * imported: with it, the task table and the bands' resource tables are * real Lattice grids configured with the normal column API (`columns`, * `workload.columns`, `histogram.columns`), row heights and vertical * scroll locked to the timeline. */ createGrid?: (element: unknown, config: GridConfig) => Grid; /** * Further grid configuration every table grid is created with, e.g. `{ theme, locale }`. * The split view builds its tables with no cell ranges and no fill handle (the task table keeps row * selection and editing); this is laid over those defaults, so `{ selection: { ranges: true, fillHandle: true } }` * restores both. `band` is laid over it again for the workload and histogram band tables only. */ gridConfig?: Partial<GridConfig> & { band?: Partial<GridConfig> }; /** * Draw a small corner mark, theme token * `--lattice-dirty-mark`) on every built-in task-table cell whose field * differs from the committed baseline - cleared the instant `commit()` * runs or the plan loads clean. Default false; needs `createGrid`, since * the mark is a cell `classWhen` rule on the real grid's built-in * columns. */ dirtyMarks?: boolean; /** * A resource workload band beneath the split view: * one row per resource on the left and, on the right, that resource’s * hours per time bucket - aligned column-for-column with the timeline’s * scale header, scroll-locked to it horizontally (vertically it scrolls * on its own), and redrawn in the same paint as the bars whenever the * plan changes. `true` takes the defaults below; an object overrides * them; omitted, no band is drawn. * * **Editing.** A resource row expands (a disclosure * button, `aria-expanded`) into one sub-row per task it carries. The * resource’s own cell is the read-only aggregate; a SUB-ROW cell accepts * a typed number of hours on a double-click whenever the view’s * `editable` is on. What is typed is written to that task’s `work` * contour through the same `applyEdit` choke point (and the same * `beforeTaskEdit` veto) a bar drag uses, so the bar, the table row and * the band all move in one paint - including the span, which follows the * contour: type into a column beyond the bar and the bar grows to reach * it. A bucket containing no working day declines the edit and says so. * * **Where the hours come from.** They are DERIVED from the tasks, never * supplied: a task’s own `work` (or `hours`) field when it carries a * finite one, otherwise `working days × hoursPerDay × units`, divided * between the task’s assignments in proportion to their units and spread * evenly over the working days the task spans. Working days are the days * this view already shades - pass the `calendar`/`nonWorking` option the * plan is scheduled with. Resources, units and capacities are the gantt’s * existing vocabulary (`assignee`/`assignees`/`owner`/`assignments` on a * task; `resources`/`defaultCapacity` on `createGantt`); a task naming no * resource is carried on an "Unassigned" row rather than dropped. An * empty bucket is blank, not `0`, and a bucket over * `capacity × hoursPerDay × the bucket’s working days` is marked with a * class and an accessible label. */ workload?: boolean | { /** The band's resource-table columns when `createGrid` is injected: built-in ids `'resource'` (required) and `'total'`, overrides of them, and host columns, including {@link GanttBandColumn} ones with their own `value`/`render`. */ columns?: Array<string | GanttGridColumn | GanttBandColumn>; /** Hours a full-time (`units: 1`) resource works in a working day; default 8. */ hoursPerDay?: number; /** The band’s height in pixels, taken from the view’s own `height`; default 160. */ height?: number; /** * The band's maximum height in pixels: the band sizes * to its rows (the header, one row per resource and sub-row, and the * totals row) and caps at this many pixels, scrolling on its own past * it instead of squeezing the plan - which always keeps at least half * the view. Omitted, the band keeps its fixed `height`. */ maxHeight?: number; /** A band row’s height in pixels; default 28. */ rowHeight?: number; /** Maximum decimal places in a cell, trailing zeros dropped; default 1. */ decimals?: number; /** Draw the totals row and totals column; default true. */ totals?: boolean; /** * `'hours'` (default; an existing page is unchanged) draws booked * hours; `'percent'` draws hours as a share of the resource’s own * available hours that bucket - `booked ÷ available × 100`, the SAME * available-hours figure the over-capacity highlight already uses * (the per-resource calendar included), falling back to hours for a * resource with no known capacity; a bucket with a KNOWN zero (a * part-timer’s non-working day, a week wholly on leave) reads "-", * never `Infinity`/`NaN`. An `<button * aria-pressed>` switch in the band’s header toggles it (and the * histogram band’s, when one is mounted) live; the same switch is * reachable off the view as `setWorkloadUnit`/`getWorkloadUnit`. * Editing in percent mode writes HOURS: typing `50` books half the * resource’s available hours in that bucket, through the identical * `applyEdit` path an hours edit uses. `'cost'` draws hours × the * resource’s rate in force each day, read off the * SAME `computeWorkload` pass, falling back to hours for a resource * with no rate data; cost has no capacity concept, so there is no * over-highlight or capacity line in this unit. */ unit?: 'hours' | 'percent' | 'cost'; /** * Utilisation thresholds colouring each cell (and the histogram * band’s bars, when one is mounted) by how full the bucket is *: each entry is `{ at, className | colour }`, * `at` a fraction of the bucket’s available hours (0.8 = 80% * booked), and either a `className` added to the cell/bar or a * `colour` applied inline (spelled `colour` or `color`). The highest * threshold a bucket reaches wins. Applies in every unit - hours, * percent and cost - because the fraction is always read off the * hours figures. Omitted, no threshold colouring. */ thresholds?: Array<{ at: number; className?: string; colour?: string; color?: string }>; /** Draw the Unassigned row (default true); `false` hides it. */ showUnassigned?: boolean; /** * Group the resource rows by a resource field (e.g. `'department'`) or * a function of the resource spec: one collapsible * header row per group, carrying the group's summed load and available * hours, with that group's resources beneath it. Unset, no grouping. */ groupBy?: string | ((resource: Record<string, unknown>) => unknown); /** * Load-only bookings from other projects: hours * that count in this band, the histogram and the band’s own * over-allocation highlight, and are listed as "other project" * sub-rows under their resource, but that name no task - never drawn * as a row or a bar, and never touching the critical path, the * project finish or a summary. See {@link GanttWorkloadExternal}. * The equivalent per-task spelling is a task’s own `loadOnly: true` * flag. */ external?: GanttWorkloadExternal[]; /** * The resource-name column’s width in pixels, * sized independently of the task table’s first column (which is what * it followed before, so a narrow WBS column truncated every name). * Defaults to the band’s own label width; the full name is always * carried as a title. */ labelWidth?: number; /** * The hover/focus tooltip on a bucket cell: `false` * hides it, and a function supplies the host's own content, receiving * the cell payload ({@link GanttWorkloadCellEvent}) and returning a * `string`, a DOM node, or `null` to show nothing. Omitted, the * built-in card lists the contributing tasks. */ tooltip?: boolean | ((payload: GanttWorkloadCellEvent) => string | Node | null | undefined); }; /** * A resource histogram band beneath the split view, below the workload * band when both are mounted: one row per resource, * drawn as bars per time bucket rather than a table of numbers, with a * capacity line and a hover/focus tooltip. `true` takes the defaults * below; an object overrides them; omitted, no band is drawn. * * The bars come from the SAME derivation the workload band uses * (`computeWorkload`, driven off `work`/`hours`/`assignments` exactly as * documented on `workload` above) - this is a second PICTURE of the same * numbers, not a second source of them. The non-stacked bar for a bucket * over capacity carries both the normal fill up to capacity and the * over-allocation fill above it; `stacked: 'task'` additionally splits * the within-capacity portion into one coloured segment per contributing * task, still capped by the one over-allocation segment on top. An * accessible table (resource, period, hours/percent, capacity, * over-allocated) is rebuilt alongside the bars on every redraw. * * Mounted alongside `workload` at a fixed `height`, the two bands share * it rather than squeezing the timeline: the timeline keeps at least * half the view, the bands shrink in proportion when both do not * otherwise fit, and - rather than ever reading as an unreadably thin * sliver of bars - the histogram collapses to its header strip (with a * console warning naming the height it needs) when even its * proportional share would leave too little room for the plot. */ histogram?: boolean | { /** The band's resource-table columns when `createGrid` is injected: the built-in id `'resource'` (required), an override of it, and host columns, including {@link GanttBandColumn} ones with their own `value`/`render`. */ columns?: Array<string | GanttGridColumn | GanttBandColumn>; /** Which resources to draw, and in what order; omitted draws every resource `computeWorkload` finds, Unassigned last. */ resources?: string[]; /** `'hours'` (default) draws raw hours; `'percent'` draws hours as a percentage of capacity, falling back to `'hours'` for a resource with no capacity; `'cost'` draws hours × rate, falling back to `'hours'` for a resource with no rate data, with no capacity line in this unit. */ unit?: 'hours' | 'percent' | 'cost'; /** `'task'` stacks one segment per contributing task; `false` (default) draws one solid bar per bucket. */ stacked?: 'task' | false; /** The band’s total height in pixels; default 160. */ height?: number; /** * The band's maximum height in pixels, exactly as the * workload band's `maxHeight`: the band sizes to its rows and caps at * this many pixels, scrolling on its own past it instead of squeezing * the plan. Omitted, the band keeps its fixed `height`. */ maxHeight?: number; /** A band row’s height in pixels; default 48. */ rowHeight?: number; /** * The resource-name column’s width in pixels, * sized independently of the task table’s first column, exactly as * the workload band’s `labelWidth`; the full name is always carried * as a title. */ labelWidth?: number; }; /** * The row/bar context menu: a function returning the * menu items for a task, shown in the grid’s own menu component on a * right-click (or Menu key / Shift+F10) over that task’s table row or * timeline bar. Omit it and no context menu is installed - the grid’s * own cell menu is left alone. See {@link GanttContextMenu}. */ contextMenu?: GanttContextMenu; /** * A collapsible scheduling-conflicts issues panel inside the view chrome *: lists each conflict with a count badge, keyboard * operable, a click that scrolls to and highlights the tasks, and the fix * actions safe for each kind (remove the offending link, set the * constraint to as-soon-as-possible, unlock the task, clear the deadline), * each one undo step. `false` (the default) draws no panel. */ issuesPanel?: boolean; /** * Linked data bands under the gantt and the resource bands *: each a measure per task (or summary, resource or * host group) per timeline bucket, with totals, formats, thresholds, * optional editing and a series that follows its task when it moves. * See {@link GanttDataBand}. */ dataBands?: GanttDataBand[]; }): GanttSplitView | container: unknownoptions?: { /** * The view's height in pixels. A number sets the root * to exactly that many pixels and the rows scroll inside. Omitted (or * `null`), the view fills its host container when the container has a * definite height - the rows scroll inside and the view follows the * container as it resizes - and otherwise sizes to its rows up to a * sensible maximum. */ height?: number | null; rowHeight?: number; headerHeight?: number; gridWidth?: number; indent?: number; /** * Whether a task name in the plain (no `createGrid`) table wraps onto * further lines, growing its row to fit, instead of staying on one line * with a trailing ellipsis. Off by default - the full * name is still available on hover (native `title`) and to a screen * reader (accessible name). */ wrapNames?: boolean; zoom?: GanttZoom | number; /** Whether a two-finger pinch over the timeline steps the zoom one level; the cause reads `'gesture'`. Default true; `false` leaves the page's own touch handling alone. */ pinchZoom?: boolean; /** * The built-in zoom control: a row of day / week / * month / quarter / fit-to-plan buttons, each a real keyboard-operable * `<button aria-pressed>` labelled from the gantt catalogue, drawn in * the view's own header. `true` takes the defaults; an object sets * `position` and `levels`. See {@link GanttZoomControlOptions}. Falsy * draws none. */ zoomControl?: boolean | GanttZoomControlOptions; today?: number; /** * Vertical date markers: one labelled full-height line * per entry at its `date`, or a shaded range for a `{ from, to }` entry, * drawn over the timeline and labelled in the time header at every zoom. * See {@link GanttMarker}. */ markers?: GanttMarker[]; nonWorking?: 'weekends' | ((day: number) => boolean); calendar?: GanttCalendar | null; showArrows?: boolean; showProgress?: boolean; showBaseline?: boolean; /** * Whether hovering a row or bar highlights that task's whole dependency * chain: itselfevery predecessor and every successor* across the left panel's rowsthe timeline's bars and the links between * them (default true). The highlight distinguishes the three roles with * theme tokens - the hovered task in the accent colourpredecessors in * the success coloursuccessors in the info colour - and the hovered * bar's accessible description names the chain (`N predecessors, M * successors`)so the highlight explains itself. `false` turns the * highlight off entirely. */ hoverChain?: boolean; /** * Whether to mark the zero-float critical path: critical * bars and links drawn in the critical colour. Default true; `false` marks * nothing. Toggle it live with {@link GanttSplitView.setShowCritical}. */ showCritical?: boolean; /** * How a critical bar is marked. `'fill'` (the default) * redraws it in the critical fill; `'outline'` keeps the bar’s own fill * colour and only adds the critical outlineso the marking sits over * per-task colours. */ criticalStyle?: GanttCriticalStyle; barLabel?: GanttBarLabel | ((task: GanttScheduledTask) => string); /** Where a bar’s own label is drawn: `inside` centres it on the bar, `right` just past its edge, `auto` inside only when it fits; a label too wide for its bar falls back to the right under `inside` or `auto`. */ barLabelPosition?: GanttBarLabelPosition; /** The percent a progress-handle drag snaps to, and an Alt+Shift+arrow progress edit steps by; clamped results stay within 0-100. Default `5`. */ progressStep?: number; /** * A per-task bar colour, drawn on a task’s bar, summary * bar or milestone with a matching darker progress fill. Return a CSS * colour for a task, or null/undefined to fall back to that task’s own * `colour` field, then to the theme token. It is a data colour, written * literally rather than through a `--lattice-*` token, so it reads the * same in light and dark themes. */ barColour?: (task: GanttScheduledTask) => string | null | undefined; /** * The locale the built-in `start` and `end` columns format their dates in *. Falls back to `gridConfig.locale`, then the bound * grid’s own locale, then the page’s. `en-GB` makes the columns read * `dd/mm/yyyy`. Overridden by `dateFormat`. */ locale?: string; /** * The weekday the week starts on (0-6, Sunday=0;: * drives the header's week band, the week zoom level's column starts * and the workload/histogram week buckets, all through one resolver so * they can't disagree. Left unset, it falls back to `locale`'s own * default (Monday for en-GB and most locales, Sunday for en-US) via * `Intl.Locale`'s `weekInfo`, else a small table, else Sunday. */ weekStartDay?: number; /** * How the built-in `start` and `end` columns format their dates *: a token pattern (`'dd/MM/yyyy'`) or a function. The * function receives the grid’s format params - `value` is the stored ISO * date, with the grid’s `locale` alongside. Overrides the * locale-derived default; a per-column `dateFormat` overrides this. */ dateFormat?: string | ((p: { value: unknown; locale?: string }) => string); /** * Surface earned-value metrics in `kind: 'evm'` columns. * `true` computes EVM at the today line (or the project finish); an object * overrides the status date and the cost field names. */ evm?: boolean | { statusDate?: number | string | Date; costField?: string; actualCostField?: string }; /** * The S-curve status-date progress line: a vertical line * at the status date that jogs, at each task row, to that task’s actual * progress point (start + %complete × duration). `true` uses the today line * (or the project finish); an object sets the status date explicitly. The * same status date drives the `plannedPercentComplete` column. */ progressLine?: boolean | { statusDate?: number | string | Date }; /** * Whether the plan can be edited: pointer drags on the timeline and the * left panel’s inline cell editors (default true). Same meaning and * default as `mount`'s. */ editable?: boolean; /** * Keyboard editing + focusable bars + ARIA announcements on the timeline * (default true). Same meaning and default as `mount`'s. */ keyboard?: boolean; /** * Pixels from a bar’s right edge that begin a resize rather than a move * (default 6). Same meaning and default as `mount`'s. */ resizeZone?: number; /** * The task tooltip: the SAME positioned card `mount` * draws, anchored to the hovered or keyboard-focused bar. Default true. * See {@link GanttTooltip}. */ tooltip?: GanttTooltip; /** * A `{ t(key, params) }` resolver for the view’s own text - the live * region’s edit announcements. Omit it and a gantt bound to a grid borrows * that grid’s catalogue; a standalone plan falls back to English. */ messages?: { t: (key: string, params?: Record<stringunknown>) => string }; /** * The columns of the left panel. `kind` decides what the cell shows and what a * double-click edits: `'name'` the WBS tree (edits the name), `'assignee'` * the avatars, `'progress'` the % ring (edits `percentComplete`), `'evm'` * an earned-value `metric`, and `'start'`/`'end'`/`'duration'` the * scheduled window - an ISO date, an ISO date, and a whole number of days, * each of which edits the plan through the same path a bar drag takes *. `'effort'` (hours) and `'units'` (a percentage, 100 = * one full-time resource) are the optional effort-driven columns *; each edits through `applyEdit`, so the task * reschedules per its `schedulingMode`. `'slack'` shows * the task’s total float in whole working days, read-only. `'unscheduled'` * is a read-only boolean column that marks the tasks with * no dates or duration yet; on a real grid table it filters like any * boolean column. `'wbs'` is a read-only column * showing each task's derived outline code (`1`, `1.1`, `1.2`, `2`, …), * sortable on a real grid table under the same tree-aware sort * as every other column. A bare string names a built-in * column, so `['name', 'duration', 'effort', 'units']` is enough. On an * editable plan the assignee cell opens the assignment picker. A column * with no `kind` shows the raw task’s `key` and edits it only with * `editable: true`. * * Every column header carries a draggable right border with a * col-resize cursor: a pointer drag, or arrow keys * once the border itself is focused (8px a press, 32px with Shift). * `minWidth`/`maxWidth` clamp how far it goes; `resizable: false` omits * the border entirely. The table/timeline divider (`gridWidth`) is the * same gesture; a settled drag or keystroke on either fires exactly one * `columnResize`, and every width - the divider’s included, under the * key `'grid'` - round-trips through `getState`/`setState`. */ columns?: Array<'name' | 'start' | 'end' | 'duration' | 'assignee' | 'progress' | 'effort' | 'units' | 'slack' | 'deadline' | 'unscheduled' | 'earlyStart' | 'lateStart' | 'lateFinish' | 'baselineStart' | 'baselineEnd' | 'variance' | 'note' | 'sequence' | 'constraint' | 'constraintDate' | 'wbs' | 'predecessors' | 'successors' | { key: string; title?: string; width?: number; kind?: 'name' | 'assignee' | 'progress' | 'evm' | 'plannedPercentComplete' | 'start' | 'end' | 'duration' | 'effort' | 'units' | 'slack' | 'deadline' | 'unscheduled' | 'earlyStart' | 'lateStart' | 'lateFinish' | 'baselineStart' | 'baselineEnd' | 'variance' | 'note' | 'sequence' | 'constraint' | 'constraintDate' | 'wbs' | 'predecessors' | 'successors' | 'number'; metric?: 'bac' | 'pv' | 'ev' | 'ac' | 'sv' | 'cv' | 'spi' | 'cpi'; digits?: number; editable?: boolean; editField?: string; dateFormat?: string | ((p: { value: unknown; locale?: string }) => string); /** The narrowest this column can be dragged/keyed to; default 160 for `kind: 'name'`, 40 otherwise. */ minWidth?: number; /** The widest this column can be dragged/keyed to; default unbounded. */ maxWidth?: number; /** `false` omits the resize border on this column’s header; default true. */ resizable?: boolean; render?: (task: GanttScheduledTask, ctx: { rawTask: GanttTask; depth: number }) => unknown; } | GanttGridColumn>; /** * The host’s grid factory, injected rather than * imported: with it, the task table and the bands' resource tables are * real Lattice grids configured with the normal column API (`columns`, * `workload.columns`, `histogram.columns`), row heights and vertical * scroll locked to the timeline. */ createGrid?: (element: unknown, config: GridConfig) => Grid; /** * Further grid configuration every table grid is created with, e.g. `{ theme, locale }`. * The split view builds its tables with no cell ranges and no fill handle (the task table keeps row * selection and editing); this is laid over those defaults, so `{ selection: { ranges: true, fillHandle: true } }` * restores both. `band` is laid over it again for the workload and histogram band tables only. */ gridConfig?: Partial<GridConfig> & { band?: Partial<GridConfig> }; /** * Draw a small corner mark, theme token * `--lattice-dirty-mark`) on every built-in task-table cell whose field * differs from the committed baseline - cleared the instant `commit()` * runs or the plan loads clean. Default false; needs `createGrid`, since * the mark is a cell `classWhen` rule on the real grid's built-in * columns. */ dirtyMarks?: boolean; /** * A resource workload band beneath the split view: * one row per resource on the left and, on the right, that resource’s * hours per time bucket - aligned column-for-column with the timeline’s * scale header, scroll-locked to it horizontally (vertically it scrolls * on its own), and redrawn in the same paint as the bars whenever the * plan changes. `true` takes the defaults below; an object overrides * them; omitted, no band is drawn. * * **Editing.** A resource row expands (a disclosure * button, `aria-expanded`) into one sub-row per task it carries. The * resource’s own cell is the read-only aggregate; a SUB-ROW cell accepts * a typed number of hours on a double-click whenever the view’s * `editable` is on. What is typed is written to that task’s `work` * contour through the same `applyEdit` choke point (and the same * `beforeTaskEdit` veto) a bar drag uses, so the bar, the table row and * the band all move in one paint - including the span, which follows the * contour: type into a column beyond the bar and the bar grows to reach * it. A bucket containing no working day declines the edit and says so. * * **Where the hours come from.** They are DERIVED from the tasks, never * supplied: a task’s own `work` (or `hours`) field when it carries a * finite one, otherwise `working days × hoursPerDay × units`, divided * between the task’s assignments in proportion to their units and spread * evenly over the working days the task spans. Working days are the days * this view already shades - pass the `calendar`/`nonWorking` option the * plan is scheduled with. Resources, units and capacities are the gantt’s * existing vocabulary (`assignee`/`assignees`/`owner`/`assignments` on a * task; `resources`/`defaultCapacity` on `createGantt`); a task naming no * resource is carried on an "Unassigned" row rather than dropped. An * empty bucket is blank, not `0`, and a bucket over * `capacity × hoursPerDay × the bucket’s working days` is marked with a * class and an accessible label. */ workload?: boolean | { /** The band's resource-table columns when `createGrid` is injected: built-in ids `'resource'` (required) and `'total'`, overrides of them, and host columns, including {@link GanttBandColumn} ones with their own `value`/`render`. */ columns?: Array<string | GanttGridColumn | GanttBandColumn>; /** Hours a full-time (`units: 1`) resource works in a working day; default 8. */ hoursPerDay?: number; /** The band’s height in pixels, taken from the view’s own `height`; default 160. */ height?: number; /** * The band's maximum height in pixels: the band sizes * to its rows (the header, one row per resource and sub-row, and the * totals row) and caps at this many pixels, scrolling on its own past * it instead of squeezing the plan - which always keeps at least half * the view. Omitted, the band keeps its fixed `height`. */ maxHeight?: number; /** A band row’s height in pixels; default 28. */ rowHeight?: number; /** Maximum decimal places in a cell, trailing zeros dropped; default 1. */ decimals?: number; /** Draw the totals row and totals column; default true. */ totals?: boolean; /** * `'hours'` (default; an existing page is unchanged) draws booked * hours; `'percent'` draws hours as a share of the resource’s own * available hours that bucket - `booked ÷ available × 100`, the SAME * available-hours figure the over-capacity highlight already uses * (the per-resource calendar included), falling back to hours for a * resource with no known capacity; a bucket with a KNOWN zero (a * part-timer’s non-working day, a week wholly on leave) reads "-", * never `Infinity`/`NaN`. An `<button * aria-pressed>` switch in the band’s header toggles it (and the * histogram band’s, when one is mounted) live; the same switch is * reachable off the view as `setWorkloadUnit`/`getWorkloadUnit`. * Editing in percent mode writes HOURS: typing `50` books half the * resource’s available hours in that bucket, through the identical * `applyEdit` path an hours edit uses. `'cost'` draws hours × the * resource’s rate in force each day, read off the * SAME `computeWorkload` pass, falling back to hours for a resource * with no rate data; cost has no capacity concept, so there is no * over-highlight or capacity line in this unit. */ unit?: 'hours' | 'percent' | 'cost'; /** * Utilisation thresholds colouring each cell (and the histogram * band’s bars, when one is mounted) by how full the bucket is *: each entry is `{ at, className | colour }`, * `at` a fraction of the bucket’s available hours (0.8 = 80% * booked), and either a `className` added to the cell/bar or a * `colour` applied inline (spelled `colour` or `color`). The highest * threshold a bucket reaches wins. Applies in every unit - hours, * percent and cost - because the fraction is always read off the * hours figures. Omitted, no threshold colouring. */ thresholds?: Array<{ at: number; className?: string; colour?: string; color?: string }>; /** Draw the Unassigned row (default true); `false` hides it. */ showUnassigned?: boolean; /** * Group the resource rows by a resource field (e.g. `'department'`) or * a function of the resource spec: one collapsible * header row per group, carrying the group's summed load and available * hours, with that group's resources beneath it. Unset, no grouping. */ groupBy?: string | ((resource: Record<string, unknown>) => unknown); /** * Load-only bookings from other projects: hours * that count in this band, the histogram and the band’s own * over-allocation highlight, and are listed as "other project" * sub-rows under their resource, but that name no task - never drawn * as a row or a bar, and never touching the critical path, the * project finish or a summary. See {@link GanttWorkloadExternal}. * The equivalent per-task spelling is a task’s own `loadOnly: true` * flag. */ external?: GanttWorkloadExternal[]; /** * The resource-name column’s width in pixels, * sized independently of the task table’s first column (which is what * it followed before, so a narrow WBS column truncated every name). * Defaults to the band’s own label width; the full name is always * carried as a title. */ labelWidth?: number; /** * The hover/focus tooltip on a bucket cell: `false` * hides it, and a function supplies the host's own content, receiving * the cell payload ({@link GanttWorkloadCellEvent}) and returning a * `string`, a DOM node, or `null` to show nothing. Omitted, the * built-in card lists the contributing tasks. */ tooltip?: boolean | ((payload: GanttWorkloadCellEvent) => string | Node | null | undefined); }; /** * A resource histogram band beneath the split view, below the workload * band when both are mounted: one row per resource, * drawn as bars per time bucket rather than a table of numbers, with a * capacity line and a hover/focus tooltip. `true` takes the defaults * below; an object overrides them; omitted, no band is drawn. * * The bars come from the SAME derivation the workload band uses * (`computeWorkload`, driven off `work`/`hours`/`assignments` exactly as * documented on `workload` above) - this is a second PICTURE of the same * numbers, not a second source of them. The non-stacked bar for a bucket * over capacity carries both the normal fill up to capacity and the * over-allocation fill above it; `stacked: 'task'` additionally splits * the within-capacity portion into one coloured segment per contributing * task, still capped by the one over-allocation segment on top. An * accessible table (resource, period, hours/percent, capacity, * over-allocated) is rebuilt alongside the bars on every redraw. * * Mounted alongside `workload` at a fixed `height`, the two bands share * it rather than squeezing the timeline: the timeline keeps at least * half the view, the bands shrink in proportion when both do not * otherwise fit, and - rather than ever reading as an unreadably thin * sliver of bars - the histogram collapses to its header strip (with a * console warning naming the height it needs) when even its * proportional share would leave too little room for the plot. */ histogram?: boolean | { /** The band's resource-table columns when `createGrid` is injected: the built-in id `'resource'` (required), an override of it, and host columns, including {@link GanttBandColumn} ones with their own `value`/`render`. */ columns?: Array<string | GanttGridColumn | GanttBandColumn>; /** Which resources to draw, and in what order; omitted draws every resource `computeWorkload` finds, Unassigned last. */ resources?: string[]; /** `'hours'` (default) draws raw hours; `'percent'` draws hours as a percentage of capacity, falling back to `'hours'` for a resource with no capacity; `'cost'` draws hours × rate, falling back to `'hours'` for a resource with no rate data, with no capacity line in this unit. */ unit?: 'hours' | 'percent' | 'cost'; /** `'task'` stacks one segment per contributing task; `false` (default) draws one solid bar per bucket. */ stacked?: 'task' | false; /** The band’s total height in pixels; default 160. */ height?: number; /** * The band's maximum height in pixels, exactly as the * workload band's `maxHeight`: the band sizes to its rows and caps at * this many pixels, scrolling on its own past it instead of squeezing * the plan. Omitted, the band keeps its fixed `height`. */ maxHeight?: number; /** A band row’s height in pixels; default 48. */ rowHeight?: number; /** * The resource-name column’s width in pixels, * sized independently of the task table’s first column, exactly as * the workload band’s `labelWidth`; the full name is always carried * as a title. */ labelWidth?: number; }; /** * The row/bar context menu: a function returning the * menu items for a task, shown in the grid’s own menu component on a * right-click (or Menu key / Shift+F10) over that task’s table row or * timeline bar. Omit it and no context menu is installed - the grid’s * own cell menu is left alone. See {@link GanttContextMenu}. */ contextMenu?: GanttContextMenu; /** * A collapsible scheduling-conflicts issues panel inside the view chrome *: lists each conflict with a count badge, keyboard * operable, a click that scrolls to and highlights the tasks, and the fix * actions safe for each kind (remove the offending link, set the * constraint to as-soon-as-possible, unlock the task, clear the deadline), * each one undo step. `false` (the default) draws no panel. */ issuesPanel?: boolean; /** * Linked data bands under the gantt and the resource bands *: each a measure per task (or summary, resource or * host group) per timeline bucket, with totals, formats, thresholds, * optional editing and a series that follows its task when it moves. * See {@link GanttDataBand}. */ dataBands?: GanttDataBand[]; } | GanttSplitView | Mount the JOINED split view: one continuous, row-aligned surface with a left task-grid panel - by default the Task Name tree with expand/collapse, start, finish, duration, assignee avatars and a circular % ring, plus any host columns - and the right timeline, sharing a single vertical scroll so every grid row lines up exactly with its bar row. The timeline scrolls horizontally on its own. Composes the controller's schedule; makes no change to grid core. The plan is editable from BOTH panes: every gesture `mount` has - pointer drag to move, drag on the right edge to resize, arrow-key move, Shift+arrow resize, `l` to link, Delete - works on the timeline here, and a `start`/`end`/`duration`/`progress`/`name` column in the left panel is inline-editable on a double-click. Both routes commit through the same `applyEdit` choke point, so `beforeTaskMove`, `beforeTaskResize`, `beforeProgressChange` and `beforeTaskEdit` stay the single veto whichever pane the edit came from. The three switches that govern it carry the same meaning and the same defaults as `mount`'s: `editable` (default true) turns every edit on or off, both panes at once; `keyboard` (default true) turns off the focusable bars, the arrow-key gestures and the ARIA announcements while leaving pointer editing alone; and `resizeZone` (default 6) is how many pixels in from a bar's right edge begin a resize rather than a move. `workload` adds the resource band beneath the plan, which is display-only - it reports hours, it does not accept them. |
| captureBaseline | (): Array<{ id: string; baselineStart: number; baselineEnd: number; baselineDuration: number }> | - | Array<{ id: string; baselineStart: number; baselineEnd: number; baselineDuration: number }> | Capture a baseline (planned) snapshot of the current schedule as HOST data (this does not mutate the tasks). Store it and feed it back as `baselineStart`/`baselineEnd` task fields to get variance and ghost bars. |
| earnedValue | (evmOpts?: { statusDate?: number | string | Date; costField?: string; actualCostField?: string }): GanttEarnedValue | evmOpts?: { statusDate?: number | string | Date; costField?: string; actualCostField?: string } | GanttEarnedValue | Compute earned-value (EVM) metrics for the current plan at a status date: PV/EV/AC and the derived SV/CV/SPI/CPI per task, rolled up to summaries and the project. Budget (BAC) is the task's `cost`, or its duration when no cost is given; AC comes from `actualCost`. |
| sCurve | (scurveOpts?: { statusDate?: number | string | Date; bucket?: 'day' | 'week' | 'month'; costField?: string; actualCostField?: string }): GanttSCurvePoint[] | scurveOpts?: { statusDate?: number | string | Date; bucket?: 'day' | 'week' | 'month'; costField?: string; actualCostField?: string } | GanttSCurvePoint[] | Compute the cumulative S-curve for the current plan - one `{ date, pv, ev, ac }` point per time bucket from the project start to the project finish. The same arithmetic `earnedValue` performs, evaluated over time: PV climbs to the total budget at the finish, EV and AC stop at the status date. |
| mountSCurve | (container: unknown, scurveOpts?: { width?: number; height?: number; bucket?: 'day' | 'week' | 'month'; statusDate?: number | string | Date; costField?: string; actualCostField?: string }): { refresh(): void; destroy(): void } | container: unknownscurveOpts?: { width?: number; height?: number; bucket?: 'day' | 'week' | 'month'; statusDate?: number | string | Date; costField?: string; actualCostField?: string } | { refresh(): void; destroy(): void } | Mount a cumulative PV/EV/AC S-curve chart: three lines, a vertical status-date marker, a legend with the project SPI and CPI, and a visually hidden table of the points. Redraws on every schedule recompute; the returned view's `refresh()` redraws it now and `destroy()` detaches it. |
| unmount | (): void | - | void | Detach the mounted view, if any. The host still owns the container. |
| destroy | (): void | - | void | Destroy the mounted view, drop the grid subscriptions and clear the listeners. The controller then refuses further edits with a warning; the host still owns the grid and the container. |
Events
| Event | When | Payload | Cancellable |
|---|---|---|---|
| schedule | A recompute succeeded; the payload is the new schedule, resource load and over-allocations included. | GanttSchedule | no |
| error | A recompute failed; the previous schedule is kept and the payload says what was wrong. | GanttScheduleError | no |
| beforeTaskEdit | A task edit that is not a move, a resize or a progress change is about to be applied; cancellable. | GanttTaskEditEvent | yes |
| beforeTaskMove | A task is about to be moved - the patch sets `start` or `end`; cancellable. | GanttTaskEditEvent | yes |
| beforeTaskResize | A task is about to be resized - the patch sets `duration`; cancellable. | GanttTaskEditEvent | yes |
| beforeProgressChange | A task's progress is about to change - the patch sets `percentComplete`; cancellable. | GanttTaskEditEvent | yes |
| beforeMilestoneMove | A milestone is about to be moved - a move patch on a zero-length task; cancellable. | GanttTaskEditEvent | yes |
| beforeDependencyCreate | One or more dependency links are about to be created; cancellable. | GanttDependencyCreateEvent | yes |
| beforeTaskDelete | A task is about to be deleted, along with every link touching it; cancellable. | GanttTaskDeleteEvent | yes |
| beforeDependencyDelete | One link is about to be removed - the selected-link Delete key or the link editor's own delete action; cancellable. | GanttDependencyDeleteEvent | yes |
| beforeRowMove | A row is about to move or re-parent - a drag handle, a drop onto another row, Alt+Up/Down or Tab/Shift+Tab; cancellable. | GanttRowMoveEvent | yes |
| beforeRowDrop | A table row is about to be dropped in a new position - the drop-specific gate of `rowDrop`, carrying `position`/`target`; cancellable. | GanttRowDropEvent | - |
| taskEdit:cancelled | A `beforeTaskEdit` handler refused the edit. | GanttTaskEditCancelledEvent | no |
| taskMove:cancelled | A `beforeTaskMove` handler refused the move. | GanttTaskEditCancelledEvent | no |
| taskResize:cancelled | A `beforeTaskResize` handler refused the resize. | GanttTaskEditCancelledEvent | no |
| progressChange:cancelled | A `beforeProgressChange` handler refused the progress change. | GanttTaskEditCancelledEvent | no |
| milestoneMove:cancelled | A `beforeMilestoneMove` handler refused the milestone move. | GanttTaskEditCancelledEvent | no |
| dependencyCreate:cancelled | A `beforeDependencyCreate` handler refused the links. | GanttDependencyCreateCancelledEvent | no |
| taskDelete:cancelled | A `beforeTaskDelete` handler refused the delete. | GanttTaskDeleteCancelledEvent | no |
| beforeTaskPaste | Tasks are about to be pasted - Ctrl/Cmd+V or `pasteTasks`; cancellable. | GanttTaskPasteEvent | yes |
| taskPaste:cancelled | A `beforeTaskPaste` handler refused the paste. | GanttTaskPasteCancelledEvent | no |
| beforeTaskAdd | A task is about to be added - `addTask`, Insert or the default menu; cancellable. | GanttTaskAddEvent | yes |
| taskAdd:cancelled | A `beforeTaskAdd` handler (or a read-only anchor) refused the add. | GanttTaskAddCancelledEvent | no |
| beforeMilestoneConvert | A task is about to become a milestone or a task again; cancellable. | GanttMilestoneConvertEvent | yes |
| milestoneConvert:cancelled | A `beforeMilestoneConvert` handler (or a summary, or a read-only task) refused the conversion. | GanttMilestoneConvertCancelledEvent | no |
| dependencyDelete:cancelled | A `beforeDependencyDelete` handler refused the removal. | GanttDependencyDeleteCancelledEvent | no |
| rowMove:cancelled | A `beforeRowMove` handler refused the move. | GanttRowMoveCancelledEvent | no |
| rowDrop:cancelled | A `beforeRowDrop` handler refused the drop. | GanttRowDropCancelledEvent | no |
| dependencies | The dependency list changed - links were added, removed, or both, already allowed through whichever gate applied. | GanttDependenciesChangedEvent | no |
| history | The undo/redo timeline changed - an edit was recorded, undone, redone or cleared. | GanttHistoryEvent | no |
| columnResize | A split-view column border or the table/timeline divider finished a drag or keyboard resize. Fired once per gesture, never mid-drag. | GanttColumnResizeEvent | no |
| workload:click | A workload band bucket cell was clicked, with that bucket's resource, span and bookings. | GanttWorkloadClickEvent | no |
| workloadCellClick | A workload-band bucket cell or histogram bar was clicked, with the resource, the half-open bucket span and its bookings. | GanttWorkloadCellEvent | no |
| workloadCellDblClick | A workload-band bucket cell or histogram bar was double-clicked; `preventDefault` claims it so a host opens its own editor. | GanttWorkloadCellDblClickEvent | - |
| workloadCellContextMenu | A workload-band bucket cell or histogram bar was right-clicked; `preventDefault` suppresses the browser's own menu. | GanttWorkloadCellContextMenuEvent | - |
| workloadCellHover | The pointer or keyboard focus reached a workload-band bucket cell or histogram bar. | GanttWorkloadCellEvent | no |
| workloadCellHoverEnd | The pointer or keyboard focus left a workload-band bucket cell or histogram bar. | GanttWorkloadCellEvent | no |
| workloadResourceClick | A workload-band resource-name cell was clicked. | GanttWorkloadResourceClickEvent | no |
| dataBand:change | A data band rewrote a task's series - a reschedule shifted or stretched it, or an allowed cell edit wrote a bucket. | GanttDataBandChangeEvent | no |
| beforeDataBandEdit | A data band cell edit is about to be written into the task's series; cancellable. | GanttDataBandEditEvent | yes |
| dataBandEdit:cancelled | A `beforeDataBandEdit` handler refused the edit. | GanttDataBandEditCancelledEvent | no |
| cell:click | A task-table cell was clicked, carrying the task, the column's key and the cell element. | GanttCellClickEvent | no |
| cellClick | A task-table cell was clicked, with `id`/`rawTask`/`task` and the column/value/element. | GanttCellPointerEvent | - |
| cellDblClick | A task-table cell was double-clicked. | GanttCellDblClickEvent | - |
| cellContextMenu | A task-table cell's context menu was requested, by a right-click or the keyboard. | GanttCellContextMenuEvent | - |
| beforeCellEditStart | An editable task-table cell is about to open its editor - a double-click, Enter, F2 or a printable key; cancellable. | GanttBeforeCellEditStartEvent | - |
| cellEditStart | A task-table cell's editor opened, once allowed through `beforeCellEditStart`. | GanttCellEditStartEvent | no |
| cellEditStart:cancelled | A `beforeCellEditStart` handler refused the open, or a key reached it while deferring. | GanttCellEditStartCancelledEvent | no |
| cellEditCommit | A task-table cell's edit committed, with the old and new value. | GanttCellEditCommitEvent | no |
| cellEditCancel | A task-table cell's edit was abandoned - Escape, or a vetoed commit. | GanttCellEditCancelEvent | no |
| rowClick | A task-table row was clicked, alongside `cellClick` for the cell under the pointer. | GanttRowClickEvent | - |
| rowDblClick | A task-table row was double-clicked, alongside `cellDblClick` for the cell under the pointer. | GanttRowDblClickEvent | - |
| task:filtered | `scrollToTask` named a task the task table's filter hides, so it did not scroll. | GanttTaskFilteredEvent | no |
| draw | A completed paint of the timeline and bands, fired once per paint with the drawn range and zoom. | GanttDrawEvent | no |
| beforeTaskExpand | A summary row is about to expand its children; cancellable. | GanttTaskToggleEvent | yes |
| beforeTaskCollapse | A summary row is about to collapse its children; cancellable. | GanttTaskToggleEvent | yes |
| taskExpand | A summary row expanded - pointer, keyboard or the `view.toggle` API. | GanttTaskToggledEvent | no |
| taskCollapse | A summary row collapsed - pointer, keyboard or the `view.toggle` API. | GanttTaskToggledEvent | no |
| taskExpand:cancelled | A `beforeTaskExpand` handler refused the expand. | GanttTaskToggleCancelledEvent | no |
| taskCollapse:cancelled | A `beforeTaskCollapse` handler refused the collapse. | GanttTaskToggleCancelledEvent | no |
| resourceExpand | A workload-band resource row expanded its per-task sub-rows. | GanttResourceToggleEvent | no |
| resourceCollapse | A workload-band resource row collapsed its per-task sub-rows. | GanttResourceToggleEvent | no |
| selectionChange | The task selection changed - a click, a Ctrl/Shift click, an arrow key or the select/deselect API. | GanttSelectionChangeEvent | no |
| rowMove | A row moved or was re-parented - a drag, Alt+Up/Down or Tab/Shift+Tab, once allowed through `beforeRowMove`. | GanttRowMovedEvent | no |
| rowDrop | A row dropped into a new position - `rowMove` plus `position`/`target`, once allowed through `beforeRowDrop`. | GanttRowDroppedEvent | - |
| schedulingConflict | The scheduling-conflict set changed after a recompute - a cycle, constraint, lock or deadline was added or resolved. | GanttSchedulingConflictEvent | no |
| taskAdd | A task was added by a committed change, with its raw row, scheduled record and cause. | GanttTaskChangeEvent | no |
| taskRemove | A task was removed by a committed change, with the row and record it had and its cause. | GanttTaskChangeEvent | no |
| taskUpdate | A task was changed by a committed change - a field-level diff, its scheduled record and cause. | GanttTaskChangeEvent | no |
| dependencyAdd | A dependency link was added by a committed change, with the link and its cause. | GanttDependencyChangeEvent | no |
| dependencyRemove | A dependency link was removed by a committed change, with the link and its cause. | GanttDependencyChangeEvent | no |
| dependencyUpdate | A dependency link was changed in place - a lag or other non-identity edit - with a field-level diff. | GanttDependencyUpdateEvent | no |
| assignmentChange | A task's resource roster changed by a committed change, with the old and new assignments. | GanttAssignmentChangeEvent | no |
| datesChanged | The date-only projection of a committed change: every task whose scheduled dates moved. | GanttDatesChangedEvent | no |
| change | One batch per committed action, carrying every individual data-change event it produced as an array. | GanttChangeBatchEvent | no |
| ready | The controller finished its first successful schedule and every API on it is ready to call; fires once. | GanttReadyEvent | no |
| beforeTaskDrag | A move drag of a bar or milestone passed the drag threshold, or a keyboard move is about to apply; cancellable. | GanttBeforeTaskDragEvent | yes |
| taskDragStart | A move drag started - after `beforeTaskDrag` allowed it, before any position changed. | GanttTaskDragStartEvent | no |
| taskDrag | A move drag frame, at most once per animation frame, with the snapped landing position. | GanttTaskDragEvent | no |
| taskDrag:cancelled | A move drag was refused at its start or abandoned with Escape. | GanttTaskDragCancelledEvent | no |
| beforeTaskResizeStart | A resize drag passed the drag threshold, or a keyboard resize is about to apply; cancellable. | GanttBeforeTaskResizeStartEvent | yes |
| taskResizeStart | A resize started. | GanttTaskResizeStartEvent | no |
| taskPartialResize | A resize drag frame, at most once per animation frame. | GanttTaskResizeEvent | no |
| taskResizeEnd | A resize was released (or a keyboard resize applied), before the commit's own `beforeTaskResize`. | GanttTaskResizeEvent | no |
| beforePercentBarDrag | A progress-handle drag passed the drag threshold, or a keyboard progress edit is about to apply; cancellable. | GanttBeforePercentBarDragEvent | yes |
| percentBarDragStart | A progress-handle drag started. | GanttPercentBarDragStartEvent | no |
| percentBarDrag | A progress-handle drag frame, at most once per animation frame. | GanttPercentBarDragEvent | no |
| percentBarDrop | A progress-handle drag was released (or a keyboard progress edit applied), before the commit's own `beforeProgressChange`. | GanttPercentBarDragEvent | no |
| percentBarDrag:cancelled | A progress-handle drag was refused at its start or abandoned with Escape. | GanttPercentBarDragCancelledEvent | no |
| taskClick | A single click (not a drag release) on a bar, milestone or summary bar, or Enter/Space on a focused one. | GanttTaskClickEvent | no |
| taskDblClick | A double click on a bar, milestone or summary bar; `preventDefault` claims it so a host opens its own editor. | GanttTaskDblClickEvent | yes |
| taskMouseOver | The pointer entered a bar, milestone or summary bar, or keyboard focus reached one. | GanttTaskMouseEvent | no |
| taskMouseOut | The pointer left a bar, milestone or summary bar, or keyboard focus left one. | GanttTaskMouseEvent | no |
| taskContextMenu | A context-menu request on a bar, milestone or summary bar - a right-click or the Menu key / Shift+F10 on a focused row; cancellable. | GanttTaskContextMenuEvent | yes |
| linkClick | A single click on a dependency link's arrow. | GanttLinkClickEvent | - |
| linkDblClick | A double click on a dependency link's arrow, or Enter/Space on a focused one; cancellable, claiming it stops the link editor from opening. | GanttLinkDblClickEvent | - |
| linkContextMenu | A context-menu request on a dependency link's arrow; cancellable. | GanttLinkContextMenuEvent | - |
| linkHover | The pointer entered a dependency link's arrow. | GanttLinkHoverEvent | - |
| tooltipShow | A tooltip is about to be shown for a task; cancellable, a veto keeps the built-in card from showing. | GanttTooltipShowEvent | yes |
| tooltipHide | A shown tooltip was hidden. | GanttTooltipHideEvent | - |
| zoomChange | The split view's time scale changed, with the level before and after and what drove it. | GanttZoomChangeEvent | no |
| visibleRangeChange | The dates under the split view's timeline edges changed, coalesced to once per animation frame. | GanttVisibleRangeChangeEvent | no |
| scroll | The split view's viewport scrolled, with the settled offsets, coalesced to once per animation frame. | GanttScrollEvent | no |
| viewMount | A view - the plain `mount` view or the `mountSplit` split view - mounted and painted its first frame. | GanttViewMountEvent | no |
| viewDestroy | A mounted view was torn down - a remount, `unmount()` or `destroy()`. | GanttViewDestroyEvent | no |
| dirtyChange | Whether the plan differs from the committed baseline flipped: fires once on the transition, never on a second edit made while already dirty or a partial undo. | GanttDirtyChangeEvent | no |
GanttMSPDIModel
The model {@link importMSPDI} returns and {@link exportMSPDI} takes.
| Property | Type | Description |
|---|---|---|
| tasks | GanttTask[] | The plan's tasks, written out with their outline level, summary and milestone flags, constraints, baseline and progress. |
| dependencies | GanttDependency[] | The typed links, written as predecessor links with their lag on the successor task. (optional) |
| resources | GanttResourceSpec | The resource list, written with each resource's capacity as `MaxUnits`. Only the array form is read here: a name-to-capacity map is ignored, and its resources then appear only through the tasks' assignments, at capacity 1. (optional) |
| projectStart | number | string | Date | The project's start date. Defaults to the schedule's own start when a schedule is supplied. (optional) |
| calendar | GanttCalendar | null | The working-time calendar written as the project's base calendar - a `weekends` preset or explicit workdays and holidays. Null writes no calendar. (optional) |
| calendars | GanttCalendars | The named calendars tasks and resources may reference; each is written as its own calendar. (optional) |
| weekStartDay | number | The project's week start (0-6, Sunday=0;, written as MSPDI's own `<WeekStartDay>`. Omitted writes no element. (optional) |
| schedule | GanttSchedule | A computed schedule, so the written start and finish dates are the scheduled ones. Without it (or with a failed one) the tasks' own placements are used. (optional) |