Skip to content
FluxGantt

API Surface

A hand-written reference for @fluxgantt/core's public API. (An auto-generated reference will follow once the API stabilizes past 1.0.)

createGantt(config)

function createGantt(config: GanttConfig): GanttInstance;

Builds a headless GanttInstance. Throws if the initial tasks/dependencies are invalid (duplicate id, self-link, duplicate pair, or cycle).

Capability mixins

The base instance is state + compute only. Rendering, interaction and import/export each live behind an opt-in mixin on its own subpath, so a consumer only downloads what they compose:

import { createGantt } from '@fluxgantt/core';
import { withRender } from '@fluxgantt/core/render';
import { withInteraction } from '@fluxgantt/core/interaction';
import { withIo } from '@fluxgantt/core/io';
 
const gantt = withIo(withInteraction(withRender(createGantt({ tasks: [/* … */] }))));
MixinSubpathAdds
withRender(instance)@fluxgantt/core/rendermount(), unmount(), refresh()
withInteraction(instance)@fluxgantt/core/interactionNo new methods — wires drag/keyboard/click into every mount()
withIo(instance)@fluxgantt/core/ioexportJson(), exportCsv(), exportSvg(), exportPng(), importJson(), importCsv()
withViewport(instance)@fluxgantt/core/viewportscrollToTask(), scrollToDate(), setDensity(). See Scrolling and density
withConflicts(instance, options?)@fluxgantt/core/conflictsdetectConflicts() and the conflict:detected event. Headless. See Schedule conflicts
withEditHooks(instance, options?)@fluxgantt/core/edit-hooksNo new methods — emits task:activated and task:context-requested so your app can build its own editor and menu. See Edit hooks
withRtl(instance)@fluxgantt/core/rtlNo new methods — mirrors the chart for right-to-left locales when rtl: true; inert otherwise. See Right-to-left charts
withResponsive(instance, options?)@fluxgantt/core/responsiveisCoarsePointer(), getLabelColumnWidth() — and adapts density, hit targets, the label column and touch panning under (pointer: coarse). See Responsive & touch

Each mixin mutates and returns the SAME instance, so application order does not matter — but all of them must be applied before the first mount(): mount() reads the interaction hooks at mount time, so composing withInteraction onto an already-mounted instance takes effect only on the next mount.

The React and Vue wrappers compose withRender + withInteraction + withEditHooks + withViewport + withTheme + withRtl for you, and forward every GanttConfig field. All of them are construction-time only except onTaskChange and theme, which are reactive. The one deliberate deviation from "unset means core default": the wrappers' theme defaults to 'light', not 'auto' (wrapper charts have always been light); pass theme="auto" to follow the OS.

GanttConfig

