@toclocoinc/lattice-grid 1.7.1 → 1.8.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
@@ -397,6 +397,7 @@
397
397
  <a href="#presentation">grid.presentation</a>
398
398
  <a href="#redaction">grid.redaction</a>
399
399
  <a href="#formatting">grid.formatting</a>
400
+ <a href="#statistics">grid.statistics</a>
400
401
  <a href="#highlight">grid.highlight</a>
401
402
  <a href="#views">grid.views</a>
402
403
  <a href="#diff">grid.diff</a>
@@ -793,7 +794,7 @@ autoInit(document); <span class="cmt">// builds every [data-lattice-grid] under
793
794
  <tbody>
794
795
  <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>
795
796
  <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>
796
- <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>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.</td></tr>
797
+ <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.</td></tr>
797
798
  <tr><td class="name">contextMenu</td><td class="type">boolean | (p) =&gt; MenuItem[]</td><td class="desc">Right-click menu. The function form is <code>(params, defaults) =&gt; items</code> — see <a href="#custom-menu">custom items</a>. <code>false</code> suppresses it — what a read-only grid wants, since the default menu offers Paste, Clear and Fill down.</td></tr>
798
799
  <tr><td class="name">columnMenu</td><td class="type">boolean | (p) =&gt; MenuItem[]</td><td class="desc">The header's 3-dot menu, and a right-click on a column heading. The function form is <code>(params, defaults) =&gt; items</code>, with <code>params</code> carrying <code>colId</code>, <code>column</code> and <code>grid</code> — see <a href="#custom-menu">custom items</a>. <code>false</code> suppresses it.</td></tr>
799
800
  <tr><td class="name">shortcuts</td><td class="type">boolean</td><td class="dflt">true</td><td class="desc">The <kbd>?</kbd> keyboard shortcut overlay. <code>false</code> suppresses it, for a host that wants <kbd>?</kbd> for itself. See <a href="api-detail.html#keyboard">Keyboard</a>.</td></tr>
@@ -1563,6 +1564,89 @@ grid.formatting.clear('margin'); // or clear() for everything</
1563
1564
  </div>
1564
1565
  <div class="note"><p>A rule held here must be JSON: <code>style</code> may not be a function, because the rules are serialised into views and undo slices. Config-time <code>cell.style</code> still accepts one. Group rows are not formatted, matching the way decoration is dropped for them.</p></div>
1565
1566
 
