@toclocoinc/lattice-grid 1.32.0 → 1.34.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 +1 -1
- package/docs/API.html +420 -2
- package/docs/api-detail.html +5 -2
- package/lattice-grid.d.ts +730 -1
- package/lattice-grid.esm.min.js +4 -4
- package/lattice-grid.min.cjs +4 -4
- package/lattice-grid.min.js +4 -4
- package/modules/angular.esm.min.js +2 -2
- package/modules/angular.min.cjs +2 -2
- package/modules/angular.min.js +2 -2
- package/modules/chart-alluvial.esm.min.js +143 -0
- package/modules/chart-arc.esm.min.js +102 -0
- package/modules/chart-bubblemap.esm.min.js +86 -0
- package/modules/chart-bump.esm.min.js +105 -0
- package/modules/chart-calendar.esm.min.js +124 -0
- package/modules/chart-decomposition.esm.min.js +127 -0
- package/modules/chart-diverging.esm.min.js +99 -0
- package/modules/chart-dumbbell.esm.min.js +110 -0
- package/modules/chart-fan.esm.min.js +135 -0
- package/modules/chart-hexbin.esm.min.js +144 -0
- package/modules/chart-hexmap.esm.min.js +120 -0
- package/modules/chart-icicle.esm.min.js +92 -0
- package/modules/chart-parallel.esm.min.js +114 -0
- package/modules/chart-ridgeline.esm.min.js +114 -0
- package/modules/chart-roc.esm.min.js +177 -0
- package/modules/chart-slope.esm.min.js +95 -0
- package/modules/chart-splom.esm.min.js +120 -0
- package/modules/chart-waffle.esm.min.js +92 -0
- package/modules/charts.esm.min.js +107 -9
- package/modules/charts.min.cjs +105 -9
- package/modules/charts.min.js +105 -9
- package/modules/data-router.esm.min.js +1546 -8
- package/modules/data-router.min.cjs +1546 -8
- package/modules/data-router.min.js +1546 -8
- package/modules/devtools.esm.min.js +2 -2
- package/modules/devtools.min.cjs +2 -2
- package/modules/devtools.min.js +2 -2
- package/modules/dhtmlx-compat.esm.min.js +4 -4
- package/modules/dhtmlx-compat.min.cjs +4 -4
- package/modules/dhtmlx-compat.min.js +4 -4
- package/modules/gantt.esm.min.js +2004 -0
- package/modules/gantt.min.cjs +1996 -0
- package/modules/gantt.min.js +1996 -0
- package/modules/htmx.esm.min.js +4 -4
- package/modules/htmx.min.cjs +4 -4
- package/modules/htmx.min.js +4 -4
- package/modules/kanban.esm.min.js +2278 -0
- package/modules/kanban.min.cjs +2281 -0
- package/modules/kanban.min.js +2281 -0
- package/modules/mock-socket.esm.min.js +2 -2
- package/modules/mock-socket.min.cjs +242 -0
- package/modules/mock-socket.min.js +242 -0
- package/modules/react.esm.min.js +2 -2
- package/modules/react.min.cjs +2 -2
- package/modules/react.min.js +2 -2
- package/modules/svelte.esm.min.js +2 -2
- package/modules/svelte.min.cjs +2 -2
- package/modules/svelte.min.js +2 -2
- package/modules/vue.esm.min.js +2 -2
- package/modules/vue.min.cjs +2 -2
- package/modules/vue.min.js +2 -2
- package/modules/webcomponent.esm.min.js +4 -4
- package/modules/webcomponent.min.cjs +4 -4
- package/modules/webcomponent.min.js +4 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
dependencies, no build step required. Optional adapters for React, Vue, Svelte
|
|
5
5
|
and Web Components ship alongside it.
|
|
6
6
|
|
|
7
|
-
Version 1.
|
|
7
|
+
Version 1.34.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
package/docs/API.html
CHANGED
|
@@ -4094,6 +4094,93 @@ createGrid(el, {
|
|
|
4094
4094
|
|
|
4095
4095
|
<div class="note"><p>The module imports nothing from the grid: <code>createChart</code> is handed a grid rather than importing one. That is what keeps the charts bundle to the drawing, and it is why the grid must be created first, and why a chart cannot outlive it.</p></div>
|
|
4096
4096
|
|
|
4097
|
+
<h3 id="chart-extension-types">Extension chart types — pay only for what you draw</h3>
|
|
4098
|
+
<p>The base charts bundle draws the built-in <code>TYPES</code> and nothing else. A new chart type is a <strong>separate, opt-in module</strong> a caller imports only if they use it (BACKLOG-0000886, on the slim-core seam BACKLOG-0000884). Importing it self-registers the type with the base module through <code>registerChartType</code>; the base <code>Chart</code> consults that registry for any type it does not draw natively. Because the base never imports the extension, the base bundle <strong>does not grow</strong> for a type a caller never uses.</p>
|
|
4099
|
+
<pre><code><span class="kw">import</span> '@toclocoinc/lattice-grid/modules/charts'; <span class="cmt">// the base</span>
|
|
4100
|
+
<span class="kw">import</span> '@toclocoinc/lattice-grid/modules/chart-ridgeline'; <span class="cmt">// opt in to one type</span>
|
|
4101
|
+
|
|
4102
|
+
createChart({ grid, container: '#dist', type: 'ridgeline', x: 'segment', y: 'value' });</code></pre>
|
|
4103
|
+
<p>An extension declares <code>{ draw, bind?, freeform?, labelled? }</code>. Its <code>draw(ctx)</code> receives the same context a built-in drawer gets — <code>plot</code>, <code>bound</code>, <code>groups</code>, <code>scheme</code>, <code>typography</code>, <code>labels</code>, <code>grid</code>, <code>spec</code> — plus <code>ctx.helpers</code>, the base's own toolkit (element factory, scales, axis drawers, mark pool, distribution kernels). So an extension imports nothing heavy from the base: it receives the toolkit and ships only its own geometry. <code>registeredChartTypes()</code> lists what is registered. <strong>Ridgeline</strong> (<code>drawRidgeline</code>) is the first: one kernel-density ridge per category, stacked and overlapping, over the distribution of a measure — the reading for "how did this distribution change across segments".</p>
|
|
4104
|
+
<pre data-run="js" data-expect="true | true | function" data-covers="export:registerChartType export:registeredChartTypes export:drawRidgeline"><code><span class="kw">const</span> charts = <span class="kw">await</span> import('../packages/modules/charts/index.js');
|
|
4105
|
+
<span class="kw">const</span> ridge = <span class="kw">await</span> import('../packages/modules/chart-ridgeline/index.js');
|
|
4106
|
+
|
|
4107
|
+
<span class="cmt">// Importing the module self-registered the type against the base registry.</span>
|
|
4108
|
+
<span class="kw">const</span> registered = charts.registeredChartTypes().includes('ridgeline');
|
|
4109
|
+
<span class="cmt">// registerChartType is idempotent by name, so re-registering is safe.</span>
|
|
4110
|
+
charts.registerChartType('ridgeline', { draw: ridge.drawRidgeline });
|
|
4111
|
+
|
|
4112
|
+
<span class="kw">return</span> [
|
|
4113
|
+
registered,
|
|
4114
|
+
charts.registeredChartTypes().includes('ridgeline'),
|
|
4115
|
+
typeof ridge.drawRidgeline,
|
|
4116
|
+
].join(' | ');</code></pre>
|
|
4117
|
+
|
|
4118
|
+
<p>More lead-pick types ship the same way, each in its own opt-in module — the base bundle stays flat as they are added. <strong>Calendar heatmap</strong> (<code>drawCalendar</code>, <code>type: 'calendar'</code>) lays a measure out value-by-day, GitHub-style, from a date column. <strong>Scatter-plot matrix</strong> (<code>drawSplom</code>/<code>bindSplom</code>, <code>type: 'splom'</code>) crosses every pair of numeric <code>columns</code>. <strong>Hexbin</strong> (<code>drawHexbin</code>/<code>bindHexbin</code>, <code>type: 'hexbin'</code>) bins a scatter into count-shaded hexagons so a million rows read as a density field. Each reads the grid through the public row API, so it follows the grid's filters and sort.</p>
|
|
4119
|
+
<pre data-run="js" data-expect="true | function | function | function | function | function" data-covers="export:drawCalendar export:drawSplom export:bindSplom export:drawHexbin export:bindHexbin"><code><span class="kw">const</span> charts = <span class="kw">await</span> import('../packages/modules/charts/index.js');
|
|
4120
|
+
<span class="kw">const</span> cal = <span class="kw">await</span> import('../packages/modules/chart-calendar/index.js');
|
|
4121
|
+
<span class="kw">const</span> splom = <span class="kw">await</span> import('../packages/modules/chart-splom/index.js');
|
|
4122
|
+
<span class="kw">const</span> hex = <span class="kw">await</span> import('../packages/modules/chart-hexbin/index.js');
|
|
4123
|
+
|
|
4124
|
+
<span class="cmt">// Each import self-registered its type; the base bundle carries none of them.</span>
|
|
4125
|
+
<span class="kw">const</span> types = charts.registeredChartTypes();
|
|
4126
|
+
<span class="kw">return</span> [
|
|
4127
|
+
['calendar', 'splom', 'hexbin'].every((t) => types.includes(t)),
|
|
4128
|
+
typeof cal.drawCalendar,
|
|
4129
|
+
typeof splom.drawSplom, typeof splom.bindSplom,
|
|
4130
|
+
typeof hex.drawHexbin, typeof hex.bindHexbin,
|
|
4131
|
+
].join(' | ');</code></pre>
|
|
4132
|
+
|
|
4133
|
+
<p>The model-evaluation and time-series lead picks ship the same way. <strong>ROC / PR / calibration</strong> (<code>drawRoc</code>/<code>bindRoc</code>, <code>type: 'roc'</code>, <code>curve: 'roc' | 'pr' | 'calibration'</code>) evaluates a classifier from a <code>label</code> and a <code>score</code> column, with the AUC. <strong>Fan / forecast</strong> (<code>drawFan</code>/<code>bindFan</code>, <code>type: 'fan'</code>) draws history, a point forecast, and a widening prediction interval from <code>y</code>/<code>forecast</code>/<code>lower</code>/<code>upper</code>. <strong>Decomposition panel</strong> (<code>drawDecomposition</code>/<code>bindDecomposition</code>, <code>type: 'decomposition'</code>) stacks the <code>observed</code>/<code>trend</code>/<code>seasonal</code>/<code>residual</code> components on one x axis — the companion to the grid's own time-series shadow columns.</p>
|
|
4134
|
+
<pre data-run="js" data-expect="true | function | function | function | function | function | function" data-covers="export:drawRoc export:bindRoc export:drawFan export:bindFan export:drawDecomposition export:bindDecomposition"><code><span class="kw">const</span> charts = <span class="kw">await</span> import('../packages/modules/charts/index.js');
|
|
4135
|
+
<span class="kw">const</span> roc = <span class="kw">await</span> import('../packages/modules/chart-roc/index.js');
|
|
4136
|
+
<span class="kw">const</span> fan = <span class="kw">await</span> import('../packages/modules/chart-fan/index.js');
|
|
4137
|
+
<span class="kw">const</span> decomp = <span class="kw">await</span> import('../packages/modules/chart-decomposition/index.js');
|
|
4138
|
+
|
|
4139
|
+
<span class="kw">const</span> types = charts.registeredChartTypes();
|
|
4140
|
+
<span class="kw">return</span> [
|
|
4141
|
+
['roc', 'fan', 'decomposition'].every((t) => types.includes(t)),
|
|
4142
|
+
typeof roc.drawRoc, typeof roc.bindRoc,
|
|
4143
|
+
typeof fan.drawFan, typeof fan.bindFan,
|
|
4144
|
+
typeof decomp.drawDecomposition, typeof decomp.bindDecomposition,
|
|
4145
|
+
].join(' | ');</code></pre>
|
|
4146
|
+
|
|
4147
|
+
<p>The comparison and ranking family ships the same way. <strong>Slope</strong> (<code>drawSlope</code>, <code>type: 'slope'</code>) connects each series across two periods; <strong>dumbbell</strong> (<code>drawDumbbell</code>/<code>bindDumbbell</code>, <code>type: 'dumbbell'</code>) shows a <code>start</code>/<code>end</code> gap per category; <strong>bump</strong> (<code>drawBump</code>, <code>type: 'bump'</code>) plots rank-over-time; <strong>diverging</strong> (<code>drawDiverging</code>, <code>type: 'diverging'</code>) grows bars from a central zero; <strong>parallel coordinates</strong> (<code>drawParallel</code>/<code>bindParallel</code>, <code>type: 'parallel'</code>) draws one polyline per row across several numeric axes.</p>
|
|
4148
|
+
<pre data-run="js" data-expect="true | function | function | function | function | function | function | function" data-covers="export:drawSlope export:drawDumbbell export:bindDumbbell export:drawBump export:drawDiverging export:drawParallel export:bindParallel"><code><span class="kw">const</span> charts = <span class="kw">await</span> import('../packages/modules/charts/index.js');
|
|
4149
|
+
<span class="kw">const</span> slope = <span class="kw">await</span> import('../packages/modules/chart-slope/index.js');
|
|
4150
|
+
<span class="kw">const</span> dumbbell = <span class="kw">await</span> import('../packages/modules/chart-dumbbell/index.js');
|
|
4151
|
+
<span class="kw">const</span> bump = <span class="kw">await</span> import('../packages/modules/chart-bump/index.js');
|
|
4152
|
+
<span class="kw">const</span> diverging = <span class="kw">await</span> import('../packages/modules/chart-diverging/index.js');
|
|
4153
|
+
<span class="kw">const</span> parallel = <span class="kw">await</span> import('../packages/modules/chart-parallel/index.js');
|
|
4154
|
+
|
|
4155
|
+
<span class="kw">const</span> types = charts.registeredChartTypes();
|
|
4156
|
+
<span class="kw">return</span> [
|
|
4157
|
+
['slope', 'dumbbell', 'bump', 'diverging', 'parallel'].every((t) => types.includes(t)),
|
|
4158
|
+
typeof slope.drawSlope,
|
|
4159
|
+
typeof dumbbell.drawDumbbell, typeof dumbbell.bindDumbbell,
|
|
4160
|
+
typeof bump.drawBump,
|
|
4161
|
+
typeof diverging.drawDiverging,
|
|
4162
|
+
typeof parallel.drawParallel, typeof parallel.bindParallel,
|
|
4163
|
+
].join(' | ');</code></pre>
|
|
4164
|
+
|
|
4165
|
+
<p>The hierarchy, flow and geographic remainder ships the same way — treemap, sunburst, funnel, radar, sankey, chord and network are already built in, so the new opt-in modules are: <strong>icicle</strong> (<code>drawIcicle</code>, <code>type: 'icicle'</code>, drawn from the grid's group tree), <strong>waffle</strong> (<code>drawWaffle</code>, <code>type: 'waffle'</code>), <strong>alluvial</strong> (<code>drawAlluvial</code>/<code>bindAlluvial</code>, <code>type: 'alluvial'</code>), <strong>arc diagram</strong> (<code>drawArc</code>/<code>bindArc</code>, <code>type: 'arc'</code>), <strong>bubble map</strong> (<code>drawBubbleMap</code>/<code>bindBubbleMap</code>, <code>type: 'bubblemap'</code>) and <strong>hexbin map</strong> (<code>drawHexMap</code>/<code>bindHexMap</code>, <code>type: 'hexmap'</code>). The two maps place <code>lon</code>/<code>lat</code> directly, so they need no outline data and fetch nothing.</p>
|
|
4166
|
+
<pre data-run="js" data-expect="true | function | function | function | function | function | function | function | function | function | function" data-covers="export:drawIcicle export:drawWaffle export:drawAlluvial export:bindAlluvial export:drawArc export:bindArc export:drawBubbleMap export:bindBubbleMap export:drawHexMap export:bindHexMap"><code><span class="kw">const</span> charts = <span class="kw">await</span> import('../packages/modules/charts/index.js');
|
|
4167
|
+
<span class="kw">const</span> icicle = <span class="kw">await</span> import('../packages/modules/chart-icicle/index.js');
|
|
4168
|
+
<span class="kw">const</span> waffle = <span class="kw">await</span> import('../packages/modules/chart-waffle/index.js');
|
|
4169
|
+
<span class="kw">const</span> alluvial = <span class="kw">await</span> import('../packages/modules/chart-alluvial/index.js');
|
|
4170
|
+
<span class="kw">const</span> arc = <span class="kw">await</span> import('../packages/modules/chart-arc/index.js');
|
|
4171
|
+
<span class="kw">const</span> bubblemap = <span class="kw">await</span> import('../packages/modules/chart-bubblemap/index.js');
|
|
4172
|
+
<span class="kw">const</span> hexmap = <span class="kw">await</span> import('../packages/modules/chart-hexmap/index.js');
|
|
4173
|
+
|
|
4174
|
+
<span class="kw">const</span> types = charts.registeredChartTypes();
|
|
4175
|
+
<span class="kw">return</span> [
|
|
4176
|
+
['icicle', 'waffle', 'alluvial', 'arc', 'bubblemap', 'hexmap'].every((t) => types.includes(t)),
|
|
4177
|
+
typeof icicle.drawIcicle, typeof waffle.drawWaffle,
|
|
4178
|
+
typeof alluvial.drawAlluvial, typeof alluvial.bindAlluvial,
|
|
4179
|
+
typeof arc.drawArc, typeof arc.bindArc,
|
|
4180
|
+
typeof bubblemap.drawBubbleMap, typeof bubblemap.bindBubbleMap,
|
|
4181
|
+
typeof hexmap.drawHexMap, typeof hexmap.bindHexMap,
|
|
4182
|
+
].join(' | ');</code></pre>
|
|
4183
|
+
|
|
4097
4184
|
<h2 id="datarouter">The data router</h2>
|
|
4098
4185
|
<p><code>modules/data-router</code> is a host-layer demultiplexer: it takes <strong>one</strong> arriving stream or dataset, splits it by what each record <em>is</em>, and routes each partition to its own grid — or to a headless grid driving a chart. One round-trip, or one live feed, hydrates a whole screen of grids that each see only their slice. It is optional, imports nothing from the grid, and adds no core hook: every grid is driven through the <strong>public</strong> incremental path, <code>grid.rows.apply({ add, update, remove })</code>.</p>
|
|
4099
4186
|
<pre><code>import { createDataRouter } from '@toclocoinc/lattice-grid/modules/data-router';
|
|
@@ -4131,6 +4218,9 @@ router.apply([{ op: 'upsert', row }, { op: 'delete', row }]); <span class="cmt">
|
|
|
4131
4218
|
<tr><td class="sig">flushStream()</td><td class="desc"><strong>v3:</strong> apply the buffered deltas now, coalesced into a single apply (a deterministic point, and for tests).</td></tr>
|
|
4132
4219
|
<tr><td class="sig">dropped</td><td class="desc"><strong>v3:</strong> how many stale/duplicate deltas the <code>seq</code> dedupe gate has dropped.</td></tr>
|
|
4133
4220
|
<tr><td class="sig">lastSeq() / checkpoint() / seenThrough(mark)</td><td class="desc"><strong>v3:</strong> the resume point — the highest applied <code>seq</code>, a per-record checkpoint to persist, and a way to prime it after a reconnect so an early replay is dropped.</td></tr>
|
|
4221
|
+
<tr><td class="sig">subscribe(predicate, handler, opts?)</td><td class="desc"><strong>v5:</strong> route a slice to any non-grid view. <code>handler(change)</code> receives the same keyed diff <code>{ add, update, remove }</code> a grid does — drive a KPI tile, detail pane, map or form. A peer to <code>attach</code>: same partitioning and the same <code>transform</code>/<code>filter</code>/<code>sort</code>/<code>rollup</code> options; grids and charts are unchanged.</td></tr>
|
|
4222
|
+
<tr><td class="sig">alert(predicate, condition, handler, { filter?, debounce?, rowKey? })</td><td class="desc"><strong>v5:</strong> watch a slice and emit rather than render. <code>condition(rows)</code> is evaluated over the slice on every load and delta; when it first becomes truthy, <code>handler(signal, rows)</code> fires. Edge-triggered (once per crossing, re-arms on release), <code>debounce</code> coalesces a burst, and it never competes for a partition or touches a grid.</td></tr>
|
|
4223
|
+
<tr><td class="sig">configure(spec)</td><td class="desc"><strong>v5:</strong> the whole routing graph as one data spec — <code>routes</code> (grid/<code>default</code>/<code>subscribe</code>/<code>alert</code> entries), <code>links</code>, <code>relate</code>, <code>buffer</code> — desugared to the imperative API. Composes with imperative calls and round-trips to identical behaviour. Also accepted as <code>createDataRouter({ config })</code>.</td></tr>
|
|
4134
4224
|
<tr><td class="sig">detach(grid)</td><td class="desc">Stop routing to a grid and forget its slice; drop any link/edge it is part of (restoring a filtered sibling). The host still owns and destroys the grid.</td></tr>
|
|
4135
4225
|
<tr><td class="sig">destroy()</td><td class="desc">Detach every grid, drop every link, edge and subscription. <strong>Detaches only</strong> — the host owns and destroys its grids.</td></tr>
|
|
4136
4226
|
</tbody>
|
|
@@ -4313,6 +4403,331 @@ router.apply([{ op: 'upsert', row: { id: 'o2', type: 'invoice', amt: 25 } }]);
|
|
|
4313
4403
|
orders.destroy(); invoices.destroy(); rest.destroy(); router.destroy();
|
|
4314
4404
|
<span class="kw">return</span> [fanned, moved, router.unrouted].join(' | ');</code></pre>
|
|
4315
4405
|
|
|
4406
|
+
<h2 id="ganttmodule">The Gantt module</h2>
|
|
4407
|
+
<p><code>modules/gantt</code> is a separate, opt-in project-planning module — its own bundle, imported only when you want it, changing nothing in the grid core. It turns a task list into a real schedule: a <strong>CPM (Critical Path Method) engine</strong> computes each task's early/late start and finish, its slack (total float), and the zero-float <strong>critical path</strong>, recomputing on every edit. <code>computeSchedule(tasks, deps)</code> is the pure engine; <code>createGantt(opts)</code> is a controller that holds the model, recomputes on <code>setTasks</code>/<code>setDependencies</code>/<code>applyEdit</code>, and emits <code>schedule</code> (or <code>error</code>). Dependencies are the four standard link types — <code>LINK_TYPES</code> is <code>['FS','SS','FF','SF']</code> — each with optional lag/lead. A <strong>milestone</strong> is a zero-duration task scheduled as a point; a <strong>summary</strong> task (any task named as another's <code>parent</code>) is derived from its children (start = earliest child, end = latest child, duration-weighted progress) and is not scheduled itself. Bad input never throws or loops: a dependency cycle is refused and reported with a code from <code>SCHEDULE_ERROR</code>, and <code>findViolations</code> flags any task placed earlier than its predecessors allow. <code>toISODate</code> converts an engine day-number back to a calendar date for display.</p>
|
|
4408
|
+
<pre><code>import { createGantt, computeSchedule } from '@toclocoinc/lattice-grid/modules/gantt';
|
|
4409
|
+
|
|
4410
|
+
const plan = createGantt({
|
|
4411
|
+
tasks: [
|
|
4412
|
+
{ id: 'design', duration: 5 },
|
|
4413
|
+
{ id: 'build', duration: 6 },
|
|
4414
|
+
{ id: 'launch', milestone: true },
|
|
4415
|
+
],
|
|
4416
|
+
dependencies: [
|
|
4417
|
+
{ from: 'design', to: 'build', type: 'FS' },
|
|
4418
|
+
{ from: 'build', to: 'launch', type: 'FS' },
|
|
4419
|
+
],
|
|
4420
|
+
});
|
|
4421
|
+
plan.on('schedule', (s) => console.log(s.critical, s.projectDuration));
|
|
4422
|
+
plan.applyEdit({ id: 'design', duration: 7 }); // recomputes; the critical path shifts</code></pre>
|
|
4423
|
+
<div class="table-wrap">
|
|
4424
|
+
<table>
|
|
4425
|
+
<thead><tr><th>Function</th><th>What it does</th></tr></thead>
|
|
4426
|
+
<tbody>
|
|
4427
|
+
<tr><td class="sig">createGantt({ tasks?, dependencies?, projectStart?, autoSchedule?, grid? })</td><td class="desc">Create a controller over a task list and a dependency list. Computes the CPM schedule immediately and on every edit; <code>on('schedule'|'error', fn)</code> subscribes; <code>applyEdit</code>/<code>setTasks</code>/<code>setDependencies</code> mutate and recompute; <code>grid</code> is kept for the write-back binding.</td></tr>
|
|
4428
|
+
<tr><td class="sig">computeSchedule(tasks, deps?, { projectStart? })</td><td class="desc">The pure CPM engine: forward/backward passes over the leaf tasks honouring FS/SS/FF/SF + lag, slack/float and the zero-float critical path, with summaries derived and cycles refused. Returns <code>{ ok, tasks, critical, criticalPaths, projectDuration, ... }</code> or <code>{ ok:false, error }</code>.</td></tr>
|
|
4429
|
+
<tr><td class="sig">findViolations(tasks, schedule)</td><td class="desc">The tasks whose user-placed start begins earlier than CPM allows (the manual-with-validation flag). Summaries, whose dates are derived, are skipped.</td></tr>
|
|
4430
|
+
<tr><td class="sig">toISODate(day)</td><td class="desc">Format an engine day-number as an ISO calendar date (<code>YYYY-MM-DD</code>, UTC).</td></tr>
|
|
4431
|
+
<tr><td class="sig">LINK_TYPES</td><td class="desc">The four dependency link types, in order: <code>['FS','SS','FF','SF']</code>.</td></tr>
|
|
4432
|
+
<tr><td class="sig">SCHEDULE_ERROR</td><td class="desc">The error codes the engine reports rather than throwing (<code>cycle</code>, <code>duplicate-id</code>, <code>unknown-task</code>, <code>bad-duration</code>, <code>bad-link-type</code>, <code>unknown-parent</code>, <code>parent-cycle</code>, …).</td></tr>
|
|
4433
|
+
</tbody>
|
|
4434
|
+
</table>
|
|
4435
|
+
</div>
|
|
4436
|
+
<pre data-run="js" data-expect="15 | design-build-test-launch | 1970-01-01 | FS,SS,FF,SF | true | 0" data-covers="export:computeSchedule export:createGantt export:findViolations export:LINK_TYPES export:SCHEDULE_ERROR export:toISODate"><code>const { computeSchedule, findViolations, createGantt, LINK_TYPES, SCHEDULE_ERROR, toISODate } = await import('../packages/modules/gantt/index.js');
|
|
4437
|
+
const tasks = [
|
|
4438
|
+
{ id: 'design', duration: 5, percentComplete: 100 },
|
|
4439
|
+
{ id: 'build', duration: 6, percentComplete: 50 },
|
|
4440
|
+
{ id: 'test', duration: 4 },
|
|
4441
|
+
{ id: 'launch', milestone: true },
|
|
4442
|
+
];
|
|
4443
|
+
const deps = [
|
|
4444
|
+
{ from: 'design', to: 'build' },
|
|
4445
|
+
{ from: 'build', to: 'test' },
|
|
4446
|
+
{ from: 'test', to: 'launch' },
|
|
4447
|
+
];
|
|
4448
|
+
const s = computeSchedule(tasks, deps);
|
|
4449
|
+
const plan = createGantt({ tasks, dependencies: deps });
|
|
4450
|
+
const cyc = computeSchedule([{ id: 'a', duration: 1 }, { id: 'b', duration: 1 }], [{ from: 'a', to: 'b' }, { from: 'b', to: 'a' }]);
|
|
4451
|
+
return [plan.schedule.projectDuration, s.critical.join('-'), toISODate(0), LINK_TYPES.join(','), cyc.error.code === SCHEDULE_ERROR.CYCLE, findViolations(tasks, s).length].join(' | ');</code></pre>
|
|
4452
|
+
<p>Milestones and summary (WBS) tasks: a summary is derived from its children and a dependency may target it.</p>
|
|
4453
|
+
<pre data-run="js" data-expect="0 | 5 | 60 | true | 5" data-covers="export:computeSchedule"><code>const { computeSchedule } = await import('../packages/modules/gantt/index.js');
|
|
4454
|
+
const tasks = [
|
|
4455
|
+
{ id: 'phase', name: 'Phase 1' },
|
|
4456
|
+
{ id: 'a', duration: 3, parent: 'phase', percentComplete: 100 },
|
|
4457
|
+
{ id: 'b', duration: 2, parent: 'phase', percentComplete: 0 },
|
|
4458
|
+
{ id: 'ship', milestone: true },
|
|
4459
|
+
];
|
|
4460
|
+
const s = computeSchedule(tasks, [{ from: 'a', to: 'b' }, { from: 'phase', to: 'ship' }]);
|
|
4461
|
+
const phase = s.tasks.get('phase');
|
|
4462
|
+
return [phase.es, phase.ef, phase.percentComplete, phase.isSummary, s.tasks.get('ship').es].join(' | ');</code></pre>
|
|
4463
|
+
<p>Render the plan as an SVG timeline with <code>mount(container, options)</code> — bars on a time scale, dependency arrows with a per-link-type anchor and a lag/lead label, the critical path highlighted, a today line, optional non-working-day shading, milestones as diamonds, a progress bar-fill and configurable labels. The view redraws itself whenever the schedule recomputes; <code>unmount()</code> detaches it. All geometry is computed from the schedule, so it draws identically headless or in a browser.</p>
|
|
4464
|
+
<pre><code>import { createGantt } from '@toclocoinc/lattice-grid/modules/gantt';
|
|
4465
|
+
|
|
4466
|
+
const gantt = createGantt({ tasks, dependencies });
|
|
4467
|
+
gantt.mount(document.querySelector('#plan'), {
|
|
4468
|
+
today: 20340, // a day-number; draws the today line
|
|
4469
|
+
nonWorking: 'weekends', // shade Saturdays and Sundays
|
|
4470
|
+
label: 'percent', // bar label: 'name' | 'percent' | 'dates' | (task) => string
|
|
4471
|
+
dateAxis: true, // axis ticks as calendar dates
|
|
4472
|
+
zoom: 'week', // 'day' | 'week' | 'month' | 'quarter' | pixels-per-day; omit to fit width
|
|
4473
|
+
scrollToToday: true, // scroll so the today line is in view
|
|
4474
|
+
groupBy: 'assignee', // swimlanes by a task property or (task) => key
|
|
4475
|
+
rowHeight: 26,
|
|
4476
|
+
width: 720,
|
|
4477
|
+
});
|
|
4478
|
+
gantt.view.scrollToToday(); // also callable on demand
|
|
4479
|
+
gantt.applyEdit({ id: 'design', duration: 7 }); // the view redraws automatically
|
|
4480
|
+
gantt.unmount();</code></pre>
|
|
4481
|
+
<p>Bars are draggable: drag the body to move a task, drag the right edge to resize it. When a <code>grid</code> and a <code>columns</code> map are given, each drag writes the new dates back through the grid's public edit surface (<code>grid.edit.setCells</code>) and reconciles a reverted or conflicted write; <code>autoSchedule: true</code> cascades dependents. A task placed earlier than its predecessors allow is flagged (<code>findViolations</code>), not silently moved.</p>
|
|
4482
|
+
<pre><code>import { createGantt } from '@toclocoinc/lattice-grid/modules/gantt';
|
|
4483
|
+
|
|
4484
|
+
const gantt = createGantt({
|
|
4485
|
+
tasks, dependencies, grid, // a Lattice grid over the same tasks
|
|
4486
|
+
columns: { start: 'start', duration: 'duration' }, // task field -> grid column
|
|
4487
|
+
autoSchedule: true,
|
|
4488
|
+
});
|
|
4489
|
+
gantt.mount(document.querySelector('#plan'), { editable: true });
|
|
4490
|
+
// drag a bar -> gantt.applyEdit(..., { writeBack: true }) -> grid.edit.setCells(...)</code></pre>
|
|
4491
|
+
<p>Hovering a bar shows a tooltip with its dates, duration, % complete and slack. Tasks are flagged when they slip: <strong>overdue</strong> (incomplete and finishing before <code>today</code>) and <strong>at-risk</strong> (negative total float). Negative float needs a target: pass a <code>deadline</code> (a day-number) to <code>createGantt</code> and any task that cannot meet it gets negative slack and is drawn at-risk.</p>
|
|
4492
|
+
<p>Export: <code>gantt.toCSV()</code> writes the scheduled tasks as CSV (<code>{ dates: true }</code> for ISO dates); when a grid is bound, the grid's own Excel/CSV export works too. <code>gantt.view.toSVG()</code> serialises the drawn chart to a standalone SVG string — the handoff for turning it into an image or PDF.</p>
|
|
4493
|
+
<p>Accessibility: bars are focusable and carry an <code>aria-label</code> describing the task (name, dates, progress, slack, critical). With the keyboard, arrows move a focused task, Shift+arrows resize it, and <kbd>L</kbd> links two tasks (press it on the source, then on the successor) with a finish-to-start dependency; every edit is announced in a polite live region and focus follows the edited task. Set <code>keyboard: false</code> to opt out.</p>
|
|
4494
|
+
<p><strong>Split view.</strong> The Gantt does not build a grid or the two-pane layout — you create and place a normal Lattice grid over the same task rows, and the Gantt <em>consumes</em> it. Binding is two-way: a drag on the timeline writes back through the grid (cycle 3), and an edit in the grid pane (any editor) reflects on the timeline. v1 assumes both panes share the same row height; <code>view.linkVerticalScroll(el)</code> mirrors vertical scroll so the rows stay aligned.</p>
|
|
4495
|
+
<pre><code><div class="split" style="display:grid;grid-template-columns:360px 1fr">
|
|
4496
|
+
<div id="tasks"></div> <!-- the grid pane -->
|
|
4497
|
+
<div id="plan"></div> <!-- the timeline pane -->
|
|
4498
|
+
</div>
|
|
4499
|
+
<script type="module">
|
|
4500
|
+
import { createGrid } from '@toclocoinc/lattice-grid';
|
|
4501
|
+
import { createGantt } from '@toclocoinc/lattice-grid/modules/gantt';
|
|
4502
|
+
|
|
4503
|
+
const grid = createGrid({
|
|
4504
|
+
element: document.querySelector('#tasks'),
|
|
4505
|
+
columns: [{ field: 'name' }, { field: 'start', type: 'number', edit: { enabled: true } },
|
|
4506
|
+
{ field: 'duration', type: 'number', edit: { enabled: true } }],
|
|
4507
|
+
rows: tasks, rowKey: 'id', edit: { enabled: true },
|
|
4508
|
+
});
|
|
4509
|
+
const gantt = createGantt({ tasks, dependencies, grid, columns: { start: 'start', duration: 'duration' } });
|
|
4510
|
+
gantt.mount(document.querySelector('#plan'));
|
|
4511
|
+
gantt.view.linkVerticalScroll(grid.element); // keep the two panes aligned
|
|
4512
|
+
</script></code></pre>
|
|
4513
|
+
<p><strong>Live data.</strong> The Gantt exposes the same consumer surface as the grid — <code>gantt.rows.apply({ add, update, remove })</code>, keyed by its <code>rowKey</code> (default <code>id</code>) — so a <a href="#datarouter">Data Router</a> attaches to it exactly as it does a grid, a board or a chart. One arriving stream can hydrate and update a whole screen, the Gantt included; each change recomputes the schedule and redraws.</p>
|
|
4514
|
+
<pre><code>import { createDataRouter } from '@toclocoinc/lattice-grid/modules/data-router';
|
|
4515
|
+
|
|
4516
|
+
const router = createDataRouter({ key: 'kind', rowKey: 'id' });
|
|
4517
|
+
router.attach(grid, 'order').attach(gantt, 'task'); // one feed drives both
|
|
4518
|
+
router.load(snapshot); // task rows land in the Gantt
|
|
4519
|
+
router.apply(deltas); // updates recompute the schedule</code></pre>
|
|
4520
|
+
|
|
4521
|
+
<h2 id="kanban">The board (kanban) view</h2>
|
|
4522
|
+
<p><code>modules/kanban</code> is an opt-in view of grid rows as <strong>cards</strong>, grouped into <strong>columns</strong> by a configurable property — a status, a stage, a state GUID, whatever the host's schema calls it. It is a separate bundle in the same shape as charts and the data router: it adds no weight to a page that does not load it, changes nothing in grid core, and pulls in no dependency. A board is <em>just another dataset viewer</em>: it consumes data through the same keyed-diff contract a grid exposes, <code>board.rows.apply({ add, update, remove })</code>, so a Data Router can <code>attach(value, board)</code> and drive a kanban beside a grid and a chart off one feed. (Distinct from the grid-core board <em>presentation mode</em>, <code>config.board</code> in §7.14, which lays a single grid's rows out as lanes; this is a standalone view module.)</p>
|
|
4523
|
+
<pre><code>import { createKanban } from '@toclocoinc/lattice-grid/modules/kanban';
|
|
4524
|
+
|
|
4525
|
+
const board = createKanban(document.querySelector('#board'), {
|
|
4526
|
+
rows, <span class="cmt">// or { grid } to bind to a live grid; reuses its column formatters</span>
|
|
4527
|
+
rowKey: 'id',
|
|
4528
|
+
columnProperty: 'status', <span class="cmt">// the group-by property (configurable)</span>
|
|
4529
|
+
columns: [ <span class="cmt">// shown even when empty; this order is honoured</span>
|
|
4530
|
+
{ id: 'todo', title: 'To do' },
|
|
4531
|
+
{ id: 'doing', title: 'In progress', wipLimit: 3 },
|
|
4532
|
+
{ id: 'done', title: 'Done', color: '#2e7d32' },
|
|
4533
|
+
],
|
|
4534
|
+
pointsProperty: 'points', <span class="cmt">// configurable; drives the header points sum</span>
|
|
4535
|
+
showPoints: true,
|
|
4536
|
+
card: { title: 'title', subtitle: 'assignee', labels: 'tags' },
|
|
4537
|
+
readonly: { columns: { done: true } }, <span class="cmt">// granular: whole board / per column / per card</span>
|
|
4538
|
+
onCardClick: ({ card }) => open(card.row),
|
|
4539
|
+
});</code></pre>
|
|
4540
|
+
<p>Every structural property is named in config — <code>columnProperty</code>, <code>pointsProperty</code>, <code>orderProperty</code>, <code>swimlaneProperty</code>, <code>sprintProperty</code>, <code>epicProperty</code> — so the same board maps DemandFlow (a status field, <code>points</code>, <code>sprint</code>, <code>epic</code>, a swimlane) and any customer schema without code change. Configured columns show even when empty; a data value outside them gets its own column rather than being dropped. Each column header carries its card <strong>count</strong> and, with <code>showPoints</code>, its <strong>points sum</strong>, and is flagged when the count exceeds a column's <code>wipLimit</code>. Accessibility is built in: the board is a labelled group, each column a labelled list, each card a list item in a roving-tabindex focus ring with arrow-key navigation, and a polite live region is present for the move announcements a later cycle adds.</p>
|
|
4541
|
+
<div class="table-wrap">
|
|
4542
|
+
<table>
|
|
4543
|
+
<thead><tr><th>Member</th><th>Description</th></tr></thead>
|
|
4544
|
+
<tbody>
|
|
4545
|
+
<tr><td class="sig">createKanban(el, config)</td><td class="desc">Create a board. Pass a DOM element to render into, or <code>null</code> for a headless board that computes the same column/card model without a DOM.</td></tr>
|
|
4546
|
+
<tr><td class="sig">columns() / column(id)</td><td class="desc">The columns with their cards and aggregates, or one column by id (<code>id</code>, <code>title</code>, <code>count</code>, <code>points</code>, <code>wipLimit</code>, <code>over</code>, <code>cards</code>).</td></tr>
|
|
4547
|
+
<tr><td class="sig">count(id) / points(id)</td><td class="desc">A column's card count, and its points sum from <code>pointsProperty</code>.</td></tr>
|
|
4548
|
+
<tr><td class="sig">cards() / card(key)</td><td class="desc">Every card model, or one by its key.</td></tr>
|
|
4549
|
+
<tr><td class="sig">rows.apply({ add, update, remove })</td><td class="desc">The keyed-diff consumer contract a grid shares, so the board is a drop-in Data Router target. Also <code>rows.forEach</code> and <code>rows.count</code>.</td></tr>
|
|
4550
|
+
<tr><td class="sig">move(keys, toColumn, toIndex?)</td><td class="desc">Move one or more cards to a column (and, with <code>orderProperty</code>, to a position within it) — the entry point behind drag-and-drop and keyboard move. Runs <code>onBeforeMove</code> first, then writes through the grid's shipped write-back path.</td></tr>
|
|
4551
|
+
<tr><td class="sig">select / selection / isSelected / clearSelection</td><td class="desc">Card selection: <code>select(keys, 'set'|'add'|'toggle'|'remove')</code>, the selected keys, a membership test, and a clear. Click selects; Ctrl/Cmd toggles; Shift extends within a column.</td></tr>
|
|
4552
|
+
<tr><td class="sig">collapseColumn(id) / collapseLane(id)</td><td class="desc">Collapse, expand or toggle a column or swimlane; emits <code>column:collapse</code> / <code>swimlane:collapse</code>. State survives a keyed-diff update.</td></tr>
|
|
4553
|
+
<tr><td class="sig">reorderColumns(order) / moveColumn(id, before)</td><td class="desc">Reorder the columns (also done by dragging a column header); emits <code>column:reorder</code>.</td></tr>
|
|
4554
|
+
<tr><td class="sig">setQuickFilter(text) / setFilter(fn) / facets(property)</td><td class="desc">Quick text search across card fields, a predicate filter, and distinct-value counts for a facet control.</td></tr>
|
|
4555
|
+
<tr><td class="sig">setSprint(id) / showBacklog() / sprints()</td><td class="desc">Sprint view and switcher: show one sprint, the backlog (<code>board.BACKLOG</code> — cards with no sprint), or all; and the distinct sprint values. Emits <code>sprint:changed</code>.</td></tr>
|
|
4556
|
+
<tr><td class="sig">setEpic(id) / epics() / epicRollup() / rollup(property)</td><td class="desc">Epic view and rollup: filter to an epic, list epics, and roll rows up by epic (or any property) into count, points, and progress toward the <code>done</code> columns.</td></tr>
|
|
4557
|
+
<tr><td class="sig">expand(key) / closeDetail() / canExpand(card)</td><td class="desc">Pop a card's children out as a nested child grid (or board) in a drawer/modal/inline container; emits <code>card:expand</code> and <code>card:drill</code>.</td></tr>
|
|
4558
|
+
<tr><td class="sig">editCard(key, field) / applyEdit(key, field, value)</td><td class="desc">Inline-edit a card field opted in with <code>card: { title: { field, edit: true } }</code>: grid-bound it commits through the grid's own field editor path (<code>grid.edit.setCells</code>); standalone it uses a host editor factory or a default input, reverting when <code>onCardEdit</code> rejects. Double-click a card to edit; emits <code>card:edit</code>.</td></tr>
|
|
4559
|
+
<tr><td class="sig">addCard(columnId, seed?)</td><td class="desc">Add a card to a column (with <code>config.addCard</code>'s per-column affordance) and open it in inline edit; a host <code>onAddCard(columnId)</code> supplies the row, or one is generated (grid-bound via <code>grid.edit.addRow</code>). Emits <code>card:add</code>.</td></tr>
|
|
4560
|
+
<tr><td class="sig">getState() / setState(snapshot)</td><td class="desc">Serialise and restore the board state — collapsed columns/lanes, column order, quick filter, sprint/epic selection and selection. Also accepted as <code>config.state</code> at construction.</td></tr>
|
|
4561
|
+
<tr><td class="sig">setLoading(bool) / setError(message)</td><td class="desc">A loading state and a host-supplied error banner; empty columns already render their placeholder.</td></tr>
|
|
4562
|
+
<tr><td class="sig">setRows(rows) / refresh()</td><td class="desc">Replace the source rows, or recompute and re-render.</td></tr>
|
|
4563
|
+
<tr><td class="sig">on(name, fn) / off(name, fn)</td><td class="desc">Events: <code>card:click</code>, <code>card:dblclick</code>, <code>card:contextmenu</code>, <code>card:move</code>, <code>card:reverted</code>, <code>selection:changed</code>, <code>drag:start</code>, <code>drag:end</code> (plus the vocabulary the later cycles emit).</td></tr>
|
|
4564
|
+
<tr><td class="sig">readonly(scope)</td><td class="desc">Whether a scope is readonly — the whole board, a <code>{ column }</code> or a <code>{ card }</code>. A readonly card is not draggable; a move into a readonly column is refused.</td></tr>
|
|
4565
|
+
<tr><td class="sig">destroy()</td><td class="desc">Empty the element and drop the model. The host still owns any bound grid.</td></tr>
|
|
4566
|
+
</tbody>
|
|
4567
|
+
</table>
|
|
4568
|
+
</div>
|
|
4569
|
+
<p><strong>Drag-and-drop and keyboard move (write-back).</strong> Cards drag between columns (writing <code>columnProperty</code>) and within a column into a position (writing <code>orderProperty</code> with fractional ranking, so only the moved cards' order is written). The same move is available from the keyboard: focus a card, press <kbd>Space</kbd> to grab it, use the arrows to choose a target column and position (announced on a live region), <kbd>Space</kbd>/<kbd>Enter</kbd> to drop, <kbd>Escape</kbd> to cancel. Multi-select drags every selected card. A move calls <code>onBeforeMove(card, from, to, index)</code> first — return <code>false</code> (or a promise of it) to veto — then persists: <strong>grid-bound</strong>, through <code>grid.edit.setCells</code> (the same public edit-commit path inline editing and the write-back adapter use, so the grid's own pipeline owns the optimistic apply, the confirm and the revert); <strong>standalone</strong>, optimistically with a revert when <code>onCardMove</code> returns <code>false</code>/rejects. A configurable per-card <code>contextMenu</code> (an array or <code>fn(card, selected)</code>) replaces the <code>card:contextmenu</code> event when present.</p>
|
|
4570
|
+
<p><strong>Swimlanes, collapse, reorder and search.</strong> Set <code>swimlanes: true</code> to render a 2D lane×column grid grouped by <code>swimlaneProperty</code>: one band per lane with its own count/points, columns aligned across every lane, the board scrolling vertically through lanes and horizontally through columns inside its own box. A drag across lanes writes the swimlane property too. Columns and lanes collapse (their state survives a keyed-diff update); columns reorder by dragging their header (<code>reorderColumns</code>/<code>moveColumn</code>). <code>setQuickFilter(text)</code> searches across card fields, <code>setFilter(fn)</code> applies a predicate, and <code>facets(property)</code> returns distinct-value counts to build a facet control. Naming the <code>swimlaneProperty</code> is separate from turning on the lane view, so a board can carry it for a cross-lane move without switching layout.</p>
|
|
4571
|
+
<p><strong>Sprint, epic and card pop-out.</strong> <code>setSprint(id)</code> shows one sprint, <code>showBacklog()</code> the cards with no sprint, and <code>sprints()</code> feeds a switcher; <code>setEpic(id)</code> narrows to an epic and <code>epicRollup()</code> (or <code>rollup(property)</code>) returns per-epic count, points and progress toward the <code>done</code> columns. A card can <strong>pop out a nested grid of its children</strong> — an epic's stories, a story's tasks, recursively. The child relationship is a <code>childrenProperty</code> (parent-id within the dataset) and/or a <code>loadChildren(card)</code> (per-card dataset or async fetch), and the child is a full composed <code>createGrid</code> (sort/filter/edit/write-back) — supplied as <code>children.factory</code> — opened in a <code>drawer</code> (default), <code>modal</code> or <code>inline</code>. With <code>children.asBoard</code> the child is itself a board, so it can pop its own children. This reuses the grid by composition and adds no grid-core coupling. <code>expand(key)</code> and the per-card drill affordance emit <code>card:expand</code>; a deeper open emits <code>card:drill</code>.</p>
|
|
4572
|
+
<p><strong>Live updates.</strong> Because the board consumes data through the same keyed-diff contract a grid does, a <a href="#datarouter">Data Router</a> drives it directly — <code>router.attach(board, predicate)</code> — and one feed fans out to a grid, a kanban, a chart and a KPI tile at once. A live <code>rows.apply({ add, update, remove })</code> is applied as a keyed diff (an unchanged card keeps its model) and re-rendered <strong>preserving</strong> scroll, focus, selection, collapsed columns/lanes and any open pop-out, so a card can appear, move or update under the user without losing their place.</p>
|
|
4573
|
+
<p><strong>Scale, state and accessibility.</strong> <code>virtualize</code> renders only a scroll window of a tall column (with true-height spacers so the scrollbar stays honest), for boards of thousands of cards. <code>getState()</code>/<code>setState()</code> (and <code>config.state</code>) save and restore the collapsed columns and lanes, the column order, the quick filter and the sprint/epic selection, so a reopened board comes back as it was; <code>setLoading</code>/<code>setError</code> add loading and error states. Accessibility runs throughout: the board is a labelled group of labelled column lists, cards are a roving-tabindex focus ring (arrows to move focus, Enter to activate), the move is fully keyboard-driven (<kbd>Space</kbd> grab, arrows for column/position, <kbd>Alt</kbd>+<kbd>↑/↓</kbd> across swimlanes, <kbd>Space</kbd>/<kbd>Enter</kbd> drop, <kbd>Escape</kbd> cancel) with live-region announcements, and every affordance carries a name.</p>
|
|
4574
|
+
<p><strong>Inline edit and add-card.</strong> A field is opted into inline edit with the object mapping form — <code>card: { title: { field: 'title', edit: true } }</code>. Double-clicking a card (or <code>editCard(key, field)</code>) edits it in place: <strong>grid-bound</strong>, through the grid's own field editor for that column via its public edit path, so the column's parse, validate and optimistic/confirm/revert all run; <strong>standalone</strong>, through a host <code>editor</code> factory (or a default input), reverting when <code>onCardEdit</code> rejects. A per-column add-card affordance (<code>config.addCard</code>) creates a card carrying the column's group value — from a host <code>onAddCard(columnId)</code>, or generated, or appended through <code>grid.edit.addRow</code> when bound — and opens it straight in inline edit on its title, so the user just types. The module imports nothing from the grid's DOM package: grid-bound edits ride the grid's public edit API, standalone edits use the host's editor, so a board-only page never pulls the grid in.</p>
|
|
4575
|
+
<h3 id="kanban-example">A board, grouped and aggregated, executed</h3>
|
|
4576
|
+
<p class="section-note">A DemandFlow-shaped set — statuses as columns, points, an empty configured column,
|
|
4577
|
+
a WIP limit, and one status outside the configured set — grouped headless, then a keyed diff applied
|
|
4578
|
+
through the same <code>rows.apply</code> contract the Data Router drives. Run on every build.</p>
|
|
4579
|
+
<pre data-run="js" data-expect="1/3 | 2/7 | over | archived | 2/13" data-covers="export:createKanban"><code><span class="kw">const</span> { createKanban } = <span class="kw">await</span> import('../packages/modules/kanban/index.js');
|
|
4580
|
+
|
|
4581
|
+
<span class="kw">const</span> board = createKanban(null, {
|
|
4582
|
+
rows: [
|
|
4583
|
+
{ id: 'a', status: 'todo', points: 3, title: 'Login form' },
|
|
4584
|
+
{ id: 'b', status: 'doing', points: 5, title: 'OAuth' },
|
|
4585
|
+
{ id: 'c', status: 'doing', points: 2, title: 'Reset flow' },
|
|
4586
|
+
{ id: 'd', status: 'done', points: 8, title: 'Audit log' },
|
|
4587
|
+
{ id: 'z', status: 'archived', points: 1, title: 'Old ticket' }, <span class="cmt">// outside the configured columns</span>
|
|
4588
|
+
],
|
|
4589
|
+
rowKey: 'id',
|
|
4590
|
+
columnProperty: 'status',
|
|
4591
|
+
columns: [
|
|
4592
|
+
{ id: 'todo', title: 'To do' },
|
|
4593
|
+
{ id: 'doing', title: 'In progress', wipLimit: 1 },
|
|
4594
|
+
{ id: 'done', title: 'Done' },
|
|
4595
|
+
{ id: 'blocked', title: 'Blocked' }, <span class="cmt">// configured but empty; still shown</span>
|
|
4596
|
+
],
|
|
4597
|
+
pointsProperty: 'points',
|
|
4598
|
+
card: { title: 'title' },
|
|
4599
|
+
});
|
|
4600
|
+
|
|
4601
|
+
<span class="kw">const</span> todo = board.count('todo') + '/' + board.points('todo'); <span class="cmt">// 1/3</span>
|
|
4602
|
+
<span class="kw">const</span> doing = board.count('doing') + '/' + board.points('doing'); <span class="cmt">// 2/7</span>
|
|
4603
|
+
<span class="kw">const</span> over = board.column('doing').over ? 'over' : 'ok'; <span class="cmt">// over the WIP limit of 1</span>
|
|
4604
|
+
<span class="kw">const</span> extra = board.column('archived') ? 'archived' : 'dropped'; <span class="cmt">// an out-of-set value is kept</span>
|
|
4605
|
+
|
|
4606
|
+
<span class="cmt">// The keyed-diff consumer contract a Data Router drives: move b to done, drop c.</span>
|
|
4607
|
+
board.rows.apply({ update: [{ id: 'b', status: 'done', points: 5, title: 'OAuth' }], remove: ['c'] });
|
|
4608
|
+
<span class="kw">const</span> done = board.count('done') + '/' + board.points('done'); <span class="cmt">// 2/13</span>
|
|
4609
|
+
|
|
4610
|
+
board.destroy();
|
|
4611
|
+
<span class="kw">return</span> [todo, doing, over, extra, done].join(' | ');</code></pre>
|
|
4612
|
+
|
|
4613
|
+
<h3 id="kanban-move-example">Move, reorder and veto, executed</h3>
|
|
4614
|
+
<p class="section-note">The move path drag-and-drop and keyboard move share: a reorder writes the order
|
|
4615
|
+
property with fractional ranking, a cross-column move writes the column property, and
|
|
4616
|
+
<code>onBeforeMove</code> can veto. Run headless on every build.</p>
|
|
4617
|
+
<pre data-run="js" data-expect="c,a,b | done | todo" data-covers="export:createKanban"><code><span class="kw">const</span> { createKanban } = <span class="kw">await</span> import('../packages/modules/kanban/index.js');
|
|
4618
|
+
|
|
4619
|
+
<span class="kw">const</span> board = createKanban(null, {
|
|
4620
|
+
rows: [
|
|
4621
|
+
{ id: 'a', status: 'todo', ord: 1, title: 'A' },
|
|
4622
|
+
{ id: 'b', status: 'todo', ord: 2, title: 'B' },
|
|
4623
|
+
{ id: 'c', status: 'todo', ord: 3, title: 'C' },
|
|
4624
|
+
],
|
|
4625
|
+
rowKey: 'id',
|
|
4626
|
+
columnProperty: 'status',
|
|
4627
|
+
columns: [{ id: 'todo' }, { id: 'doing' }, { id: 'done' }],
|
|
4628
|
+
orderProperty: 'ord',
|
|
4629
|
+
<span class="cmt">// Veto any move into 'doing'; allow the rest.</span>
|
|
4630
|
+
onBeforeMove: (card, from, to) => to !== 'doing',
|
|
4631
|
+
});
|
|
4632
|
+
|
|
4633
|
+
<span class="kw">await</span> board.move('c', 'todo', 0); <span class="cmt">// reorder C to the top of todo</span>
|
|
4634
|
+
<span class="kw">const</span> order = board.column('todo').cards.map((x) => x.key).join(','); <span class="cmt">// c,a,b</span>
|
|
4635
|
+
|
|
4636
|
+
<span class="kw">await</span> board.move('a', 'done'); <span class="cmt">// cross-column move writes the column</span>
|
|
4637
|
+
<span class="kw">const</span> moved = board.card('a').columnId; <span class="cmt">// done</span>
|
|
4638
|
+
|
|
4639
|
+
<span class="kw">await</span> board.move('b', 'doing'); <span class="cmt">// vetoed by onBeforeMove</span>
|
|
4640
|
+
<span class="kw">const</span> vetoed = board.card('b').columnId; <span class="cmt">// still todo</span>
|
|
4641
|
+
|
|
4642
|
+
<span class="kw">return</span> [order, moved, vetoed].join(' | ');</code></pre>
|
|
4643
|
+
|
|
4644
|
+
<h3 id="kanban-swimlane-example">Swimlanes, collapse and search, executed</h3>
|
|
4645
|
+
<p class="section-note">Lanes with per-lane aggregates, a collapsed column, a reorder, and a quick-filter —
|
|
4646
|
+
all headless, run on every build.</p>
|
|
4647
|
+
<pre data-run="js" data-expect="Ann 2/8 | done,todo,doing | 1" data-covers="export:createKanban"><code><span class="kw">const</span> { createKanban } = <span class="kw">await</span> import('../packages/modules/kanban/index.js');
|
|
4648
|
+
|
|
4649
|
+
<span class="kw">const</span> board = createKanban(null, {
|
|
4650
|
+
rows: [
|
|
4651
|
+
{ id: 1, status: 'todo', assignee: 'Ann', points: 3, title: 'Login' },
|
|
4652
|
+
{ id: 2, status: 'doing', assignee: 'Ann', points: 5, title: 'OAuth' },
|
|
4653
|
+
{ id: 3, status: 'doing', assignee: 'Bob', points: 2, title: 'Reset' },
|
|
4654
|
+
{ id: 4, status: 'done', assignee: 'Bob', points: 8, title: 'Audit' },
|
|
4655
|
+
],
|
|
4656
|
+
rowKey: 'id',
|
|
4657
|
+
columnProperty: 'status',
|
|
4658
|
+
columns: [{ id: 'todo' }, { id: 'doing' }, { id: 'done' }],
|
|
4659
|
+
swimlaneProperty: 'assignee',
|
|
4660
|
+
swimlanes: true,
|
|
4661
|
+
pointsProperty: 'points',
|
|
4662
|
+
card: { title: 'title' },
|
|
4663
|
+
});
|
|
4664
|
+
|
|
4665
|
+
<span class="kw">const</span> ann = board.model.lanes.find((l) => l.id === 'Ann');
|
|
4666
|
+
<span class="kw">const</span> lane = 'Ann ' + ann.count + '/' + ann.points; <span class="cmt">// Ann 2/8</span>
|
|
4667
|
+
|
|
4668
|
+
board.reorderColumns(['done', 'todo', 'doing']);
|
|
4669
|
+
<span class="kw">const</span> order = board.columns().map((c) => c.id).join(','); <span class="cmt">// done,todo,doing</span>
|
|
4670
|
+
|
|
4671
|
+
board.setQuickFilter('reset');
|
|
4672
|
+
<span class="kw">const</span> shown = board.model.cardsByKey.size; <span class="cmt">// 1 (only "Reset" matches)</span>
|
|
4673
|
+
|
|
4674
|
+
<span class="kw">return</span> [lane, order, shown].join(' | ');</code></pre>
|
|
4675
|
+
|
|
4676
|
+
<h3 id="kanban-sprint-example">Sprint, backlog and epic rollup, executed</h3>
|
|
4677
|
+
<p class="section-note">A sprint view, the backlog, and an epic rollup with progress toward the done column —
|
|
4678
|
+
headless, run on every build.</p>
|
|
4679
|
+
<pre data-run="js" data-expect="2 | 1 | 10 0.5" data-covers="export:createKanban"><code><span class="kw">const</span> { createKanban } = <span class="kw">await</span> import('../packages/modules/kanban/index.js');
|
|
4680
|
+
|
|
4681
|
+
<span class="kw">const</span> board = createKanban(null, {
|
|
4682
|
+
rows: [
|
|
4683
|
+
{ id: 's1', status: 'todo', epic: 'E', sprint: 'S1', points: 3, title: 'Story 1' },
|
|
4684
|
+
{ id: 's2', status: 'done', epic: 'E', sprint: 'S1', points: 5, title: 'Story 2' },
|
|
4685
|
+
{ id: 's3', status: 'todo', epic: 'E', sprint: null, points: 2, title: 'Story 3' },
|
|
4686
|
+
],
|
|
4687
|
+
rowKey: 'id',
|
|
4688
|
+
columnProperty: 'status',
|
|
4689
|
+
columns: [{ id: 'todo' }, { id: 'done', done: true }],
|
|
4690
|
+
pointsProperty: 'points',
|
|
4691
|
+
sprintProperty: 'sprint',
|
|
4692
|
+
epicProperty: 'epic',
|
|
4693
|
+
});
|
|
4694
|
+
|
|
4695
|
+
board.setSprint('S1');
|
|
4696
|
+
<span class="kw">const</span> inSprint = board.model.cardsByKey.size; <span class="cmt">// 2 (s1, s2 are in S1; s3 has no sprint)</span>
|
|
4697
|
+
board.showBacklog();
|
|
4698
|
+
<span class="kw">const</span> backlog = board.model.cardsByKey.size; <span class="cmt">// 1 (s3)</span>
|
|
4699
|
+
board.setSprint(undefined);
|
|
4700
|
+
|
|
4701
|
+
<span class="kw">const</span> e = board.epicRollup()[0];
|
|
4702
|
+
<span class="kw">const</span> rollup = e.points + ' ' + e.progress; <span class="cmt">// 10 0.5</span>
|
|
4703
|
+
|
|
4704
|
+
<span class="kw">return</span> [inSprint, backlog, rollup].join(' | ');</code></pre>
|
|
4705
|
+
|
|
4706
|
+
<h3 id="kanban-live-example">Live, driven by a Data Router, executed</h3>
|
|
4707
|
+
<p class="section-note">One feed, routed to a board through the same keyed-diff contract a grid uses:
|
|
4708
|
+
a snapshot hydrates it and a delta moves a card. Run headless on every build.</p>
|
|
4709
|
+
<pre data-run="js" data-expect="1,1 | 0,2" data-covers="export:createKanban"><code><span class="kw">const</span> { createKanban } = <span class="kw">await</span> import('../packages/modules/kanban/index.js');
|
|
4710
|
+
<span class="kw">const</span> { createDataRouter } = <span class="kw">await</span> import('../packages/modules/data-router/index.js');
|
|
4711
|
+
|
|
4712
|
+
<span class="kw">const</span> board = createKanban(null, {
|
|
4713
|
+
rows: [], rowKey: 'id', columnProperty: 'status',
|
|
4714
|
+
columns: [{ id: 'todo' }, { id: 'done' }], card: { title: 'title' },
|
|
4715
|
+
});
|
|
4716
|
+
|
|
4717
|
+
<span class="kw">const</span> router = createDataRouter({ key: 'kind', rowKey: 'id' });
|
|
4718
|
+
router.attach(board, 'task'); <span class="cmt">// the board is a drop-in router target</span>
|
|
4719
|
+
router.load([
|
|
4720
|
+
{ id: 't1', kind: 'task', status: 'todo', title: 'T1' },
|
|
4721
|
+
{ id: 't2', kind: 'task', status: 'done', title: 'T2' },
|
|
4722
|
+
]);
|
|
4723
|
+
<span class="kw">const</span> hydrated = board.count('todo') + ',' + board.count('done'); <span class="cmt">// 1,1</span>
|
|
4724
|
+
|
|
4725
|
+
router.apply([{ op: 'upsert', row: { id: 't1', kind: 'task', status: 'done', title: 'T1' } }]);
|
|
4726
|
+
<span class="kw">const</span> afterDelta = board.count('todo') + ',' + board.count('done'); <span class="cmt">// 0,2</span>
|
|
4727
|
+
|
|
4728
|
+
router.destroy();
|
|
4729
|
+
<span class="kw">return</span> [hydrated, afterDelta].join(' | ');</code></pre>
|
|
4730
|
+
|
|
4316
4731
|
<h2 id="mocksocket">The mock socket</h2>
|
|
4317
4732
|
<p><code>modules/mock-socket</code> is a serverless stand-in for a live <code>WebSocket</code> feed, for building and demonstrating a real-time UI with <strong>no backend</strong>. <code>MockWebSocket</code> presents the same surface as the browser's <code>WebSocket</code> — the same <code>readyState</code> and state constants, the same <code>onopen</code>, <code>onmessage</code>, <code>onclose</code> and <code>onerror</code>, <code>addEventListener</code>, <code>send</code> and <code>close</code> — so the code that reads it does not change when it is swapped for a real one. It fires an initial snapshot the moment it opens, then a stream of deltas on a timer, all from a generator you hand it. It is a dev and test utility: optional, imports nothing from the grid, and is never pulled into the core bundle. It pairs naturally with the data router (one mock stream, partitioned to many grids), but depends on it no more than a real socket does.</p>
|
|
4318
4733
|
<pre><code>import { MockWebSocket, opsFeed } from '@toclocoinc/lattice-grid/modules/mock-socket';
|
|
@@ -4893,8 +5308,8 @@ spans.addSpan('R0', 'name', 3, 2); <span class="cmt">// rowspan 3, colspan 2</sp
|
|
|
4893
5308
|
].join('|');</code></pre>
|
|
4894
5309
|
|
|
4895
5310
|
<h3 id="module-exports-example">Every module export, executed</h3>
|
|
4896
|
-
<p class="section-note">
|
|
4897
|
-
<pre data-run="js" data-expect="
|
|
5311
|
+
<p class="section-note">Every shipped module’s exports, resolved against its own barrel on every build.</p>
|
|
5312
|
+
<pre data-run="js" data-expect="67" data-covers="export:ContextMenu export:Messages export:Registry export:autoInit export:createGrid export:createLocalViewStorage export:createMessages export:createStat export:deltaOf export:gridElementsWithin export:hydrateTable export:mountPanel export:readTable export:toneOf export:Chart export:PALETTE export:SCHEMES export:TYPES export:createChart export:chartRange export:canChartRange export:deriveRangeSpec export:regressionPlots export:registerScheme export:resolveScheme export:schemeNames export:setDefaultScheme export:HTML_ROW_WARNING_THRESHOLD export:QUERY_CHANGED_EVENT export:SCROLL_NEAR_END_EVENT export:attach export:destroyWithin export:driveInfiniteScroll export:driveOobUpdates export:driveServerMode export:ingestResponse export:initWithin export:queryParams export:restoreStateWithin export:rowsFromFragment export:rowsFromJson export:saveStateWithin export:ATTRIBUTE_CONFIG export:EVENT_PREFIX export:GridElementController export:LatticeGrid export:TAG_NAME export:createLatticeGridElement export:defineLatticeGrid export:domEventName export:observedAttributeNames export:CONSOLE_ACTIVATION export:createDevtools export:expose export:EVENT_NAMES export:handlerName export:createLatticeGrid export:dashedName export:createLatticeAction export:Grid export:warnIfLargeHtmlPayload export:createDataRouter export:MockWebSocket export:rng export:opsFeed export:priceFeed export:createKanban"><code><span class="cmt">// Every declared export of every shipped module, resolved against its own</span>
|
|
4898
5313
|
<span class="cmt">// barrel. A module that stopped exporting something fails here.</span>
|
|
4899
5314
|
<span class="kw">const</span> modules = [
|
|
4900
5315
|
[<span class="kw">await</span> import('../packages/dom/src/index.js'), [
|
|
@@ -4941,6 +5356,9 @@ spans.addSpan('R0', 'name', 3, 2); <span class="cmt">// rowspan 3, colspan 2</sp
|
|
|
4941
5356
|
[<span class="kw">await</span> import('../packages/modules/mock-socket/index.js'), [
|
|
4942
5357
|
'MockWebSocket', 'rng', 'opsFeed', 'priceFeed',
|
|
4943
5358
|
]],
|
|
5359
|
+
[<span class="kw">await</span> import('../packages/modules/kanban/index.js'), [
|
|
5360
|
+
'createKanban',
|
|
5361
|
+
]],
|
|
4944
5362
|
];
|
|
4945
5363
|
|
|
4946
5364
|
<span class="kw">let</span> present = 0;
|
package/docs/api-detail.html
CHANGED
|
@@ -437,7 +437,7 @@
|
|
|
437
437
|
<div class="shell">
|
|
438
438
|
<aside class="rail">
|
|
439
439
|
<p class="rail__brand">Lattice Grid</p>
|
|
440
|
-
<p class="rail__sub">Developer guide · v1.
|
|
440
|
+
<p class="rail__sub">Developer guide · v1.34.0</p>
|
|
441
441
|
<nav>
|
|
442
442
|
<div class="rail__group">
|
|
443
443
|
<span class="rail__label">Start here</span>
|
|
@@ -6238,12 +6238,15 @@ grid.state.apply(savedView.state);
|
|
|
6238
6238
|
<tr><td class="name">canChartRange</td><td class="desc">Whether <code>chartRange</code> would draw something for the grid’s current selection — the question a menu asks before offering the item.</td></tr>
|
|
6239
6239
|
<tr><td class="name">deriveRangeSpec</td><td class="desc">Decide what a chart of a range should be without drawing it: the type, the category column, the measures, and a spec ready for <code>createChart</code>.</td></tr>
|
|
6240
6240
|
<tr><td class="name">regressionPlots</td><td class="desc">Turn a fitted regression model into diagnostic chart specs ready for <code>createChart</code>: the fit line with its confidence band, residuals-vs-fitted, a QQ plot of the residuals, and a multicollinearity correlogram with the model’s VIF. The plots that need a per-row or per-coefficient quantity the grid has no column for (scale-location, residuals-vs-leverage, the coefficient forest) are returned as a null spec carrying the reason rather than dropped.</td></tr>
|
|
6241
|
-
<tr><td class="name">createDataRouter</td><td class="desc">Split one arriving stream or dataset across many grids by what each record is — a property or a predicate — driving each grid through the public keyed <code>rows.apply</code> path so a snapshot is a diff, a delta is applied in place, a moved partition moves the row rather than duplicating it, and an unmatched record is counted, sunk and never dropped. v2 adds cross-grid selection filtering: <code>link(source, target, relation)</code> makes a selection in one grid filter what another receives — by a key map or a predicate function, multi-select as an IN set, debounced — re-pushed through the same keyed-diff path so the target stays dumb. Detaches its grids on <code>destroy</code>; the host owns them.</td></tr>
|
|
6241
|
+
<tr><td class="name">createDataRouter</td><td class="desc">Split one arriving stream or dataset across many grids by what each record is — a property or a predicate — driving each grid through the public keyed <code>rows.apply</code> path so a snapshot is a diff, a delta is applied in place, a moved partition moves the row rather than duplicating it, and an unmatched record is counted, sunk and never dropped. v2 adds cross-grid selection filtering: <code>link(source, target, relation)</code> makes a selection in one grid filter what another receives — by a key map or a predicate function, multi-select as an IN set, debounced — re-pushed through the same keyed-diff path so the target stays dumb. v5 adds wedge-conversion primitives: <code>subscribe(value, handler)</code> routes a slice to any non-grid view (KPI tile, detail pane, map, form) as the same keyed diff a grid gets; <code>alert(value, condition, handler)</code> evaluates a condition over a slice and emits (edge-triggered, debounced) rather than rendering; and <code>configure(spec)</code> (or <code>createDataRouter({ config })</code>) takes the whole routing graph as one declarative data spec that desugars to the imperative API and composes with it. Detaches its grids on <code>destroy</code>; the host owns them.</td></tr>
|
|
6242
|
+
<tr><td class="name">createKanban</td><td class="desc">A board (kanban) view of rows as cards, grouped into columns by a configurable property (a status, a stage, a state field), with per-column card count and an optional points sum, a WIP over-limit flag, configured columns shown even when empty, granular readonly (whole board / per column / per card), field-mapped card templates that reuse the grid’s own column formatters when a grid is bound, and the core pointer events (<code>card:click</code>, <code>card:dblclick</code>, <code>card:contextmenu</code>). It is a dataset viewer like any other: it consumes data through the same keyed-diff <code>rows.apply({ add, update, remove })</code> contract a grid exposes, so a Data Router can <code>attach(value, board)</code> and drive a kanban beside a grid and a chart off one feed. Accessibility is built in from the start — a labelled group of labelled column lists, cards in a roving-tabindex focus ring with arrow-key navigation, and a polite live region. Every structural property is named in config (<code>columnProperty</code>, <code>pointsProperty</code>, <code>orderProperty</code>, <code>swimlaneProperty</code>, <code>sprintProperty</code>, <code>epicProperty</code>) so it maps DemandFlow and any customer schema without code change. Pass <code>null</code> as the element for a headless board that computes the same model without a DOM. Cards drag between columns (writing the column property) and within a column into a position (writing the order property with fractional ranking), and the same move is keyboard-accessible — <kbd>Space</kbd> to grab, arrows to choose a target, <kbd>Space</kbd>/<kbd>Enter</kbd> to drop, <kbd>Escape</kbd> to cancel — announced on the live region; multi-select drags every selected card. A move calls <code>onBeforeMove(card, from, to, index)</code> first (return <code>false</code> to veto) and then persists through the grid’s shipped write-back path — grid-bound via <code>grid.edit.setCells</code> (the same public edit-commit path inline editing uses, so the grid’s pipeline owns optimistic apply / confirm / revert), standalone with a revert when <code>onCardMove</code> rejects — emitting <code>card:move</code>. A configurable per-card <code>contextMenu</code> replaces the <code>card:contextmenu</code> event when present. With <code>swimlanes: true</code> it renders a 2D lane×column grid grouped by <code>swimlaneProperty</code> (per-lane count/points, columns aligned across lanes, a cross-lane drag writing the swimlane property); columns and lanes collapse (state surviving a keyed-diff update), columns reorder by header drag, and <code>setQuickFilter</code>/<code>setFilter</code>/<code>facets</code> drive search and faceting. <code>setSprint</code>/<code>showBacklog</code>/<code>sprints</code> give a sprint view, switcher and backlog; <code>setEpic</code>/<code>epicRollup</code>/<code>rollup</code> give an epic view and rollups (count, points, progress toward the <code>done</code> columns). A card can pop out a nested grid of its children (an epic's stories, a story's tasks, recursively) via a <code>childrenProperty</code> and/or <code>loadChildren(card)</code>: the child is a full composed <code>createGrid</code> (or, with <code>asBoard</code>, a nested board) opened in a drawer/modal/inline container — reuse by composition, no grid-core coupling — emitting <code>card:expand</code>/<code>card:drill</code>. Live updates arrive through the same keyed-diff contract a grid uses, so a Data Router drives the board directly (<code>attach(board, predicate)</code>); a live <code>rows.apply</code> re-renders preserving scroll, focus, selection, collapse and any open pop-out. <code>virtualize</code> renders only a scroll window of a tall column; <code>getState</code>/<code>setState</code> (and <code>config.state</code>) save and restore collapse, order, filter and sprint/epic selection; <code>setLoading</code>/<code>setError</code> give loading and error states. The move is fully keyboard-driven — Space to grab, arrows for column/position, Alt+Up/Down across swimlanes, Space/Enter to drop, Escape to cancel — announced on a live region. A field opted in with <code>card: { title: { field, edit: true } }</code> edits inline (double-click or <code>editCard</code>): grid-bound through the grid's own field editor via its public edit path, standalone through a host editor factory or a default input with an <code>onCardEdit</code> revert; a per-column add-card (<code>config.addCard</code>/<code>onAddCard</code>, or <code>grid.edit.addRow</code>) creates a card and opens it in edit. The module imports nothing from the grid's DOM package.</td></tr>
|
|
6242
6243
|
<tr><td class="name">createDevtools</td><td class="desc">Mount the devtools panel against a grid, including its accessibility checks.</td></tr>
|
|
6244
|
+
<tr><td class="name">createGantt</td><td class="desc">Create a project-planning Gantt controller (module <code>gantt</code>) over a task list and a dependency list. A CPM engine (<code>computeSchedule</code>) computes each task's early/late start and finish, its slack and the zero-float critical path, honouring the four link types (<code>LINK_TYPES</code>: FS/SS/FF/SF) with lag, and recomputes on every edit — emitting <code>schedule</code> or, on a dependency cycle or bad input, <code>error</code> (a code from <code>SCHEDULE_ERROR</code>). Milestones are zero-duration points; summary (WBS) tasks are derived from their children (earliest start, latest finish, weighted progress) rather than scheduled; <code>findViolations</code> flags a task placed earlier than its predecessors allow, and <code>toISODate</code> maps an engine day-number back to a calendar date.</td></tr>
|
|
6243
6245
|
<tr><td class="name">createLatticeGridElement</td><td class="desc">Build the element class without registering it, for a custom registry.</td></tr>
|
|
6244
6246
|
<tr><td class="name">createMessages</td><td class="desc">Build a message catalogue. A partial set lays over the built-in British English one.</td></tr>
|
|
6245
6247
|
<tr><td class="name">defineLatticeGrid</td><td class="desc">Register &lt;lattice-grid&gt;, or your own tag name.</td></tr>
|
|
6246
6248
|
<tr><td class="name">defineUnit</td><td class="desc">Add one unit to a system, or override one of ours under the same name.</td></tr>
|
|
6249
|
+
<tr><td class="name">registerChartType</td><td class="desc">Register an extension chart type so <code>createChart({ type })</code> can draw it (BACKLOG-0000886). Extension types ship as their own opt-in modules (e.g. <code>modules/chart-ridgeline</code>), so the base charts bundle does not grow for a type a caller never imports — you pay only for the charts you use. <code>registeredChartTypes()</code> lists what is registered.</td></tr>
|
|
6247
6250
|
<tr><td class="name">registerModules</td><td class="desc">Install optional modules once, for every grid on the page.</td></tr>
|
|
6248
6251
|
<tr><td class="name">registerScheme</td><td class="desc">Add a colour scheme, or replace one of ours under the same name.</td></tr>
|
|
6249
6252
|
<tr><td class="name">restoreStateWithin</td><td class="desc">Put it back afterwards.</td></tr>
|