FieldTypeDefaultPurpose
tasksreadonly TaskInput[][]Initial tasks.
dependenciesreadonly DependencyInput[][]Initial dependencies ({ from, to, type?, lag? }).
calendarWorkingCalendarDEFAULT_CALENDARWorking days/hours/holidays/timezone. Immutable.
viewModeViewMode'week'Timeline zoom (day/week/month/quarter/year).
densityDensity'default'Row density (compact 24px / default 32px / comfortable 40px / touch 48px). 'touch' is what withResponsive selects under a coarse pointer; you can also set it directly. This is the construction-time default; change it at runtime with setDensity().
localestring'en'Renderer locale (dates via Intl). Construction-time only — read once at mount().
ariaLabelstring'Gantt chart'Accessible name of the chart as a whole, and the <title> of every exported SVG/PNG. Construction-time only.
messagesGanttMessages—Accessible-name builders for a non-English host. See i18n. Construction-time only.
readOnlybooleanfalseWhen true, mount() wires no drag gestures (with withInteraction applied; click-select and keyboard nav stay live).
enableDependencyDragbooleantruefalse renders no connector handles and wires no drag-create-dependency; move and resize stay available, and linkTasks() / dependencies are unaffected. Works in both renderers: in Canvas mode the handles are drawn on a hover/selection overlay instead of the painted chart. readOnly: true still wins. Construction-time only.
schedulingModeSchedulingMode'manual''auto' cascades dependents on move/resize.
initialCollapsedreadonly TaskId[][]Tasks to start collapsed. Unknown or leaf ids are dropped silently; fires no collapse:changed.
historyLimitnumber100Undo entries retained. Oldest is evicted past the limit (ring buffer). 0 opts out of undo/redo entirely without affecting anything else. Must be a non-negative integer — throws at construction otherwise. Construction-time only.
canvasViewportHeightnumber600Visible scroll-viewport height, in px, of the mounted container in Canvas mode only (no SVG equivalent). Bounds the <canvas> backing store.
theme'light' | 'dark' | 'auto''auto' in core; 'light' in the React/Vue wrappersInitial theme. Applied by withTheme; the base bundle only carries the field. The wrappers compose withTheme and keep theme reactive.
rtlbooleanfalseMirror the chart for right-to-left locales: earliest date and label column at the right, upright glyphs, native RTL scrolling. Applied by withRtl; the base bundle only carries the field. Construction-time only (rebuild, or change key in a wrapper, to switch).
rollupRollupProvider—Draw each parent at its rolled-up span (earliest descendant start to latest descendant end, duration-weighted progress) instead of its authored dates. Pass the core implementation: rollup: computeRollup (from @fluxgantt/core). Derived on read: getTasks(), export, undo and critical path still see authored dates. Mounted charts only. Construction-time only. See Hierarchy.
onTaskChange(task, prev) => void—Catch-all fired whenever a task field changes.

GanttInstance methods

Tasks

MethodReturnsPurpose
addTask(input)TaskAdd a task; emits task:added.
updateTask(id, patch)TaskPatch fields; emits task:* per changed field.
removeTask(id)voidRemove a task + descendants; emits dependency:removed/task:removed.
moveTask(id, newStart)TaskShift start+end by the same delta; emits task:moved.
resizeTask(id, newDuration)TaskSet duration (working hours); emits task:resized.
setProgress(id, progress)TaskSet progress 0..1; emits task:progressed.
getTask(id)Task | undefinedLook up one task.
getTasks()Task[]All tasks.
findTasks(predicate)Task[]Filter tasks.
duplicateTask(taskId?)Task[]Copy one task, or the whole current selection when taskId is omitted. Each copy starts where its own source ends, resets progress to 0, gets a fresh id, keeps its parent (so it lands as a sibling), and carries no dependency edges. Children are not cloned. One undo step for the whole call. An unresolvable explicit taskId throws; an omitted taskId with nothing selected is a no-op returning [].

Dependencies

MethodReturnsPurpose
linkTasks(from, to, type?, lag?)DependencyLink two tasks (type default 'FS'); emits dependency:added. Throws on cycle/self/duplicate.
unlinkTasks(from, to)voidRemove links between two tasks; emits dependency:removed.
getDependencies()Dependency[]All dependencies.
getDependenciesOf(taskId)Dependency[]Links touching a task.

Selection

Selection is hierarchical: selecting a parent selects every descendant with it, and the "selection" every method here reports is that flattened set.

MethodReturnsPurpose
select(id)voidReplace the selection with id or id[], expanded to include descendants. Ids that don't resolve are dropped silently. Emits selection:changed iff the flattened set changed.
selectAll()voidSelect every task. Emits selection:changed iff the set changed.
deselect()voidClear the selection. Emits selection:changed iff it was non-empty.
getSelection()TaskId[]Snapshot (not a live reference) of the flattened selection — explicit ids and auto-selected descendants. In insertion order, not row order (selecting a parent yields its children first). Returns [] after destroy().

Hierarchy (collapse / expand)

Base — always present, no mixin required; collapse state is headless.

