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: [/* … */] }))));| Mixin | Subpath | Adds |
|---|---|---|
withRender(instance) | @fluxgantt/core/render | mount(), unmount(), refresh() |
withInteraction(instance) | @fluxgantt/core/interaction | No new methods — wires drag/keyboard/click into every mount() |
withIo(instance) | @fluxgantt/core/io | exportJson(), exportCsv(), exportSvg(), exportPng(), importJson(), importCsv() |
withViewport(instance) | @fluxgantt/core/viewport | scrollToTask(), scrollToDate(), setDensity(). See Scrolling and density |
withConflicts(instance, options?) | @fluxgantt/core/conflicts | detectConflicts() and the conflict:detected event. Headless. See Schedule conflicts |
withEditHooks(instance, options?) | @fluxgantt/core/edit-hooks | No 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/rtl | No 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/responsive | isCoarsePointer(), 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
| Field | Type | Default | Purpose |
|---|---|---|---|
tasks | readonly TaskInput[] | [] | Initial tasks. |
dependencies | readonly DependencyInput[] | [] | Initial dependencies ({ from, to, type?, lag? }). |
calendar | WorkingCalendar | DEFAULT_CALENDAR | Working days/hours/holidays/timezone. Immutable. |
viewMode | ViewMode | 'week' | Timeline zoom (day/week/month/quarter/year). |
density | Density | '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(). |
locale | string | 'en' | Renderer locale (dates via Intl). Construction-time only — read once at mount(). |
ariaLabel | string | 'Gantt chart' | Accessible name of the chart as a whole, and the <title> of every exported SVG/PNG. Construction-time only. |
messages | GanttMessages | — | Accessible-name builders for a non-English host. See i18n. Construction-time only. |
readOnly | boolean | false | When true, mount() wires no drag gestures (with withInteraction applied; click-select and keyboard nav stay live). |
enableDependencyDrag | boolean | true | false 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. |
schedulingMode | SchedulingMode | 'manual' | 'auto' cascades dependents on move/resize. |
initialCollapsed | readonly TaskId[] | [] | Tasks to start collapsed. Unknown or leaf ids are dropped silently; fires no collapse:changed. |
historyLimit | number | 100 | Undo 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. |
canvasViewportHeight | number | 600 | Visible 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 wrappers | Initial theme. Applied by withTheme; the base bundle only carries the field. The wrappers compose withTheme and keep theme reactive. |
rtl | boolean | false | Mirror 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). |
rollup | RollupProvider | — | 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
| Method | Returns | Purpose |
|---|---|---|
addTask(input) | Task | Add a task; emits task:added. |
updateTask(id, patch) | Task | Patch fields; emits task:* per changed field. |
removeTask(id) | void | Remove a task + descendants; emits dependency:removed/task:removed. |
moveTask(id, newStart) | Task | Shift start+end by the same delta; emits task:moved. |
resizeTask(id, newDuration) | Task | Set duration (working hours); emits task:resized. |
setProgress(id, progress) | Task | Set progress 0..1; emits task:progressed. |
getTask(id) | Task | undefined | Look 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
| Method | Returns | Purpose |
|---|---|---|
linkTasks(from, to, type?, lag?) | Dependency | Link two tasks (type default 'FS'); emits dependency:added. Throws on cycle/self/duplicate. |
unlinkTasks(from, to) | void | Remove 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.
| Method | Returns | Purpose |
|---|---|---|
select(id) | void | Replace 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() | void | Select every task. Emits selection:changed iff the set changed. |
deselect() | void | Clear 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.
| Method | Returns | Purpose |
|---|---|---|
toggleCollapse(id) | void | Toggle 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) | boolean | Whether id is collapsed and still has children. Safe after destroy() (returns false). |
collapseAll() | void | Collapse every task that has children. Idempotent. |
expandAll() | void | Expand 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.
| Method | Returns | Purpose |
|---|---|---|
undo() | boolean | Undo the most recent gesture. false (a safe no-op, no event) when the undo stack is empty. |
redo() | boolean | Redo the next gesture forward. Same no-op posture. |
canUndo() | boolean | Whether undo() would currently do something. |
canRedo() | boolean | Symmetric. |
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)
| Method | Returns | Purpose |
|---|---|---|
zoomTo(mode) | void | Switch 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() | void | Step one level toward 'day'. Safe no-op at that boundary. |
zoomOut() | void | Step one level toward 'year'. Safe no-op at that boundary. |
getViewMode() | ViewMode | The 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 tostart, so a flex column with an auto height has the chart's height and a flex row with a fixed height still pins. - Container
paddingis a limit. A sticky box pins at the scroller's content edge, so withpadding: Pthe band and the column stayPpx inside the container and scrolled rows show in the strip.scrollToTask/scrollToDateare also off byP. Put the spacing on a wrapper. - The pinned column's ground is opaque and is
var(--fg-bg, #fff): set--fg-bgon 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
labelColumnWidthis therefore cut at the column edge on screen (the export keeps the full name); widenlabelColumnWidthfor 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/scrollToDateuse one model:align: 'start'parks the row directly under the 32px header, and the bar directly right of the pinned column; the usable area isclientHeight - 32byclientWidth - 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-labelover the whole container now also see the clones: scope them tosvg.fg-timeline. - While mounted, the container's inline
scroll-padding-topis32pxso natively focus-scrolled rows stop below the header; your previous inline value is restored on unmount. Firefox ignoresscroll-paddingwhen it focuses an SVG row (it scrolls the container to the top); this predates the pinned header and Chromium and WebKit honour it.
| Method | Returns | Purpose |
|---|---|---|
scrollToTask(id, options?) | boolean | Scrolls 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?) | boolean | Scrolls 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) | void | Changes 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
| Method | Returns | Purpose |
|---|---|---|
computeCriticalPath() | CriticalPathResult | Run 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.
kind | Meaning | Fields |
|---|---|---|
dependency-violation | A 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-parent | A 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-span | end 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 (setProgressincluded) 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, readdetectConflicts()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
shortfallHourschanges counts), with[]when the last conflict clears. - It fires from a microtask, after the synchronous
task:*,dependency:*andhistory:changedevents 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. CalldetectConflicts()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);
});| Event | Fired by | Payload |
|---|---|---|
task:activated | double-click; Enter on a leaf row; F2 on any row | { taskId, part, source: 'pointer' | 'keyboard', clientX, clientY } |
task:context-requested | right-click; ContextMenu key; Shift+F10; touch long-press | { taskId, part, source: 'pointer' | 'keyboard' | 'long-press', clientX, clientY, preventDefault() } |
-
partis'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
readOnlybefore offering editing. -
Keyboard row navigation comes from
withInteraction. The hooks' keys (Enter, F2, ContextMenu, Shift+F10) act on whichever row has focus. WithoutwithInteractiononly the first row is in the tab order and there are no arrow keys, so rows 2..n are reachable by pointer only. ApplywithInteractionto 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. WithoutwithInteractionthere is no collapse behaviour, so Enter activates the focused row, summary or not. -
Touch. A long-press (500 ms) emits
task:context-requestedwithsource: '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 withwithEditHooks(gantt, { longPressMs });0disables it. There is no double-tap activation, so include an "Edit" entry in the long-press menu for touch users. ComposewithResponsiveas well so the browser does not claim the gesture for scrolling. -
Platform caveats for touch. On Android 12+ Chrome fires its native
contextmenuat about 400 ms, before the 500 ms default; the chart reports the long-press at that moment instead, sopreventDefault()still works whateverlongPressMsis. iOS Safari fires nocontextmenu, sopreventDefault()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-requestedand also toggles the selection. -
withEditHooksis not part of the all-capabilities bundle measured in CI; it adds about 1 KiB gzip overwithRender. 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).
| Method | Returns | Purpose |
|---|---|---|
exportJson(options?) | ExportBundle | Serialize to the JSON bundle. Headless. |
exportCsv(options?) | string | Serialize tasks to CSV. Headless. |
exportSvg(options?) | string | Serialize 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?) | ImportSummary | Validate + replace the dataset atomically. |
importCsv(csv, options?) | ImportSummary | Same, from CSV. |
(The pure, instance-free importJson / importCsv functions are also exported from
@fluxgantt/core/io — see Import & Export.)
Events & lifecycle
| Method | Returns | Purpose |
|---|---|---|
on(event, callback) | UnsubscribeFn | Subscribe to a GanttEventMap event. |
mount(container) | void | Render into an element; wires drag unless readOnly. Requires withRender. |
unmount() | void | Remove the rendered chart; keep the instance. Requires withRender. |
destroy() | void | Unmount + clear listeners; the instance is dead afterward. Base — always present. |
refresh() | void | Force a re-render of the mounted chart. Requires withRender. |
GanttEventMap
| Event | Payload |
|---|---|
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:changedandcritical-path:computedeach 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:changedfires once for a whole cascade-grouped drag; an import firesdata:importedonce instead oftask:added× N. - Event timing: listeners see the final state. A mutation that writes more than once (a
moveTaskthat cascades inschedulingMode: 'auto',removeTaskof 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 andonTaskChange, in the same order as before. Sogantt.getTask()or the DOM inside atask:movedlistener already shows every cascaded shift, andcritical-path:computedfires 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 anonTaskChangehook 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 ownsignals.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
| Export | Purpose |
|---|---|
toTaskId, toDependencyId, toResourceId, toBaselineId, toProjectId | Brand 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, exportPng | Pure export functions the instance methods delegate to. (@fluxgantt/core/io) |
IoValidationError | Thrown 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_CALENDAR | The Mon–Fri 9–17 UTC default calendar. |
isWorkingDay, isHoliday, addWorkingHours, subtractWorkingHours, differenceInWorkingHours, normalizeDate | Calendar-aware date helpers. |
computeCriticalPath | The 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[]. |
CyclicDependencyError | Thrown by CPM on a cyclic graph. |
signal, computed, effect, batch, untracked | The 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).