@toclocoinc/lattice-grid 1.28.0 → 1.30.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/docs/API.html CHANGED
@@ -848,7 +848,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
848
848
  <tbody>
849
849
  <tr><td class="name">statusBar</td><td class="type">boolean | { panels }</td><td class="desc">Composable panels along the bottom. Default set: <code>rowCount</code>, <code>selectedCount</code>, <code>aggregation</code>, <code>comments</code>, <code>updates</code>, <code>progress</code>. Each is silent when it has nothing to report.</td></tr>
850
850
  <tr><td class="name">maximise</td><td class="type">boolean</td><td class="dflt">true</td><td class="desc"><code>false</code> removes the rail button and <code>grid.maximise</code>, for an application with its own full-screen mode.</td></tr>
851
- <tr><td class="name">toolPanel</td><td class="type">boolean | object</td><td class="desc">Side dock. <code>panels</code>: <code>columns</code>, <code>filters</code>, <code>views</code>, <code>quick</code>, <code>formatting</code>, <code>statistics</code>. <code>side: 'left'</code> makes it the icon rail, which also turns on <code>actions</code> (<code>undo</code>, <code>redo</code>, <code>pause</code>, <code>restore</code>, <code>maximise</code>, then the export group: <code>export</code>, <code>excel</code>, <code>clipboard</code>, <code>print</code>: nine in all, and an array takes these names rather than the button labels) and <code>icons</code>. An explicit array <em>replaces</em> that list rather than extending it; a bare <code>'-'</code> in it renders a divider between groups. <code>exportName</code> names the CSV. <code>annotate: true</code> adds the native annotation tools — <code>pen</code>, <code>arrow</code>, <code>rect</code>, <code>highlight</code> — to the rail as toggle buttons (pressed while in use, pressed again to exit); they also appear automatically for the duration of a presentation.</td></tr>
851
+ <tr><td class="name">toolPanel</td><td class="type">boolean | object</td><td class="desc">Side dock. <code>panels</code>: <code>columns</code>, <code>filters</code>, <code>views</code>, <code>quick</code>, <code>formatting</code>, <code>statistics</code>, <code>regression</code>. <code>side: 'left'</code> makes it the icon rail, which also turns on <code>actions</code> (<code>undo</code>, <code>redo</code>, <code>pause</code>, <code>restore</code>, <code>maximise</code>, then the export group: <code>export</code>, <code>excel</code>, <code>clipboard</code>, <code>print</code>: nine in all, and an array takes these names rather than the button labels) and <code>icons</code>. An explicit array <em>replaces</em> that list rather than extending it; a bare <code>'-'</code> in it renders a divider between groups. <code>exportName</code> names the CSV. <code>annotate: true</code> adds the native annotation tools — <code>pen</code>, <code>arrow</code>, <code>rect</code>, <code>highlight</code> — to the rail as toggle buttons (pressed while in use, pressed again to exit); they also appear automatically for the duration of a presentation.</td></tr>
852
852
  <tr><td class="name">groupPanel</td><td class="type">boolean | object</td><td class="desc">A drag-and-drop group-by strip above the column header — the row-group panel. Drag a heading into it to group by that column; the active groups show as removable, reorderable chips, and reordering the chips changes the nesting order. It is keyboard-operable — arrows move between chips, <code>Shift</code> with an arrow reorders, <code>Delete</code> ungroups, and an add control groups any column — and every change is announced through the live region. Off by default and non-breaking; it drives the same model as <code>grid.columns.group()</code> and reimplements nothing. The object form takes <code>hint</code>, the placeholder shown while nothing is grouped.</td></tr>
853
853
  <tr><td class="name">kpis</td><td class="type">StatConfig[]</td><td class="desc">A built-in KPI/stat strip: a labelled band of stat tiles the grid places for you above the column header. Each entry is a <code>createStat</code> spec — <code>of</code>, <code>fn</code>, <code>title</code>, <code>interval</code>, <code>footer</code>, <code>format</code> and the rest, minus <code>grid</code> and <code>container</code>, which the grid supplies — so a strip tile and a hand-placed one are the same object. The tiles follow the grid's filters, recomputing on every change like a stand-alone stat does. Off by default and non-breaking; it reuses <code>createStat</code> and reimplements no compute.</td></tr>
854
854
  <tr><td class="name">timeZone</td><td class="type">string</td><td class="desc">An IANA zone every date column formats and parses in, so a grid shows one zone whatever the viewer's machine says. Individual columns may override it.</td></tr>
@@ -990,7 +990,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
990
990
  <tr><td class="name">sort</td><td class="desc"><code>enabled</code>, <code>direction</code>, <code>order</code>, <code>nullsFirst</code></td></tr>
991
991
  <tr><td class="name">filter</td><td class="desc"><code>enabled</code>, <code>type</code>, <code>props</code></td></tr>
992
992
  <tr><td class="name">layout</td><td class="desc"><code>width</code>, <code>min</code>, <code>max</code>, <code>flex</code>, <code>pin</code>, <code>hidden</code>, <code>resizable</code>, <code>movable</code>, <code>lockVisible</code>, <code>lockPosition</code>. A <code>pin</code> of <code>&#39;start&#39;</code> or <code>&#39;end&#39;</code> holds the viewport edge while there is something to scroll; where the columns do not fill the grid, spare width falls beyond the last column rather than in front of it.</td></tr>
993
- <tr><td class="name">header</td><td class="desc"><code>template</code>, <code>render</code>, <code>props</code>, <code>class</code>, <code>tooltip</code>, <code>align</code></td></tr>
993
+ <tr><td class="name">header</td><td class="desc"><code>template</code>, <code>render</code>, <code>props</code>, <code>class</code>, <code>tooltip</code>, <code>align</code>. <code>render</code> draws a custom heading and may be a <strong>function</strong> or a <strong>component</strong> (a class with a <code>render</code> method); the two forms are interchangeable and each may either append to the passed heading element itself (returning nothing) or <em>return</em> an <code>Element</code> (attached for you) or a <code>string</code> (used as the heading text). <code>class</code> adds a class to the heading cell; <code>template</code> is not read.</td></tr>
994
994
  </tbody>
995
995
  </table>
996
996
  </div>
@@ -1673,6 +1673,26 @@ grid.presentation.stop(); <span class="cmt">// or Esc</sp
1673
1673
  </div>
1674
1674
  <p>Enlargement is a CSS scale factor multiplied into the same tokens <a href="#config">density</a> uses, so text, rows, padding and controls grow together rather than the grid being zoomed as an image. Font size is damped against it: type that scaled linearly with a 2× row height reads as shouting.</p>
1675
1675
 
1676
+ <h2 id="annotate">grid.annotate</h2>
1677
+ <p>The drawing layer over the grid: pixels on a transparent canvas, never data. A presenter picks a tool (<code>pen</code>, <code>arrow</code>, <code>rect</code>, <code>highlight</code>) and draws; the layer is inert until one is chosen, so scrolling and selection pass straight through otherwise. Marks are stored in <strong>content coordinates</strong>, so a circle drawn round a cell stays on that cell as the grid scrolls and resizes rather than hanging over the viewport.</p>
1678
+ <p>Marks can also be <strong>seeded and added without drawing</strong> (BACKLOG-0000813), which is what lets a host ship a pre-drawn callout or restore one from storage. A mark descriptor is <code>{ type, points, colour? }</code> — <code>type</code> is <code>freehand</code>, <code>arrow</code>, <code>rect</code> or <code>highlight</code> (<code>pen</code> is accepted as an alias for <code>freehand</code>); <code>points</code> are <code>{x, y}</code> in content coordinates (a trail for freehand, the two endpoints for an arrow or rectangle). Seeded and added marks are <em>durable</em>: they survive a presentation ending, unlike a live-drawn mark, and they round-trip through <code>getState</code> and a saved view.</p>
1679
+ <pre><code><span class="cmt">// Seed a mark at construction — rendered on first paint, the way redaction seeds.</span>
1680
+ createGrid(el, {
1681
+ columns, rows,
1682
+ annotate: true,
1683
+ state: { annotations: [
1684
+ { type: 'arrow', points: [{ x: 40, y: 120 }, { x: 220, y: 80 }], colour: '#e0245e' },
1685
+ ] },
1686
+ });
1687
+
1688
+ <span class="cmt">// Or add one durably at runtime — no synthesised pointer input.</span>
1689
+ grid.annotate.add({ type: 'rect', points: [{ x: 40, y: 100 }, { x: 260, y: 160 }] });
1690
+
1691
+ <span class="cmt">// Persist and restore: seeded and added marks come back out of the state.</span>
1692
+ const marks = grid.getState().annotations; <span class="cmt">// [{ type, points, colour }, …]</span>
1693
+ grid.state.apply({ annotations: marks }); <span class="cmt">// re-seed a fresh grid</span></code></pre>
1694
+ <div class="note"><p><code>annotate.add</code> adds to the model and paints — it never synthesises pointer events, so a mark is exactly what the descriptor says. <code>undo()</code> removes the most recent mark and <code>clear()</code> removes them all, as before; <code>annotation:changed</code> still fires on every change. A presentation ending clears the presenter's live-drawn marks but keeps the durable ones, which are view state a host means to persist.</p></div>
1695
+
1676
1696
  <h2 id="redaction">grid.redaction</h2>
1677
1697
  <p>Obscures a column's values on screen while leaving the shape of the data (row count, sort, filters, layout) perfectly readable. Built for presenting and screen sharing. Right-click a column heading for <strong>Redact column</strong>.</p>
1678
1698
  <p><strong>This is not a security control.</strong> The values stay in the model, the DOM, the clipboard and every export; anyone with the page can read them from devtools or by turning off one CSS rule. It defeats a camera, which is the whole claim. For a value that must not reach the browser at all, use <a href="#permissions">permissions</a> with <code>writeOnly</code>.</p>