MethodReturnsPurpose
toggleCollapse(id)voidToggle a task's collapsed state, hiding/showing its whole subtree. No-op on a leaf or unknown id. Emits collapse:changed iff the set changed.
isCollapsed(id)booleanWhether id is collapsed and still has children. Safe after destroy() (returns false).
collapseAll()voidCollapse every task that has children. Idempotent.
expandAll()voidExpand everything. Idempotent.

History (undo / redo)

Every mutation through the facade — addTask, updateTask, moveTask, resizeTask, setProgress, removeTask, linkTasks, unlinkTasks, duplicateTask — records one undo entry per logical gesture, not per internal write. A cascade-grouped drag, a multi-select Delete, and a duplicateTask() that produced eight copies each undo in a single step.

None of these are gated by readOnly: that config governs rendered interactivity, not the method surface. All four throw after destroy() except canUndo/canRedo, which return false.

MethodReturnsPurpose
undo()booleanUndo the most recent gesture. false (a safe no-op, no event) when the undo stack is empty.
redo()booleanRedo the next gesture forward. Same no-op posture.
canUndo()booleanWhether undo() would currently do something.
canRedo()booleanSymmetric.

Wire buttons off the history:changed event rather than polling:

gantt.on('history:changed', ({ canUndo, canRedo }) => {
  undoButton.disabled = !canUndo;
  redoButton.disabled = !canRedo;
});

With withInteraction applied, a mounted chart also binds Ctrl/Cmd+Z and Ctrl/Cmd+Shift+Z to these — see the keyboard shortcut table.

Events emitted during an undo() or redo() carry a third meta argument, { source: 'undo' | 'redo' }, so a listener can tell a replayed change from a fresh one:

gantt.on('task:moved', (task, prevStart, meta) => {
  if (meta?.source) return; // replayed by undo/redo — don't re-persist it
  save(task);
});

Viewport (zoom)

MethodReturnsPurpose
zoomTo(mode)voidSwitch view mode ('day'/'week'/'month'/'quarter'/'year'). Works headlessly, before mount(). When mounted, the date centered in the viewport stays centered. No-op when already in mode. Throws on an unknown mode.
zoomIn()voidStep one level toward 'day'. Safe no-op at that boundary.
zoomOut()voidStep one level toward 'year'. Safe no-op at that boundary.
getViewMode()ViewModeThe current view mode. Safe after destroy().

The order is fixed: day → week → month → quarter → year. None of these are gated by readOnly — zoom changes no data. A mounted chart with withInteraction also binds Ctrl/Cmd+Plus / Ctrl/Cmd+Minus and Ctrl+wheel (which is how a macOS trackpad pinch arrives).

Scrolling and density

Added by withViewport (@fluxgantt/core/viewport). Call these after mount() — on a chart above 2000 tasks the Canvas renderer loads asynchronously, so wait for renderer:selected.

In SVG mode the mount container is the scroll viewport: give it a bounded height and overflow: auto (for example style="height: 420px; overflow: auto"). The SVG renderer does not make its container scrollable, so on a container without overflow the browser ignores the scroll offsets. The methods cannot detect that and still return true, because the return value reports that the target exists and a scroll was requested, not that the container moved. Canvas mode sets overflow-y: auto itself.

