Lattice Grid Buy a licence

api reference

The Gantt module

createGantt and the scheduling engine, the four dependency links, milestones and summaries, editable bars that write back, and the critical path.

API reference › The Gantt module

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.

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
FunctionWhat it does
createGantt({ tasks?, dependencies?, projectStart?, autoSchedule?, grid? })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.
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_TYPESThe four dependency link types, in order: ['FS','SS','FF','SF'].
SCHEDULE_ERRORThe error codes the engine reports rather than throwing (cycle, duplicate-id, unknown-task, bad-duration, bad-link-type, unknown-parent, parent-cycle, …).
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(' | ');

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: 20340,             // a day-number; 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: 720,
});
gantt.view.scrollToToday();  // also callable on demand
gantt.applyEdit({ id: 'design', duration: 7 }); // the view redraws automatically
gantt.unmount();

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.

Split view. The Gantt does not build a grid or the two-pane layout - you create and place a normal Lattice grid over the same task rows, and the Gantt consumes it. Binding is two-way: a drag on the timeline writes back through the grid (cycle 3), and an edit in the grid pane (any editor) reflects on the timeline. v1 assumes both panes share the same row height; view.linkVerticalScroll(el) mirrors vertical scroll so the rows stay aligned.

<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>

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