@@ -1819,6 +1839,8 @@ grid.statistics.shadow('price', 'delta', 'R42'); // one row's shadow value
1819
1839
  grid.statistics.rebase('price'); // "mark all": today's values become the baseline
1820
1840
  grid.statistics.tracking(); // { columns, rows, forgotten }</code></pre>
1821
1841
  <p>The <code>statistics</code> tool panel is the end-user half of <code>profile()</code>: a column picker, the twelve figures and a histogram of the column's shape, all following the filters. Add it with <code>toolPanel: { panels: ['columns', 'statistics'] }</code>.</p>
1842
+ <p>The <code>regression</code> tool panel is its multi-column sibling (BACKLOG-0000812): it fits the model you name and shows the coefficient table — each term's estimate ± standard error with its t and p — alongside R² and adjusted R², the variance-inflation factor per predictor, and the Breusch–Pagan heteroscedasticity flag, all over the filtered rows and computed by the one core engine (<code>grid.statistics.regressionModel</code>). Name the model on the panel: <code>toolPanel: { panels: ['columns', { name: 'regression', props: { predictors: ['x1', 'x2'], response: 'y' } }] }</code>. p-values are reported as numbers with a documented method, never a significance verdict.</p>
1843
+ <p>The same fitted model can live in the data as shadow columns (BACKLOG-0000812): <code>shadow: { kind: 'fitPredicted', model: { predictors: ['x'], response: 'y' } }</code>, and likewise <code>fitResidual</code> and <code>fitInfluence</code> — plus <code>fitStdResidual</code>, <code>fitLeverage</code> and <code>fitCooksD</code> (BACKLOG-0000872), which surface the internally studentised residual, the hat-matrix leverage and Cook's distance the engine already computes. They are ordinary numeric/boolean cells — sortable, filterable, groupable, exportable — that read the fit by row key and follow the grid's filters (the model refits over the filtered rows); a row outside the fit reads <code>null</code>. <code>fitInfluence</code> flags Cook's D &gt; 4/n by default (overridable with <code>threshold</code>), keeping "not influential" (<code>false</code>) and "cannot tell" (<code>null</code>) distinct.</p>
1822
1844
  <p>It shows the twelve one-pass figures, then <strong>Shape</strong> (skewness, kurtosis, Jarque–Bera), <strong>Robust</strong> (trimmed and winsorized means, MAD, robust outliers), <strong>Concentration</strong> (Gini, HHI, entropy, evenness, top-3 share) and <strong>Capability</strong> where the column declares a <code>spec</code>. A section whose reductions all return null is left out rather than shown as a column of dashes.</p>
1823
1845
  <p>Or put it in your own page. <code>mountPanel</code> takes no dock and does not create one: <code>toolPanel</code> may be off entirely, so a statistics readout can sit beside a chart, in your own sidebar, or in a settings dialog, at whatever size you give it. It repaints on the same events the rail does, so it stays in step with filters, edits and saved views without you subscribing to anything.</p>
1824
1846
  <pre><code>import { mountPanel } from '@toclocoinc/lattice-grid';
@@ -1826,7 +1848,7 @@ grid.statistics.tracking(); // { columns, rows, forgotten
1826
1848
  const stats = mountPanel({ grid, panel: 'statistics', container: sidebar });
1827
1849
  stats.refresh(); // for a change the grid does not announce
1828
1850
  stats.destroy(); // yours to call: the element belongs to your page</code></pre>
1829
- <p>Any built-in panel works: <code>columns</code>, <code>filters</code>, <code>views</code>, <code>quick</code>, <code>formatting</code>, <code>statistics</code>, <code>compare</code>, <code>insights</code>, as does a constructor of your own.</p>
1851
+ <p>Any built-in panel works: <code>columns</code>, <code>filters</code>, <code>views</code>, <code>quick</code>, <code>formatting</code>, <code>statistics</code>, <code>regression</code>, <code>compare</code>, <code>insights</code>, as does a constructor of your own.</p>
1830
1852
 
1831
1853
  <h3>The insights panel</h3>
1832
1854
  <p>The <code>insights</code> tool panel is the on-screen half of the comparison analytics (§9.9): the API-only <code>subsetVsPopulation()</code>, <code>datasetVsDataset()</code>, <code>capability()</code> and <code>compareGroups()</code> rendered without you building any UI. It is opt-in — off unless you name it. Add it with <code>toolPanel: { panels: ['columns', 'insights'] }</code>.</p>
