@ganttloom/gantt-core 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Santhoshkumar Hariharan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,388 @@
1
+ # @ganttloom/gantt-core
2
+
3
+ Framework-agnostic Gantt chart engine: layout, SVG rendering, drag/resize/link
4
+ interactions, grouping, critical path, undo/redo, and more — zero runtime
5
+ dependencies. If you're using React, prefer
6
+ [`@ganttloom/gantt-react`](../gantt-react); use this package directly if
7
+ you're integrating with something else (Vue, Svelte, vanilla JS) or want full
8
+ control over the DOM lifecycle.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @ganttloom/gantt-core
14
+ ```
15
+
16
+ ## Quick start
17
+
18
+ ```ts
19
+ import { GanttChart } from "@ganttloom/gantt-core";
20
+ import "@ganttloom/gantt-core/styles.css";
21
+
22
+ const container = document.getElementById("gantt")!;
23
+
24
+ const chart = new GanttChart(
25
+ container,
26
+ [
27
+ { id: "1", name: "Design", start: new Date("2026-01-05"), end: new Date("2026-01-09"), progress: 60 },
28
+ { id: "2", name: "Build", start: new Date("2026-01-09"), end: new Date("2026-01-16"), progress: 10 },
29
+ ],
30
+ [{ fromId: "1", toId: "2", type: "FS" }],
31
+ { viewMode: "day", showCriticalPath: true, enableHistory: true }
32
+ );
33
+
34
+ chart.on("date-change", ({ task, start, end }) => {
35
+ console.log(`${task.name} moved to ${start} - ${end}`);
36
+ });
37
+ ```
38
+
39
+ Tasks and dependencies are plain objects — the chart never owns your state.
40
+ Call `chart.setTasks(...)` / `chart.setDependencies(...)` whenever your data
41
+ changes; call `chart.destroy()` when you're done with it.
42
+
43
+ ## Core concepts
44
+
45
+ - **`GanttTask`** — `id`, `name`, `start`/`end` (`Date`), optional `progress`,
46
+ `parentId` (for grouping), `isGroup`, `isMilestone`, `collapsed`, `color`,
47
+ `readonly`, `baselineStart`/`baselineEnd`, `deadline`, `constraintType` /
48
+ `constraintDate`, `assignees`, and a free-form `data` bag.
49
+ - **`GanttDependency`** — `fromId`, `toId`, a `type` of `"FS" | "SS" | "FF" | "SF"`,
50
+ and an optional `lagMs`.
51
+ - **`GanttColumn`** — describes one column of the left-hand grid panel, with
52
+ an optional `accessor` to derive its cell value from a task and `getHref`
53
+ to render it as a link.
54
+ - **`GanttOptions`** — everything else: view mode, theme, readonly, which
55
+ optional features are on, and the event callbacks. See [`src/types.ts`](./src/types.ts)
56
+ for the full, always-current list — it's the source of truth.
57
+
58
+ ## Feature options (all opt-in)
59
+
60
+ | Option | What it does |
61
+ |---|---|
62
+ | `showCriticalPath` | Highlights the critical path (longest dependency chain) |
63
+ | `showBaseline` | Renders `baselineStart`/`baselineEnd` as a faint bar behind the live one |
64
+ | `showDeadlines` (default `true`) | Renders `task.deadline` as a flag marker; red when the task's end is past it |
65
+ | `showAssigneeAvatars` | Renders `task.assignees` as small avatars/initials at the bar's left edge |
66
+ | `enableHistory` | Turns on `chart.undo()` / `chart.redo()` for move/resize/link/progress/create actions |
67
+ | `keyboardAccessible` | ARIA roles/labels on bars + arrow-key movement |
68
+ | `virtualScroll` (default `true`) | Only renders DOM/SVG nodes for rows in the visible viewport |
69
+ | `pagination` | `{ pageSize, page }` — render only a page of top-level rows |
70
+ | `autoSchedule` | Moving/resizing a task cascades date shifts to dependent successors |
71
+ | `calendar` | A `WorkingCalendar` (working days + holidays) — shades non-working time and, with `autoSchedule`, skips it when cascading |
72
+ | `snapToUnit` | Round drag/resize deltas to `"hour" \| "day" \| "week"`, or `false` for raw pixels |
73
+ | `colorScheme` | `"light" \| "dark" \| "auto"` (default `"auto"`, follows the OS preference live) |
74
+
75
+ ## Working-time calendars
76
+
77
+ ```ts
78
+ import type { WorkingCalendar } from "@ganttloom/gantt-core";
79
+
80
+ const calendar: WorkingCalendar = {
81
+ workingDays: [false, true, true, true, true, true, false], // Sun..Sat; this is also the default
82
+ holidays: ["2026-01-19", "2026-02-16"], // ISO "YYYY-MM-DD"
83
+ };
84
+
85
+ new GanttChart(container, tasks, deps, { calendar, autoSchedule: true });
86
+ ```
87
+
88
+ Non-working days are shaded in the timeline. With `autoSchedule` on, a
89
+ successor task pushed by a dependency cascade is nudged forward to the next
90
+ working day. Standalone helpers (`isWorkingDay`, `isHoliday`, `nextWorkingDay`,
91
+ `previousWorkingDay`, `shiftToWorkingDay`) are also exported if you need the
92
+ same logic outside the chart.
93
+
94
+ ## Task constraints
95
+
96
+ MSP-style constraints (`constraintType` + `constraintDate` on a `GanttTask`)
97
+ are enforced during `autoSchedule` cascades — when a dependency pushes a
98
+ constrained successor, its constraint is applied on top of the cascade
99
+ before the new dates are committed:
100
+
101
+ ```ts
102
+ { id: "3", name: "Vendor delivery", start, end, constraintType: "mso", constraintDate: new Date("2026-02-01") }
103
+ ```
104
+
105
+ `ConstraintType` is one of `"asap" | "alap" | "snet" | "snlt" | "fnet" | "fnlt" | "mso" | "mfo"`
106
+ (as-soon-as-possible / as-late-as-possible / start-no-earlier/later-than /
107
+ finish-no-earlier/later-than / must-start/finish-on). `"asap"`/`"alap"` are
108
+ no-ops — a true as-late-as-possible schedule needs a backward pass from the
109
+ project end date, which this incremental, per-drag engine doesn't attempt.
110
+ The standalone `applyConstraint(range, type, date)` function is exported if
111
+ you want the same logic elsewhere. Constraints only take effect during a
112
+ cascade; they don't restrict a direct manual drag of the constrained task
113
+ itself (matching how MSP flags rather than blocks a violated constraint).
114
+
115
+ ## Drag-to-create tasks
116
+
117
+ Opt in by supplying `onTaskCreate`: dragging on an empty part of any row's
118
+ timeline then creates a new sibling task (same `parentId` as that row) with
119
+ the dragged date range, inserted right after that row.
120
+
121
+ ```ts
122
+ new GanttChart(container, tasks, deps, {
123
+ onTaskCreate: (task) => console.log("created", task),
124
+ });
125
+ ```
126
+
127
+ Without `onTaskCreate`, drag-to-create is off — dragging empty timeline
128
+ space does nothing, so existing consumers see no behavior change.
129
+
130
+ ## Resource histogram
131
+
132
+ ```ts
133
+ import { computeResourceHistogram, renderResourceHistogramSVG } from "@ganttloom/gantt-core";
134
+
135
+ const buckets = computeResourceHistogram(tasks, { capacityHoursPerDay: 8, calendar });
136
+ // [{ assigneeId, assigneeName, date, allocatedHours, capacityHours, overallocated }, ...]
137
+
138
+ document.getElementById("histogram")!.innerHTML = renderResourceHistogramSVG(buckets);
139
+ ```
140
+
141
+ Each assignee is treated as fully allocated (one day's capacity) on every
142
+ working day a task they're on spans — there's no per-task effort/FTE field
143
+ in `GanttTask`, so that's the only allocation model the plain task data can
144
+ support. `overallocated` is `true` once a bucket's summed hours exceed
145
+ `capacityHours` (default 8/day).
146
+
147
+ ## WBS numbering
148
+
149
+ ```ts
150
+ import { computeWBSCodes } from "@ganttloom/gantt-core";
151
+
152
+ const wbs = computeWBSCodes(tasks); // Map<taskId, "1" | "1.1" | "1.2" | "2" | ...>
153
+ const columns = [{ id: "wbs", title: "WBS", accessor: (t) => wbs.get(t.id) }];
154
+ ```
155
+
156
+ Codes are computed fresh from the current `tasks` array (recompute after any
157
+ reorder/renest) rather than stored on the task, keeping with "everything is
158
+ a plain object."
159
+
160
+ ## Timeline markers
161
+
162
+ ```ts
163
+ new GanttChart(container, tasks, deps, {
164
+ markers: [{ date: new Date("2026-02-01"), label: "Sprint boundary" }],
165
+ });
166
+ ```
167
+
168
+ Arbitrary labeled vertical lines on the timeline, independent of any task —
169
+ for release dates, sprint boundaries, or any other project-wide event.
170
+
171
+ ## Grid column resize/reorder
172
+
173
+ Both off by default; supply the corresponding handler to opt in (same
174
+ construction-time-only wiring caveat as `onTaskCreate`):
175
+
176
+ ```ts
177
+ new GanttChart(container, tasks, deps, {
178
+ columns,
179
+ onColumnResize: (columnId, width) => { /* persist the new width */ },
180
+ onColumnReorder: (order) => { /* persist the new column id order */ },
181
+ });
182
+ ```
183
+
184
+ Drag a column header's right edge to resize it; drag a header itself to
185
+ reorder columns. The chart applies the change to its own internal copy of
186
+ `columns` immediately (so the drag feels live) and calls back so you can
187
+ persist it — same pattern as a task drag and `onDateChange`.
188
+
189
+ ## Task search/filter
190
+
191
+ ```ts
192
+ import { filterTasks } from "@ganttloom/gantt-core";
193
+
194
+ const { tasks: visible, matchedIds } = filterTasks(tasks, (t) =>
195
+ t.name.toLowerCase().includes(query.toLowerCase())
196
+ );
197
+ chart.setTasks(visible);
198
+ ```
199
+
200
+ Pure/compute-only — it returns the filtered subset (keeping a matched
201
+ task's ancestors and descendants by default, so the tree stays intact) and
202
+ the set of directly-matched ids, which you can use to dim ancestor/descendant
203
+ rows shown only for tree context. There's no special renderer wiring: feeding
204
+ `chart.setTasks(visible)` a smaller array hides the rest, the same as any
205
+ other plain-data update.
206
+
207
+ ## Scroll-to-today, group progress rollup, and custom cells
208
+
209
+ ```ts
210
+ chart.scrollToToday(); // centers the viewport on today's date
211
+
212
+ new GanttChart(container, tasks, deps, {
213
+ autoRollupProgress: true, // a group with no explicit `progress` averages its children's, duration-weighted
214
+ });
215
+
216
+ const columns = [
217
+ { id: "status", title: "Status", render: (task) => {
218
+ const el = document.createElement("span");
219
+ el.className = task.progress === 100 ? "badge-done" : "badge-pending";
220
+ el.textContent = task.progress === 100 ? "Done" : "Pending";
221
+ return el; // or return a plain string - never parsed as HTML, so it's XSS-safe by construction
222
+ } },
223
+ ];
224
+ ```
225
+
226
+ ## Row reorder and dependency editing
227
+
228
+ Both off by default; supply the corresponding handler to opt in:
229
+
230
+ ```ts
231
+ new GanttChart(container, tasks, deps, {
232
+ onTaskReorder: (draggedId, targetId, position) => {
233
+ // position: "before" | "after" (reorder as a sibling) | "inside" (reparent under target)
234
+ },
235
+ onDependencyDblClick: (dep) => {
236
+ // open your own "edit this link" UI, then call:
237
+ chart.updateDependency(dep.fromId, dep.toId, { type: "SS", lagMs: 3600_000 });
238
+ },
239
+ });
240
+ ```
241
+
242
+ Dragging a grid row onto another fires `onTaskReorder`; the chart applies
243
+ the reorder/reparent to its own internal task list immediately (live drag
244
+ feedback) the same way column resize does. Double-clicking a dependency
245
+ link fires `onDependencyDblClick` — the engine has no built-in "edit link"
246
+ popover (keeping with zero-dependency, minimal-UI scope), so build your own
247
+ and call `chart.updateDependency(...)` (undoable) to apply the result.
248
+
249
+ ## Multi-select and bulk actions
250
+
251
+ ```ts
252
+ new GanttChart(container, tasks, deps, {
253
+ selectable: true,
254
+ onSelectionChange: (taskIds) => console.log("selected:", taskIds),
255
+ });
256
+
257
+ // Click selects one; ctrl/cmd-click toggles; shift-click selects a range (by visible row order).
258
+ chart.selectTask("t1");
259
+ chart.selectTask("t2", { additive: true });
260
+ chart.bulkShiftDates(24 * 60 * 60 * 1000); // shift every selected task by 1 day, one undo step
261
+ chart.bulkDelete(); // remove every selected task (and touching dependencies), one undo step
262
+ ```
263
+
264
+ ## Split tasks
265
+
266
+ ```ts
267
+ { id: "qa", name: "QA pass", start, end, segments: [
268
+ { start: day1, end: day2 },
269
+ { start: day4, end: day5 }, // a gap, e.g. paused for an upstream fix
270
+ ] }
271
+ ```
272
+
273
+ `start`/`end` stay the authoritative overall envelope (used for
274
+ dependencies, critical path, baseline, etc.); `segments` only changes how
275
+ the bar is *drawn* — as separate rects joined by a dashed connector.
276
+ Dragging the whole task shifts every segment together; resizing an edge
277
+ only adjusts the first (left) or last (right) segment's outer edge, leaving
278
+ any gap untouched.
279
+
280
+ ## Task notes, auto-color, baseline history, and density presets
281
+
282
+ ```ts
283
+ { id: "t1", name: "Ship v2", start, end, notes: "Blocked on legal review" } // hover the bar for a native tooltip
284
+ ```
285
+
286
+ ```ts
287
+ import { applyColorByField } from "@ganttloom/gantt-core";
288
+
289
+ const colored = applyColorByField(tasks, (t) => t.assignees?.[0]?.id); // one color per first assignee
290
+ new GanttChart(container, colored, deps);
291
+ ```
292
+
293
+ ```ts
294
+ { id: "t1", name: "Ship v2", start, end, baselines: [
295
+ { label: "Original plan", start: d1, end: d2 },
296
+ { label: "As of last Friday", start: d3, end: d4 },
297
+ ] } // rendered as multiple faint stacked bars, oldest furthest back; requires showBaseline: true
298
+ ```
299
+
300
+ ```ts
301
+ import { DENSITY_PRESETS } from "@ganttloom/gantt-core";
302
+
303
+ new GanttChart(container, tasks, deps, { theme: { ...DENSITY_PRESETS.compact } });
304
+ ```
305
+
306
+ None of these need a core option - `notes`/`baselines` are plain `GanttTask` fields, `applyColorByField`/`DENSITY_PRESETS` are pure helpers/data you apply before constructing the chart.
307
+
308
+ ## Partial-day working hours
309
+
310
+ ```ts
311
+ const calendar: WorkingCalendar = {
312
+ holidays: ["2026-01-19"],
313
+ workingHours: { start: 9, end: 17 }, // 9am-5pm
314
+ };
315
+ new GanttChart(container, tasks, deps, { calendar, autoSchedule: true, viewMode: "hour" });
316
+ ```
317
+
318
+ Narrows *anchor points*, not duration math: a 3-day task cascaded by
319
+ `autoSchedule` lands its new start within the working window (not at
320
+ 11pm), but stays a 3-calendar-day task, not "3 working-window days." Hour
321
+ view shades individual non-working hours (not just whole non-working days).
322
+ `isWithinWorkingHours`/`isWorkingTime`/`shiftToWorkingTime` are exported if
323
+ you need the same logic elsewhere.
324
+
325
+ ## Resource leveling
326
+
327
+ ```ts
328
+ const { tasks: leveled, shifted } = chart.getLeveledTasks();
329
+ // or standalone: levelResources(tasks, dependencies, { calendar })
330
+ chart.setTasks(leveled); // apply it
331
+ console.log(shifted); // [{ taskId, delayMs }, ...] - what moved and by how much
332
+ ```
333
+
334
+ Delays (never pulls earlier) tasks so no assignee is double-booked on a
335
+ working day, using the same "fully allocated while active" model as
336
+ `computeResourceHistogram` - so a leveled schedule always shows zero
337
+ `overallocated` buckets afterward. Respects FS ("finish-to-start")
338
+ dependencies (never places a task before its FS predecessors finish);
339
+ SS/FF/SF aren't specifically enforced - a documented scope limit rather
340
+ than a full constraint solver.
341
+
342
+ ## Multi-page PNG export
343
+
344
+ ```ts
345
+ const pages = await chart.toPNGDataURLs({ pageWidthPx: 1600, pageHeightPx: 1200 });
346
+ // [dataUrl1, dataUrl2, ...] in row-major tile order, each at full fidelity
347
+ ```
348
+
349
+ For charts too large to comfortably rasterize as one image; omit the
350
+ options to get a single image at the chart's actual size (same as
351
+ `toPNGDataURL()`).
352
+
353
+ ## Keyboard navigation
354
+
355
+ With `keyboardAccessible: true`: Tab/Shift+Tab move between bars (native
356
+ browser focus order - every bar has `tabindex="0"`), arrow keys nudge a
357
+ focused bar by a day, Enter clicks it, **Home**/**End** scroll the timeline
358
+ to the start/end of the project's date range, and **Ctrl/Cmd+A** selects
359
+ every visible task when `selectable` is also on.
360
+
361
+ ## Zoom
362
+
363
+ ```ts
364
+ chart.zoomIn(); // 1.25x by default, clamps at a 400px/unit ceiling
365
+ chart.zoomOut(1.5); // custom factor
366
+ chart.getZoomLevel(); // current columnWidth, in px/unit
367
+ chart.fitToViewport(containerEl.clientWidth); // zoom-to-fit: scales so the whole project fits
368
+ ```
369
+
370
+ ## Export / import
371
+
372
+ - `chart.toSVGString()` / `chart.toPNGDataURL()` — raster/vector snapshot of the current render.
373
+ - `tasksToCSV(tasks)` / `tasksFromCSV(csv)` — round-trip through CSV.
374
+ - For PDF/PPTX, see [`@ganttloom/gantt-export`](../gantt-export), which consumes
375
+ `chart.getRenderModel()` directly.
376
+
377
+ ## Theming
378
+
379
+ Every visual token is a `--gantt-*` CSS custom property (see `styles.css`),
380
+ in addition to the JS `theme` option (`Partial<GanttTheme>`) — style via
381
+ either, or both. `colorScheme: "auto"` (the default) follows
382
+ `prefers-color-scheme` live.
383
+
384
+ ## API surface
385
+
386
+ The full, current type/function list is re-exported from
387
+ [`src/index.ts`](./src/index.ts) — treat it as the reference; this README
388
+ covers the concepts, not every signature.