The date axis stays pinned to the top of the scroll viewport in both modes, and the label column is pinned to the inline-start edge on horizontal scroll as well (in Canvas mode it is a sticky sibling canvas.fg-timeline-canvas-labels, so select the chart with .fg-timeline-canvas, not container > canvas). In SVG mode everything lives in one wrapper, div.fg-timeline__layer, that holds the chart <svg> and three aria-hidden, position: sticky layers: the label column (svg.fg-timeline__sticky-column, clones of each row label and toggle), the date axis (svg.fg-timeline__sticky-header) and the top corner (svg.fg-timeline__sticky-corner). container.querySelector('svg') is still the chart (a container > svg child selector is not: the wrapper is in between), and the exported SVG never contains any layer. Consequences worth knowing:

  • The container can be any display (block, flex, grid): the wrapper is a single in-flow child and aligns to start, so a flex column with an auto height has the chart's height and a flex row with a fixed height still pins.
  • Container padding is a limit. A sticky box pins at the scroller's content edge, so with padding: P the band and the column stay P px inside the container and scrolled rows show in the strip. scrollToTask/scrollToDate are also off by P. Put the spacing on a wrapper.
  • The pinned column's ground is opaque and is var(--fg-bg, #fff): set --fg-bg on the container (or an ancestor) to your page background, or a non-white page shows a white slab behind the names.
  • The names are drawn by the clones; the real labels stay in the chart (accessible names, export) but are painted invisible live. A name wider than labelColumnWidth is therefore cut at the column edge on screen (the export keeps the full name); widen labelColumnWidth for long names.
  • If the page itself scrolls (the container has no overflow), the layers pin to the browser viewport while the page scrolls through the chart. A fixed host navbar will cover the header.
  • Right-to-left pages are supported: the layers follow the chart's inline-start edge.
  • scrollToTask/scrollToDate use one model: align: 'start' parks the row directly under the 32px header, and the bar directly right of the pinned column; the usable area is clientHeight - 32 by clientWidth - labelColumnWidth, in SVG and Canvas alike.
  • Pointer events: a click on a pinned label or toggle behaves exactly like one on the real label (select, Shift/Ctrl, collapse, task:activated, task:context-requested, touch pan). A click on the blank column or the date band clears the selection, like any blank chart space.
  • Tests and host code that count [data-task-id], [data-row-index] or .fg-timeline__row-label over the whole container now also see the clones: scope them to svg.fg-timeline.
  • While mounted, the container's inline scroll-padding-top is 32px so natively focus-scrolled rows stop below the header; your previous inline value is restored on unmount. Firefox ignores scroll-padding when it focuses an SVG row (it scrolls the container to the top); this predates the pinned header and Chromium and WebKit honour it.
MethodReturnsPurpose
scrollToTask(id, options?)booleanScrolls the task's row and bar into view. options.align ('nearest' default / 'start' / 'center' / 'end'), options.behavior ('auto' default / 'smooth'), options.expand (default true: expands collapsed ancestors, firing collapse:changed; false makes it a no-op). Returns false when unmounted or when the row is hidden and expand: false. Throws on an unknown id. Never moves focus or selection — use select(id) + scrollToTask(id) to go to and announce a task.
scrollToDate(date, options?)booleanScrolls horizontally to a date (string, Date, ZonedDateTime or PlainDate). options.align is 'center' (default) / 'start' / 'end'. Returns false when unmounted or when the date lies outside the chart's time range (it still scrolls as far as it can). Throws on an invalid date.
setDensity(density)voidChanges row density at runtime ('compact'/'default'/'comfortable'/'touch'), keeping the top visible row in place, and emits viewport:changed with { viewMode, density }. Works before mount(). Throws on an unknown value.

behavior: 'smooth' falls back to an instant scroll under prefers-reduced-motion: reduce. None of these are gated by readOnly, and none is an undo step.

Calling setDensity() takes density ownership: withResponsive still adapts the label column and touch gestures but stops switching density. A host that pins 'compact' on a phone takes responsibility for touch-target size. The React/Vue density prop stays construction-time; call ref.setDensity() to change it later.

Computation

MethodReturnsPurpose
computeCriticalPath()CriticalPathResultRun CPM; emits critical-path:computed. Throws if no tasks / on cycle.

Schedule conflicts

Added by withConflicts (@fluxgantt/core/conflicts). Headless: it needs no withRender and no DOM.

import { createGantt } from '@fluxgantt/core';
import { withConflicts } from '@fluxgantt/core/conflicts';
 
const gantt = withConflicts(createGantt({ tasks, dependencies }));
gantt.detectConflicts(); // Conflict[] — the current state, a pure query
gantt.on('conflict:detected', (conflicts) => showWarnings(conflicts));