1567
+ <h3>Rules that describe the data, not a threshold</h3>
1568
+ <p><code>gt: 100</code> needs somebody to know that 100 is the interesting number. Often nobody does — the interesting cells are <em>the top decile</em>, or <em>the outliers</em>, and where those fall is a property of the data rather than of the rule. These operators say that directly, and the grid works out the threshold from the column itself, over the filtered rows.</p>
1569
+ <pre><code>grid.formatting.add('margin', { when: { op: 'outlier' }, style: { background: '#fbeceb' } });
1570
+ grid.formatting.add('qty', { when: { op: 'topPercent', value: 10 }, style: { bold: true } });
1571
+ grid.formatting.add('score', { scale: { from: 'quantile', colours: ['#f8f9fa', '#1a6bc7'] } });
1572
+
1573
+ grid.formatting.distribution('margin'); // { n, min, max, mean, stddev, median, q1, q3, iqr }
1574
+ grid.formatting.restat(); // re-derive every threshold from the data as it stands</code></pre>
1575
+ <div class="table-wrap">
1576
+ <table>
1577
+ <thead><tr><th>Operator</th><th><code>value</code></th><th>Marks</th></tr></thead>
1578
+ <tbody>
1579
+ <tr><td class="sig">topPercent</td><td class="type">10 or 0.1</td><td class="desc">The top tenth of the column. Written either way; both mean the same thing.</td></tr>
1580
+ <tr><td class="sig">bottomPercent</td><td class="type">10 or 0.1</td><td class="desc">The bottom tenth.</td></tr>
1581
+ <tr><td class="sig">topN</td><td class="type">5</td><td class="desc">The five largest, ties included — three rows sharing second place in a top three all take the colour.</td></tr>
1582
+ <tr><td class="sig">bottomN</td><td class="type">5</td><td class="desc">The five smallest.</td></tr>
1583
+ <tr><td class="sig">aboveMean / belowMean</td><td class="type">—</td><td class="desc">Either side of the mean.</td></tr>
1584
+ <tr><td class="sig">aboveMedian / belowMedian</td><td class="type">—</td><td class="desc">Either side of the median, which is the one to reach for on a skewed column.</td></tr>
1585
+ <tr><td class="sig">zAbove / zBelow</td><td class="type">2</td><td class="desc">That many standard deviations from the mean. A column with no spread marks nothing rather than everything.</td></tr>
1586
+ <tr><td class="sig">outlier</td><td class="type">1.5</td><td class="desc">Outside Tukey's fences at that many IQRs — the same definition a box plot draws, so the marked cells are the ones its whiskers exclude.</td></tr>
1587
+ </tbody>
1588
+ </table>
1589
+ </div>
1590
+ <p>A colour scale can take its bounds the same way, with <code>from</code> in place of <code>min</code> and <code>max</code>: <code>'minmax'</code> spans the data, <code>'quantile'</code> spans <code>low</code> to <code>high</code> (5th to 95th percentile by default), <code>'stddev'</code> spans <code>deviations</code> either side of the mean. The quantile form is the better default on real data — one mistyped order of magnitude otherwise compresses every real value into the first swatch.</p>
1591
+ <div class="note"><p>Thresholds are <strong>pinned</strong> when the rules compile and do not move on their own. That is deliberate: a boundary that re-derived itself as rows were filtered would repaint cells whose values had not changed, and nobody comparing two screenshots could tell which of the two things had moved. <code>grid.formatting.restat()</code> is how you move it, and a "recalculate" control is the natural place to put it.</p></div>
1592
+
1593
+ <h2 id="statistics">grid.statistics</h2>
1594
+ <p>What the grid knows about its own numbers, and about how they have changed since the page loaded. Every figure is computed over the <em>filtered</em> rows, through the same column handles the totals row uses — so a median here and a median in the footer are the same number, by the same definition (R type 7).</p>
1595
+ <pre><code>grid.statistics.profile('margin');
1596
+ // { column, rows, present, missing, distinct, min, max, mean, median,
1597
+ // q1, q3, iqr, stddev, outliers, histogram: [{ from, to, count }, …] }
1598
+
1599
+ grid.statistics.reduce('margin', 'p95'); // any registered kernel
1600
+ grid.statistics.correlation('spend', 'revenue'); // Pearson's r, clamped to [-1, 1]
1601
+ grid.statistics.weightedAverage('price', 'qty');
1602
+
1603
+ grid.statistics.shadow('price', 'delta', 'R42'); // one row's shadow value
1604
+ grid.statistics.rebase('price'); // "mark all" — today's values become the baseline
1605
+ grid.statistics.tracking(); // { columns, rows, forgotten }</code></pre>
1606
+ <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>
1607
+
1608
+ <h3>Shadow columns</h3>
1609
+ <p>A shadow column is declared against another column and maintained by the grid. It has no field in the data and it is not a pure computed column either, because its value depends on what happened <em>before</em>. It is a real column throughout: sortable, filterable, totalled, grouped, exported, saved into a view — which is what makes "show me every circuit repriced more than twice this session, most-changed first" one gesture rather than a report.</p>
1610
+ <pre><code>columns: [
1611
+ { field: 'price', type: 'number' },
1612
+ { id: 'moved', title: 'Change', shadow: { of: 'price', kind: 'delta' } },
1613
+ { id: 'churn', title: 'Updates', shadow: { of: 'price', kind: 'updates' } },
1614
+ { id: 'run', title: 'Streak', shadow: 'streak' }, // shorthand: shadows the column beside it
1615
+ ]</code></pre>
1616
+ <div class="table-wrap">
1617
+ <table>
1618
+ <thead><tr><th>Kind</th><th>Value</th></tr></thead>
1619
+ <tbody>
1620
+ <tr><td class="sig">updates</td><td class="desc">How many times the row's value has changed. Arrival is not a change, so a freshly loaded grid reads zero rather than one.</td></tr>
1621
+ <tr><td class="sig">updatedAt</td><td class="desc">When it last changed, as a <code>Date</code>.</td></tr>
1622
+ <tr><td class="sig">sinceUpdate</td><td class="desc">Milliseconds since it last changed.</td></tr>
1623
+ <tr><td class="sig">delta</td><td class="desc">Current value minus the baseline.</td></tr>
1624
+ <tr><td class="sig">deltaPercent</td><td class="desc">The same as a percentage. A change from nothing has no percentage and reads null rather than infinity.</td></tr>
1625
+ <tr><td class="sig">rate</td><td class="desc">Change per second, from the last two readings.</td></tr>
1626
+ <tr><td class="sig">history</td><td class="desc">The recent readings, oldest first. <code>depth</code> sets how many; the default is 20.</td></tr>
1627
+ <tr><td class="sig">firstValue</td><td class="desc">The baseline itself.</td></tr>
1628
+ <tr><td class="sig">streak</td><td class="desc">Consecutive moves in one direction, signed. It resets on a turn, because "seven rises" means something and "seven changes" does not.</td></tr>
1629
+ </tbody>
1630
+ </table>
1631
+ </div>
1632
+ <p>A second family answers where the row sits among the others rather than what it did before. They share the same declaration and the same state — the tracker already holds every row's current value <em>and</em> its baseline, which is exactly what a rank and a rank <em>change</em> need.</p>
1633
+ <div class="table-wrap">
1634
+ <table>
1635
+ <thead><tr><th>Kind</th><th>Value</th></tr></thead>
1636
+ <tbody>
1637
+ <tr><td class="sig">rank</td><td class="desc">Competition rank, largest first: ties share the better rank and the next value skips, so two firsts are followed by a third.</td></tr>
1638
+ <tr><td class="sig">rankAsc</td><td class="desc">The same ranking read from the other end.</td></tr>
1639
+ <tr><td class="sig">rankChange</td><td class="desc">Places climbed since the baseline. Positive means climbed, even though the rank number itself falls — this is the "top movers" column.</td></tr>
1640
+ <tr><td class="sig">percentile</td><td class="desc">The share of rows at or below this one, 0 to 100.</td></tr>
1641
+ <tr><td class="sig">quartile</td><td class="desc">1 to 4, agreeing with <code>percentile</code>: the 60th percentile is in the third quartile.</td></tr>
1642
+ <tr><td class="sig">zScore</td><td class="desc">Deviations from the mean. A column with no spread reads null rather than zero.</td></tr>
1643
+ <tr><td class="sig">shareOfTotal</td><td class="desc">The value over the column's total, as a percentage. A total of zero — a column of offsetting positions — reads null rather than a division by it.</td></tr>
1644
+ </tbody>
1645
+ </table>
1646
+ </div>
1647
+ <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. For the filtered answer, use <code>grid.statistics.profile()</code> or a distribution formatting rule.</p></div>
1648
+ <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>
1649
+
1566
1650
  <h2 id="highlight">grid.highlight</h2>