@@ -1994,6 +2016,162 @@ createGrid(el, {
1994
2016
  <div class="note"><p>Positional kinds rank over every <em>tracked</em> row, not over the filtered set: a rank that changed as you filtered would make "the top ten movers" depend on what happened to be on screen, and the column would disagree with itself between two views of the same data. Pass <code>scope: 'filtered'</code> on the shadow spec to rank within what the filters left instead: both answers are legitimate, which is why it is a choice rather than a default.</p></div>
1995
2017
  <div class="note"><p>Shadow state is keyed by row key, never by index: after any sort an index-keyed history would report one row's past against another row's present, and the wrong number would be <em>sortable</em>. Memory is capped at 200,000 tracked rows per column; past that the oldest are dropped and <code>tracking().forgotten</code> says how many, rather than a smaller number being reported as though it were the truth.</p></div>
1996
2018
 
2019
+ <h3 id="rolling-shadows">Rolling time-series columns</h3>
2020
+ <p>A total answers "how much"; a <em>rolling</em> total answers "how much lately, as the series ran" — the seven-day average that smooths a daily figure, the trailing sum, the change on the period before. These are rolling shadow kinds (BACKLOG-0000748): real columns, sortable and filterable and exportable like any other, computed in one ordered pass and cached by row key. They <strong>compose the windowed-aggregate model</strong> the grid already uses for "the average lately" over a live stream, asked instead over a column arranged in a stated order.</p>
2021
+ <p>The order is explicit and required — an <code>orderBy</code> column, never the screen sort, because a rolling figure defined by the current sort would change on every header click and a column sorted on its own rolling value would define itself. The window is the last <code>span</code> rows (<code>count</code>), the last span of the order axis (<code>time</code>), or the whole series so far (<code>session</code>). The first rows carry a <em>partial</em> window; that figure is still emitted, but a <code>windowCoverage</code> companion stamps how much of the window it actually covers, so a two-day average is never shown as a seven-day one. <code>within</code> chooses per-group (the default, partitioned by the grid's grouping) or across the whole dataset.</p>
2022
+ <p>A <code>rollingQuantile</code> (a trailing median, a p95, set by <code>q</code>) is exact while the window is small and comes from a KLL sketch past an internal span cap and for a session window — where a <code>windowApproximate</code> companion reports which rows are approximate, so a sketched quantile is never presented as exact. At a million rows the single ordered pass stays well within the suite's budget (≈380ms for the window aggregates, ≈490ms for the exact rolling median, ≈420ms for the session sketch on the reference bench).</p>
2023
+ <pre><code>columns: [
2024
+ { field: 'day', type: 'date' },
2025
+ { field: 'sales', type: 'number' },
2026
+ { id: 'ma7', title: '7-day avg', shadow: { kind: 'rollingAvg', of: 'sales', orderBy: 'day', window: { kind: 'count', span: 7 } } },
2027
+ { id: 'cover', title: 'Coverage', shadow: { kind: 'windowCoverage', of: 'sales', orderBy: 'day', window: { kind: 'count', span: 7 } } },
2028
+ { id: 'p50', title: '30-day median', shadow: { kind: 'rollingQuantile', of: 'sales', orderBy: 'day', window: { kind: 'count', span: 30 }, q: 0.5 } },
2029
+ { id: 'ytd', title: 'Cumulative', shadow: { kind: 'cumulativeToDate', of: 'sales', orderBy: 'day' } },
2030
+ { id: 'delta', title: 'vs prev', shadow: { kind: 'periodOverPeriod', of: 'sales', orderBy: 'day' } },
2031
+ ]</code></pre>
2032
+ <p>A rolling window is a property of the series, so it is computed over every row before any filter: hiding rows with a filter narrows what you see, never what "the last seven" means. A missing reading is a gap, skipped rather than treated as a zero that would report a plunge and a rebound the series never made.</p>
2033
+ <pre data-run="js" data-expect="11|3.6667|4.6667|0.33|20|2|4" data-covers="export:createHeadlessGrid"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
2034
+ <span class="cmt">// A short series, ordered by t: values 2,4,5,4,5.</span>
2035
+ <span class="kw">const</span> base = { of: 'v', orderBy: 't', within: 'all' };
2036
+ <span class="kw">const</span> win = { kind: 'count', span: 3 };
2037
+ <span class="kw">const</span> grid = createHeadlessGrid({
2038
+ columns: [
2039
+ { field: 't', type: 'number' },
2040
+ { field: 'v', type: 'number' },
2041
+ { id: 'sum', shadow: { kind: 'rollingSum', window: win, ...base } },
2042
+ { id: 'avg', shadow: { kind: 'rollingAvg', window: win, ...base } },
2043
+ { id: 'cov', shadow: { kind: 'windowCoverage', window: win, ...base } },
2044
+ { id: 'med', shadow: { kind: 'rollingQuantile', window: win, q: 0.5, ...base } },
2045
+ { id: 'cum', shadow: { kind: 'cumulativeToDate', ...base } },
2046
+ { id: 'pop', shadow: { kind: 'periodOverPeriod', ...base } },
2047
+ ],
2048
+ rows: [
2049
+ { id: 'r1', t: 1, v: 2 }, { id: 'r2', t: 2, v: 4 }, { id: 'r3', t: 3, v: 5 },
2050
+ { id: 'r4', t: 4, v: 4 }, { id: 'r5', t: 5, v: 5 },
2051
+ ],
2052
+ rowKey: 'id',
2053
+ });
2054
+ <span class="kw">const</span> round4 = (x) =&gt; Math.round(x * 10000) / 10000;
2055
+ <span class="kw">const</span> round2 = (x) =&gt; Math.round(x * 100) / 100;
2056
+ <span class="kw">return</span> [
2057
+ grid.rows.value('r3', 'sum'), <span class="cmt">// 2+4+5 = 11</span>
2058
+ round4(grid.rows.value('r3', 'avg')), <span class="cmt">// 11/3</span>
2059
+ round4(grid.rows.value('r5', 'avg')), <span class="cmt">// (5+4+5)/3</span>
2060
+ round2(grid.rows.value('r1', 'cov')), <span class="cmt">// 1/3 of the window filled</span>
2061
+ grid.rows.value('r5', 'cum'), <span class="cmt">// running total to the end</span>
2062
+ grid.rows.value('r2', 'pop'), <span class="cmt">// 4 - 2</span>
2063
+ grid.rows.value('r3', 'med'), <span class="cmt">// median of 2,4,5 = 4</span>
2064
+ ].join('|');</code></pre>
2065
+
2066
+ <h3 id="seasonal-decomposition">Seasonal decomposition</h3>
2067
+ <p>Splitting a series into <strong>trend + seasonal + residual</strong> (BACKLOG-0000873) answers "what's the underlying trend with the weekly pattern removed?". It is classical decomposition — the same algorithm <code>statsmodels.seasonal_decompose</code> uses, verified against it in the reference suite — delivered as four shadow columns over the same ordered pass: <code>tsTrend</code> (a centred moving average), <code>tsSeasonal</code> (the repeating index), <code>tsResidual</code> (what the two leave behind), and <code>tsCoverage</code>.</p>
2068
+ <p>The <code>period</code> is <strong>caller-declared and required</strong> — 7 for a weekly cycle in daily data, 12 for a monthly cycle in monthly data; there is no auto-detection in v1. The model is <code>additive</code> by default; <code>decomposition: 'multiplicative'</code> is a declared option that is undefined on a non-positive series (those rows report null, with a warning). The centred window runs off the ends, so the leading and trailing rows have no trend — they are <em>partial edges</em>, reported as null and stamped <code>tsCoverage: 0</code> rather than emitted as if full.</p>
2069
+ <pre><code>columns: [
2070
+ { field: 'day', type: 'date' },
2071
+ { field: 'sales', type: 'number' },
2072
+ { id: 'trend', title: 'Trend', shadow: { kind: 'tsTrend', of: 'sales', orderBy: 'day', period: 7 } },
2073
+ { id: 'season', title: 'Weekly', shadow: { kind: 'tsSeasonal', of: 'sales', orderBy: 'day', period: 7 } },
2074
+ { id: 'resid', title: 'Residual', shadow: { kind: 'tsResidual', of: 'sales', orderBy: 'day', period: 7 } },
2075
+ { id: 'cover', title: 'Coverage', shadow: { kind: 'tsCoverage', of: 'sales', orderBy: 'day', period: 7 } },
2076
+ ]</code></pre>
2077
+ <pre data-run="js" data-expect="14|2|0|1|null|0" data-covers="export:createHeadlessGrid"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
2078
+ <span class="cmt">// A period-4 series: trend 10+i plus a season [2,-1,0,-1], so value = trend + season.</span>
2079
+ <span class="kw">const</span> season = [2, -1, 0, -1];
2080
+ <span class="kw">const</span> base = { of: 'v', orderBy: 't', within: 'all', period: 4 };
2081
+ <span class="kw">const</span> grid = createHeadlessGrid({
2082
+ columns: [
2083
+ { field: 't', type: 'number' },
2084
+ { field: 'v', type: 'number' },
2085
+ { id: 'trend', shadow: { kind: 'tsTrend', ...base } },
2086
+ { id: 'season', shadow: { kind: 'tsSeasonal', ...base } },
2087
+ { id: 'resid', shadow: { kind: 'tsResidual', ...base } },
2088
+ { id: 'cover', shadow: { kind: 'tsCoverage', ...base } },
2089
+ ],
2090
+ rows: Array.from({ length: 8 }, (unused, i) => ({ id: String(i), t: i, v: (10 + i) + season[i % 4] })),
2091
+ rowKey: 'id',
2092
+ });
2093
+ <span class="kw">return</span> [
2094
+ grid.rows.value('4', 'trend'), <span class="cmt">// centred MA recovers the trend: 14</span>
2095
+ grid.rows.value('4', 'season'), <span class="cmt">// the phase-0 seasonal index: 2</span>
2096
+ grid.rows.value('4', 'resid'), <span class="cmt">// nothing left over: 0</span>
2097
+ grid.rows.value('4', 'cover'), <span class="cmt">// interior row: full, 1</span>
2098
+ grid.rows.value('0', 'trend') === null ? 'null' : 'x', <span class="cmt">// partial edge: null, not invented</span>
2099
+ grid.rows.value('0', 'cover'), <span class="cmt">// edge stamped partial: 0</span>
2100
+ ].join('|');</code></pre>
2101
+
2102
+ <h3 id="exponential-smoothing">Exponential smoothing</h3>
2103
+ <p>Smoothing pulls the signal out of a noisy series (BACKLOG-0000873). <code>tsSmoothed</code> is the fitted <strong>level</strong> — not a forecast of the future — from single exponential smoothing (<code>smoothing: 'ses'</code>, the default) or Holt's level+trend (<code>smoothing: 'holt'</code>). The recursion matches statsmodels and is verified against it in the reference suite. Holt-Winters (seasonal) smoothing is deferred; seasonality is covered by decomposition above.</p>
2104
+ <p>The smoothing factor is either <strong>caller-set</strong> (<code>alpha</code>, and <code>beta</code> for Holt) or <strong>fit by minimising the in-sample SSE</strong> when omitted — and the chosen value is reported, not hidden, by the <code>tsSmoothingAlpha</code> / <code>tsSmoothingBeta</code> companion columns.</p>
2105
+ <pre><code>columns: [
2106
+ { field: 'day', type: 'date' },
2107
+ { field: 'sales', type: 'number' },
2108
+ { id: 'level', title: 'Smoothed', shadow: { kind: 'tsSmoothed', of: 'sales', orderBy: 'day', smoothing: 'holt' } },
2109
+ { id: 'a', title: 'α', shadow: { kind: 'tsSmoothingAlpha', of: 'sales', orderBy: 'day', smoothing: 'holt' } },
2110
+ { id: 'b', title: 'β', shadow: { kind: 'tsSmoothingBeta', of: 'sales', orderBy: 'day', smoothing: 'holt' } },
2111
+ ]</code></pre>
2112
+ <pre data-run="js" data-expect="6|8|0.5" data-covers="export:createHeadlessGrid"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
2113
+ <span class="cmt">// SES at alpha 0.5 over 4,8,6,10: level runs 4, 6, 6, 8.</span>
2114
+ <span class="kw">const</span> base = { of: 'v', orderBy: 't', within: 'all', smoothing: 'ses', alpha: 0.5 };
2115
+ <span class="kw">const</span> grid = createHeadlessGrid({
2116
+ columns: [
2117
+ { field: 't', type: 'number' },
2118
+ { field: 'v', type: 'number' },
2119
+ { id: 'sm', shadow: { kind: 'tsSmoothed', ...base } },
2120
+ { id: 'a', shadow: { kind: 'tsSmoothingAlpha', ...base } },
2121
+ ],
2122
+ rows: [4, 8, 6, 10].map((v, i) => ({ id: String(i), t: i, v })),
2123
+ rowKey: 'id',
2124
+ });
2125
+ <span class="kw">return</span> [
2126
+ grid.rows.value('1', 'sm'), <span class="cmt">// 0.5*8 + 0.5*4 = 6</span>
2127
+ grid.rows.value('3', 'sm'), <span class="cmt">// 0.5*10 + 0.5*6 = 8</span>
2128
+ grid.rows.value('0', 'a'), <span class="cmt">// the factor used, reported: 0.5</span>
2129
+ ].join('|');</code></pre>
2130
+
2131
+ <h3 id="stationarity">Stationarity (ADF)</h3>
2132
+ <p>Before you compare two series or detrend one, it helps to know whether it is <strong>stationary</strong> — reverting to a level or trend — or wandering with a unit root. <code>grid.statistics.adf</code> runs the Augmented Dickey-Fuller test (BACKLOG-0000873) and returns a scalar readout, not a per-row column: the statistic, the augmenting lag chosen by AIC, MacKinnon's critical values, an interpolated p-value (stamped approximate), and a plain-language verdict at the 5% level. The constant+trend regression and the AIC lag choice match statsmodels' <code>adfuller</code>, against which the statistic and lag are verified.</p>
2133
+ <pre data-run="js" data-expect="non-stationary|0" data-covers="method:statistics"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
2134
+ <span class="cmt">// A random walk (a unit root): it wanders rather than reverting.</span>
2135
+ <span class="kw">const</span> walk = [0.138, -0.725, -1.26, -0.536, -0.267, 0.167, -0.765, -0.146, -0.311, -0.586,
2136
+ 0.376, -0.41, 0.282, 0.865, -0.024, 0.149, -0.2, -0.654, -1.338, -1.917, -1.832, -1.333,
2137
+ -0.492, -1.259, -1.89, -2.145, -2.146, -1.297, -1.26, -0.716, -0.846, -1.206, -2.126,
2138
+ -1.724, -0.945, -1.599, -1.316, -0.413, 0.304, 0.732, -0.257, 0.086, -0.572, -0.501,
2139
+ -1.153, -1.186, -1.455, -1.607];
2140
+ <span class="kw">const</span> grid = createHeadlessGrid({
2141
+ columns: [{ field: 't', type: 'number' }, { field: 'v', type: 'number' }],
2142
+ rows: walk.map((v, i) => ({ id: String(i), t: i, v })),
2143
+ rowKey: 'id',
2144
+ });
2145
+ <span class="kw">const</span> adf = grid.statistics.adf({ of: 'v', orderBy: 't' });
2146
+ <span class="kw">return</span> [adf.verdict, adf.usedLag].join('|'); <span class="cmt">// non-stationary, 0 lags</span></code></pre>
2147
+
2148
+ <h3 id="autocorrelation">Autocorrelation (ACF / PACF)</h3>
2149
+ <p><code>grid.statistics.acf</code> shows how far back a series depends on itself (BACKLOG-0000873): the autocorrelation (ACF) and partial autocorrelation (PACF) arrays out to a maximum lag, each with the approximate <code>±1.96/√n</code> white-noise band (stamped approximate) — a lag whose bar clears the band is evidence of real dependence. The estimators are the biased ACF and the Yule-Walker (Levinson-Durbin) PACF, matching statsmodels, verified in the reference suite. <strong>Lag 1 is the single source of truth</strong>: <code>acf[1]</code> is the same number <code>statistics.series(...).autocorrelation</code> reports, and <code>pacf[1] === acf[1]</code>.</p>
2150
+ <p>The correlogram is the arrays fed to a bar chart over <a href="#seasonal-decomposition">explicit points</a>, with the band as reference lines — reusing the existing chart primitives:</p>
2151
+ <pre><code>const { acf, bounds } = grid.statistics.acf({ of: 'sales', orderBy: 'day', maxlag: 20 });
2152
+ createChart({
2153
+ grid, container: '#acf', type: 'bar',
2154
+ points: acf.map((v, lag) =&gt; ({ x: lag, y: v })),
2155
+ reference: [{ value: bounds.upper }, { value: bounds.lower }, { value: 0 }],
2156
+ });</code></pre>
2157
+ <pre data-run="js" data-expect="1|true|true" data-covers="method:statistics"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
2158
+ <span class="cmt">// A deterministic AR(1): each reading leans 0.6 on the one before.</span>
2159
+ <span class="kw">let</span> s = 5; <span class="kw">const</span> rand = () =&gt; { s = (Math.imul(s, 1664525) + 1013904223) &gt;&gt;&gt; 0; return s / 4294967296 - 0.5; };
2160
+ <span class="kw">const</span> y = []; <span class="kw">let</span> prev = 0;
2161
+ <span class="kw">for</span> (<span class="kw">let</span> i = 0; i &lt; 200; i++) { const v = 0.6 * prev + rand(); y.push(v); prev = v; }
2162
+ <span class="kw">const</span> grid = createHeadlessGrid({
2163
+ columns: [{ field: 't', type: 'number' }, { field: 'v', type: 'number' }],
2164
+ rows: y.map((v, i) =&gt; ({ id: String(i), t: i, v })),
2165
+ rowKey: 'id',
2166
+ });
2167
+ <span class="kw">const</span> res = grid.statistics.acf({ of: 'v', orderBy: 't', maxlag: 6 });
2168
+ <span class="kw">const</span> series = grid.statistics.series('v', { by: 't' });
2169
+ <span class="kw">return</span> [
2170
+ res.acf[0], <span class="cmt">// lag 0 is always 1</span>
2171
+ res.pacf[1] === res.acf[1], <span class="cmt">// the first partial equals the first acf</span>
2172
+ Math.abs(res.acf[1] - series.autocorrelation) &lt; 1e-9, <span class="cmt">// lag 1 is the single source of truth</span>
2173
+ ].join('|');</code></pre>
2174
+
1997
2175
  <h2 id="highlight">grid.highlight</h2>
1998
2176
  <p>One mechanism for two jobs: the flash a changed cell makes, and a marker you paint deliberately. A target is a cell (<code>{key, colId}</code>), a row (<code>{key}</code>, or a bare row key) or a column (<code>{colId}</code>). Cell beats row beats column, so a specific highlight is never hidden by a broad one laid over it.</p>
1999
2177
  <pre><code>createGrid(el, {
@@ -3404,6 +3582,7 @@ off(); <span class="cmt">// on() returns i
3404
3582
  <tr><td class="name">licence:changed</td><td class="type">{ info, state }</td><td class="desc">A key was installed, and again when verification settles.</td></tr>
3405
3583
  <tr><td class="name">columns:changed</td><td class="desc">The column set was replaced or reordered wholesale.</td></tr>
3406
3584
  <tr><td class="name">columns:tagged</td><td class="desc">A column's tags changed.</td></tr>
3585
+ <tr><td class="name">columngroup:changed</td><td class="desc">A banded header was formed, renamed, dissolved, moved, or a column joined or left one — by drag, keyboard or API.</td></tr>
3407
3586
  <tr><td class="name">detail:toggled</td><td class="desc">A master-detail row opened or closed.</td></tr>
3408
3587
  <tr><td class="name">formatting:changed</td><td class="desc">A conditional formatting rule was added, edited, reordered or restated.</td></tr>
3409
3588
  <tr><td class="name">redaction:changed</td><td class="desc">A redaction rule changed.</td></tr>
@@ -3818,6 +3997,65 @@ createGrid(el, {
3818
3997
  },
3819
3998
  });</code></pre>
3820
3999
 
4000
+ <h3 id="regression-diagnostics">Regression diagnostics</h3>
4001
+ <p>A regression is not finished when it has coefficients; it is finished when the residuals
4002
+ have been looked at. <code>regressionPlots</code> turns a fitted model — the one
4003
+ <code>grid.statistics.regressionModel</code> returns — into ready chart specs, so the
4004
+ diagnostic pictures are one call rather than a hand-assembled spec each. It reimplements no
4005
+ charting and no statistics: the fit line’s confidence band is the module’s own ribbon
4006
+ primitive fed by the model’s own interval, and the multicollinearity plot is the existing
4007
+ correlogram paired with the model’s VIF.</p>
4008
+ <p>The presets that map onto grid columns are returned as drawable specs: the fit with its
4009
+ band, residuals-vs-fitted (over the <code>fitPredicted</code> and <code>fitResidual</code>
4010
+ shadow columns), a QQ plot of the residuals, the multicollinearity correlogram, and — over the
4011
+ <code>fitStdResidual</code>, <code>fitLeverage</code> and <code>fitCooksD</code> columns
4012
+ (BACKLOG-0000872) — residuals-vs-leverage, a bubble sized by Cook's distance. Scale-location
4013
+ (√|standardised residual| vs fitted) is drawn from explicit points computed off the model,
4014
+ since its y is a transform no column holds; the coefficient <code>forest</code> plot draws one
4015
+ row per coefficient — its estimate with a confidence whisker and a line at zero — through the
4016
+ explicit-bound error-bar primitive. A preset a given model cannot support (no multicollinearity
4017
+ for one predictor, no band for several) is returned as a null spec carrying a machine-readable
4018
+ reason rather than silently dropped.</p>
4019
+ <pre data-run="js" data-expect="scatter|5|scatter|qq|bubble|cook|scatter|forest" data-covers="export:regressionPlots"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
4020
+ <span class="kw">const</span> { regressionPlots } = <span class="kw">await</span> import('../packages/modules/charts/index.js');
4021
+
4022
+ <span class="cmt">// x=1..5, y=2,4,5,4,5. Shadow columns carry the model's per-row diagnostics —</span>
4023
+ <span class="cmt">// fitted value, residual, standardised residual, leverage and Cook's D — so a</span>
4024
+ <span class="cmt">// diagnostic chart is a plain chart over columns.</span>
4025
+ <span class="kw">const</span> model = { predictors: ['x'], response: 'y' };
4026
+ <span class="kw">const</span> grid = createHeadlessGrid({
4027
+ columns: [
4028
+ { field: 'x', type: 'number' },
4029
+ { field: 'y', type: 'number' },
4030
+ { id: 'yhat', title: 'Fitted', shadow: { kind: 'fitPredicted', model } },
4031
+ { id: 'resid', title: 'Residual', shadow: { kind: 'fitResidual', model } },
4032
+ { id: 'sresid', title: 'Std residual', shadow: { kind: 'fitStdResidual', model } },
4033
+ { id: 'lev', title: 'Leverage', shadow: { kind: 'fitLeverage', model } },
4034
+ { id: 'cook', title: "Cook's D", shadow: { kind: 'fitCooksD', model } },
4035
+ ],
4036
+ rows: [
4037
+ { id: 'r1', x: 1, y: 2 }, { id: 'r2', x: 2, y: 4 }, { id: 'r3', x: 3, y: 5 },
4038
+ { id: 'r4', x: 4, y: 4 }, { id: 'r5', x: 5, y: 5 },
4039
+ ],
4040
+ rowKey: 'id',
4041
+ source: { mode: 'memory', columnarBelow: 0 },
4042
+ });
4043
+
4044
+ <span class="kw">const</span> { plots } = regressionPlots(grid, {
4045
+ spec: model, fitted: 'yhat', residual: 'resid', stdResidual: 'sresid', leverage: 'lev', cooksD: 'cook',
4046
+ });
4047
+
4048
+ <span class="kw">return</span> [
4049
+ plots.fit.spec.type, <span class="cmt">// scatter, with fit:true and band</span>
4050
+ plots.fit.spec.band.points.length, <span class="cmt">// a band point per row</span>
4051
+ plots.residualsFitted.spec.type, <span class="cmt">// residual vs fitted, a scatter</span>
4052
+ plots.qq.spec.type, <span class="cmt">// a QQ plot of the residuals</span>
4053
+ plots.residualsLeverage.spec.type, <span class="cmt">// bubble, over the diagnostic columns</span>
4054
+ plots.residualsLeverage.spec.size, <span class="cmt">// sized by Cook's D</span>
4055
+ plots.scaleLocation.spec.type, <span class="cmt">// scatter, from explicit points off the model</span>
4056
+ plots.coefficientForest.spec.type, <span class="cmt">// forest, estimate + whisker per coefficient</span>
4057
+ ].join('|');</code></pre>
4058
+
3821
4059
  <h3>Maps</h3>
3822
4060
  <p>A <code>geomap</code> takes an ISO code from one column and a value from another. Alpha-2, alpha-3 and numeric codes are all accepted, and continent codes draw a continent map without any outline data. Country outlines are yours to supply through <code>shapes</code>, because a world atlas is larger than the whole library and this package fetches nothing at runtime.</p>
3823
4061
  <p>Codes that match nothing are counted and reported on the chart rather than dropped, a map missing half its data looks exactly like a map of a world where half the data is zero. The full code tables are in <a href="CHART-CODES.md">CHART-CODES.md</a>.</p>
@@ -4025,7 +4263,7 @@ createGrid(el, {
4025
4263
  <span class="chip">text</span><span class="chip">number</span><span class="chip">boolean</span><span class="chip">date</span><span class="chip">dateString</span><span class="chip">lookup</span><span class="chip">object</span>
4026
4264
  </div>
4027
4265
  <div class="chips">
4028
- <span class="chip chip--new">time</span><span class="chip chip--new">datetime</span><span class="chip chip--new">duration</span>
4266
+ <span class="chip chip--new">time</span><span class="chip chip--new">datetime</span><span class="chip chip--new">timestamp</span><span class="chip chip--new">duration</span>
4029
4267
  <span class="chip chip--new">ipv4</span><span class="chip chip--new">cidr</span><span class="chip chip--new">ipv6</span>
4030
4268
  <span class="chip chip--new">json</span><span class="chip chip--new">secret</span>
4031
4269
  <span class="chip chip--new">hex</span><span class="chip chip--new">hex8</span><span class="chip chip--new">hex16</span><span class="chip chip--new">hex32</span>
@@ -4213,7 +4451,7 @@ grid.destroy();
4213
4451
  <h3 id="events-example">Events, executed</h3>
4214
4452
  <p class="section-note">Fourteen events raised by ordinary calls, asserted on every build. An event that
4215
4453
  stopped firing, or changed name, fails here rather than in a consumer.</p>
4216
- <pre data-run="js" data-expect="14" data-covers="event:sort:changed event:filter:changed event:column:visible event:column:moved event:column:pinned event:config:changed event:rows:changed event:model:changed event:state:reset event:state:changed method:on method:destroy"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
4454
+ <pre data-run="js" data-expect="15" data-covers="event:sort:changed event:filter:changed event:column:visible event:column:moved event:column:pinned event:columngroup:changed event:config:changed event:rows:changed event:model:changed event:state:reset event:state:changed method:on method:destroy"><code><span class="kw">const</span> { createHeadlessGrid } = <span class="kw">await</span> import('../packages/core/src/index.js');
4217
4455
 
4218
4456
  <span class="kw">const</span> grid = createHeadlessGrid({
4219
4457
  rowKey: 'id',
@@ -4230,6 +4468,7 @@ grid.filters.set({ col: 's', op: 'gt', value: 0 }); <span class="cmt">// filter:
4230
4468
  grid.columns.hide('n'); <span class="cmt">// column:visible</span>
4231
4469
  grid.columns.move('n', 1); <span class="cmt">// column:moved</span>
4232
4470
  grid.columns.pin('n', 'left'); <span class="cmt">// column:pinned</span>
4471
+ grid.columns.groupColumns(['n', 's'], { title: 'Both' }); <span class="cmt">// columngroup:changed</span>
4233
4472
  grid.set('rowHeight', 30); <span class="cmt">// config:changed</span>
4234
4473
  grid.rows.apply({ update: [{ id: '1', s: 9 }] }); <span class="cmt">// rows:changed, model:changed</span>
4235
4474
  grid.state.reset(); <span class="cmt">// state:reset, state:changed</span>
@@ -4372,7 +4611,7 @@ spans.addSpan('R0', 'name', 3, 2); <span class="cmt">// rowspan 3, colspan 2</sp
4372
4611
 
4373
4612
  <h3 id="module-exports-example">Every module export, executed</h3>
4374
4613
  <p class="section-note">Nine shipped modules, each export resolved against its own barrel on every build.</p>
4375
- <pre data-run="js" data-expect="60" 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: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"><code><span class="cmt">// Every declared export of every shipped module, resolved against its own</span>
4614
+ <pre data-run="js" data-expect="61" 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"><code><span class="cmt">// Every declared export of every shipped module, resolved against its own</span>
4376
4615
  <span class="cmt">// barrel. A module that stopped exporting something fails here.</span>
4377
4616
  <span class="kw">const</span> modules = [
4378
4617
  [<span class="kw">await</span> import('../packages/dom/src/index.js'), [
@@ -4383,7 +4622,7 @@ spans.addSpan('R0', 'name', 3, 2); <span class="cmt">// rowspan 3, colspan 2</sp
4383
4622
  ]],
4384
4623
  [<span class="kw">await</span> import('../packages/modules/charts/index.js'), [
4385
4624
  'Chart', 'PALETTE', 'SCHEMES', 'TYPES',
4386
- 'createChart', 'chartRange', 'canChartRange', 'deriveRangeSpec',
4625
+ 'createChart', 'chartRange', 'canChartRange', 'deriveRangeSpec', 'regressionPlots',
4387
4626
  'registerScheme', 'resolveScheme', 'schemeNames',
4388
4627
  'setDefaultScheme',
4389
4628
  ]],
@@ -4787,6 +5026,39 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
4787
5026
  <!-- BEGIN GENERATED TYPE REFERENCE -->
4788
5027
  <h2 id="type-reference">Type reference</h2>
4789
5028
  <p class="section-note">Every interface the library declares, with the type of each member. The sections above describe how the grid is used; this one is the complete surface, generated from the type declarations so that it always matches the release.</p>
5029
+ <h3 id="type-AcfResult">AcfResult</h3>
5030
+ <p class="section-note">Autocorrelation (ACF) and partial autocorrelation (PACF) arrays (BACKLOG-0000873).</p>
5031
+ <div class="table-wrap">
5032
+ <table>
5033
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
5034
+ <tbody>
5035
+ <tr><td class="name">acf</td><td class="type">number[]</td><td class="desc">The autocorrelation at each lag; index 0 is lag 0 and is always 1.</td></tr>
5036
+ <tr><td class="name">pacf</td><td class="type">number[]</td><td class="desc">The partial autocorrelation at each lag; index 0 is 1, and `pacf[1] === acf[1]`.</td></tr>
5037
+ <tr><td class="name">bounds</td><td class="type">{ upper: number; lower: number }</td><td class="desc">The approximate ±1.96/√n white-noise confidence band.</td></tr>
5038
+ <tr><td class="name">n</td><td class="type">number</td><td class="desc">The series length the ACF/PACF were computed over.</td></tr>
5039
+ <tr><td class="name">nlags</td><td class="type">number</td><td class="desc">The maximum lag.</td></tr>
5040
+ <tr><td class="name">approximate</td><td class="type">boolean</td><td class="desc">Always true: the ±1.96/√n band is an approximation.</td></tr>
5041
+ </tbody>
5042
+ </table>
5043
+ </div>
5044
+ <h3 id="type-AdfResult">AdfResult</h3>
5045
+ <p class="section-note">The Augmented Dickey-Fuller stationarity test result (BACKLOG-0000873).</p>
5046
+ <div class="table-wrap">
5047
+ <table>
5048
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
5049
+ <tbody>
5050
+ <tr><td class="name">statistic</td><td class="type">number</td><td class="desc">The ADF t-statistic on the lagged level.</td></tr>
5051
+ <tr><td class="name">usedLag</td><td class="type">number</td><td class="desc">The number of augmenting lags chosen by AIC.</td></tr>
5052
+ <tr><td class="name">nobs</td><td class="type">number</td><td class="desc">The observations the final regression used.</td></tr>
5053
+ <tr><td class="name">criticalValues</td><td class="type">{ '1%': number; '5%': number; '10%': number }</td><td class="desc">MacKinnon's constant+trend critical values at the 1%, 5% and 10% levels.</td></tr>
5054
+ <tr><td class="name">pValue</td><td class="type">number</td><td class="desc">An approximate p-value, interpolated across the critical-value ladder.</td></tr>
5055
+ <tr><td class="name">pApproximate</td><td class="type">boolean</td><td class="desc">Always true: the p-value is an interpolation, not the MacKinnon surface.</td></tr>
5056
+ <tr><td class="name">stationary</td><td class="type">boolean</td><td class="desc">Whether the series is stationary at the 5% level.</td></tr>
5057
+ <tr><td class="name">verdict</td><td class="type">string</td><td class="desc">The plain-language verdict: `'stationary'` or `'non-stationary'`.</td></tr>
5058
+ <tr><td class="name">regression</td><td class="type">'ct'</td><td class="desc">The regression form used — always `'ct'` (constant + trend) in v1.</td></tr>
5059
+ </tbody>
5060
+ </table>
5061
+ </div>
4790
5062
  <h3 id="type-AggregateProvenance">AggregateProvenance</h3>
4791
5063
  <p class="section-note">How one aggregate was routed, for `lastPlan()` provenance.</p>
4792
5064
  <div class="table-wrap">
@@ -4832,7 +5104,6 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
4832
5104
  </table>
4833
5105
  </div>
4834
5106
  <h3 id="type-AnnotationApi">AnnotationApi</h3>
4835
- <p class="section-note">The presenter's drawing layer. Pixels over the grid, it never reads or writes data, and it is inert until a tool is chosen, so scrolling and selection pass straight through. Marks are held in content coordinates, so they stay with the cells they annotate when the grid scrolls, and are cleared when a presentation ends.</p>
4836
5107
  <div class="table-wrap">
4837
5108
  <table>
4838
5109
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
@@ -4840,12 +5111,26 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
4840
5111
  <tr><td class="name">tool</td><td class="type">'pen' | 'arrow' | 'rect' | 'highlight' | null</td><td class="desc"><small>(read-only)</small></td></tr>
4841
5112
  <tr><td class="name">count</td><td class="type">number</td><td class="desc"><small>(read-only)</small></td></tr>
4842
5113
  <tr><td class="name">use</td><td class="type">(tool: 'pen' | 'arrow' | 'rect' | 'highlight' | null, opts?: { colour?: string }): string | null</td><td class="desc"></td></tr>
5114
+ <tr><td class="name">add</td><td class="type">(mark: AnnotationMark): number</td><td class="desc">Add a durable mark from a descriptor, without synthesising pointer input (BACKLOG-0000813). The mark is painted, survives a presentation ending, and round-trips through `getState`. Returns the mark count.</td></tr>
5115
+ <tr><td class="name">list</td><td class="type">(): AnnotationMark[]</td><td class="desc">Every mark on the layer, as descriptors — the shape `getState` persists.</td></tr>
4843
5116
  <tr><td class="name">undo</td><td class="type">(): number</td><td class="desc"></td></tr>
4844
5117
  <tr><td class="name">clear</td><td class="type">(): void</td><td class="desc"></td></tr>
4845
5118
  <tr><td class="name">redraw</td><td class="type">(): void</td><td class="desc"></td></tr>
4846
5119
  </tbody>
4847
5120
  </table>
4848
5121
  </div>
5122
+ <h3 id="type-AnnotationMark">AnnotationMark</h3>
5123
+ <p class="section-note">A durable annotation mark descriptor (BACKLOG-0000813) — the shape a host seeds through `state.annotations`, adds through {@link AnnotationApi.add}, and reads back through {@link AnnotationApi.list} and `getState`. `points` are in **content coordinates** (the same space user-drawn marks are stored in), so a mark tracks scroll and resize rather than hanging over the viewport. A `freehand` mark is a trail of points; `arrow` and `rect` are their two endpoints. Text marks are a deliberate follow-up. `pen` is accepted as an alias for `freehand` on input; `list()` reports `freehand`.</p>
5124
+ <div class="table-wrap">
5125
+ <table>
5126
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
5127
+ <tbody>
5128
+ <tr><td class="name">type</td><td class="type">'freehand' | 'arrow' | 'rect' | 'highlight'</td><td class="desc"></td></tr>
5129
+ <tr><td class="name">points</td><td class="type">{ x: number; y: number }[]</td><td class="desc"></td></tr>
5130
+ <tr><td class="name">colour</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
5131
+ </tbody>
5132
+ </table>
5133
+ </div>
4849
5134
  <h3 id="type-AnomalyReason">AnomalyReason</h3>
4850
5135
  <div class="table-wrap">
4851
5136
  <table>
@@ -5117,6 +5402,8 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
5117
5402
  <tr><td class="name">font</td><td class="type">object</td><td class="desc"><small>(optional)</small></td></tr>
5118
5403
  <tr><td class="name">margin</td><td class="type">number | { top?: number; right?: number; bottom?: number; left?: number }</td><td class="desc"><small>(optional)</small></td></tr>
5119
5404
  <tr><td class="name">fit</td><td class="type">boolean | 'line'</td><td class="desc">A least-squares line through a scatter or bubble chart, one per series. `true` draws the line and its R²; `'line'` draws the line alone. Only where the x axis is numeric: on a band scale the positions are categories in an arbitrary order, and a slope through them would be a slope through the order they happened to be listed in. <small>(optional)</small></td></tr>
5405
+ <tr><td class="name">band</td><td class="type">(RegressionBand &amp; { line?: boolean }) | null</td><td class="desc">A pointwise confidence band, drawn as a varying-width ribbon beneath the fit line (BACKLOG-0000812). Fed by a fitted model's own interval — the `band` from {@link StatisticsApi.regressionModel}, or as produced by {@link regressionPlots} — so the ribbon and the diagnostics report the one computation rather than a slope redrawn here. `line: false` suppresses the band's own centre line, for a chart that already draws the fit with `fit`. Only where the x axis is numeric, for the same reason `fit` is. <small>(optional)</small></td></tr>
5406
+ <tr><td class="name">points</td><td class="type">{</td><td class="desc">An explicit point set, bypassing the by-column binder (BACKLOG-0000872): a cartesian chart whose values are not a grid column — a scale-location plot's √|standardised residual|, a coefficient forest's per-coefficient estimate — hands its points in directly. Each is `{x, y}` with an optional `label`, `size` (a bubble's third channel) and `lower`/`upper` (interval bounds the error-bar primitive reads). Numeric `x` throughout gives a continuous axis. <small>(optional)</small></td></tr>
5120
5407
  <tr><td class="name">error</td><td class="type">boolean | { of?: string; confidence?: number }</td><td class="desc">Whiskers showing the uncertainty in each mark. `true` computes a confidence interval from the readings behind the mark; `of` takes a symmetric margin from another column instead. <small>(optional)</small></td></tr>
5121
5408
  <tr><td class="name">reference</td><td class="type">{ value: number; label?: string; axis?: 'left' | 'right' }[]</td><td class="desc">Horizontal reference lines. On a dual-axis bar or line chart (see {@link ChartMeasure.axis}) a line naming `axis: 'right'` is placed on the right-hand scale, so it means what the right axis says rather than landing at the same number on the scale it does not belong to. <small>(optional)</small></td></tr>
5122
5409
  <tr><td class="name">annotations</td><td class="type">ChartAnnotation[]</td><td class="desc">The declarative annotation layer: reference and target lines, shaded bands and callouts, each naming the axis it reads and each described into the accessible table as a sentence. A value may be a constant or `compute`d from the data it annotates, so it follows the chart as the grid is filtered. <small>(optional)</small></td></tr>
@@ -5193,7 +5480,7 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
5193
5480
  <tr><td class="name">edit</td><td class="type">ColumnEditSpec | boolean | string</td><td class="desc">Whether and how the cell can be edited. A string names an editor. <small>(optional)</small></td></tr>
5194
5481
  <tr><td class="name">sort</td><td class="type">ColumnSortSpec | boolean</td><td class="desc">Whether the column sorts, and by what comparison. `false` refuses it. <small>(optional)</small></td></tr>
5195
5482
  <tr><td class="name">filter</td><td class="type">ColumnFilterSpec | boolean | FilterName</td><td class="desc">Whether the column filters, and with which filter. A string names one. <small>(optional)</small></td></tr>
5196
- <tr><td class="name">group</td><td class="type">{ enabled?: boolean; index?: number; explode?: boolean } | boolean</td><td class="desc">Row grouping by this column. `index` fixes its place among several; `explode` gives a multi-value cell one group per value rather than one group for the combination. <small>(optional)</small></td></tr>
5483
+ <tr><td class="name">group</td><td class="type">{</td><td class="desc">Row grouping by this column. `index` fixes its place among several; `explode` gives a multi-value cell one group per value rather than one group for the combination. `granularity` and `weekStart` apply to a `timestamp` column: it buckets by civil `day` (the default), `week` or `month` in the display zone, or `instant` for one group per exact moment. `weekStart` is the first weekday, 1=Monday (default) to 7=Sunday. <small>(optional)</small></td></tr>
5197
5484
  <tr><td class="name">pivot</td><td class="type">{ enabled?: boolean; index?: number } | boolean</td><td class="desc">Use this column as a pivot dimension, and where it sits among several. <small>(optional)</small></td></tr>
5198
5485
  <tr><td class="name">total</td><td class="type">TotalName | TotalFn</td><td class="desc">The reduction shown in the totals row and in group footers. <small>(optional)</small></td></tr>
5199
5486
  <tr><td class="name">groupTotal</td><td class="type">TotalName | TotalFn</td><td class="desc">The reduction for group subtotals — group footers, tree-node rollups and pivot cells — where it should differ from the grand total. Overrides `total` for those scopes only; when omitted the column's `total` applies to both. Lets a column average within each group while the grand total sums, for example (BACKLOG-0000726). <small>(optional)</small></td></tr>
@@ -5341,15 +5628,29 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
5341
5628
  </tbody>
5342
5629
  </table>
5343
5630
  </div>
5631
+ <h3 id="type-ColumnGroupState">ColumnGroupState</h3>
5632
+ <p class="section-note">A persisted banded-header node (§15, BACKLOG-0000739): a band with a `columns` list whose members are leaf ids or nested bands. This is what round-trips a drag-created group through a saved view.</p>
5633
+ <div class="table-wrap">
5634
+ <table>
5635
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
5636
+ <tbody>
5637
+ <tr><td class="name">id</td><td class="type">string</td><td class="desc"></td></tr>
5638
+ <tr><td class="name">title</td><td class="type">string</td><td class="desc"></td></tr>
5639
+ <tr><td class="name">collapsible</td><td class="type">boolean</td><td class="desc"></td></tr>
5640
+ <tr><td class="name">openByDefault</td><td class="type">boolean</td><td class="desc"></td></tr>
5641
+ <tr><td class="name">columns</td><td class="type">Array&lt;string | ColumnGroupState&gt;</td><td class="desc"></td></tr>
5642
+ </tbody>
5643
+ </table>
5644
+ </div>
5344
5645
  <h3 id="type-ColumnHeaderSpec">ColumnHeaderSpec</h3>
5345
5646
  <div class="table-wrap">
5346
5647
  <table>
5347
5648
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
5348
5649
  <tbody>
5349
- <tr><td class="name">template</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
5350
- <tr><td class="name">render</td><td class="type">string | RendererCtor</td><td class="desc"><small>(optional)</small></td></tr>
5351
- <tr><td class="name">props</td><td class="type">Record&lt;string, unknown&gt;</td><td class="desc"><small>(optional)</small></td></tr>
5352
- <tr><td class="name">class</td><td class="type">string | string[]</td><td class="desc"><small>(optional)</small></td></tr>
5650
+ <tr><td class="name">template</td><td class="type">string</td><td class="desc">Not read by the header renderer; use `render` to draw a custom heading. <small>(optional)</small></td></tr>
5651
+ <tr><td class="name">render</td><td class="type">string | RendererCtor</td><td class="desc">A custom heading renderer: a function, or a component (a class with a `render` method). A string names a registered renderer. Either form draws the same two ways and they are interchangeable — it may append to the passed label element itself and return nothing, or return an `Element` (attached for you) or a `string` (used as the heading text). <small>(optional)</small></td></tr>
5652
+ <tr><td class="name">props</td><td class="type">Record&lt;string, unknown&gt;</td><td class="desc">Props passed to `render` as `params.props`. <small>(optional)</small></td></tr>
5653
+ <tr><td class="name">class</td><td class="type">string | string[]</td><td class="desc">A class, or classes, added to the heading cell. <small>(optional)</small></td></tr>
5353
5654
  <tr><td class="name">tooltip</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
5354
5655
  <tr><td class="name">align</td><td class="type">Align</td><td class="desc"><small>(optional)</small></td></tr>
5355
5656
  </tbody>
@@ -5427,6 +5728,11 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
5427
5728
  <tr><td class="name">show</td><td class="type">(ids: string | string[]): void</td><td class="desc"></td></tr>
5428
5729
  <tr><td class="name">hide</td><td class="type">(ids: string | string[]): void</td><td class="desc"></td></tr>
5429
5730
  <tr><td class="name">move</td><td class="type">(id: string, to: number): void</td><td class="desc"></td></tr>
5731
+ <tr><td class="name">groupColumns</td><td class="type">(ids: string | string[], opts?: { title?: string; at?: number; groupId?: string }): string | null</td><td class="desc">Wrap leaf columns in a banded header, or add them to an existing band (BACKLOG-0000739). Header banding, not row grouping (see {@link group}); the band is a {@link ColumnGroup} node so a drag-, keyboard- or config-built band is the same tree, and it round-trips through a saved view. Emits `columngroup:changed`.</td></tr>
5732
+ <tr><td class="name">ungroupColumn</td><td class="type">(id: string): void</td><td class="desc">Take a leaf out of its band; a band emptied by the move is dissolved.</td></tr>
5733
+ <tr><td class="name">renameGroup</td><td class="type">(groupId: string, title: string): void</td><td class="desc">Rename a banded header.</td></tr>
5734
+ <tr><td class="name">dissolveGroup</td><td class="type">(groupId: string): void</td><td class="desc">Dissolve a band, returning its columns to the enclosing level in place.</td></tr>
5735
+ <tr><td class="name">moveGroup</td><td class="type">(groupId: string, to: number): void</td><td class="desc">Move a whole band among its siblings, its columns travelling as a block.</td></tr>
5430
5736
  <tr><td class="name">pin</td><td class="type">(id: string, side: 'start' | 'end' | null): void</td><td class="desc"></td></tr>
5431
5737
  <tr><td class="name">resize</td><td class="type">(id: string, px: number): void</td><td class="desc"></td></tr>
5432
5738
  <tr><td class="name">decorate</td><td class="type">(id: string, decoration: DecorationName | DecorationSpec | null, opts?: { variant?: VariantSpec }): void</td><td class="desc">Set, change or clear a column's decoration at runtime (§8.7). Pass `null` to clear it back to plain text. Presentation config: it is not on the undo timeline and is not carried in a saved view — use `grid.formatting` for durable, view-persisted conditional styling.</td></tr>
@@ -6449,6 +6755,7 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
6449
6755
  <tr><td class="name">version</td><td class="type">number</td><td class="desc"></td></tr>
6450
6756
  <tr><td class="name">columns</td><td class="type">ColumnState[]</td><td class="desc"><small>(optional)</small></td></tr>
6451
6757
  <tr><td class="name">columnOrder</td><td class="type">string[]</td><td class="desc"><small>(optional)</small></td></tr>
6758
+ <tr><td class="name">columnGroups</td><td class="type">ColumnGroupState[]</td><td class="desc">The banded-header tree, when the grid has one (BACKLOG-0000739). <small>(optional)</small></td></tr>
6452
6759
  <tr><td class="name">filters</td><td class="type">FilterSet</td><td class="desc"><small>(optional)</small></td></tr>
6453
6760
  <tr><td class="name">quick</td><td class="type">string</td><td class="desc"><small>(optional)</small></td></tr>
6454
6761
  <tr><td class="name">sort</td><td class="type">SortEntry[]</td><td class="desc"><small>(optional)</small></td></tr>
@@ -6456,6 +6763,7 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
6456
6763
  <tr><td class="name">pivot</td><td class="type">{ enabled: boolean; columns: string[] }</td><td class="desc"><small>(optional)</small></td></tr>
6457
6764
  <tr><td class="name">pivotView</td><td class="type">{ rowsCollapsed: string[]; columnsCollapsed: string[] }</td><td class="desc">The pivot presentation's collapse state (§10, BACKLOG-0000738): which row-axis and column-axis nodes are collapsed. Absent when the matrix is fully expanded, and tolerated as "expand all" when applied. <small>(optional)</small></td></tr>
6458
6765
  <tr><td class="name">formatting</td><td class="type">Record&lt;string, FormattingRule[]&gt;</td><td class="desc"><small>(optional)</small></td></tr>
6766
+ <tr><td class="name">annotations</td><td class="type">AnnotationMark[]</td><td class="desc">Durable annotation marks (BACKLOG-0000813): seeded from here on first paint, and written back by `getState` so a host can persist and restore them. In content coordinates, so they track scroll and resize. <small>(optional)</small></td></tr>
6459
6767
  <tr><td class="name">expanded</td><td class="type">string[]</td><td class="desc"><small>(optional)</small></td></tr>
6460
6768
  <tr><td class="name">selection</td><td class="type">string[]</td><td class="desc"><small>(optional)</small></td></tr>
6461
6769
  <tr><td class="name">scroll</td><td class="type">{ top: number; left: number }</td><td class="desc"><small>(optional)</small></td></tr>
@@ -6512,6 +6820,19 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
6512
6820
  </tbody>
6513
6821
  </table>
6514
6822
  </div>
6823
+ <h3 id="type-Heteroscedasticity">Heteroscedasticity</h3>
6824
+ <p class="section-note">The Breusch–Pagan heteroscedasticity test result.</p>
6825
+ <div class="table-wrap">
6826
+ <table>
6827
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
6828
+ <tbody>
6829
+ <tr><td class="name">statistic</td><td class="type">number</td><td class="desc"></td></tr>
6830
+ <tr><td class="name">df</td><td class="type">number</td><td class="desc"></td></tr>
6831
+ <tr><td class="name">p</td><td class="type">number</td><td class="desc"></td></tr>
6832
+ <tr><td class="name">heteroscedastic</td><td class="type">boolean</td><td class="desc">True when the test rejects homoscedasticity at the 0.05 level.</td></tr>
6833
+ </tbody>
6834
+ </table>
6835
+ </div>
6515
6836
  <h3 id="type-HighlightApi">HighlightApi</h3>
6516
6837
  <div class="table-wrap">
6517
6838
  <table>
@@ -7189,6 +7510,33 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
7189
7510
  </tbody>
7190
7511
  </table>
7191
7512
  </div>
7513
+ <h3 id="type-RegressionBand">RegressionBand</h3>
7514
+ <p class="section-note">A pointwise confidence band for the mean response of a single-predictor fit.</p>
7515
+ <div class="table-wrap">
7516
+ <table>
7517
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7518
+ <tbody>
7519
+ <tr><td class="name">confidence</td><td class="type">number</td><td class="desc"></td></tr>
7520
+ <tr><td class="name">points</td><td class="type">{ x: number; yhat: number; lower: number; upper: number }[]</td><td class="desc"></td></tr>
7521
+ </tbody>
7522
+ </table>
7523
+ </div>
7524
+ <h3 id="type-RegressionCoefficient">RegressionCoefficient</h3>
7525
+ <p class="section-note">One fitted coefficient, with the uncertainty around it.</p>
7526
+ <div class="table-wrap">
7527
+ <table>
7528
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7529
+ <tbody>
7530
+ <tr><td class="name">name</td><td class="type">string</td><td class="desc">`(intercept)` or the predictor's column id.</td></tr>
7531
+ <tr><td class="name">estimate</td><td class="type">number</td><td class="desc"></td></tr>
7532
+ <tr><td class="name">stdError</td><td class="type">number</td><td class="desc"></td></tr>
7533
+ <tr><td class="name">t</td><td class="type">number</td><td class="desc">estimate ÷ standard error.</td></tr>
7534
+ <tr><td class="name">p</td><td class="type">number</td><td class="desc">Two-sided Student-t p-value; a number with a documented method, not a verdict.</td></tr>
7535
+ <tr><td class="name">lower</td><td class="type">number | null</td><td class="desc">The Wald confidence interval at the model's confidence level (BACKLOG-0000872) — the whiskers a coefficient forest plot draws. Null when there is no residual degree of freedom to form a critical value.</td></tr>
7536
+ <tr><td class="name">upper</td><td class="type">number | null</td><td class="desc"></td></tr>
7537
+ </tbody>
7538
+ </table>
7539
+ </div>
7192
7540
  <h3 id="type-RegressionFit">RegressionFit</h3>
7193
7541
  <div class="table-wrap">
7194
7542
  <table>
@@ -7202,6 +7550,47 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
7202
7550
  </tbody>
7203
7551
  </table>
7204
7552
  </div>
7553
+ <h3 id="type-RegressionModel">RegressionModel</h3>
7554
+ <p class="section-note">A fitted multi-predictor linear model and its diagnostics (BACKLOG-0000792).</p>
7555
+ <div class="table-wrap">
7556
+ <table>
7557
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7558
+ <tbody>
7559
+ <tr><td class="name">method</td><td class="type">string</td><td class="desc"></td></tr>
7560
+ <tr><td class="name">coefficients</td><td class="type">RegressionCoefficient[]</td><td class="desc"></td></tr>
7561
+ <tr><td class="name">r2</td><td class="type">number</td><td class="desc"></td></tr>
7562
+ <tr><td class="name">adjR2</td><td class="type">number</td><td class="desc"></td></tr>
7563
+ <tr><td class="name">n</td><td class="type">number</td><td class="desc"></td></tr>
7564
+ <tr><td class="name">df</td><td class="type">number</td><td class="desc">Residual degrees of freedom, n − p.</td></tr>
7565
+ <tr><td class="name">sigma2</td><td class="type">number</td><td class="desc">Residual variance, RSS ÷ df.</td></tr>
7566
+ <tr><td class="name">fitted</td><td class="type">number[]</td><td class="desc"></td></tr>
7567
+ <tr><td class="name">residuals</td><td class="type">number[]</td><td class="desc"></td></tr>
7568
+ <tr><td class="name">leverage</td><td class="type">number[]</td><td class="desc">Hat-diagonal leverage per row.</td></tr>
7569
+ <tr><td class="name">cooksD</td><td class="type">(number | null)[]</td><td class="desc">Cook's distance per row; null where it cannot be computed.</td></tr>
7570
+ <tr><td class="name">vif</td><td class="type">number[]</td><td class="desc">Variance-inflation factor per predictor; Infinity when exactly collinear.</td></tr>
7571
+ <tr><td class="name">heteroscedasticity</td><td class="type">Heteroscedasticity | null</td><td class="desc"></td></tr>
7572
+ <tr><td class="name">band</td><td class="type">RegressionBand | null</td><td class="desc"></td></tr>
7573
+ <tr><td class="name">weights</td><td class="type">number[] | null</td><td class="desc">Per-row weights actually used (robust/WLS), or null for OLS.</td></tr>
7574
+ <tr><td class="name">predictors</td><td class="type">string[]</td><td class="desc"></td></tr>
7575
+ <tr><td class="name">response</td><td class="type">string</td><td class="desc"></td></tr>
7576
+ <tr><td class="name">rows</td><td class="type">number[]</td><td class="desc">The physical rows the diagnostics are aligned to, in order.</td></tr>
7577
+ </tbody>
7578
+ </table>
7579
+ </div>
7580
+ <h3 id="type-RegressionSpec">RegressionSpec</h3>
7581
+ <p class="section-note">The specification of a multi-predictor model (BACKLOG-0000792).</p>
7582
+ <div class="table-wrap">
7583
+ <table>
7584
+ <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7585
+ <tbody>
7586
+ <tr><td class="name">predictors</td><td class="type">string[]</td><td class="desc">The predictor column ids.</td></tr>
7587
+ <tr><td class="name">response</td><td class="type">string</td><td class="desc">The response column id.</td></tr>
7588
+ <tr><td class="name">method</td><td class="type">'ols' | 'wls' | 'robust' | 'quantile'</td><td class="desc">`ols` (default), `wls` or `robust`. `quantile` is reserved (coming next). <small>(optional)</small></td></tr>
7589
+ <tr><td class="name">weights</td><td class="type">string</td><td class="desc">A weights column id, required for `wls`. <small>(optional)</small></td></tr>
7590
+ <tr><td class="name">confidence</td><td class="type">number</td><td class="desc">The confidence level for the band; 0.95 by default. <small>(optional)</small></td></tr>
7591
+ </tbody>
7592
+ </table>
7593
+ </div>
7205
7594
  <h3 id="type-RejectedRow">RejectedRow</h3>
7206
7595
  <p class="section-note">A row a change could not apply, and why. Reported, never thrown.</p>
7207
7596
  <div class="table-wrap">
@@ -7288,7 +7677,7 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
7288
7677
  <tr><td class="name">edit</td><td class="type">ColumnEditSpec</td><td class="desc"></td></tr>
7289
7678
  <tr><td class="name">sort</td><td class="type">ColumnSortSpec</td><td class="desc"></td></tr>
7290
7679
  <tr><td class="name">filter</td><td class="type">ColumnFilterSpec</td><td class="desc"></td></tr>
7291
- <tr><td class="name">group</td><td class="type">{ enabled: boolean; index: number; explode: boolean }</td><td class="desc"></td></tr>
7680
+ <tr><td class="name">group</td><td class="type">{ enabled: boolean; index: number; explode: boolean; granularity?: 'day' | 'week' | 'month' | 'instant'; weekStart?: number }</td><td class="desc"></td></tr>
7292
7681
  <tr><td class="name">pivot</td><td class="type">{ enabled: boolean; index: number }</td><td class="desc"></td></tr>
7293
7682
  <tr><td class="name">total</td><td class="type">TotalName | TotalFn | null</td><td class="desc"></td></tr>
7294
7683
  <tr><td class="name">groupTotal</td><td class="type">TotalName | TotalFn | null</td><td class="desc">The group-subtotal override, or null when group subtotals follow `total` (BACKLOG-0000726).</td></tr>
@@ -7602,6 +7991,8 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
7602
7991
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
7603
7992
  <tbody>
7604
7993
  <tr><td class="name">shadow</td><td class="type">(colId: string, kind: ShadowKind, rowKey: string,</td><td class="desc">One shadow value for one row, by the column it shadows and the kind. For `kind: 'specStatus'`, `spec` carries the `{lower, upper, warnLower, warnUpper}` limits to judge the row's value against; other kinds ignore it.</td></tr>
7994
+ <tr><td class="name">fitShadow</td><td class="type">(kind: 'fitPredicted' | 'fitResidual' | 'fitInfluence'</td><td class="desc">One regression shadow value for a row, by key (BACKLOG-0000812): the predicted value, residual, or Cook's-distance influence flag from the fitted model, over the filtered rows. Null for a row outside the fit.</td></tr>
7995
+ <tr><td class="name">rowKey</td><td class="type">string, spec: RegressionSpec): number | boolean | null</td><td class="desc"></td></tr>
7605
7996
  <tr><td class="name">running</td><td class="type">(colId: string, kind: 'total' | 'percent', rowKey: string): number | null</td><td class="desc">A running total at one row, down the grid as it is currently ordered.</td></tr>
7606
7997
  <tr><td class="name">rebase</td><td class="type">(colId?: string): void</td><td class="desc">Make the current values the new baseline: "mark all".</td></tr>
7607
7998
  <tr><td class="name">tracking</td><td class="type">(): { columns: string[]; rows: number; forgotten: number }</td><td class="desc">What the shadow histories are costing.</td></tr>
@@ -7614,6 +8005,9 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
7614
8005
  <tr><td class="name">correlation</td><td class="type">(a: string, b: string): number | null</td><td class="desc">Pearson's correlation between two columns.</td></tr>
7615
8006
  <tr><td class="name">covariance</td><td class="type">(a: string, b: string, opts?: { population?: boolean }): number | null</td><td class="desc">Covariance, a correlation before the scales are divided out.</td></tr>
7616
8007
  <tr><td class="name">regression</td><td class="type">(a: string, b: string): RegressionFit | null</td><td class="desc">Least-squares fit of `b` on `a`: in finance, beta and alpha.</td></tr>
8008
+ <tr><td class="name">regressionModel</td><td class="type">(spec: RegressionSpec): RegressionModel | null</td><td class="desc">Fit a multi-predictor linear model over the filtered rows and return the full diagnostic set — coefficients with standard errors, t and p; R² and adjusted R²; per-row fitted values, residuals, leverage and Cook's D; VIF per predictor; a Breusch–Pagan heteroscedasticity flag; and, for a single predictor, a pointwise confidence band. `method` is `ols`, `wls` (needs a `weights` column) or `robust`; `quantile` is reserved and the regularised families refuse. Null on degenerate input (BACKLOG-0000792).</td></tr>
8009
+ <tr><td class="name">adf</td><td class="type">(spec: { of: string; orderBy: string; maxlag?: number }): AdfResult | null</td><td class="desc">The Augmented Dickey-Fuller stationarity test over the `of` series in `orderBy` order (BACKLOG-0000873), constant+trend form with the lag order chosen by AIC up to an optional cap. Returns the statistic, the lag used, MacKinnon's critical values, an approximate (interpolated) p-value and a plain-language verdict at the 5% level — a scalar readout, not a column.</td></tr>
8010
+ <tr><td class="name">acf</td><td class="type">(spec: { of: string; orderBy: string; maxlag?: number }): AcfResult | null</td><td class="desc">The autocorrelation (ACF) and partial autocorrelation (PACF) of the `of` series in `orderBy` order out to `maxlag` (BACKLOG-0000873), with the approximate ±1.96/√n band. A short-series readout; feed the arrays to a bar chart over explicit points with the band as reference lines. The lag-1 autocorrelation matches `series(...).autocorrelation`.</td></tr>
7617
8011
  <tr><td class="name">spearman</td><td class="type">(a: string, b: string): number | null</td><td class="desc">Spearman's rank correlation, which one outlier cannot drag.</td></tr>
7618
8012
  <tr><td class="name">kendall</td><td class="type">(a: string, b: string): number | null</td><td class="desc">Kendall's tau-b. Null past 5,000 rows: it is quadratic.</td></tr>
7619
8013
  <tr><td class="name">weightedQuantile</td><td class="type">(colId: string, weightId: string, p?: number): number | null</td><td class="desc">A quantile of one column weighted by another; the median by default.</td></tr>