detectConflicts() reports where the schedule you authored contradicts a rule Core enforces elsewhere. That happens in schedulingMode: 'manual' (the default), after an import, after undo()/redo(), and for a task you drag earlier than its predecessor in 'auto' mode.

kindMeaningFields
dependency-violationA successor starts earlier than one dependency allows. One entry per violated dependency, using the same rule as the auto-scheduling cascade (FS/SS/FF/SF, lag in working hours).dependencyId, from, to, dependencyType, lagHours, requiredStart (a Temporal.ZonedDateTime, call .toString() to serialize), shortfallHours
outside-parentA child's span is not contained in its parent's span. Turned off automatically when GanttConfig.rollup is set, because the parent is then drawn at its derived span.taskId, parentId, edge ('start', 'end' or 'both')
invalid-spanend is before start. The task is skipped by the other two checks.taskId

shortfallHours is exact up to a gap of about 100 years between the successor's start and requiredStart; beyond that it is a lower bound (it saturates), so one mistyped date cannot stall detection.

Under GanttConfig.rollup the renderers draw a summary task at its derived span, but dependency-violation uses the summary's authored start/end, exactly as the auto-scheduling cascade does. A dependency arrow that visibly runs backwards from or to a summary may therefore not be reported, and the reverse can also happen. Leaf-to-leaf dependencies are unaffected.

Conflicts come back in a stable order (kind, then id). The payload carries only ids, enums and numbers, never task names. Cycles, task constraints and resource over-allocation are not Core conflicts. Pass withConflicts(instance, { kinds: ['dependency-violation'] }) to opt out of a kind. The set of kinds may grow in minor releases, so keep a default branch in any switch (conflict.kind).

conflict:detected is a change notification, not a poll:

  • It runs only while at least one listener is subscribed. With no listener there is no cost. Subscribing runs one full detection synchronously inside on(), and afterwards every store mutation (setProgress included) runs one detection per microtask turn. Measured with the polyfill on Node 24: about 10 ms per detection at 1,000 tasks / 2,000 dependencies when the dependencies are plain zero-lag FS/SS, about 0.3 to 0.8 s when every edge is violated or lagged, and 1.4 to 4.4 s in that worst case at 5,000 tasks. Native Temporal is faster. For very large charts, read detectConflicts() on demand instead of subscribing.
  • It does not fire when you subscribe. Read the initial state with detectConflicts().
  • It fires only when the set of conflicts changes (a conflict whose shortfallHours changes counts), with [] when the last conflict clears.
  • It fires from a microtask, after the synchronous task:*, dependency:* and history:changed events of the mutation. A multi-step change (a cascading drag, an undo, an import) therefore produces one event and never announces a violation the same call fixes. Call detectConflicts() if you need the answer synchronously.

withConflicts is idempotent, so the wrappers do not need a prop for it. Options apply on the first call only: calling it again with different kinds logs a console.warn and keeps the first configuration. With React, seed the state with detectConflicts() because the event does not fire on subscribe:

const { instance } = useFluxGantt(config);
const [conflicts, setConflicts] = useState<readonly Conflict[]>([]);
useEffect(() => {
  const g = withConflicts(instance);
  setConflicts(g.detectConflicts());
  return g.on('conflict:detected', setConflicts);
}, [instance]);

In Vue, hold a shallowRef<readonly Conflict[]>([]), call withConflicts(instance) in onMounted, assign detectConflicts() to it, subscribe, and return the unsubscribe function from onUnmounted. Nothing is painted for a conflict yet: build your own indicator from the event and the data-task-id attributes the renderers expose.

Edit hooks

Added by withEditHooks (@fluxgantt/core/edit-hooks). FluxGantt ships no editor and no menu: it tells you what the user asked for, and your app (your form components, your i18n, your permissions) answers. It needs a mounted chart, and adds no methods. Pointer hooks work without withInteraction; keyboard access to every row needs it (see the keyboard note below).

