Headless core
Run the chart's tree and calendar math in Node, a worker or a test.
The chart's tree, row-order and calendar math is a set of plain functions that need no React, no stylesheet and no DOM. Run them on your server so a report and the chart cannot disagree about a phase's dates.
Import the helpers
They come from the package root, the same specifier as the component.
import { buildTaskTree, rollUpTasks, createWorkingCalendar } from '@jaeungkim/gantt-chart';The headless set is eight functions and values: buildTaskTree, collectSubtreeIds,
rollUpTasks, sortTasksBySequence, validateMove, moveTask, CALENDAR_DAYS and
createWorkingCalendar. Their only runtime dependency is dayjs with the utc plugin.
There is no /core subpath. The package declares ., ./style.css and ./package.json, and
ships as one bundle that includes the React component. react and react-dom are external, so a
Node script that imports rollUpTasks still needs both (^18 or ^19) installed and resolvable.
Roll up a tree
buildTaskTree normalizes parents, children and depths once. collectSubtreeIds gathers every id
under a row. rollUpTasks returns the summary dates and progress the chart draws.
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import {
buildTaskTree,
createWorkingCalendar,
rollUpTasks,
type Task,
} from '@jaeungkim/gantt-chart';
dayjs.extend(utc);
const tasks: Task[] = [
{ id: 'P', name: 'Phase 1', parentId: null, sequence: '1',
startDate: '2025-06-02', endDate: '2025-06-03' },
{ id: 'A', name: 'Survey', parentId: 'P', sequence: '1.1', progress: 100,
startDate: '2025-06-02', endDate: '2025-06-05' },
{ id: 'B', name: 'Frame', parentId: 'P', sequence: '1.2', progress: 40,
startDate: '2025-06-05', endDate: '2025-06-10' },
];
const calendar = createWorkingCalendar({ holidays: ['2025-06-06'] });
for (const task of rollUpTasks(tasks, buildTaskTree(tasks))) {
const days = calendar.daysBetween(dayjs.utc(task.startDate), dayjs.utc(task.endDate));
console.log(task.id, task.startDate.slice(0, 10), task.endDate.slice(0, 10), `${days}wd`, task.progress);
}It prints:
P 2025-06-02 2025-06-10 5wd 63
A 2025-06-02 2025-06-05 3wd 100
B 2025-06-05 2025-06-10 2wd 40P was declared as ending on 2025-06-03 and comes back ending on 2025-06-10, with a
progress of 63 the input never set. A rewritten parent comes back as a full ISO string, while
rows the roll-up left alone keep the strings you passed in.
The tree argument is optional and defaults to a fresh buildTaskTree(tasks), so a call inside a
loop rebuilds the tree every time. Task list and hierarchy covers what a summary row
overwrites, and Tree helpers has the roll-up rule.
Count working days
A calendar is an object your app calls directly. CALENDAR_DAYS counts all seven days.
createWorkingCalendar skips weekends and holidays, so five days across a weekend count as three.
isWorkingDay, addDays, daysBetween, snapForward and the skipsNonWorkingDays flag are
public API. No other core function takes a calendar, and the tree and move helpers count no days.
The chart builds the same calendar from its workingWeekdays and holidays props, so the days a
report counts and the days the chart shades cannot drift. Here holidays is a flat list of UTC
YYYY-MM-DD strings; names, colors and multi-day ranges belong to the chart prop. See
the timeline for shading, Working calendar for what the chart
snaps, and Working calendar helpers for the options.
Reorder rows outside the chart
Row order lives in sequence, a dotted path where "2.1" is the first child of "2".
sortTasksBySequence compares the segments as numbers, so "1.10" sorts after "1.9".
validateMove returns a rejection reason, or null when the move is allowed. moveTask applies
the move and returns the new array plus a change naming the old and new position. It returns
null for any move validateMove rejects, so call validateMove first to learn why.
const result = moveTask(tasks, { taskId: 'B', toParentId: null, toIndex: 0 }, { hierarchy: true });
if (result) await save(result.tasks);moveTask renumbers every task whose path changed and rewrites parentId on the moved task only.
Tasks that stayed put keep their object identity. Reordering rows covers the row drag
inside the chart.
Before you integrate
rollUpTasksskips a child date it cannot parse. When no child yields a finite date the parent is left alone, so bad input produces a stale summary row rather than an error.buildTaskTreecuts a broken parent link rather than reporting it. An unknownparentId, a self-parent and a cyclic chain all become roots, and nothing is logged.createWorkingCalendardegrades whenworkingWeekdaysis empty. Every day counts as non-working,addDaysfalls back to plain days andsnapForwardreturns its input.validateMoveandmoveTaskre-sort the tasks and rebuild the sibling lists on every call, and neither one changes a date. Caching is your app's job.- Dates are UTC. A string with no zone is read as UTC rather than as server-local time, so a process in Asia/Seoul that writes local timestamps is 9 hours out. Task data has the full date contract.
Rendering and formatting stay outside the core. Bar geometry, arrows, shading and every date string
a reader sees come from the chart. No core function reads Task.dependencies or touches a locale.
Helpers that are not on the list above are internal, so do not plan around importing them. Props
and defaults: GanttProps.