apexgantt 3.12.0 → 3.14.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/README.md CHANGED
@@ -74,36 +74,51 @@ The layout can be configured by passing a second argument to `ApexGantt` with th
74
74
  | `snapUnit` | `'day' \| 'hour' \| 'minute'` | `'day'` | Granularity at which task drag, resize, and inline edits snap. Independent of timeline header tier. |
75
75
  | `snapValue` | `number` | `1` | Multiplier applied to `snapUnit`, e.g. `snapUnit: 'minute', snapValue: 15` → 15-min steps. |
76
76
  | `inputDateFormat` | `string` | `'MM-DD-YYYY'` | dayjs-compatible format used to parse `startTime` / `endTime` values. Include time tokens (e.g. `'YYYY-MM-DD HH:mm'`) to enable sub-day editing — inline-edit inputs switch to `datetime-local` automatically. |
77
+ | `locale` | `{ direction?: 'ltr' \| 'rtl' \| 'auto', dateLocale?: GanttDateLocale, messages?: Partial<GanttMessages> }` | `{ direction: 'ltr' }` | Localization, date-locale, and text-direction. See [Localization & RTL](#localization--rtl). |
77
78
  | `canvasStyle` | `string` | `''` | Arbitrary CSS injected onto the root container element. |
78
79
  | `backgroundColor` | `string` | `'#FFFFFF'` | Background color of the chart container. |
79
- | `headerBackground` | `string` | `'#f3f3f3'` | Background color of the header row. |
80
- | `rowHeight` | `number` | `28` | Height of each task row in pixels. |
80
+ | `headerBackground` | `string` | `'#F8F9FB'` (light) / `'#2A2A2A'` (dark) | Background color of the header row. |
81
+ | `rowHeight` | `number` | `40` | Height of each task row in pixels. |
81
82
  | `rowBackgroundColors` | `string[]` | `['#FFFFFF']` | Alternating row background colors; the pattern cycles automatically. |
82
83
  | `tasksContainerWidth` | `number` | `425` | Initial pixel width of the task-list panel. |
83
84
  | `columnConfig` | `ColumnListItem[]` | `undefined` | Custom column definitions for the task-list panel. Controls which columns are shown, their order, titles, and widths. See [Column Configuration](#column-configuration). |
85
+ | `sortBy` | `SortCriterion \| SortCriterion[]` | `[{ key: 'startTime', direction: 'asc' }]` | Initial sort for the task list. **Hierarchy-preserving**: siblings are reordered within each parent, never flattened. Each `SortCriterion` is `{ key, direction?: 'asc' \| 'desc' }`; pass an array for multi-key sort (first key wins, ties break on the next). Omit for the default (start-time ascending); pass `[]` for natural (input) order. Summary (parent) rows sort by their rolled-up span/value. See [Sorting & Filtering](#sorting--filtering). |
86
+ | `filterBy` | `(task: Task) => boolean` | `undefined` | Initial filter for the task list. A predicate run against each task; a task is kept when it **matches or has a matching descendant** (so ancestors of matches stay visible to preserve tree context). Filtering is view-only — it changes which rows render but not the tree, WBS, or task data. See [Sorting & Filtering](#sorting--filtering). |
87
+ | `enableQuickFilter` | `boolean` | `false` | Show a built-in quick-filter search box in the toolbar. Typing filters the task list to rows whose configured fields contain the query (ancestors of matches stay visible); clearing it removes the filter. Drives the same view-only filter as `gantt.filter()`. See [Sorting & Filtering](#sorting--filtering). |
88
+ | `quickFilter` | `QuickFilterOptions` | `undefined` | Fine-tunes the quick-filter box: `placeholder` (defaults to the localized `quickFilterPlaceholder` message), `fields` (task string fields matched against the query; default `['name']`), and `caseSensitive` (default `false`). |
89
+ | `filterRules` | `FilterRuleSet` | `undefined` | Initial structured filter (advanced filter builder): `{ match: 'all' \| 'any', rules: FilterRule[] }`. Each `FilterRule` is `{ field, operator, value? }`; combined with `'all'` (AND) or `'any'` (OR). Compiles to the same view-only filter as `filterBy` and takes precedence over it. See [Sorting & Filtering](#sorting--filtering). |
90
+ | `enableFilterBuilder` | `boolean` | `false` | Show a built-in advanced filter builder in the toolbar: a "Filter" button (with an active-rule count badge) that opens a popover for composing field/operator/value conditions combined with All / Any. Drives the same view-only filter as `gantt.setFilterRules()`. See [Sorting & Filtering](#sorting--filtering). |
91
+ | `groupBy` | `GroupCriterion \| ColumnKey \| string` | `undefined` | Initial grouping. When set, the parent/child tree is suspended and tasks are bucketed under collapsible group headers (label + member count). Pass a bare column key, or a `GroupCriterion` (`{ field, accessor?, label?, direction? }`) for custom value extraction, labelling, and header order. Also available at runtime via `gantt.groupBy()` / `gantt.clearGrouping()`. See [Grouping](#grouping). |
84
92
  | `barBackgroundColor` | `string` | `'#318CE7'` (light) / `'#818CF8'` (dark) | Default background fill color for task bars. |
85
93
  | `barBorderRadius` | `string` | `'5px'` | CSS border-radius applied to task bars. |
86
94
  | `barMargin` | `number` | `4` | Top and bottom margin inside each row for the task bar. |
87
95
  | `barTextColor` | `string` | `'#FFFFFF'` | Text color rendered inside task bars. |
88
96
  | `barLabel` | `BarLabelOptions` | `{ position: 'right', field: 'name' }` | Per-task bar label. Defaults to `position: 'right'` so labels stay visible regardless of bar width. Set `position: 'inside'` to render centered inside the bar, `'left'` to render before it, or `'auto'` to pick between `'inside'` and `'right'` based on bar width; supply `render: (task) => string \| HTMLElement` to compose the name with chips, icons, or owner info. When `position: 'left'`, the timeline auto-reserves `120px` of leading space so labels don't disappear under the task-list panel — tune via `barLabel.leadingPadding`. Outside labels are not currently rendered for milestones. |
89
- | `summaryBarColor` | `string` | `'#B9CECE'` (light) / `'#8FBCBC'` (dark) | Fill color for summary (group) bars. |
97
+ | `summaryBarColor` | `string` | `'#94A3B8'` (light) / `'#8FBCBC'` (dark) | Fill color for summary (group) bars. |
90
98
  | `milestoneColor` | `string` | `'#7C3AED'` (light) / `'#A78BFA'` (dark) | Fill color for milestone diamonds. |
91
99
  | `arrowColor` | `string` | `'#94A3B8'` (light) / `'#94A3B8'` (dark) | Color of dependency arrows between tasks. Subtle slate-gray by default so links don't compete with bars. |
92
100
  | `dependencies` | `DependencyOptions` | `{}` | Polish + editing for dependency arrows: `cornerRadius` (rounded joints), `hitWidth` (invisible thicker stroke for hover/click targeting), `tooltipTemplate: (ctx) => string` (HTML on hover — requires non-zero `hitWidth`), `classBuilder: (ctx) => string \| string[]` (per-arrow CSS class for cross-team / blocked / etc), `editable: boolean` (hover darkens the arrow, click selects it and reveals a "✕" affordance — click the ✕ or press <kbd>Delete</kbd> to remove via the undoable command path; on bar hover two anchor circles appear at the bar's start/finish edges, and dragging from one to another bar draws a dashed preview that commits the new dependency on release — the (source-anchor, target-anchor) pair determines the type: right→left FS, left→left SS, right→right FF, left→right SF), `allowSummaryDescendantLinks: boolean` (allow drawing edges between a summary task and its descendants; off by default since such edges confuse downstream date math). When `editable: true`, `hitWidth` is auto-bumped to at least `12` if unset. The `ctx` carries `fromTask`, `toTask`, `type`, and `lag`. |
93
101
  | `calendar` | `CalendarOptions` | `undefined` | Working-calendar config. When set, weekends + holidays drive duration math, summary aggregation, and timeline stripes — durations stop counting calendar days and start counting working days. Fields: `workingWeekdays: number[]` (0 = Sunday … 6 = Saturday; default `[1, 2, 3, 4, 5]`), `holidays: Array<string \| Date \| { date, label? }>` (non-working dates regardless of weekday; mixed forms allowed), `showNonWorkingStripes: boolean` (render hatched bands over weekend/holiday columns; default `true`), `holidayTooltip: (ctx) => string` (optional HTML rendered when the user hovers a holiday stripe — `ctx.date` is the Date, `ctx.label` is whichever string was supplied in the matching entry), `dragSnapMode: 'next' \| 'previous' \| 'allow'` (what happens when a drag/resize commits onto a non-working day — `'next'` (default) snaps forward, `'previous'` snaps backward, `'allow'` permits non-working start/end. Drag preserves the source task's working-day duration across the snap; resize snaps the moving endpoint only. No effect when `snapUnit !== 'day'` or the input format carries time tokens). Absent calendar = today's behavior (every day is a working day, no stripes, no snap). |
94
- | `borderColor` | `string` | `'#DFE0E1'` | Color of cell and row divider lines. |
95
- | `cellBorderColor` | `string` | `'#D0D7DE'` | Border color for all cells in the task table and timeline grid. |
102
+ | `borderColor` | `string` | `'#E5E7EB'` (light) / `'#3A3A3A'` (dark) | Color of cell and row divider lines. |
103
+ | `cellBorderColor` | `string` | `'#EDEFF2'` (light) / `'#3A3A3A'` (dark) | Border color for all cells in the task table and timeline grid. |
96
104
  | `cellBorderWidth` | `string` | `'1px'` | CSS border-width for all cell lines. |
97
105
  | `columnLines` | `boolean` | `true` | Whether to draw vertical lines between timeline columns. Set to `false` for a cleaner look that keeps only the horizontal row dividers. |
98
106
  | `enableProjectBoundary` | `boolean` | `false` | When `true`, two vertical lines mark the project's earliest start and latest end across all rows. Auto-recomputes when tasks are added, removed, or rescheduled. |
99
107
  | `projectBoundaryColor` | `string` | `'#7C3AED'` | Stroke colour for the project-boundary lines. Falls back to `annotationBorderColor` when omitted. |
100
108
  | `enableRollups` | `boolean` | `false` | When `true`, summary (parent) rows display thin rollup markers below the summary bar at each leaf descendant's date range. Useful for keeping children visible at a glance even when the parent is collapsed. |
101
109
  | `enableResize` | `boolean` | `true` | Allow the task-list panel to be resized by dragging the divider. |
102
- | `enableExport` | `boolean` | `true` | Show the SVG export button in the toolbar. |
110
+ | `enableExport` | `boolean` | `true` | Show the export button in the toolbar. |
111
+ | `exportFormat` | `'svg' \| 'png' \| 'pdf'` | `'svg'` | Format produced by the toolbar export button. `svg` is vector; `png` rasterizes the chart; `pdf` embeds a raster on a single page. Any format is also available programmatically via `gantt.exportChart(format)`. |
112
+ | `autoSizeColumns` | `boolean` | `true` | Auto-size each task-list column to fit its header title and cell content, growing the panel (never below `tasksContainerWidth`) so nothing is clipped. `minWidth` sets a column's preferred (default) width and `maxWidth` (default `320px`) the ceiling. The panel stays freely resizable: dragging the divider stretches columns proportionally or shrinks them (down to a small floor, so a large `minWidth` never blocks resizing). Set `false` for the legacy behavior where the panel width is split purely by `flexGrow`. See [Column Configuration](#column-configuration). |
113
+ | `resizableColumns` | `boolean` | `true` | Allow individual columns to be resized by dragging the handle at the trailing edge of each column header. A resized column is pinned to its chosen pixel width and the other columns absorb the remaining space, so you can keep some columns wide and others thin. Double-click a handle to reset that column to its auto width. Opt a single column out with `columnConfig[].resizable: false`. Also available programmatically via `gantt.setColumnWidth()` / `gantt.resetColumnWidths()`. See [Column Configuration](#column-configuration). |
114
+ | `reorderableColumns` | `boolean` | `true` | Allow columns to be reordered by dragging a column header left or right onto another column (a drop indicator shows the insertion point). The new order is reflected in the grid, included in `getState()` / `setState()`, and emitted as a `columnReorder` event. Also available programmatically via `gantt.setColumnOrder()`. See [Column Configuration](#column-configuration). |
115
+ | `persistState` <a id="uistate"></a> | `boolean \| { key?: string }` | `false` | Persist and restore the UI view state (zoom, scroll, sort, filter, collapse, selection) via `localStorage`. `true` uses the default key (`'apexgantt-state'`); an object sets a custom `key`. Restored on the first `render()`; saved (debounced) whenever the view changes. Also available programmatically via `gantt.getState()` / `gantt.setState()`. See [UI state persistence](#ui-state-persistence). |
103
116
  | `enableInlineEdit` | `boolean` | `false` | Allow editing task fields directly in the task-list cells (double-click to edit `name`, `startTime`, `endTime`, `duration`, `progress`). Auto-enabled when `enableTaskEdit` is `true`; set explicitly to `false` to opt out. See [Inline Editing](#inline-editing). |
104
117
  | `enableTaskDrag` | `boolean` | `true` | Allow tasks to be reordered by dragging rows in the task list. |
105
118
  | `enableTaskEdit` | `boolean` | `false` | Show the inline task-edit form when a task row is clicked. |
106
119
  | `enableTaskResize` | `boolean` | `true` | Allow task bars to be resized by dragging their handles. |
120
+ | `enableDrawTask` | `boolean` | `false` | Allow creating a task by dragging across an empty stretch of the timeline: press on an empty row, drag horizontally to sweep out the date range, and release to add a task with those (snapped) start/end dates. Press `Escape` mid-drag to cancel. Off by default so existing empty-area drags stay inert. Respects `beforeTaskAdd` and is recorded in undo history. See [Drawing tasks on the timeline](#drawing-tasks-on-the-timeline). |
121
+ | `enableScrollButtons` | `boolean` | `true` | Show a small chevron at the edge of a task's row when that task's bar is scrolled out of the visible timeline window; clicking it scrolls the bar back into view (nearest-edge). Also available programmatically via `gantt.scrollToTask()`. |
107
122
  | `enableProgressDrag` | `boolean` | `true` | Allow editing task progress by dragging the small handle that appears at the bottom of the bar on hover. Snaps to whole percent on commit and emits a `taskProgressChanged` event. |
108
123
  | `enableTaskEditingShortcuts` | `boolean` | `false` | Enable keyboard shortcuts on the task-list panel: `Delete` / `Backspace` removes the focused task (cascade: children), `Tab` indents under the previous sibling, `Shift+Tab` outdents to the grandparent. Off by default; opt in once you've decided the shortcuts fit your app. All operations are recorded in the undo history and respect `beforeTaskDelete` / `beforeTaskMove` hooks. |
109
124
  | `enableTaskCRUDToolbar` | `boolean` | `false` | Show built-in `+ Add task` and trash-icon `Delete` buttons in the toolbar. Delete is auto-disabled when nothing is selected and deletes every selected task (cascade: children) on click; Add inserts a root-level "New task" using placeholder dates derived from the current project span. Combine with `enableSelection: true` for the delete button to be useful. |
@@ -122,15 +137,15 @@ The layout can be configured by passing a second argument to `ApexGantt` with th
122
137
  | `criticalBarColor` | `string` | `'#E53935'` (light) / `'#F87171'` (dark) | Fill color for task bars on the critical path. |
123
138
  | `criticalArrowColor` | `string` | `'#E53935'` (light) / `'#F87171'` (dark) | Stroke color for dependency arrows on the critical path. |
124
139
  | `enableCrosshair` | `boolean` | `false` | Show a vertical crosshair line that follows the cursor across the timeline, with a label showing the precise date/time at the pointer position. |
125
- | `crosshairColor` | `string` | `'#318CE7'` (light) / `'#818CF8'` (dark) | Color of the crosshair line and the label background. |
140
+ | `crosshairColor` | `string` | `'#3B82F6'` (light) / `'#818CF8'` (dark) | Color of the crosshair line and the label background. |
126
141
  | `crosshairLabelFormat` | `(date, tier) => string` | _auto_ | Custom formatter for the crosshair label. Receives the date under the cursor and the active sub-tier (`'minute' \| 'hour' \| 'halfday' \| 'day' \| 'week' \| 'month' \| 'quarter' \| 'year'`). When omitted, the label auto-adapts to the active tier — `'ddd MM/DD/YYYY'` for day-and-coarser tiers, `'MM/DD HH:mm'` for halfday/hour/minute tiers. |
127
- | `baseline` | `Partial<BaselineOptions>` | `undefined` | When `enabled: true`, renders a thin baseline bar below each task bar. Hovering the baseline shows a tooltip with its planned start/end dates. When `rowHeight` isn't explicitly set, the default rowHeight is bumped to make room for the baseline without squeezing the actual bar. Fields: `color: string` (primary fill / stripe color; default `'#9E9E9E'`), `striped: boolean` (fill the bar with thick diagonal stripes instead of a flat color; default `true`), `stripeColor: string` (color of the gaps between stripes; default `'#FFFFFF'`), `stripeWidth: number` (px width of each stripe band, larger values = thicker; default `3`), `stripeAngle: number` (stripe angle in degrees; default `45`). By default baselines render as thick grey/white diagonal stripes; set `striped: false` for a solid `color` fill. |
142
+ | `baseline` | `Partial<BaselineOptions>` | `undefined` | When `enabled: true`, renders a thin baseline bar below each task bar. Hovering the baseline shows a tooltip with its planned start/end dates. When `rowHeight` isn't explicitly set, the default rowHeight is bumped to make room for the baseline without squeezing the actual bar. Fields: `color: string` (fill / stripe color; when omitted it defaults to the task bar's progress shade, i.e. the darker tone of the bar drawn above, so the baseline matches its bar; set an explicit color to override), `striped: boolean` (fill the bar with thick diagonal stripes instead of a flat color; default `true`), `stripeColor: string` (color of the gaps between stripes; default `'#FFFFFF'`), `stripeWidth: number` (px width of each stripe band, larger values = thicker; default `3`), `stripeAngle: number` (stripe angle in degrees; default `45`). By default baselines render as thick diagonal stripes tinted with the bar's progress color over a white background; set `striped: false` for a solid fill. |
128
143
  | `tooltipId` | `string` | `'apexgantt-tooltip-container'` | HTML `id` for the tooltip container element. |
129
144
  | `tooltipTemplate` | `(task, dateFormat) => string` | built-in | Custom function returning an HTML string for the task tooltip. |
130
- | `tooltipBorderColor` | `string` | `'#BCBCBC'` | Border color of the tooltip. |
145
+ | `tooltipBorderColor` | `string` | `'#E5E7EB'` (light) / `'#444444'` (dark) | Border color of the tooltip. |
131
146
  | `tooltipBGColor` | `string` | `'#FFFFFF'` | Background color of the tooltip. |
132
- | `fontColor` | `string` | `'#000000'` | Color for all text in the chart. |
133
- | `fontFamily` | `string` | `''` | CSS font-family for the chart. |
147
+ | `fontColor` | `string` | `'#1F2933'` (light) / `'#E0E0E0'` (dark) | Color for all text in the chart. |
148
+ | `fontFamily` | `string` | `'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'` | CSS font-family for the chart. |
134
149
  | `fontSize` | `string` | `'14px'` | CSS font-size for the chart. |
135
150
  | `fontWeight` | `string` | `'400'` | CSS font-weight for the chart. |
136
151
  | `annotationBgColor` | `string` | `'#F9D1FC'` | Background color of annotation markers. |
@@ -143,6 +158,34 @@ The layout can be configured by passing a second argument to `ApexGantt` with th
143
158
  | `toolbarItems` | `ToolbarItem[]` | `[]` | Custom controls rendered in the toolbar alongside the built-in zoom and export buttons. Each item can be a `ToolbarButton`, `ToolbarSelect`, or `ToolbarSeparator`. |
144
159
  | `taskListAriaLabel` | `string` | `'Task list'` | `aria-label` for the task-list table, used by screen readers. |
145
160
 
161
+ ### Localization & RTL
162
+
163
+ Pass a `locale` object to translate strings, localize dates, and flip text direction. With the defaults (`{ direction: 'ltr' }`, no `dateLocale`, no `messages`) the output is byte-for-byte unchanged.
164
+
165
+ ```ts
166
+ import localeFr from 'dayjs/locale/fr';
167
+
168
+ const gantt = new ApexGantt('#chart', {
169
+ series: tasks,
170
+ locale: {
171
+ direction: 'rtl',
172
+ dateLocale: localeFr, // months/weekdays in the timeline header render in French
173
+ messages: {
174
+ addTask: 'Ajouter une tâche',
175
+ editTaskTitle: (name) => `Modifier : ${name}`,
176
+ },
177
+ },
178
+ });
179
+ ```
180
+
181
+ | `locale` key | Type | Default | Description |
182
+ | --- | --- | --- | --- |
183
+ | `direction` | `'ltr' \| 'rtl' \| 'auto'` | `'ltr'` | Text/layout direction. `'rtl'` sets `dir="rtl"` on the container (flowing the toolbar, task list, task form, context menu, and tooltips right-to-left and moving the task-list panel to the right) **and mirrors the timeline time axis** so time flows right-to-left: the earliest date sits on the right, bars/header cells/grid lines/non-working stripes/dependency arrows/annotations are all mirrored, progress fills from the start (right) edge, and drag/resize/progress interactions are direction-aware. `'auto'` defers to the document/element. |
184
+ | `dateLocale` | `GanttDateLocale` (a `dayjs/locale/*` object) | `undefined` | Localizes every date, month, weekday, and quarter label the timeline renders. Import the dayjs locale object and pass it; ApexGantt registers it on its own bundled dayjs, so you do not need to configure dayjs yourself. |
185
+ | `messages` | `Partial<GanttMessages>` | `{}` | Overrides any subset of the generated strings (toolbar, context menu, task form + validation, baseline tooltip, add-task row, and bar aria-labels). Unset keys keep their English defaults, exported as `DEFAULT_GANTT_MESSAGES`. |
186
+
187
+ `GanttMessages` keys include: `addTask`, `deleteSelected`, `undo`, `redo`, `exportAsSvg`, `exportAsPng`, `exportAsPdf`, `quickFilterPlaceholder`, `filterButton`, `filterHeading`, `filterMatchLabel`, `filterMatchAll`, `filterMatchAny`, `filterAddCondition`, `filterApply`, `filterClear`, `filterRemoveCondition`, `filterNoConditions`, `filterOperatorLabel(operator)`, `groupNone`, `groupCountLabel(count)`, `exportFailedNoChart`, `exportFailedGeneric`, `editTask`, `addChildTask`, `addSiblingTask`, `indent`, `outdent`, `deleteTask`, `deleteTaskWithChildren`, `formTaskName`, `formStartDate`, `formEndDate`, `formProgress`, `formSubmit`, `editTaskTitle(name)`, `validationStartRequired`, `validationEndRequired`, `validationEndAfterStart`, `validationNameRequired`, `validationProgressRequired`, `validationProgressRange`, `addTaskRowLabel`, `baselineLabel`, `startLabel`, `endLabel`, `summaryAriaLabel(ctx)`, `barAriaLabel(ctx)`, `barAriaValueText(ctx)`, and `progressAriaLabel(name)`. Column headers (set via the columns API) and the default tooltip template (`tooltipTemplate`) are localized through their own options.
188
+
146
189
  Default tooltip template
147
190
 
148
191
  ```js
@@ -259,7 +302,31 @@ Each tasks should be in below format
259
302
 
260
303
  Customize which columns appear in the task-list panel, their order, titles, and widths. When `columnConfig` is provided, **it is authoritative** — only the columns you list are rendered, in the order you specify. Omitted columns are hidden. Each entry is merged with defaults, so you only need to specify overrides.
261
304
 
262
- Available column keys: `Name`, `StartTime`, `EndTime`, `Duration`, `Progress`, `ProgressRing`, `Wbs`.
305
+ By default ([`autoSizeColumns`](#options)), each column is sized to fit its header title and the widest cell content, and the task-list panel grows so nothing is clipped. `minWidth` sets a column's preferred (default) width and `maxWidth` (default `320px`) the ceiling. The panel remains freely resizable: dragging the divider distributes width proportionally to content when widening, and shrinks columns toward a small floor when narrowing (so a large `minWidth` never blocks the resize). Set `autoSizeColumns: false` to split the fixed `tasksContainerWidth` purely by `flexGrow` (the legacy behavior), in which case columns clip to their `minWidth` when the panel is too narrow.
306
+
307
+ **Resizing a single column.** With `resizableColumns` on (the default), each column header has a drag handle at its trailing edge. Drag it to pin that column to an exact pixel width; the remaining columns absorb the leftover panel space, so you can keep some columns wide and others thin. Double-click a handle to reset that column to its auto width. Lock one column with `resizable: false` in its `columnConfig` entry. The same overrides are available programmatically via `gantt.setColumnWidth(key, px)`, `gantt.resetColumnWidths(key?)`, and `gantt.getColumnWidths()`, are included in `getState()` / `setState()`, and emit a `columnResize` event.
308
+
309
+ **Reordering columns.** With `reorderableColumns` on (the default), drag a column header left or right onto another column to move it; a vertical drop indicator shows where it will land, and a short drag threshold keeps a plain click sorting as usual. Drive it programmatically with `gantt.setColumnOrder([...keys])` / `gantt.getColumnOrder()`. The order is included in `getState()` / `setState()` and emits a `columnReorder` event.
310
+
311
+ Available built-in column keys:
312
+
313
+ | Key | Title | Shows |
314
+ | --- | --- | --- |
315
+ | `Name` | Task Name | Task name with the collapse chevron + indentation |
316
+ | `StartTime` | Start | Start date (rolled-up start for summary rows) |
317
+ | `EndTime` | End | End date (rolled-up end for summary rows) |
318
+ | `Duration` | Duration | Duration in the active snap unit (working days when a calendar is set) |
319
+ | `Progress` | Progress | Progress percent as text |
320
+ | `ProgressRing` | Progress | Progress as an SVG ring |
321
+ | `Wbs` | WBS | Work Breakdown Structure code (`1`, `1.2`, …) |
322
+ | `Assignees` | Assignees | Comma-joined assignee names (from `task.assignees`) |
323
+ | `Predecessors` | Predecessors | Predecessor tasks by WBS, with a non-`FS` type + non-zero lag suffix (e.g. `1.2, 3SS+2d`) |
324
+ | `Successors` | Successors | Successor tasks by WBS (same notation) |
325
+ | `BaselineStart` | Baseline Start | Baseline start date (from `task.baseline.start`) |
326
+ | `BaselineEnd` | Baseline End | Baseline end date (from `task.baseline.end`) |
327
+ | `BaselineVariance` | Variance | Finish variance vs baseline in days (`+` = late, `-` = early) |
328
+
329
+ `Predecessors` / `Successors` are derived from the dependency graph and refresh automatically as dependencies or WBS change. Every built-in column except `Wbs` is sortable and filterable; `Assignees`, `Predecessors`, and `Successors` filter as text, the baseline dates as dates, and `BaselineVariance` as a number. All are groupable via [`gantt.groupBy()`](#grouping).
263
330
 
264
331
  ### Customize column widths
265
332
 
@@ -385,11 +452,202 @@ The code is also available on each `Task` as `task.wbs` if you need to read it f
385
452
  | --- | --- | --- | --- |
386
453
  | `key` | `ColumnKey \| string` | — | Built-in column identifier or any custom string id when `render` is supplied (required). |
387
454
  | `title` | `string` | from defaults | Header text displayed for the column. |
388
- | `minWidth` | `string` | `'30px'` | Minimum CSS width (used in `minmax()`). |
455
+ | `minWidth` | `string` | `'30px'` | Minimum CSS width (used in `minmax()`). With `autoSizeColumns` on this is the column's preferred (default) width. |
456
+ | `maxWidth` | `string` | `'320px'` | Ceiling for the auto-sized width, so one long value can't dominate the panel. Ignored when `autoSizeColumns` is off. |
389
457
  | `flexGrow` | `number` | `1` | Flex proportion (used as `fr` units in CSS Grid). |
390
458
  | `visible` | `boolean` | `true` | Set to `false` to hide the column. |
459
+ | `resizable` | `boolean` | `true` | Set to `false` to lock this column at its auto/configured width (no drag handle) when `resizableColumns` is on. |
391
460
  | `render` | `ColumnRenderer` | — | Custom cell renderer. Required for custom columns; ignored for built-in keys. |
392
- | `accessor` | `(task) => unknown` | — | Extracts the cell's underlying value (used by SVG export and future sort/filter features). |
461
+ | `accessor` | `(task) => unknown` | — | Extracts the cell's underlying value. Used by SVG export and by sorting/filtering — it is what makes a **custom** column sortable. |
462
+ | `sortable` | `boolean` | _auto_ | Whether the column participates in sorting (header click + `sortBy` / `gantt.sort()`). Defaults to `true` for built-in value columns, `false` for `Wbs`, and `true` for custom columns only when an `accessor` or `comparator` is supplied. |
463
+ | `comparator` | `(a: Task, b: Task) => number` | — | Custom sort comparator for this column. Takes precedence over `accessor`. Return negative / zero / positive for ascending order; the active direction is applied on top, and ties fall back to natural (input) order. |
464
+
465
+ ## Sorting & Filtering
466
+
467
+ ### Sorting
468
+
469
+ Sorting a Gantt is **hierarchy-preserving**: siblings are reordered within each parent, never flattened, so summary bars, WBS codes, and rollups stay coherent. Summary (parent) rows sort by their rolled-up span (for date/duration columns) or aggregate value.
470
+
471
+ Set an initial sort with the `sortBy` option, or drive it imperatively. By default the chart sorts by start time ascending; pass `sortBy: []` for natural (input) order.
472
+
473
+ ```js
474
+ import {ColumnKey} from 'apexgantt';
475
+
476
+ const gantt = new ApexGantt(element, {
477
+ series: tasks,
478
+ sortBy: {key: ColumnKey.Name, direction: 'asc'},
479
+ // multi-key: sort by progress desc, then name asc
480
+ // sortBy: [{ key: ColumnKey.Progress, direction: 'desc' }, { key: ColumnKey.Name }],
481
+ });
482
+
483
+ gantt.sort({key: ColumnKey.Progress, direction: 'desc'}); // replace the sort
484
+ gantt.getSort(); // [{ key: 'progress', direction: 'desc' }]
485
+ gantt.clearSort(); // back to natural (input) order
486
+ ```
487
+
488
+ **Clicking a sortable column header** cycles its sort ascending → descending → none (natural order), showing a ▲ / ▼ caret. **Shift+click** a header to add it as an additional sort key (multi-column sort) — each Shift+clicked column cycles ascending → descending → removed, the others are kept, and a small precedence number (1, 2, 3…) appears next to each caret. A plain click always collapses back to a single-key sort. Built-in value columns are sortable out of the box; `Wbs` is not (it is purely positional). A custom column becomes sortable when you give it an `accessor` (or a `comparator`). Override per column with `sortable`.
489
+
490
+ > Sorting is applied on demand. Editing a task's dates (drag / resize / inline edit) does not automatically re-sort the row — call `gantt.sort(gantt.getSort())` to re-apply the current sort if you want the row to move. Manual sibling re-ordering is not offered in this release, so there is no drag-vs-sort conflict.
491
+
492
+ ### Filtering
493
+
494
+ A filter is a predicate run against each task. A task is **kept when it matches or has a matching descendant**, so the ancestors of matches stay visible and the tree context is preserved. Filtering is view-only: it changes which rows render but never the tree, WBS, or task data.
495
+
496
+ ```js
497
+ const gantt = new ApexGantt(element, {
498
+ series: tasks,
499
+ filterBy: (task) => task.progress < 100, // initial filter: show incomplete work
500
+ });
501
+
502
+ gantt.filter((task) => /design/i.test(task.name)); // apply a filter
503
+ gantt.isFiltered(); // true
504
+ gantt.clearFilter(); // show every row again
505
+ ```
506
+
507
+ **Built-in quick filter.** Set `enableQuickFilter: true` to render a search box in the toolbar; typing filters by the configured `quickFilter.fields` (default `['name']`).
508
+
509
+ ```js
510
+ const gantt = new ApexGantt(element, {
511
+ series: tasks,
512
+ enableQuickFilter: true,
513
+ quickFilter: {placeholder: 'Find a task…', fields: ['name'], caseSensitive: false},
514
+ });
515
+ ```
516
+
517
+ **Advanced filter builder.** Set `enableFilterBuilder: true` for a "Filter" toolbar button that opens a popover for composing multiple conditions (field + operator + value) combined with **All** (AND) or **Any** (OR). The valid operators adapt to the column's type:
518
+
519
+ - **Text** (Name, custom): `contains`, `notContains`, `equals`, `notEquals`, `startsWith`, `endsWith`, `isEmpty`, `notEmpty`
520
+ - **Number** (Progress, Duration): `equals`, `notEquals`, `gt`, `gte`, `lt`, `lte`, `isEmpty`, `notEmpty`
521
+ - **Date** (Start, End): `on`, `before`, `after`, `isEmpty`, `notEmpty`
522
+
523
+ The same structured filter is available programmatically, with or without the UI:
524
+
525
+ ```js
526
+ import {ColumnKey} from 'apexgantt';
527
+
528
+ gantt.setFilterRules({
529
+ match: 'all', // 'all' = AND, 'any' = OR
530
+ rules: [
531
+ {field: ColumnKey.Progress, operator: 'lt', value: 100},
532
+ {field: ColumnKey.StartTime, operator: 'after', value: '2024-02-01'},
533
+ ],
534
+ });
535
+
536
+ gantt.getFilterRules(); // the active FilterRuleSet, or null
537
+ gantt.clearFilter(); // drop it
538
+ ```
539
+
540
+ A custom column is filterable when it exposes an `accessor` (treated as text). For fully bespoke logic, wire `gantt.filter(predicate)` to your own controls (see the `sort-filter` demo).
541
+
542
+ > Filtering respects collapse state: a matching descendant under a collapsed parent stays hidden until the parent is expanded.
543
+
544
+ ## UI state persistence
545
+
546
+ The Gantt can remember "where the user left off": zoom, scroll position, expand/collapse state, selection, sort, and filter. This view state is separate from the task data.
547
+
548
+ **Manual (`getState` / `setState`).** Capture a JSON-serializable snapshot and restore it whenever you like (for example, persist it to your own backend keyed by user).
549
+
550
+ ```js
551
+ const saved = gantt.getState();
552
+ // { version: 1, zoom, scroll, collapsed, selected, sort, filterRules, quickFilter }
553
+
554
+ // later, or in a new session:
555
+ gantt.setState(saved);
556
+ ```
557
+
558
+ `setState()` accepts a partial state, so you can restore just one slice:
559
+
560
+ ```js
561
+ gantt.setState({sort: [{key: ColumnKey.Name, direction: 'asc'}]});
562
+ gantt.setState({collapsed: ['phase-1', 'phase-2']}, {silent: true}); // no sortChange/filterChange
563
+ ```
564
+
565
+ **Automatic (`persistState`).** Set the `persistState` option to have the Gantt read from and write to `localStorage` for you. State is restored on the first `render()` and saved (debounced) whenever the view changes.
566
+
567
+ ```js
568
+ const gantt = new ApexGantt(element, {series: tasks, persistState: true});
569
+ // or a custom storage key:
570
+ const gantt = new ApexGantt(element, {series: tasks, persistState: {key: 'project-42-gantt'}});
571
+ ```
572
+
573
+ State stored under an incompatible schema `version` (or malformed JSON) is ignored, so upgrading the library never breaks a load. Persistence is a no-op in environments without `localStorage` (for example, server-side rendering).
574
+
575
+ ## Grouping
576
+
577
+ Group the task grid by a field to bucket tasks under collapsible group headers. While grouping is active the parent/child tree is **suspended**: every task appears flat under its group header (with a member count), and the timeline shows each task's bar aligned to its grouped row.
578
+
579
+ Set it up front with the `groupBy` option or at runtime with `gantt.groupBy()`:
580
+
581
+ ```js
582
+ // Bare column key — group by that column's value.
583
+ gantt.groupBy(ColumnKey.Progress);
584
+
585
+ // GroupCriterion — custom value extraction, labelling, and header order.
586
+ gantt.groupBy({
587
+ field: 'status',
588
+ accessor: (task) => task.status, // defaults to the column's accessor
589
+ label: (value) => `Status: ${value}`, // defaults to String(value)
590
+ direction: 'desc', // header order; defaults to 'asc'
591
+ });
592
+
593
+ gantt.getGroupBy(); // the active GroupCriterion, or null
594
+ gantt.isGrouping(); // boolean
595
+ gantt.clearGrouping(); // restore the tree view
596
+ ```
597
+
598
+ Grouping composes with sorting and filtering: members are ordered within each group by the active sort, and a filter drops non-matching members (empty groups disappear). Tasks whose group value is empty collect under a localized "(None)" group, which always sorts last. Each group header is collapsible; clicking its chevron hides that group's members. Group headers themselves have no task bar.
599
+
600
+ > Grouping is view-only — it never mutates the task tree, WBS, or task data; clearing it returns the exact prior hierarchy. A `GroupCriterion`'s `accessor`/`label` functions are not serialized by [`persistState`](#ui-state-persistence) / `getState()` (only the `field` and `direction` are), so a restored grouping falls back to the column's own accessor.
601
+
602
+ ## Split tasks
603
+
604
+ A task can be split into several **worked segments** separated by gaps on a single row (for example: work Mon–Wed, pause, resume Fri). Provide a `segments` array on the task, or split at runtime with `gantt.splitTask()`.
605
+
606
+ ```js
607
+ const gantt = new ApexGantt(element, {
608
+ series: [
609
+ {
610
+ id: 't1',
611
+ name: 'Implementation',
612
+ startTime: '2026-06-01',
613
+ endTime: '2026-06-20',
614
+ progress: 50,
615
+ segments: [
616
+ {start: '2026-06-01', end: '2026-06-06'},
617
+ {start: '2026-06-12', end: '2026-06-20'},
618
+ ],
619
+ },
620
+ ],
621
+ });
622
+ ```
623
+
624
+ The task's `startTime` / `endTime` are the **envelope** (first segment start → last segment end) and are derived automatically from the segments, so summary rollups, dependencies, the timeline header, and the duration column all keep working. The bar renders one filled piece per segment joined by a thin connector line across the gaps; progress fills the worked segments left-to-right (gaps carry no progress).
625
+
626
+ Split at runtime, then clear the split by setting `segments` back to an empty array:
627
+
628
+ ```js
629
+ gantt.splitTask('t1', '2026-06-08', {resumeAt: '2026-06-12'}); // open a gap
630
+ gantt.isSplit('t1'); // true
631
+ gantt.updateTask('t1', {segments: []}); // back to one contiguous bar
632
+ ```
633
+
634
+ Interactions are preserved: dragging a split task moves all its segments together, and resizing adjusts its outer segment — both inferred automatically, so the same drag/resize handles work as for a normal bar.
635
+
636
+ > v1 is API-driven. Interactive split gestures (double-click / context-menu "split here"), per-segment drag/resize, and critical-path math that excludes gap time are not yet implemented; the envelope is treated as the task's span.
637
+
638
+ ## Drawing tasks on the timeline
639
+
640
+ Set `enableDrawTask: true` to let users create a task by dragging across an empty stretch of the timeline: press on an empty row, drag horizontally to sweep out the range, and release. A preview bar tracks the cursor during the drag, and on release a task is added with the swept start/end **snapped to the active snap unit** (`snapUnit` / `snapValue`). A press that doesn't move far enough counts as a click and creates nothing; press `Escape` mid-drag to cancel.
641
+
642
+ ```js
643
+ const gantt = new ApexGantt(element, {
644
+ series: tasks,
645
+ enableDrawTask: true,
646
+ snapUnit: 'day',
647
+ });
648
+ ```
649
+
650
+ The gesture only starts on empty timeline background (never on an existing bar or its handles), goes through the same path as `gantt.addTask()` (respects `beforeTaskAdd`, recorded in undo history, emits `taskAdded`), and the new root task then sorts into position by its dates like any other add. It's off by default so existing charts' empty-area drags stay inert.
393
651
 
394
652
  ## Custom column renderers
395
653
 
@@ -518,6 +776,18 @@ The library invokes the cleanup function in **all three** of these cases:
518
776
 
519
777
  You do not need to listen for DOM mutations yourself. If your renderer returns a function, the library will call it.
520
778
 
779
+ ## Touch & pointer support
780
+
781
+ All interactive gestures are built on **Pointer Events**, so they work with mouse, touch, and pen alike — no separate touch mode to enable. This covers dragging a task bar to reschedule it, resizing via the edge handles, dragging the in-bar progress handle, drawing dependencies from a bar's anchors, and dragging the task-list / timeline split bar.
782
+
783
+ Implementation notes:
784
+
785
+ - Interactive elements set `touch-action: none` so a touch-drag isn't stolen by the browser's pan/zoom (which would otherwise cancel the gesture).
786
+ - Each gesture is owned by the pointer that started it, so a second finger can't hijack an in-progress drag.
787
+ - If the OS interrupts a gesture (`pointercancel`), an in-flight bar drag reverts to its starting position rather than committing a partial move.
788
+
789
+ > Hover-only affordances (the crosshair, bar tooltips) naturally don't appear on touch since there is no hover, but every editing gesture and tap/selection works.
790
+
521
791
  ## Sub-day scheduling
522
792
 
523
793
  Set `inputDateFormat` to a format that includes time tokens (e.g. `'YYYY-MM-DD HH:mm'`) and choose a finer `snapUnit` to schedule tasks at the hour or minute level:
@@ -925,6 +1195,150 @@ Zooms out the gantt based on current view mode. View mode direction for zoom in
925
1195
  ganttInstance.zoomOut();
926
1196
  ```
927
1197
 
1198
+ ### 15. `sort(criteria)`
1199
+
1200
+ Apply a hierarchy-preserving sort. Pass one `SortCriterion` or an array (multi-key). Re-renders and emits `sortChange`. See [Sorting & Filtering](#sorting--filtering).
1201
+
1202
+ ```js
1203
+ gantt.sort({key: ColumnKey.Name, direction: 'asc'});
1204
+ ```
1205
+
1206
+ ### 16. `clearSort()`
1207
+
1208
+ Clear the active sort and return to natural (input) order. Emits `sortChange`.
1209
+
1210
+ ### 17. `getSort()`
1211
+
1212
+ Return the active sort criteria as `SortCriterion[]` (empty array = natural order).
1213
+
1214
+ ### 18. `toggleSort(key, opts?)`
1215
+
1216
+ Cycle a column's sort ascending → descending → none. Backs the column-header click UX; no-op for non-sortable columns. Pass `{ append: true }` (Shift+click) to add the column as an extra sort key for multi-column sort instead of replacing the current one. Emits `sortChange`.
1217
+
1218
+ ### 19. `filter(predicate)`
1219
+
1220
+ Apply a filter. `predicate: (task) => boolean`; a task is kept when it matches or has a matching descendant. Re-renders and emits `filterChange`.
1221
+
1222
+ ```js
1223
+ gantt.filter((task) => task.progress < 100);
1224
+ ```
1225
+
1226
+ ### 20. `clearFilter()`
1227
+
1228
+ Clear the active filter so every row is shown again. Emits `filterChange`.
1229
+
1230
+ ### 21. `isFiltered()`
1231
+
1232
+ Returns `true` when a filter is currently active.
1233
+
1234
+ ### 22. `setFilterRules(ruleSet)`
1235
+
1236
+ Apply a structured filter (`FilterRuleSet`) — conditions combined with `'all'` (AND) or `'any'` (OR). Pass `null` or an empty rule list to clear. Compiles to the same view-only filter as `filter()`. Emits `filterChange`.
1237
+
1238
+ ### 23. `getFilterRules()`
1239
+
1240
+ Return the active `FilterRuleSet`, or `null` when none is set.
1241
+
1242
+ ### 24. `exportChart(format?)`
1243
+
1244
+ Export the chart and trigger a download. `format` is `'svg'` (vector), `'png'` (raster), or `'pdf'` (single-page, image-based); defaults to the configured `exportFormat`. Under row virtualization the full dataset is expanded for the snapshot and restored afterward. Returns a `Promise` that resolves once the download has been triggered.
1245
+
1246
+ ```js
1247
+ await gantt.exportChart('png');
1248
+ ```
1249
+
1250
+ > PNG and PDF rasterize the chart via a `<canvas>`, so they run in the browser (not in headless/jsdom environments). The PDF is generated dependency-free (a single page embedding a JPEG of the chart).
1251
+
1252
+ ### 25. `getState()`
1253
+
1254
+ Capture the current UI view state (zoom, scroll, collapse, selection, sort, filter) as a JSON-serializable `GanttUiState`. Pair with `setState()` to save and restore "where the user left off", for example to your own backend.
1255
+
1256
+ ```js
1257
+ const saved = gantt.getState();
1258
+ // { version: 1, zoom, scroll, collapsed, selected, sort, filterRules, quickFilter }
1259
+ ```
1260
+
1261
+ ### 26. `setState(state, opts?)`
1262
+
1263
+ Restore a state produced by `getState()`. Any omitted field is left untouched, so a partial state (for example `{ sort: [...] }`) applies just that slice. Re-renders once, then emits `sortChange` / `filterChange` for the parts that changed. Pass `{ silent: true }` to suppress those events.
1264
+
1265
+ ```js
1266
+ gantt.setState(saved);
1267
+ gantt.setState({sort: [{key: ColumnKey.Name, direction: 'asc'}]}); // partial
1268
+ ```
1269
+
1270
+ > To have the Gantt save and restore state automatically via `localStorage`, set the [`persistState`](#uistate) option instead of calling these by hand. See [UI state persistence](#ui-state-persistence).
1271
+
1272
+ ### 27. `groupBy(criterion)`
1273
+
1274
+ Group the task grid by a field. While grouping is active the parent/child tree is suspended and every task appears flat under a collapsible group header (label + member count). Pass a bare column key or a `GroupCriterion` (`{ field, accessor?, label?, direction? }`). Re-renders and emits `groupChange`. See [Grouping](#grouping).
1275
+
1276
+ ```js
1277
+ gantt.groupBy(ColumnKey.Progress);
1278
+ gantt.groupBy({field: 'status', label: (v) => `Status: ${v}`, direction: 'desc'});
1279
+ ```
1280
+
1281
+ ### 28. `clearGrouping()`
1282
+
1283
+ Clear the active grouping and restore the tree view. Emits `groupChange`.
1284
+
1285
+ ### 29. `getGroupBy()` / `isGrouping()`
1286
+
1287
+ `getGroupBy()` returns the active `GroupCriterion` (or `null`); `isGrouping()` returns whether grouping is active.
1288
+
1289
+ ### 30. `splitTask(taskId, at, options?)`
1290
+
1291
+ Split a task into separate worked segments at `at`, producing a gap on the timeline (see [Split tasks](#split-tasks)). The segment containing `at` is cut so its first piece ends at `at` and the rest resumes at `options.resumeAt` (default: the next day — pass a later `resumeAt` for a wider gap). Routed through `updateTask`, so it is validated, undoable, and emits `taskUpdate`. No-op on milestones, summary bars, or when `at` is not strictly inside a worked span.
1292
+
1293
+ ```js
1294
+ gantt.splitTask('t3', '2026-06-10', {resumeAt: '2026-06-14'});
1295
+ ```
1296
+
1297
+ ### 31. `isSplit(taskId)`
1298
+
1299
+ Returns whether a task is currently split into multiple worked segments.
1300
+
1301
+ ### 32. `setColumnWidth(key, width)`
1302
+
1303
+ Pin a task-list column to an exact pixel width (the other columns absorb the remaining panel space). Mirrors dragging the column-header resize handle. Re-renders and emits `columnResize`.
1304
+
1305
+ ```js
1306
+ gantt.setColumnWidth(ColumnKey.Name, 260);
1307
+ ```
1308
+
1309
+ ### 33. `resetColumnWidths(key?)`
1310
+
1311
+ Clear the manual width of one column (pass its `key`) or of every column (omit the argument), returning them to their auto/flex width. Re-renders and emits `columnResize`.
1312
+
1313
+ ```js
1314
+ gantt.resetColumnWidths(ColumnKey.Name); // one column
1315
+ gantt.resetColumnWidths(); // all columns
1316
+ ```
1317
+
1318
+ ### 34. `getColumnWidths()`
1319
+
1320
+ Return the active manual column-width overrides as a plain object (`key` → pixels). Included in `getState()` and restorable via `setState({ columnWidths })`.
1321
+
1322
+ ### 35. `setColumnOrder(keys)`
1323
+
1324
+ Set the left-to-right order of the task-list columns by key. Keys you list are placed first in that order; any visible columns you omit keep their relative position at the end. Mirrors dragging a column header. Re-renders and emits `columnReorder`.
1325
+
1326
+ ```js
1327
+ gantt.setColumnOrder([ColumnKey.Name, ColumnKey.Progress, ColumnKey.StartTime]);
1328
+ ```
1329
+
1330
+ ### 36. `getColumnOrder()`
1331
+
1332
+ Return the current column order as an array of keys (left-to-right), reflecting any reorder. Included in `getState()` and restorable via `setState({ columnOrder })`.
1333
+
1334
+ ### 37. `scrollToTask(taskId)`
1335
+
1336
+ Scroll the timeline (and, if needed, the row list) so a task's bar is in view, using nearest-edge alignment. Backs the per-row scroll chevrons; call it directly to "locate" a task from search results, selection, or your own toolbar button. Returns `true` when a scroll was applied, `false` when the task is unknown or already fully visible.
1337
+
1338
+ ```js
1339
+ gantt.scrollToTask('task-42');
1340
+ ```
1341
+
928
1342
  ## Events
929
1343
 
930
1344
  ApexGantt emits CustomEvents on the container element for various user interactions, allowing you to track and respond to changes in real-time.
@@ -946,6 +1360,11 @@ ApexGantt emits CustomEvents on the container element for various user interacti
946
1360
  | `taskResized` | Task bar is resized | `{ taskId, resizeHandle, oldStartTime, oldEndTime, newStartTime, newEndTime, durationChange, timestamp }` |
947
1361
  | `taskProgressChanged` | In-bar progress handle is dragged to a new value | `{ taskId, oldProgress, newProgress, timestamp }` |
948
1362
  | `historyChange` | The undo/redo stack changed — `kind` is `'record'`, `'undo'`, `'redo'`, or `'clear'` | `{ kind, canUndo, canRedo, undoSize, redoSize, topUndoLabel?, topRedoLabel?, timestamp }` |
1363
+ | `sortChange` | Active sort changed via API or header click | `{ criteria: { key, direction }[], timestamp }` |
1364
+ | `filterChange` | Active filter changed | `{ active, visibleCount, timestamp }` |
1365
+ | `groupChange` | Active grouping changed via `gantt.groupBy()` / `gantt.clearGrouping()` | `{ active, field, groupCount, timestamp }` |
1366
+ | `columnResize` | A task-list column was resized (header drag or API) | `{ key, width, widths, timestamp }` |
1367
+ | `columnReorder` | Task-list columns were reordered (header drag or API) | `{ order, movedKey, timestamp }` |
949
1368
 
950
1369
  ### Events Usage
951
1370