@toclocoinc/lattice-grid 1.29.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/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.29.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
7
+ Version 1.30.0 · [latticegrid.dev](https://www.latticegrid.dev) · TOCLOCO Inc
8
8
 
9
9
  ---
10
10
 
package/docs/API.html CHANGED
@@ -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>
@@ -2043,6 +2063,115 @@ createGrid(el, {
2043
2063
  grid.rows.value('r3', 'med'), <span class="cmt">// median of 2,4,5 = 4</span>
2044
2064
  ].join('|');</code></pre>
2045
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
+
2046
2175
  <h2 id="highlight">grid.highlight</h2>
2047
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>
2048
2177
  <pre><code>createGrid(el, {
@@ -4897,6 +5026,39 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
4897
5026
  <!-- BEGIN GENERATED TYPE REFERENCE -->
4898
5027
  <h2 id="type-reference">Type reference</h2>
4899
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>
4900
5062
  <h3 id="type-AggregateProvenance">AggregateProvenance</h3>
4901
5063
  <p class="section-note">How one aggregate was routed, for `lastPlan()` provenance.</p>
4902
5064
  <div class="table-wrap">
@@ -4942,7 +5104,6 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
4942
5104
  </table>
4943
5105
  </div>
4944
5106
  <h3 id="type-AnnotationApi">AnnotationApi</h3>
4945
- <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>
4946
5107
  <div class="table-wrap">
4947
5108
  <table>
4948
5109
  <thead><tr><th>Member</th><th>Type</th><th>Description</th></tr></thead>
@@ -4950,12 +5111,26 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
4950
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>
4951
5112
  <tr><td class="name">count</td><td class="type">number</td><td class="desc"><small>(read-only)</small></td></tr>
4952
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>
4953
5116
  <tr><td class="name">undo</td><td class="type">(): number</td><td class="desc"></td></tr>
4954
5117
  <tr><td class="name">clear</td><td class="type">(): void</td><td class="desc"></td></tr>
4955
5118
  <tr><td class="name">redraw</td><td class="type">(): void</td><td class="desc"></td></tr>
4956
5119
  </tbody>
4957
5120
  </table>
4958
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>
4959
5134
  <h3 id="type-AnomalyReason">AnomalyReason</h3>
4960
5135
  <div class="table-wrap">
4961
5136
  <table>
@@ -6588,6 +6763,7 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
6588
6763
  <tr><td class="name">pivot</td><td class="type">{ enabled: boolean; columns: string[] }</td><td class="desc"><small>(optional)</small></td></tr>
6589
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>
6590
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>
6591
6767
  <tr><td class="name">expanded</td><td class="type">string[]</td><td class="desc"><small>(optional)</small></td></tr>
6592
6768
  <tr><td class="name">selection</td><td class="type">string[]</td><td class="desc"><small>(optional)</small></td></tr>
6593
6769
  <tr><td class="name">scroll</td><td class="type">{ top: number; left: number }</td><td class="desc"><small>(optional)</small></td></tr>
@@ -7830,6 +8006,8 @@ return `methods ${ANOMALY_METHODS.length}; flagged ${z.flagged}; upper ${fence.u
7830
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>
7831
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>
7832
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>
7833
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>
7834
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>
7835
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>
@@ -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.29.0</p>
440
+ <p class="rail__sub">Developer guide · v1.30.0</p>
441
441
  <nav>
442
442
  <div class="rail__group">
443
443
  <span class="rail__label">Start here</span>
package/lattice-grid.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Lattice Grid 1.29.0, type declarations
2
+ * Lattice Grid 1.30.0, type declarations
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -712,6 +712,36 @@ export interface Column {
712
712
  * value comes from a sketch and is stamped by a `windowApproximate` column.
713
713
  */
714
714
  q?: number;
715
+ /**
716
+ * For a seasonal-decomposition kind (`tsTrend`/`tsSeasonal`/`tsResidual`/
717
+ * `tsCoverage`, BACKLOG-0000873), the season length — **required**, since
718
+ * there is no auto-detection in v1: 7 for a weekly cycle in daily data, 12
719
+ * for a monthly cycle in monthly data. An integer of at least 2.
720
+ */
721
+ period?: number;
722
+ /**
723
+ * For a decomposition kind, the classical model: additive by default, or
724
+ * `multiplicative` (which is undefined on a non-positive series, so those
725
+ * rows report null and the caller is warned).
726
+ */
727
+ decomposition?: 'additive' | 'multiplicative';
728
+ /**
729
+ * For an exponential-smoothing kind (`tsSmoothed`/`tsSmoothingAlpha`/
730
+ * `tsSmoothingBeta`, BACKLOG-0000873), the model: single exponential
731
+ * smoothing (`ses`, the default) or Holt's level+trend (`holt`).
732
+ */
733
+ smoothing?: 'ses' | 'holt';
734
+ /**
735
+ * For a smoothing kind, the level factor in `[0, 1]`. Omit to fit it by
736
+ * minimising in-sample SSE; the chosen value is reported by a
737
+ * `tsSmoothingAlpha` column.
738
+ */
739
+ alpha?: number;
740
+ /**
741
+ * For `smoothing: 'holt'`, the trend factor in `[0, 1]`. Omit to fit it;
742
+ * reported by a `tsSmoothingBeta` column.
743
+ */
744
+ beta?: number;
715
745
  /**
716
746
  * For a `fit*` kind (BACKLOG-0000812), the regression model the shadow reads
717
747
  * — predictors, response, method and confidence. Its predictors/response may
@@ -2193,6 +2223,12 @@ export interface GridState {
2193
2223
  */
2194
2224
  pivotView?: { rowsCollapsed: string[]; columnsCollapsed: string[] };
2195
2225
  formatting?: Record<string, FormattingRule[]>;
2226
+ /**
2227
+ * Durable annotation marks (BACKLOG-0000813): seeded from here on first paint,
2228
+ * and written back by `getState` so a host can persist and restore them. In
2229
+ * content coordinates, so they track scroll and resize.
2230
+ */
2231
+ annotations?: AnnotationMark[];
2196
2232
  expanded?: string[];
2197
2233
  selection?: string[];
2198
2234
  scroll?: { top: number; left: number };
@@ -2555,6 +2591,22 @@ export interface StatisticsApi {
2555
2591
  * families refuse. Null on degenerate input (BACKLOG-0000792).
2556
2592
  */
2557
2593
  regressionModel(spec: RegressionSpec): RegressionModel | null;
2594
+ /**
2595
+ * The Augmented Dickey-Fuller stationarity test over the `of` series in
2596
+ * `orderBy` order (BACKLOG-0000873), constant+trend form with the lag order
2597
+ * chosen by AIC up to an optional cap. Returns the statistic, the lag used,
2598
+ * MacKinnon's critical values, an approximate (interpolated) p-value and a
2599
+ * plain-language verdict at the 5% level — a scalar readout, not a column.
2600
+ */
2601
+ adf(spec: { of: string; orderBy: string; maxlag?: number }): AdfResult | null;
2602
+ /**
2603
+ * The autocorrelation (ACF) and partial autocorrelation (PACF) of the `of`
2604
+ * series in `orderBy` order out to `maxlag` (BACKLOG-0000873), with the
2605
+ * approximate ±1.96/√n band. A short-series readout; feed the arrays to a bar
2606
+ * chart over explicit points with the band as reference lines. The lag-1
2607
+ * autocorrelation matches `series(...).autocorrelation`.
2608
+ */
2609
+ acf(spec: { of: string; orderBy: string; maxlag?: number }): AcfResult | null;
2558
2610
  /** Spearman's rank correlation, which one outlier cannot drag. */
2559
2611
  spearman(a: string, b: string): number | null;
2560
2612
  /** Kendall's tau-b. Null past 5,000 rows: it is quadratic. */
@@ -2803,6 +2855,26 @@ export type ShadowKind =
2803
2855
  * sketched quantile is never presented as exact.
2804
2856
  */
2805
2857
  | 'rollingQuantile' | 'windowApproximate'
2858
+ /**
2859
+ * Classical seasonal decomposition over a declared `period` (BACKLOG-0000873),
2860
+ * matching `statsmodels.seasonal_decompose`: `tsTrend` is the centred
2861
+ * moving-average trend, `tsSeasonal` the repeating seasonal index, `tsResidual`
2862
+ * what the two leave behind, and `tsCoverage` the stamp (1 for an interior row,
2863
+ * 0 for a partial edge where the centred window runs off the end, so an edge is
2864
+ * never emitted as full). Additive by default; `decomposition: 'multiplicative'`
2865
+ * is a declared option, undefined on a non-positive series.
2866
+ */
2867
+ | 'tsTrend' | 'tsSeasonal' | 'tsResidual' | 'tsCoverage'
2868
+ /**
2869
+ * Exponential smoothing over the `orderBy` series (BACKLOG-0000873):
2870
+ * `tsSmoothed` is the fitted level from single exponential smoothing (`ses`) or
2871
+ * Holt's level+trend (`holt`) — the signal with the noise removed, not a
2872
+ * forecast. The smoothing factor(s) are caller-set or fit by minimising
2873
+ * in-sample SSE, and reported by the `tsSmoothingAlpha` / `tsSmoothingBeta`
2874
+ * companion columns. Holt-Winters (seasonal) smoothing is deferred; seasonality
2875
+ * is covered by decomposition.
2876
+ */
2877
+ | 'tsSmoothed' | 'tsSmoothingAlpha' | 'tsSmoothingBeta'
2806
2878
  /**
2807
2879
  * Model-backed regression shadows (BACKLOG-0000812): the predicted value, the
2808
2880
  * residual, and a Cook's-distance influence flag for the row, read from the
@@ -2886,6 +2958,44 @@ export interface Heteroscedasticity {
2886
2958
  heteroscedastic: boolean;
2887
2959
  }
2888
2960
 
2961
+ /** The Augmented Dickey-Fuller stationarity test result (BACKLOG-0000873). */
2962
+ export interface AdfResult {
2963
+ /** The ADF t-statistic on the lagged level. */
2964
+ statistic: number;
2965
+ /** The number of augmenting lags chosen by AIC. */
2966
+ usedLag: number;
2967
+ /** The observations the final regression used. */
2968
+ nobs: number;
2969
+ /** MacKinnon's constant+trend critical values at the 1%, 5% and 10% levels. */
2970
+ criticalValues: { '1%': number; '5%': number; '10%': number };
2971
+ /** An approximate p-value, interpolated across the critical-value ladder. */
2972
+ pValue: number;
2973
+ /** Always true: the p-value is an interpolation, not the MacKinnon surface. */
2974
+ pApproximate: boolean;
2975
+ /** Whether the series is stationary at the 5% level. */
2976
+ stationary: boolean;
2977
+ /** The plain-language verdict: `'stationary'` or `'non-stationary'`. */
2978
+ verdict: string;
2979
+ /** The regression form used — always `'ct'` (constant + trend) in v1. */
2980
+ regression: 'ct';
2981
+ }
2982
+
2983
+ /** Autocorrelation (ACF) and partial autocorrelation (PACF) arrays (BACKLOG-0000873). */
2984
+ export interface AcfResult {
2985
+ /** The autocorrelation at each lag; index 0 is lag 0 and is always 1. */
2986
+ acf: number[];
2987
+ /** The partial autocorrelation at each lag; index 0 is 1, and `pacf[1] === acf[1]`. */
2988
+ pacf: number[];
2989
+ /** The approximate ±1.96/√n white-noise confidence band. */
2990
+ bounds: { upper: number; lower: number };
2991
+ /** The series length the ACF/PACF were computed over. */
2992
+ n: number;
2993
+ /** The maximum lag. */
2994
+ nlags: number;
2995
+ /** Always true: the ±1.96/√n band is an approximation. */
2996
+ approximate: boolean;
2997
+ }
2998
+
2889
2999
  /** A fitted multi-predictor linear model and its diagnostics (BACKLOG-0000792). */
2890
3000
  export interface RegressionModel {
2891
3001
  method: string;
@@ -3822,10 +3932,35 @@ export interface CaptureOptions {
3822
3932
  * they stay with the cells they annotate when the grid scrolls, and are
3823
3933
  * cleared when a presentation ends.
3824
3934
  */
3935
+ /**
3936
+ * A durable annotation mark descriptor (BACKLOG-0000813) — the shape a host
3937
+ * seeds through `state.annotations`, adds through {@link AnnotationApi.add}, and
3938
+ * reads back through {@link AnnotationApi.list} and `getState`.
3939
+ *
3940
+ * `points` are in **content coordinates** (the same space user-drawn marks are
3941
+ * stored in), so a mark tracks scroll and resize rather than hanging over the
3942
+ * viewport. A `freehand` mark is a trail of points; `arrow` and `rect` are their
3943
+ * two endpoints. Text marks are a deliberate follow-up. `pen` is accepted as an
3944
+ * alias for `freehand` on input; `list()` reports `freehand`.
3945
+ */
3946
+ export interface AnnotationMark {
3947
+ type: 'freehand' | 'arrow' | 'rect' | 'highlight';
3948
+ points: { x: number; y: number }[];
3949
+ colour?: string;
3950
+ }
3951
+
3825
3952
  export interface AnnotationApi {
3826
3953
  readonly tool: 'pen' | 'arrow' | 'rect' | 'highlight' | null;
3827
3954
  readonly count: number;
3828
3955
  use(tool: 'pen' | 'arrow' | 'rect' | 'highlight' | null, opts?: { colour?: string }): string | null;
3956
+ /**
3957
+ * Add a durable mark from a descriptor, without synthesising pointer input
3958
+ * (BACKLOG-0000813). The mark is painted, survives a presentation ending, and
3959
+ * round-trips through `getState`. Returns the mark count.
3960
+ */
3961
+ add(mark: AnnotationMark): number;
3962
+ /** Every mark on the layer, as descriptors — the shape `getState` persists. */
3963
+ list(): AnnotationMark[];
3829
3964
  undo(): number;
3830
3965
  clear(): void;
3831
3966
  redraw(): void;