import { withEditHooks } from '@fluxgantt/core/edit-hooks';
 
const gantt = withEditHooks(withInteraction(withRender(createGantt({ tasks }))));
 
gantt.on('task:activated', ({ taskId, clientX, clientY }) => {
  openEditor(gantt.getTask(taskId), { x: clientX, y: clientY });
});
gantt.on('task:context-requested', (p) => {
  p.preventDefault(); // suppress the browser's own menu
  gantt.select(p.taskId); // optional: right-click does not select by itself
  openMenu(p.taskId, p.clientX, p.clientY);
});
EventFired byPayload
task:activateddouble-click; Enter on a leaf row; F2 on any row{ taskId, part, source: 'pointer' | 'keyboard', clientX, clientY }
task:context-requestedright-click; ContextMenu key; Shift+F10; touch long-press{ taskId, part, source: 'pointer' | 'keyboard' | 'long-press', clientX, clientY, preventDefault() }
  • part is 'bar', 'label' or 'row' (the grid or a dependency arrow). Keyboard gestures always report 'row'. The collapse toggle glyph, the header and empty space emit nothing. Canvas mode never reports 'bar' (its hit-test is row-wide): treat 'row' and 'bar' as "the task" and do not rely on 'bar'.

  • Coordinates are viewport (client) coordinates. For keyboard gestures they are the centre of the focused row (the pinned label column, in both modes), an anchor for a popover.

  • preventDefault() suppresses the native context menu (mouse, keyboard, and Android's touch menu) only if you call it synchronously inside the listener. With no listener, or a listener that does not call it, the browser menu opens as usual. This is deliberate: the React and Vue wrappers subscribe to every event, so "a listener exists" says nothing about whether you want the menu.

  • Right-click does not select the task. Call gantt.select(payload.taskId) yourself if you want that.

  • Read-only charts still emit both events. The event means "the user asked to open this task", not "the task is editable" (a read-only detail card on double-click is a normal use). Check your own readOnly before offering editing.

  • Keyboard row navigation comes from withInteraction. The hooks' keys (Enter, F2, ContextMenu, Shift+F10) act on whichever row has focus. Without withInteraction only the first row is in the tab order and there are no arrow keys, so rows 2..n are reachable by pointer only. Apply withInteraction to keep every hook reachable from the keyboard.

  • Enter on a summary row still collapses/expands it (with withInteraction). Use F2 or a double-click to open a summary. Without withInteraction there is no collapse behaviour, so Enter activates the focused row, summary or not.

  • Touch. A long-press (500 ms) emits task:context-requested with source: 'long-press'. It is cancelled by pointer up, a second finger, or movement: 4 px on a bar (the drag threshold, so a drag always wins) and 8 px elsewhere. Once it fires, the gesture belongs to your menu: the chart ends the press for drag and selection, so moving the finger afterwards moves, resizes and links nothing, and releasing does not select. Change the delay with withEditHooks(gantt, { longPressMs }); 0 disables it. There is no double-tap activation, so include an "Edit" entry in the long-press menu for touch users. Compose withResponsive as well so the browser does not claim the gesture for scrolling.

  • Platform caveats for touch. On Android 12+ Chrome fires its native contextmenu at about 400 ms, before the 500 ms default; the chart reports the long-press at that moment instead, so preventDefault() still works whatever longPressMs is. iOS Safari fires no contextmenu, so preventDefault() cannot stop its callout or the text-selection loupe. Add this to your CSS (core ships none):

    .fg-timeline__row, .fg-timeline__sticky-column { -webkit-touch-callout: none; user-select: none; }
  • macOS Ctrl+click is reported by browsers as a right-click, so it fires task:context-requested and also toggles the selection.

  • withEditHooks is not part of the all-capabilities bundle measured in CI; it adds about 1 KiB gzip over withRender. The React and Vue wrappers include it.

Returning focus is your job: when your editor or menu closes, put focus back where it was so keyboard users do not lose their place. Capture document.activeElement before you open the editor and restore it, falling back to the row for the task:

const opener = document.activeElement as HTMLElement | null;
// ... open the editor; on close:
(opener?.isConnected ? opener : document.querySelector<HTMLElement>(`[data-task-id="${CSS.escape(taskId)}"]`))
  ?.focus();

Clicking a bar moves the chart's keyboard position to that row, so arrow keys continue from it.

In React pass onTaskActivated / onTaskContextRequested to <FluxGantt>; in Vue listen to @task-activated / @task-context-requested. The payload's preventDefault() works in both, because the wrappers forward the event synchronously.

Import / Export

Requires the withIo mixin (@fluxgantt/core/io).

MethodReturnsPurpose
exportJson(options?)ExportBundleSerialize to the JSON bundle. Headless.
exportCsv(options?)stringSerialize tasks to CSV. Headless.
exportSvg(options?)stringSerialize the mounted chart to an SVG string. Options: inlineComputedStyle, and background (a CSS color or 'transparent'; default the chart's resolved --fg-bg, else #ffffff). Requires withRender + mount. Works in Canvas mode too (full chart, max 5,000 tasks); see Import & Export.
exportPng(options?)Promise<Blob>Rasterize the mounted chart to PNG. Options: scale, and background (same default as exportSvg). Requires withRender + mount; async. In Canvas mode the whole chart is one poster-sized raster, bounded by browser canvas limits; prefer exportSvg() for big charts.
importJson(input, options?)ImportSummaryValidate + replace the dataset atomically.
importCsv(csv, options?)ImportSummarySame, from CSV.

(The pure, instance-free importJson / importCsv functions are also exported from @fluxgantt/core/io — see Import & Export.)

Events & lifecycle

MethodReturnsPurpose
on(event, callback)UnsubscribeFnSubscribe to a GanttEventMap event.
mount(container)voidRender into an element; wires drag unless readOnly. Requires withRender.
unmount()voidRemove the rendered chart; keep the instance. Requires withRender.
destroy()voidUnmount + clear listeners; the instance is dead afterward. Base — always present.
refresh()voidForce a re-render of the mounted chart. Requires withRender.

GanttEventMap

EventPayload
task:added(task: Task)
task:moved(task: Task, prevStart: DateInput)
task:resized(task: Task, prevDuration: number)
task:progressed(task: Task, prevProgress: number)
task:removed(taskId: TaskId)
dependency:added(dependency: Dependency)
dependency:removed(dependencyId: DependencyId)
critical-path:computed(criticalTaskIds: readonly TaskId[])
selection:changed(taskIds: readonly TaskId[]) — the full flattened selection, not a delta, in insertion order
collapse:changed(collapsedTaskIds: readonly TaskId[]) — the full collapsed set, not a delta
history:changed(state: { canUndo: boolean; canRedo: boolean })
viewport:changed(state: { viewMode: ViewMode; density?: Density }) — density is present only when the event came from setDensity(). Scrolling does not emit it
data:imported(summary: ImportSummary)
conflict:detected(conflicts: readonly Conflict[]) — only with withConflicts. See Schedule conflicts
task:activated(payload: TaskActivatedPayload) — only with withEditHooks. See Edit hooks
task:context-requested(payload: TaskContextRequestedPayload) — only with withEditHooks. See Edit hooks
renderer:selected(state: { renderer: 'svg' | 'canvas'; taskCount: number; canvasFallbackReason?: 'dimension-exceeded' | 'load-failed' | 'construction-failed' })

on() returns an UnsubscribeFn (() => void).

Three conventions hold across the whole map, and they are what make these events usable as a persistence hook:

  • Snapshot, not delta. selection:changed, collapse:changed and critical-path:computed each carry the complete set. There is nothing to reconcile.
  • No-ops are suppressed. Re-selecting what is already selected, collapsing what is already collapsed, zoomTo() with the current mode, undo() on an empty stack — none of these emit. So a listener firing means something genuinely changed.
  • One fire per gesture, never per internal write. history:changed fires once for a whole cascade-grouped drag; an import fires data:imported once instead of task:added × N.
  • Event timing: listeners see the final state. A mutation that writes more than once (a moveTask that cascades in schedulingMode: 'auto', removeTask of a subtree, undo()/redo() of a grouped step, duplicateTask, Delete on a multi-selection, an import) commits ALL of its writes first, repaints a mounted chart ONCE, and only then calls your listeners and onTaskChange, in the same order as before. So gantt.getTask() or the DOM inside a task:moved listener already shows every cascaded shift, and critical-path:computed fires at most once, before that mutation's other events, with the final critical set. A listener that starts another mutation runs it immediately, as its own command, nested inside that listener call, so the remaining queued events of the outer mutation can carry snapshots older than the store at delivery time. If an onTaskChange hook throws, the remaining events still fire and the first error is rethrown afterwards (unless the mutation itself threw: that error wins and the hook error is logged). The one exception to "repaints first": a mutation made inside your own signals.batch() emits its events before the repaint, because effects are deferred to the end of your batch.

The five task/dependency events take an optional third argument, meta?: { source: 'undo' | 'redo' }, present only when the change came from a replay — see History.

renderer:selected is the reliable way to know a large chart has become visible: above the 2000-task auto-switch threshold, mount() returns synchronously but the Canvas module loads and paints afterwards, and this event fires once it has (or once the SVG fallback has, with canvasFallbackReason set).

Standalone functions & values

ExportPurpose
toTaskId, toDependencyId, toResourceId, toBaselineId, toProjectIdBrand a string as the matching branded id.
importJson(input, options?)Validate + parse a JSON bundle → { tasks, dependencies }. (@fluxgantt/core/io)
importCsv(csv, options?)Validate + parse CSV → { tasks }. (@fluxgantt/core/io)
exportJson, exportCsv, exportSvg, exportPngPure export functions the instance methods delegate to. (@fluxgantt/core/io)
IoValidationErrorThrown by imports on malformed input. (@fluxgantt/core/io)
createSvgRenderer, layoutRows, createTimeScale, …Low-level render primitives. (@fluxgantt/core/render)
enableDragMove, enableDragResize, enableKeyboardNav, …Low-level gesture recognizers. (@fluxgantt/core/interaction)
DEFAULT_CALENDARThe Mon–Fri 9–17 UTC default calendar.
isWorkingDay, isHoliday, addWorkingHours, subtractWorkingHours, differenceInWorkingHours, normalizeDateCalendar-aware date helpers.
computeCriticalPathThe pure CPM function (the instance method delegates to it).
computeRollup(tasks, calendar?)Derive each parent's span/progress/duration from its children → ReadonlyMap<TaskId, RollupResult>. Pure; never mutates. Throws on a cyclic parent chain. See Hierarchy.
detectConflicts(tasks, dependencies, calendar, options?)The pure conflict detector the withConflicts method delegates to → Conflict[].
CyclicDependencyErrorThrown by CPM on a cyclic graph.
signal, computed, effect, batch, untrackedThe reactive primitives the store is built on.

Key types

TaskId, DependencyId, ResourceId, BaselineId, ProjectId (branded strings) · Task, TaskInput, TaskPatch, TaskKind, TaskConstraint · Dependency, DependencyInput, DependencyType · DateInput · WorkingCalendar, WeekdayCode, WorkingHours · CriticalPathResult, TaskSchedule, RollupResult · ViewMode, Density, SchedulingMode · Conflict, ConflictKind · GanttConfig, GanttInstance, GanttEventMap, GanttEventName, UnsubscribeFn · EventMeta, ViewportChangedPayload, RendererSelectedPayload, ImportSummary (event payloads) · GanttMessages (see i18n).