The core engine
GanttEngine is the whole system minus the pixels. It owns the reactive state, the time-scale, the layout, the plugin host and the compute pipeline that turns rows into a scene. This page is the map of its public surface.
Constructing the engine
Every option has a default, so new GanttEngine() is valid (it renders an empty three-month window). Construction runs one full compute pass immediately.
import { GanttEngine } from '@ganttkit/core'
const engine = new GanttEngine({
rows,
viewMode: 'Month',
rowHeight: 44,
highlightToday: true,
})Options
| Option | Type | Default | Notes |
|---|---|---|---|
rows | Row[] | [] | Initial data. |
viewMode | 'Day' | 'Week' | 'Month' | 'Week' | Controls the effective day width. |
startDate | string | Date | null | null | Pin the timeline start. Otherwise derived from the earliest task. |
endDate | string | Date | null | null | Pin the timeline end. Otherwise derived from the latest task. |
rowHeight | number | 50 | Pixel height of each row. |
dayWidth | number | 60 | Base pixels per day (scaled by view mode: Day ×1.5, Week ×1, Month ×0.5). |
barPadding | number | 6 | Vertical gap between the bar and the row edge. |
highlightToday | boolean | true | Emit the "today" column background. |
draggable | boolean | true | Global drag/resize toggle (tasks can override). |
virtualize | boolean | true | Window the scene to the viewport. Turn off only for tiny charts you export. |
overscanRows | number | 4 | Rows rendered above/below the viewport. |
overscanCols | number | 6 | Day columns rendered left/right of the viewport. |
dateAdapter | DateAdapter | defaultDateAdapter | Parsing/formatting/locale strategy. |
Plugin lifecycle
Installing or removing a plugin triggers a full recompute, so the chart is always consistent.
| Method | Returns | Description |
|---|---|---|
use(plugin) | this | Install a plugin and recompute. Chainable. Throws if the plugin name is already installed. |
hasPlugin(name) | boolean | Whether a plugin with that name is installed. |
removePlugin(name) | void | Dispose a plugin (running its teardown) and recompute. |
destroy() | void | Dispose every plugin in reverse install order and clear all event handlers. |
Reading state
These accessors are read-only snapshots cheap to call in an event handler or render loop.
| Method | Returns | Description |
|---|---|---|
getState() | GanttState | { rows, viewMode, selectedTaskId, loading }. |
getOptions() | ResolvedOptions | Options with defaults applied. |
getRows() | Row[] | Rows after the rows hook what the renderer displays. |
getScene() | Scene | The current (possibly windowed) scene. |
getTimeScale() | TimeScale | The active time-scale (see below). |
getWindow() | Window | null | The resolved virtualization window, or null when not virtualizing. |
The store
State lives in a tiny observable store. Subscribers fire only when a shallow patch actually changes a reference, and re-entrant set() from a listener throws so update loops surface loudly instead of silently recursing.
const off = engine.store.subscribe((state, prev) => {
if (state.viewMode !== prev.viewMode) console.log('view mode →', state.viewMode)
})
// …
off() // unsubscribeMutations
Mutations update state and run the appropriate compute pass. The heavy ones also emit a domain event.
| Method | Effect |
|---|---|
setRows(rows) | Replace all rows, recompute, emit rows:change. |
updateTask(id, patch) | Shallow-merge a patch into one task; recompute. Returns true if found. |
updateTaskDates(id, start, end) | Convenience wrapper around updateTask for committing a drag/resize. |
setViewMode(mode) | Change the view mode (no-op if unchanged); recompute; emit viewmode:change. |
selectTask(id \| null) | Set the selected task; emit selection:change. |
setLoading(bool) | Flip the loading flag (renderers may show a spinner). |
setDateAdapter(adapter) | Swap the date strategy (e.g. locale change) and recompute. |
setViewport(vp) | Report the visible region triggers a cheap windowed rebuild only. |
setDragPreview(id, start, end) | Show a transient drag preview without committing to state. |
clearDragPreview() | Drop the preview and repaint committed state. |
refresh() | Force a full recompute (plugins call this after mutating their own state). |
Events
engine.events is an event bus with on, once, off and emit. Every on/once returns an unsubscribe function.
const off = engine.events.on('task:dragend', ({ task, start, end, changed }) => {
if (changed) save(task.id, start, end)
})| Event | Payload | Fires when |
|---|---|---|
scene:change | { scene, reason } | Scene rebuilt. reason is 'data' | 'viewport' | 'preview'. |
daterange:change | { start, end } | Timeline bounds recomputed. |
rows:change | { rows } | Rows replaced via setRows. |
viewmode:change | { viewMode } | View mode changed. |
selection:change | { taskId } | Selected task changed. |
task:click | { task, row, originalEvent } | A bar/milestone is clicked (emitted by the renderer). |
task:hover | { task, row, handle, clientX, clientY } | Pointer enters a task. handle is 'left' | 'right' | null. |
task:hoverend | {} | Pointer leaves the last hovered task. |
task:dragstart | { task, row, mode } | Drag begins. mode is 'move' | 'resize-left' | 'resize-right'. |
task:dragmove | { task, row, mode, start, end, changed } | Drag in progress (preview dates). |
task:dragend | { task, row, mode, start, end, changed } | Drag released; changed is false for a no-op. |
row:toggle | { rowId } | A sidebar chevron is clicked (handled by plugin-tree). |
The bus is untyped at runtime, so plugins can define their own event names freely. Renderers emit the pointer events (task:click, task:hover, task:drag*); the engine emits the state events. This split is why one plugin works across every renderer.
The two-pass compute pipeline
Performance comes from splitting compute into a heavy pass and a cheap pass. The renderer picks which to run from the scene:change reason.
On data / view / plugin change
Runs the rows hook, derives the date range (one O(tasks) scan), rebuilds the time-scale, then does a windowed scene build. Reason: 'data'.
On scroll / resize / drag
Lays out and builds the scene for the current window only O(visible rows + days). Never re-runs the rows hook. Reason: 'viewport' or 'preview'.
Concretely: setRows, setViewMode, use and refresh take the heavy path; setViewport (scroll) and setDragPreview take the cheap path. A 20 000-task chart rebuilds on scroll in under a millisecond because only the visible slice is ever laid out.
Virtualization & the viewport
The renderer measures its scroll container and calls setViewport, coalescing bursts to one call per animation frame. The engine resolves that into a window a row and day range plus overscan and emits only the primitives inside it.
engine.setViewport({
scrollTop: body.scrollTop,
scrollLeft: body.scrollLeft,
width: body.clientWidth,
height: body.clientHeight,
})
engine.getWindow() // { rowStart, rowEnd, dayStart, dayEnd } | nullIf you build a custom renderer, wiring setViewport to your scroll/resize handlers is the one thing you must do to get virtualization.
The time-scale
getTimeScale() returns the object that maps between dates and pixels. It's the single source of truth for horizontal geometry, so plugins and renderers never re-derive it.
| Member | Type | Description |
|---|---|---|
dateToX(date) | number | Left-edge pixel x of that day's column. |
xToDayIndex(x) | number | Whole-day index for a pixel x. |
dayWidth | number | Effective pixels per day for the current view mode. |
width | number | Total timeline width in pixels. |
totalDays | number | Number of day columns. |
days | Day[] | Per-day cells (date, isWeekend, isToday, isoWeek, …). |
weeks | Week[] | Week bands (label, range, width). |
months | Month[] | Month bands (label, width). |
// Scroll so "today" sits ~30% from the left edge
const x = engine.getTimeScale().dateToX(new Date())
body.scrollLeft = Math.max(0, x - body.clientWidth * 0.3)Hit-testing
"What's under the pointer" lives in the engine so no renderer or plugin re-implements geometry. Coordinates are in scene space (use the gantt:viewport service's clientToScene to convert).
| Method | Returns | Description |
|---|---|---|
hitTest(x, y) | { taskId, handle } | null | Topmost task at a point; handle is a resize edge or null. |
hitTestRegion(x1, y1, x2, y2) | string[] | Task ids whose bars intersect a rectangle (rubber-band selection). |
Services
The core stays feature-agnostic through a service registry. A plugin publishes a capability under a string key; renderers and other plugins consume it. Keys are unique providing a taken key throws.
const off = engine.provide('gantt:sidebar', sidebarModel) // returns a disposer
const sidebar = engine.consume('gantt:sidebar') // model | undefinedBuilt-in keys: gantt:sidebar (columns), gantt:i18n (i18n) and gantt:viewport (published by the renderer for client→scene coordinate mapping). See Writing a plugin for how to define your own.
Date adapters
All date logic goes through a DateAdapter. The default is Gregorian with a Monday week start. createIntlAdapter(locale, options?) builds a locale-aware one on top of Intl, and plugin-i18n swaps it in when the locale changes.
import { createIntlAdapter } from '@ganttkit/core'
engine.setDateAdapter(createIntlAdapter('fr-FR', {
weekStartsOn: 1, // 0=Sun … 6=Sat; inferred from the locale if omitted
weekend: [0, 6], // day indices treated as weekend
}))