1567
1651
  <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>
1568
1652
  <pre><code>createGrid(el, {
@@ -2110,10 +2194,13 @@ grid.filters.quickState(); <span class="cmt">// { text: 'acme london', mode: '
2110
2194
  <tr><td class="sig">Rounding</td><td><code>ROUND</code>, <code>ROUNDUP</code>, <code>ROUNDDOWN</code>, <code>FLOOR</code>, <code>CEILING</code></td></tr>
2111
2195
  <tr><td class="sig">Logic</td><td><code>IF</code>, <code>AND</code>, <code>OR</code>, <code>NOT</code>, <code>COALESCE</code></td></tr>
2112
2196
  <tr><td class="sig">Text</td><td><code>CONCAT</code>, <code>LEN</code>, <code>UPPER</code>, <code>LOWER</code>, <code>TRIM</code>, <code>LEFT</code>, <code>RIGHT</code></td></tr>
2197
+ <tr><td class="sig">Statistics</td><td><code>MEDIAN</code>, <code>PERCENTILE</code>, <code>QUARTILE1</code>, <code>QUARTILE3</code>, <code>IQR</code>, <code>STDEV</code>, <code>STDEVP</code>, <code>VAR</code>, <code>VARP</code>, <code>COUNTDISTINCT</code></td></tr>
2113
2198
  </tbody>
2114
2199
  </table>
2115
2200
  </div>
2116
2201
 
2202
+ <div class="note"><p>The statistical functions use R type 7 quantiles — the same definition as the totals row, <code>grid.statistics</code> and the distribution formatting rules, so the four never disagree about what a median is. <code>PERCENTILE</code> reads <code>90</code> and <code>0.9</code> as the same request. Over an empty set they return a number rather than null, because a formula is arithmetic and has to keep composing.</p></div>
2203
+
2117
2204
  <p>Operators <code>+ - * / ^</code> with parentheses, comparison for <code>IF</code>, and postfix
2118
2205
  <code>%</code>. <code>^</code> is right-associative and unary minus binds tighter than it, so
2119
2206
  <code>-2^2</code> is 4 — Excel's answer rather than mathematics'.</p>
@@ -1652,9 +1652,10 @@ grid.set('theme', <span class="kw">null</span>); <span class="cmt">// back to
1652
1652
  <div class="why">
1653
1653
  <p><strong>Why this is needed at all.</strong> A grid is mounted inside somebody else's
1654
1654
  stylesheet. A rule as ordinary as <code>section { padding: 5.5rem 0 }</code> — a marketing
1655
- page, a CMS theme, a Tailwind preflight — used to reach inside and put 88px of padding on every
1656
- filter row in the tool panel, because the grid builds those rows as <code>&lt;section&gt;</code>
1657
- elements and its own rules said nothing about padding there.</p>
1655
+ page, a CMS theme, a Tailwind preflight — matches by tag name, and the grid builds parts of
1656
+ its own interface from those tags: the tool panel's filter rows are
1657
+ <code>&lt;section&gt;</code> elements. Without the reset, 5.5rem of somebody else's padding
1658
+ lands on every one of them.</p>
1658
1659
  <p>The reset uses no <code>!important</code>. It is specificity (0,1,1) and every rule that
1659
1660
  dresses a grid element is (0,2,0) or higher, so the grid's own styling always wins and the
1660
1661
  reset only fills a gap. Yours wins too, on the same terms: a rule aimed at a Lattice class —
@@ -4440,10 +4441,11 @@ r.ok ? r.value : r.error; <span class="cmt">// 42</span></code></pre>
4440
4441
  </p>
4441
4442
  <div class="why">
4442
4443
  <p><strong>Bare arithmetic is deliberately not a formula.</strong> <code>2-1</code> is a
4443
- plausible product code and <code>1/2</code> a plausible date. Evaluating either on a guess would
4444
- repeat the bug this replaced, where the number reader stripped the operator and stored
4445
- <code>2*3</code> as <strong>23</strong>. Arithmetic without a leading <code>=</code> is now
4446
- refused outright, so the cell keeps what it had rather than taking a plausible wrong number.</p>
4444
+ plausible product code and <code>1/2</code> a plausible date. A reader that evaluated either on
4445
+ a guess would have to guess wrong sometimes, and the wrong answer is not a visible error but a
4446
+ plausible number — <code>2*3</code> stored as <strong>23</strong> looks like data. Arithmetic
4447
+ without a leading <code>=</code> is refused outright, so the cell keeps what it had rather than
4448
+ taking a number nobody typed.</p>
4447
4449
  </div>
4448
4450
 
4449
4451
  <h2 id="custom-menu">Your own menu items and buttons</h2>
package/lattice-grid.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Lattice Grid 1.7.1 — type declarations
2
+ * Lattice Grid 1.8.0 — type declarations
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -1389,15 +1389,31 @@ export interface StateApplyReport {
1389
1389
  // ---------------------------------------------------------------------------
1390
1390
 
1391
1391
  export interface FormattingCondition {
1392
- op: Operator;
1392
+ /**
1393
+ * A filter operator compared against `value`, or a distribution operator
1394
+ * whose threshold comes from the column itself — `{op: 'topPercent', value: 10}`,
1395
+ * `{op: 'outlier'}`. Distribution thresholds are pinned when the rules
1396
+ * compile; `grid.formatting.restat()` moves them.
1397
+ */
1398
+ op: Operator | DistributionOp;
1393
1399
  value?: unknown;
1394
1400
  value2?: unknown;
1395
1401
  }
1396
1402
 
1397
1403
  export interface FormattingScale {
1398
- min: number;
1399
- max: number;
1404
+ /**
1405
+ * Where the bounds come from when `min` and `max` are not given.
1406
+ * `'minmax'` spans the data, `'quantile'` spans `low` to `high`
1407
+ * (5th to 95th percentile by default), `'stddev'` spans `deviations`
1408
+ * either side of the mean.
1409
+ */
1410
+ from?: 'minmax' | 'quantile' | 'stddev';
1411
+ min?: number;
1412
+ max?: number;
1400
1413
  mid?: number;
1414
+ low?: number;
1415
+ high?: number;
1416
+ deviations?: number;
1401
1417
  colours?: string[];
1402
1418
  }
1403
1419
 
@@ -1421,6 +1437,58 @@ export interface FormattingRule {
1421
1437
  /** A column id, or `'*'` for every column. */
1422
1438
  export type FormattingScope = string;
1423
1439
 
1440
+ export interface StatisticsApi {
1441
+ /** One shadow value for one row, by the column it shadows and the kind. */
1442
+ shadow(colId: string, kind: ShadowKind, rowKey: string): unknown;
1443
+ /** Make the current values the new baseline — "mark all". */
1444
+ rebase(colId?: string): void;
1445
+ /** What the shadow histories are costing. */
1446
+ tracking(): { columns: string[]; rows: number; forgotten: number };
1447
+ /** Reduce a column by a named kernel over the filtered rows. */
1448
+ reduce(colId: string, fn: string): unknown;
1449
+ /** Everything worth knowing about one column, in one pass each. */
1450
+ profile(colId: string): ColumnProfile | null;
1451
+ /** Pearson's correlation between two columns. */
1452
+ correlation(a: string, b: string): number | null;
1453
+ /** A weighted average of one column by another. */
1454
+ weightedAverage(colId: string, weightId: string): number | null;
1455
+ /** The key a row's data resolves to. */
1456
+ keyOf(data: unknown): string | null;
1457
+ /** Which reductions can be maintained against a change, and which rescan. */
1458
+ readonly maintenance: Readonly<Record<string, 'maintained' | 'rescan'>>;
1459
+ }
1460
+
1461
+ export type ShadowKind =
1462
+ | 'updates' | 'updatedAt' | 'sinceUpdate' | 'delta' | 'deltaPercent'
1463
+ | 'rate' | 'history' | 'firstValue' | 'streak'
1464
+ /** Where the row sits among the others, over every tracked row. */
1465
+ | 'rank' | 'rankAsc' | 'rankChange' | 'percentile' | 'quartile'
1466
+ | 'zScore' | 'shareOfTotal';
1467
+
1468
+ export interface ColumnProfile {
1469
+ column: string;
1470
+ rows: number;
1471
+ present: number;
1472
+ missing: number;
1473
+ distinct: number;
1474
+ min: number | null;
1475
+ max: number | null;
1476
+ mean: number | null;
1477
+ median: number | null;
1478
+ q1: number | null;
1479
+ q3: number | null;
1480
+ iqr: number | null;
1481
+ stddev: number | null;
1482
+ outliers: number;
1483
+ histogram: HistogramBin[];
1484
+ }
1485
+
1486
+ export interface HistogramBin {
1487
+ from: number;
1488
+ to: number;
1489
+ count: number;
1490
+ }
1491
+
1424
1492
  export interface FormattingApi {
1425
1493
  list(scope?: FormattingScope): FormattingRule[];
1426
1494
  all(): Record<FormattingScope, FormattingRule[]>;
@@ -1433,6 +1501,29 @@ export interface FormattingApi {
1433
1501
  replaceAll(rules: Record<FormattingScope, FormattingRule[]>): void;
1434
1502
  clear(scope?: FormattingScope): void;
1435
1503
  styleFor(colId: string, value: unknown): CellStyle | null;
1504
+ /** Re-derive the thresholds of distribution rules from the data as it stands. */
1505
+ restat(): void;
1506
+ /** The five numbers a distribution rule resolves against for one column. */
1507
+ distribution(colId: string): ColumnDistribution | null;
1508
+ }
1509
+
1510
+ /** Operators that resolve against the column's own distribution (spec 8.12). */
1511
+ export type DistributionOp =
1512
+ | 'topPercent' | 'bottomPercent' | 'topN' | 'bottomN'
1513
+ | 'aboveMean' | 'belowMean' | 'aboveMedian' | 'belowMedian'
1514
+ | 'zAbove' | 'zBelow' | 'outlier';
1515
+
1516
+ export interface ColumnDistribution {
1517
+ n: number;
1518
+ min: number;
1519
+ max: number;
1520
+ mean: number;
1521
+ stddev: number;
1522
+ median: number;
1523
+ q1: number;
1524
+ q3: number;
1525
+ iqr: number;
1526
+ sorted: number[];
1436
1527
  }
1437
1528
 
1438
1529
  // ---------------------------------------------------------------------------
@@ -2430,6 +2521,7 @@ export interface Grid {
2430
2521
  readonly comments: CommentsApi;
2431
2522
  readonly presence: PresenceApi;
2432
2523
  readonly diagnostics: DiagnosticsApi;
2524
+ readonly statistics: StatisticsApi;
2433
2525
  readonly formatting: FormattingApi;
2434
2526
  readonly maximise?: MaximiseApi;
2435
2527
  /**