@peter.prib/renderengine 0.8.0 → 0.10.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.
@@ -3668,153 +3668,155 @@
3668
3668
  }
3669
3669
  }
3670
3670
 
3671
- /**
3672
- * PopupMenu — the one popup/context menu: a small menu at a screen position,
3673
- * closed by choosing an item, clicking elsewhere, Escape, or scrolling (it
3674
- * would no longer be beside what it was opened on). One is open at a time.
3675
- * Editor menus, the graph engine's ContextMenu and the model tree's menus all
3676
- * render through it.
3677
- *
3678
- * Items: `{ label, action, danger?, checked?, disabled? }`,
3679
- * `{ label, items: [...] }` for a submenu, or `{ separator: true }`.
3680
- */
3681
- class PopupMenu {
3682
- static _open = null;
3683
-
3684
- static show(x, y, items, doc = document) {
3685
- PopupMenu.close();
3686
- if (!items.length) return null;
3687
- const menu = el({
3688
- ...PopupMenu._panel(items, doc),
3689
- style: { ...PopupMenu._panelStyle, position: 'fixed', left: `${x}px`, top: `${y}px` },
3690
- }, doc);
3691
- doc.body.appendChild(menu);
3692
- // Opened near the window's right or bottom edge, it moves back into the window.
3693
- const win = doc.defaultView;
3694
- const box = menu.getBoundingClientRect?.();
3695
- if (win && box?.width) {
3696
- const at = PopupMenu.fit(x, y, box.width, box.height, win.innerWidth, win.innerHeight);
3697
- Object.assign(menu.style, { left: `${at.x}px`, top: `${at.y}px` });
3698
- }
3699
- // Registered after the opening click has finished propagating.
3700
- const dismiss = () => PopupMenu.close();
3701
- const onKey = (e) => { if (e.key === 'Escape') PopupMenu.close(); };
3702
- setTimeout(() => {
3703
- if (PopupMenu._open?.menu !== menu) return; // closed meanwhile
3704
- doc.addEventListener('click', dismiss, { once: true });
3705
- doc.addEventListener('scroll', dismiss, { once: true, capture: true });
3706
- doc.addEventListener('keydown', onKey);
3707
- }, 0);
3708
- PopupMenu._open = { menu, dismiss, onKey, doc };
3709
- return menu;
3710
- }
3711
-
3712
- /**
3713
- * Where a menu of a size goes, asked for at (x, y), to be all inside a
3714
- * window of a size: left of the point when it would pass the right edge,
3715
- * above it when it would pass the bottom, never past the top-left.
3716
- */
3717
- static fit(x, y, width, height, windowWidth, windowHeight, margin = 4) {
3718
- const left = x + width + margin > windowWidth ? x - width : x;
3719
- const top = y + height + margin > windowHeight ? y - height : y;
3720
- return {
3721
- x: Math.max(margin, Math.min(left, windowWidth - width - margin)),
3722
- y: Math.max(margin, Math.min(top, windowHeight - height - margin)),
3723
- };
3724
- }
3725
-
3726
- /**
3727
- * Where a submenu opens beside its item (the item's box `{left, right, top}`),
3728
- * to be all inside the window: to the right, or to the left when the right
3729
- * would pass the edge (and there is more room on the left); level with the
3730
- * item, or lifted by as much as it would pass the bottom. `top` is relative
3731
- * to the item, as the submenu is positioned.
3732
- */
3733
- static submenuPlacement(item, width, height, windowWidth, windowHeight, margin = 4) {
3734
- const fitsRight = item.right + width + margin <= windowWidth;
3735
- const side = fitsRight || item.left < windowWidth - item.right ? 'right' : 'left';
3736
- const top = -4;
3737
- const overflow = item.top + top + height + margin - windowHeight;
3738
- return { side, top: overflow > 0 ? Math.max(top - overflow, margin - item.top) : top };
3739
- }
3740
-
3741
- static close() {
3742
- const open = PopupMenu._open;
3743
- if (!open) return;
3744
- open.menu.remove();
3745
- open.doc.removeEventListener('click', open.dismiss);
3746
- open.doc.removeEventListener('scroll', open.dismiss, { capture: true });
3747
- open.doc.removeEventListener('keydown', open.onKey);
3748
- PopupMenu._open = null;
3749
- }
3750
-
3751
- static get isOpen() {
3752
- return !!PopupMenu._open;
3753
- }
3754
-
3755
- static _panelStyle = {
3756
- background: '#fff', border: '1px solid #ccc', borderRadius: '3px', boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
3757
- zIndex: '10000', minWidth: '170px', fontSize: '12px', fontFamily: 'sans-serif', padding: '4px 0',
3758
- };
3759
-
3760
- static _panel(items, doc) {
3761
- return { attrs: { role: 'menu' }, children: items.map(item => PopupMenu._item(item, doc)) };
3762
- }
3763
-
3764
- static _item(item, doc) {
3765
- if (item.separator) return { attrs: { role: 'separator' }, style: { borderTop: '1px solid #ddd', margin: '4px 0' } };
3766
- const hover = item.danger ? '#ffe8e8' : '#f0f0f0';
3767
- const base = {
3768
- attrs: { role: 'menuitem', 'aria-disabled': item.disabled ? 'true' : null, 'aria-checked': item.checked != null ? String(!!item.checked) : null },
3769
- style: {
3770
- padding: '7px 14px', cursor: item.disabled ? 'default' : 'pointer', position: 'relative', whiteSpace: 'nowrap',
3771
- color: item.disabled ? '#aaa' : item.danger ? '#cc3333' : '', background: item.checked ? '#e3f2fd' : '',
3772
- },
3773
- };
3774
- if (item.items) {
3775
- let sub;
3776
- return {
3777
- ...base,
3778
- attrs: { ...base.attrs, 'aria-haspopup': 'menu' },
3779
- on: {
3780
- mouseenter: (e) => {
3781
- e.currentTarget.style.background = hover;
3782
- sub.style.display = 'block';
3783
- // Opened near the window's right or bottom edge, it opens left, or lifts.
3784
- const win = e.currentTarget.ownerDocument.defaultView;
3785
- const box = sub.getBoundingClientRect?.();
3786
- if (!win || !box?.width) return;
3787
- const at = PopupMenu.submenuPlacement(e.currentTarget.getBoundingClientRect(), box.width, box.height, win.innerWidth, win.innerHeight);
3788
- Object.assign(sub.style, at.side === 'left' ? { left: 'auto', right: '100%' } : { left: '100%', right: 'auto' }, { top: `${at.top}px` });
3789
- },
3790
- mouseleave: (e) => { e.currentTarget.style.background = ''; sub.style.display = 'none'; },
3791
- click: (e) => e.stopPropagation(),
3792
- },
3793
- children: [
3794
- `${item.label} ▸`,
3795
- {
3796
- ...PopupMenu._panel(item.items, doc),
3797
- style: { ...PopupMenu._panelStyle, position: 'absolute', left: '100%', top: '-4px', display: 'none' },
3798
- ref: (n) => { sub = n; },
3799
- },
3800
- ],
3801
- };
3802
- }
3803
- return {
3804
- ...base,
3805
- text: item.label,
3806
- on: {
3807
- mouseenter: (e) => { if (!item.disabled) e.currentTarget.style.background = hover; },
3808
- mouseleave: (e) => { e.currentTarget.style.background = item.checked ? '#e3f2fd' : ''; },
3809
- click: (e) => {
3810
- e.stopPropagation();
3811
- if (item.disabled) return;
3812
- PopupMenu.close();
3813
- item.action();
3814
- },
3815
- },
3816
- };
3817
- }
3671
+ /**
3672
+ * PopupMenu — the one popup/context menu: a small menu at a screen position,
3673
+ * closed by choosing an item, clicking elsewhere, Escape, or scrolling (it
3674
+ * would no longer be beside what it was opened on). One is open at a time.
3675
+ * Editor menus, the graph engine's ContextMenu and the model tree's menus all
3676
+ * render through it.
3677
+ *
3678
+ * Items: `{ label, action, danger?, checked?, disabled? }`,
3679
+ * `{ label, items: [...] }` for a submenu, or `{ separator: true }`.
3680
+ */
3681
+ class PopupMenu {
3682
+ static _open = null;
3683
+
3684
+ static show(x, y, items, doc = document) {
3685
+ PopupMenu.close();
3686
+ if (!items.length) return null;
3687
+ const menu = el({
3688
+ ...PopupMenu._panel(items, doc),
3689
+ style: { ...PopupMenu._panelStyle, position: 'fixed', left: `${x}px`, top: `${y}px` },
3690
+ }, doc);
3691
+ doc.body.appendChild(menu);
3692
+ // Opened near the window's right or bottom edge, it moves back into the window.
3693
+ const win = doc.defaultView;
3694
+ const box = menu.getBoundingClientRect?.();
3695
+ if (win && box?.width) {
3696
+ const at = PopupMenu.fit(x, y, box.width, box.height, win.innerWidth, win.innerHeight);
3697
+ Object.assign(menu.style, { left: `${at.x}px`, top: `${at.y}px` });
3698
+ }
3699
+ // Registered after the opening click has finished propagating.
3700
+ const dismiss = () => PopupMenu.close();
3701
+ const onKey = (e) => { if (e.key === 'Escape') PopupMenu.close(); };
3702
+ setTimeout(() => {
3703
+ if (PopupMenu._open?.menu !== menu) return; // closed meanwhile
3704
+ doc.addEventListener('click', dismiss, { once: true });
3705
+ doc.addEventListener('scroll', dismiss, { once: true, capture: true });
3706
+ doc.addEventListener('keydown', onKey);
3707
+ }, 0);
3708
+ PopupMenu._open = { menu, dismiss, onKey, doc };
3709
+ return menu;
3710
+ }
3711
+
3712
+ /**
3713
+ * Where a menu of a size goes, asked for at (x, y), to be all inside a
3714
+ * window of a size: left of the point when it would pass the right edge,
3715
+ * above it when it would pass the bottom, never past the top-left.
3716
+ */
3717
+ static fit(x, y, width, height, windowWidth, windowHeight, margin = 4) {
3718
+ const left = x + width + margin > windowWidth ? x - width : x;
3719
+ const top = y + height + margin > windowHeight ? y - height : y;
3720
+ return {
3721
+ x: Math.max(margin, Math.min(left, windowWidth - width - margin)),
3722
+ y: Math.max(margin, Math.min(top, windowHeight - height - margin)),
3723
+ };
3724
+ }
3725
+
3726
+ /**
3727
+ * Where a submenu opens beside its item (the item's box `{left, right, top}`),
3728
+ * to be all inside the window: to the right, or to the left when the right
3729
+ * would pass the edge (and there is more room on the left); level with the
3730
+ * item, or lifted by as much as it would pass the bottom. `top` is relative
3731
+ * to the item, as the submenu is positioned.
3732
+ */
3733
+ static submenuPlacement(item, width, height, windowWidth, windowHeight, margin = 4) {
3734
+ const fitsRight = item.right + width + margin <= windowWidth;
3735
+ const side = fitsRight || item.left < windowWidth - item.right ? 'right' : 'left';
3736
+ const top = -4;
3737
+ const overflow = item.top + top + height + margin - windowHeight;
3738
+ return { side, top: overflow > 0 ? Math.max(top - overflow, margin - item.top) : top };
3739
+ }
3740
+
3741
+ static close() {
3742
+ const open = PopupMenu._open;
3743
+ if (!open) return;
3744
+ open.menu.remove();
3745
+ open.doc.removeEventListener('click', open.dismiss);
3746
+ open.doc.removeEventListener('scroll', open.dismiss, { capture: true });
3747
+ open.doc.removeEventListener('keydown', open.onKey);
3748
+ PopupMenu._open = null;
3749
+ }
3750
+
3751
+ static get isOpen() {
3752
+ return !!PopupMenu._open;
3753
+ }
3754
+
3755
+ // The text colour is set with the background: inherited from a dark-themed page it was light
3756
+ // grey on this white panel, and every item but a disabled or danger one was unreadable.
3757
+ static _panelStyle = {
3758
+ background: '#fff', color: '#222', border: '1px solid #ccc', borderRadius: '3px', boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
3759
+ zIndex: '10000', minWidth: '170px', fontSize: '12px', fontFamily: 'sans-serif', padding: '4px 0',
3760
+ };
3761
+
3762
+ static _panel(items, doc) {
3763
+ return { attrs: { role: 'menu' }, children: items.map(item => PopupMenu._item(item, doc)) };
3764
+ }
3765
+
3766
+ static _item(item, doc) {
3767
+ if (item.separator) return { attrs: { role: 'separator' }, style: { borderTop: '1px solid #ddd', margin: '4px 0' } };
3768
+ const hover = item.danger ? '#ffe8e8' : '#f0f0f0';
3769
+ const base = {
3770
+ attrs: { role: 'menuitem', 'aria-disabled': item.disabled ? 'true' : null, 'aria-checked': item.checked != null ? String(!!item.checked) : null },
3771
+ style: {
3772
+ padding: '7px 14px', cursor: item.disabled ? 'default' : 'pointer', position: 'relative', whiteSpace: 'nowrap',
3773
+ color: item.disabled ? '#aaa' : item.danger ? '#cc3333' : '', background: item.checked ? '#e3f2fd' : '',
3774
+ },
3775
+ };
3776
+ if (item.items) {
3777
+ let sub;
3778
+ return {
3779
+ ...base,
3780
+ attrs: { ...base.attrs, 'aria-haspopup': 'menu' },
3781
+ on: {
3782
+ mouseenter: (e) => {
3783
+ e.currentTarget.style.background = hover;
3784
+ sub.style.display = 'block';
3785
+ // Opened near the window's right or bottom edge, it opens left, or lifts.
3786
+ const win = e.currentTarget.ownerDocument.defaultView;
3787
+ const box = sub.getBoundingClientRect?.();
3788
+ if (!win || !box?.width) return;
3789
+ const at = PopupMenu.submenuPlacement(e.currentTarget.getBoundingClientRect(), box.width, box.height, win.innerWidth, win.innerHeight);
3790
+ Object.assign(sub.style, at.side === 'left' ? { left: 'auto', right: '100%' } : { left: '100%', right: 'auto' }, { top: `${at.top}px` });
3791
+ },
3792
+ mouseleave: (e) => { e.currentTarget.style.background = ''; sub.style.display = 'none'; },
3793
+ click: (e) => e.stopPropagation(),
3794
+ },
3795
+ children: [
3796
+ `${item.label} ▸`,
3797
+ {
3798
+ ...PopupMenu._panel(item.items, doc),
3799
+ style: { ...PopupMenu._panelStyle, position: 'absolute', left: '100%', top: '-4px', display: 'none' },
3800
+ ref: (n) => { sub = n; },
3801
+ },
3802
+ ],
3803
+ };
3804
+ }
3805
+ return {
3806
+ ...base,
3807
+ text: item.label,
3808
+ on: {
3809
+ mouseenter: (e) => { if (!item.disabled) e.currentTarget.style.background = hover; },
3810
+ mouseleave: (e) => { e.currentTarget.style.background = item.checked ? '#e3f2fd' : ''; },
3811
+ click: (e) => {
3812
+ e.stopPropagation();
3813
+ if (item.disabled) return;
3814
+ PopupMenu.close();
3815
+ item.action();
3816
+ },
3817
+ },
3818
+ };
3819
+ }
3818
3820
  }
3819
3821
 
3820
3822
  /**
@@ -6442,7 +6444,7 @@
6442
6444
  // Copyright 2026 Jaroslav Peter Prib, Purveyor of Excellence
6443
6445
  /**
6444
6446
  * Single source of truth for "what chart types does RenderEngine support" —
6445
- * see ARCHITECTURE.md's "Chart-type catalog" section for the full story.
6447
+ * see documentation/ARCHITECTURE.md's "Chart-type catalog" section for the full story.
6446
6448
  *
6447
6449
  * This is not just a flat {key,label} list. Chart types share real,
6448
6450
  * composable capabilities — every one of them takes a title; several accept
@@ -16344,7 +16346,7 @@
16344
16346
  * not once per query. Kept as a standalone copy rather than a shared
16345
16347
  * dependency — the two repos have no package link today, and this grammar is
16346
16348
  * small enough that duplicating it costs less than a cross-repo dependency
16347
- * would. See renderengine's own `tasks.md` for why this exists, and
16349
+ * would. See renderengine's own `documentation/tasks.md` for why this exists, and
16348
16350
  * sqlengine's `documentation/kernel-language.md` for the full grammar
16349
16351
  * reference this is a subset of.
16350
16352
  *
@@ -16355,7 +16357,7 @@
16355
16357
  *
16356
16358
  * NO GPU/WASM/WebNN TARGETS HERE, unlike the sqlengine original — renderengine
16357
16359
  * has no compute-backend concept and nothing has measured a need for one for
16358
- * per-point chart encodings (see tasks.md's "Gated on a measurement" entry).
16360
+ * per-point chart encodings (see documentation/tasks.md's "Gated on a measurement" entry).
16359
16361
  * This only ever emits JavaScript.
16360
16362
  */
16361
16363
 
@@ -17765,7 +17767,7 @@
17765
17767
 
17766
17768
  _cell(text, column, left, top, colW, attrs) {
17767
17769
  const end = column.align === 'end';
17768
- this.renderer.text(text, end ? left + colW - TableChart.PAD_X : left + TableChart.PAD_X, top + TableChart.ROW_H / 2, {
17770
+ return this.renderer.text(text, end ? left + colW - TableChart.PAD_X : left + TableChart.PAD_X, top + TableChart.ROW_H / 2, {
17769
17771
  textAnchor: end ? 'end' : 'start', verticalAlign: 'middle', fontSize: '12px', ...attrs,
17770
17772
  });
17771
17773
  }
@@ -17879,6 +17881,17 @@
17879
17881
  numbers: columns.filter(c => c.kind === 'numeric'),
17880
17882
  };
17881
17883
  }
17884
+
17885
+ /**
17886
+ * Whether each row has its own value of the category columns. Rows that repeat a category
17887
+ * (rows of orders, not totals per region) would be drawn as one bar per row with the same
17888
+ * label over and over — or, for two categories, one repeated pair would replace another's value
17889
+ * unseen. A chart of a value per category needs one row per category: a GROUP BY first.
17890
+ */
17891
+ static oneRowEach(rows, categories) {
17892
+ const keys = new Set(rows.map(r => JSON.stringify(categories.map(c => r[c.name] ?? null))));
17893
+ return keys.size === rows.length;
17894
+ }
17882
17895
  }
17883
17896
 
17884
17897
  /** One number and nothing to group it by: a single figure. */
@@ -17898,7 +17911,8 @@
17898
17911
  class CategoryValue extends ResultShape {
17899
17912
  static of(columns, rows) {
17900
17913
  const { categories, numbers } = ResultShape.split(columns);
17901
- return categories.length === 1 && numbers.length === 1 ? new CategoryValue(categories[0], numbers[0], rows) : null;
17914
+ return categories.length === 1 && numbers.length === 1 && ResultShape.oneRowEach(rows, categories)
17915
+ ? new CategoryValue(categories[0], numbers[0], rows) : null;
17902
17916
  }
17903
17917
  constructor(category, number, rows) { super(); this.category = category; this.number = number; this.rows = rows; }
17904
17918
  get provides() { return { category: AXIS_KIND.CATEGORICAL, value: AXIS_KIND.NUMERIC }; }
@@ -17938,7 +17952,8 @@
17938
17952
  class CategorySeries extends ResultShape {
17939
17953
  static of(columns, rows) {
17940
17954
  const { categories, numbers } = ResultShape.split(columns);
17941
- return categories.length === 1 && numbers.length >= 2 ? new CategorySeries(categories[0], numbers, rows) : null;
17955
+ return categories.length === 1 && numbers.length >= 2 && ResultShape.oneRowEach(rows, categories)
17956
+ ? new CategorySeries(categories[0], numbers, rows) : null;
17942
17957
  }
17943
17958
  constructor(category, numbers, rows) { super(); this.category = category; this.numbers = numbers; this.rows = rows; }
17944
17959
  get provides() { return { category: AXIS_KIND.CATEGORICAL, value: AXIS_KIND.NUMERIC, series: AXIS_KIND.CATEGORICAL }; }
@@ -17966,7 +17981,8 @@
17966
17981
  class TwoCategories extends ResultShape {
17967
17982
  static of(columns, rows) {
17968
17983
  const { categories, numbers } = ResultShape.split(columns);
17969
- return categories.length === 2 && numbers.length === 1 ? new TwoCategories(categories, numbers[0], rows) : null;
17984
+ return categories.length === 2 && numbers.length === 1 && ResultShape.oneRowEach(rows, categories)
17985
+ ? new TwoCategories(categories, numbers[0], rows) : null;
17970
17986
  }
17971
17987
  constructor(categories, number, rows) {
17972
17988
  super();
@@ -18003,6 +18019,398 @@
18003
18019
  /** Shapes in the order they are tried; the first that recognises the columns wins. */
18004
18020
  const RESULT_SHAPES = [SingleFigure, CategoryValue, CategorySeries, TwoCategories];
18005
18021
 
18022
+ /**
18023
+ * TableView — the state of an interactive table over result rows, kept apart from drawing so all of
18024
+ * it can be tested without a DOM: the sort, the filters, the hidden columns and the page, and the
18025
+ * menus a table offers on a cell, a row, a column and the table itself (as PopupMenu item specs).
18026
+ *
18027
+ * Filtering is by choosing values from the table, not typing: a cell's menu keeps or excludes rows
18028
+ * with its value (and, for a number, rows above or below it); a column's menu filters to one of its
18029
+ * values. The table's menu offers "Show as", the chart types ChartChooser says fit what is showing.
18030
+ *
18031
+ * Every change calls `onChange(view)`; the host redraws. A host adds its own items with
18032
+ * `menus: { cell(ctx), row(ctx), column(ctx), table(ctx) }`, each returning PopupMenu items.
18033
+ */
18034
+ const FILTER_OPS = {
18035
+ '=': (v, x) => v === x,
18036
+ '!=': (v, x) => v !== x,
18037
+ '>': (v, x) => v != null && v > x,
18038
+ '<': (v, x) => v != null && v < x,
18039
+ 'is null': (v) => v == null,
18040
+ 'is not null': (v) => v != null,
18041
+ };
18042
+
18043
+ /** Order of two values for a sort: numbers numerically, everything else as text; nulls last. */
18044
+ function compareValues(a, b) {
18045
+ if (a == null) return b == null ? 0 : 1;
18046
+ if (b == null) return -1;
18047
+ if (typeof a === 'number' && typeof b === 'number') return a - b;
18048
+ const x = String(a), y = String(b);
18049
+ return x < y ? -1 : x > y ? 1 : 0;
18050
+ }
18051
+
18052
+ /** A value as copied text: null as empty, an object as JSON. */
18053
+ function plain(value) {
18054
+ if (value == null) return '';
18055
+ if (value instanceof Date) return value.toISOString();
18056
+ return typeof value === 'object' ? JSON.stringify(value) : String(value);
18057
+ }
18058
+
18059
+ class TableView {
18060
+ /** The most distinct values a column's "Filter to" menu lists. */
18061
+ static MAX_FILTER_VALUES = 20;
18062
+
18063
+ /**
18064
+ * @param {Array<object>} rows
18065
+ * @param {object} [options]
18066
+ * @param {Array<{name, kind?, measure?}>} [options.columns] hints, as ChartChooser takes them
18067
+ * @param {boolean} [options.complete] false when the rows are only part of the answer (a top N)
18068
+ * @param {number} [options.pageSize] rows per page (50; Infinity for all)
18069
+ * @param {function} [options.onChange] called with the view after every change
18070
+ * @param {object} [options.menus] the host's own items: { cell, row, column, table } → items
18071
+ * @param {function} [options.copy] writes text to the clipboard (navigator.clipboard by default)
18072
+ * @param {ChartChooser} [options.chooser]
18073
+ * @param {function} [options.highlight] row → true for a row drawn highlighted (a host's selection)
18074
+ */
18075
+ constructor(rows, { columns = null, complete = true, pageSize = 50, onChange = null, menus = {}, copy = null, chooser = null, highlight = null } = {}) {
18076
+ this.rows = rows;
18077
+ this.columns = ChartChooser.describe(rows, columns);
18078
+ this.complete = complete;
18079
+ this.pageSize = pageSize;
18080
+ this.onChange = onChange;
18081
+ this.menus = menus;
18082
+ this.copy = copy ?? TableView.clipboard;
18083
+ this.chooser = chooser ?? new ChartChooser();
18084
+ this.highlight = highlight;
18085
+ this.sort = null; // { column, direction: 'asc' | 'desc' }
18086
+ this.filters = []; // { column, op, value }
18087
+ this.hidden = new Set();
18088
+ this.page = 0;
18089
+ }
18090
+
18091
+ static clipboard(text) {
18092
+ return globalThis.navigator?.clipboard?.writeText(text);
18093
+ }
18094
+
18095
+ // ── What is showing ───────────────────────────────────────────────────────
18096
+
18097
+ visibleColumns() { return this.columns.filter(c => !this.hidden.has(c.name)); }
18098
+
18099
+ /** Whether a row is drawn highlighted: the host's `highlight` says (its selection, say). */
18100
+ isHighlighted(row) { return this.highlight?.(row) === true; }
18101
+
18102
+ filtered() {
18103
+ return this.filters.length === 0 ? this.rows
18104
+ : this.rows.filter(r => this.filters.every(f => FILTER_OPS[f.op](r[f.column], f.value)));
18105
+ }
18106
+
18107
+ /** The filtered rows in sort order (a copy; equal values keep their order). */
18108
+ ordered() {
18109
+ const rows = this.filtered();
18110
+ if (this.sort === null) return rows;
18111
+ const { column, direction } = this.sort;
18112
+ const sign = direction === 'desc' ? -1 : 1;
18113
+ // Nulls stay last whichever way: a reader looks for values, not for blanks.
18114
+ return [...rows].sort((a, b) => (a[column] == null || b[column] == null ? compareValues(a[column], b[column]) : sign * compareValues(a[column], b[column])));
18115
+ }
18116
+
18117
+ pageCount() {
18118
+ return Number.isFinite(this.pageSize) ? Math.max(1, Math.ceil(this.filtered().length / this.pageSize)) : 1;
18119
+ }
18120
+
18121
+ /** The rows on the current page, each with its position in the ordered rows (`index`, from 0). */
18122
+ pageRows() {
18123
+ const rows = this.ordered();
18124
+ const start = Number.isFinite(this.pageSize) ? this.page * this.pageSize : 0;
18125
+ const end = Number.isFinite(this.pageSize) ? start + this.pageSize : rows.length;
18126
+ return rows.slice(start, end).map((row, i) => ({ row, index: start + i }));
18127
+ }
18128
+
18129
+ /** "Region = West · Revenue > 100", or '' with no filters. */
18130
+ describeFilters() {
18131
+ return this.filters.map(f => (f.op.startsWith('is') ? `${f.column} ${f.op}` : `${f.column} ${f.op} ${plain(f.value)}`)).join(' · ');
18132
+ }
18133
+
18134
+ // ── Changes ───────────────────────────────────────────────────────────────
18135
+
18136
+ changed() {
18137
+ this.page = Math.min(this.page, this.pageCount() - 1);
18138
+ this.onChange?.(this);
18139
+ }
18140
+
18141
+ /** A header click: unsorted → ascending → descending → unsorted. */
18142
+ toggleSort(column) {
18143
+ const current = this.sort?.column === column ? this.sort.direction : null;
18144
+ this.sortBy(column, current === null ? 'asc' : current === 'asc' ? 'desc' : null);
18145
+ }
18146
+
18147
+ sortBy(column, direction) {
18148
+ this.sort = direction === null ? null : { column, direction };
18149
+ this.page = 0;
18150
+ this.changed();
18151
+ }
18152
+
18153
+ addFilter(column, op, value = null) {
18154
+ if (!Object.hasOwn(FILTER_OPS, op)) throw new Error(`Unknown filter "${op}"`);
18155
+ this.filters = [...this.filters.filter(f => !(f.column === column && f.op === op && f.value === value)), { column, op, value }];
18156
+ this.page = 0;
18157
+ this.changed();
18158
+ }
18159
+
18160
+ removeFilters(column) {
18161
+ this.filters = this.filters.filter(f => f.column !== column);
18162
+ this.changed();
18163
+ }
18164
+
18165
+ clearFilters() {
18166
+ this.filters = [];
18167
+ this.changed();
18168
+ }
18169
+
18170
+ /** Hides a column; the last one showing cannot be hidden. */
18171
+ hide(column) {
18172
+ if (this.visibleColumns().length <= 1) return;
18173
+ this.hidden.add(column);
18174
+ this.changed();
18175
+ }
18176
+
18177
+ showAll() {
18178
+ this.hidden.clear();
18179
+ this.changed();
18180
+ }
18181
+
18182
+ goToPage(page) {
18183
+ this.page = Math.max(0, Math.min(page, this.pageCount() - 1));
18184
+ this.changed();
18185
+ }
18186
+
18187
+ // ── Text out ──────────────────────────────────────────────────────────────
18188
+
18189
+ /** Rows as tab-separated text, header first, the visible columns only. */
18190
+ asTsv(rows = this.ordered()) {
18191
+ const columns = this.visibleColumns().map(c => c.name);
18192
+ const clean = (v) => plain(v).replace(/[\t\r\n]+/g, ' ');
18193
+ return [columns.map(clean).join('\t'), ...rows.map(r => columns.map(c => clean(r[c])).join('\t'))].join('\n');
18194
+ }
18195
+
18196
+ // ── Menus (PopupMenu items) ──────────────────────────────────────────────
18197
+
18198
+ cellMenu(row, column) {
18199
+ const value = row[column];
18200
+ const kind = this.columns.find(c => c.name === column)?.kind;
18201
+ const shown = value == null ? 'empty' : plain(value);
18202
+ const items = value == null
18203
+ ? [{ label: `Only rows where ${column} is empty`, action: () => this.addFilter(column, 'is null') },
18204
+ { label: `Exclude rows where ${column} is empty`, action: () => this.addFilter(column, 'is not null') }]
18205
+ : [{ label: `Only rows where ${column} = ${shown}`, action: () => this.addFilter(column, '=', value) },
18206
+ { label: `Exclude rows where ${column} = ${shown}`, action: () => this.addFilter(column, '!=', value) },
18207
+ ...(kind === 'numeric' ? [
18208
+ { label: `Only rows where ${column} > ${shown}`, action: () => this.addFilter(column, '>', value) },
18209
+ { label: `Only rows where ${column} < ${shown}`, action: () => this.addFilter(column, '<', value) },
18210
+ ] : [])];
18211
+ return [
18212
+ ...items,
18213
+ { separator: true },
18214
+ { label: 'Copy value', action: () => this.copy(plain(value)) },
18215
+ ...this.extra('cell', { row, column, value }),
18216
+ ];
18217
+ }
18218
+
18219
+ rowMenu(row) {
18220
+ return [
18221
+ { label: 'Copy row', action: () => this.copy(this.asTsv([row])) },
18222
+ { label: 'Copy row as JSON', action: () => this.copy(JSON.stringify(row)) },
18223
+ ...this.extra('row', { row }),
18224
+ ];
18225
+ }
18226
+
18227
+ columnMenu(column) {
18228
+ const sorted = this.sort?.column === column ? this.sort.direction : null;
18229
+ const values = [...new Set(this.filtered().map(r => r[column]))].sort(compareValues);
18230
+ const listed = values.slice(0, TableView.MAX_FILTER_VALUES);
18231
+ return [
18232
+ { label: 'Sort ascending', checked: sorted === 'asc', action: () => this.sortBy(column, 'asc') },
18233
+ { label: 'Sort descending', checked: sorted === 'desc', action: () => this.sortBy(column, 'desc') },
18234
+ ...(sorted ? [{ label: 'Clear sort', action: () => this.sortBy(column, null) }] : []),
18235
+ { separator: true },
18236
+ {
18237
+ label: 'Filter to',
18238
+ items: [
18239
+ ...listed.map(v => (v == null
18240
+ ? { label: '(empty)', action: () => this.addFilter(column, 'is null') }
18241
+ : { label: plain(v), action: () => this.addFilter(column, '=', v) })),
18242
+ ...(values.length > listed.length ? [{ label: `${values.length - listed.length} more — filter from a cell`, disabled: true }] : []),
18243
+ ],
18244
+ },
18245
+ ...(this.filters.some(f => f.column === column) ? [{ label: `Clear filters on ${column}`, action: () => this.removeFilters(column) }] : []),
18246
+ { separator: true },
18247
+ { label: 'Hide column', disabled: this.visibleColumns().length <= 1, action: () => this.hide(column) },
18248
+ { label: 'Copy column', action: () => this.copy([column, ...this.ordered().map(r => plain(r[column]))].join('\n')) },
18249
+ ...this.extra('column', { column }),
18250
+ ];
18251
+ }
18252
+
18253
+ /** The table's menu; `onShowAs(choice)` draws a chosen chart (the host's — RenderEngine.resultView). */
18254
+ tableMenu(onShowAs = null) {
18255
+ const choices = onShowAs ? this.chartChoices() : [];
18256
+ return [
18257
+ ...(onShowAs ? [{
18258
+ label: 'Show as',
18259
+ items: choices.length > 0
18260
+ ? choices.map(c => ({ label: `${c.label} — ${c.reason}`, action: () => onShowAs(c) }))
18261
+ : [{ label: 'No chart fits these columns', disabled: true }],
18262
+ }, { separator: true }] : []),
18263
+ { label: 'Clear all filters', disabled: this.filters.length === 0, action: () => this.clearFilters() },
18264
+ { label: 'Show hidden columns', disabled: this.hidden.size === 0, action: () => this.showAll() },
18265
+ { label: 'Copy table', action: () => this.copy(this.asTsv()) },
18266
+ ...this.extra('table', {}),
18267
+ ];
18268
+ }
18269
+
18270
+ /**
18271
+ * The chart types that fit what the table is showing — its filtered, sorted rows and visible
18272
+ * columns — as ChartChooser gives them, without the table. Filtered rows are not the whole
18273
+ * answer, so they are never offered as shares of a whole.
18274
+ */
18275
+ chartChoices() {
18276
+ const columns = this.visibleColumns();
18277
+ const names = columns.map(c => c.name);
18278
+ const rows = this.ordered().map(r => Object.fromEntries(names.map(n => [n, r[n]])));
18279
+ return this.chooser.choose(rows, columns, { complete: this.complete && this.filters.length === 0 })
18280
+ .filter(c => c.key !== 'table');
18281
+ }
18282
+
18283
+ extra(kind, context) {
18284
+ const items = this.menus?.[kind]?.({ view: this, ...context }) ?? [];
18285
+ return items.length > 0 ? [{ separator: true }, ...items] : [];
18286
+ }
18287
+ }
18288
+
18289
+ /**
18290
+ * TableViewChart — the interactive table: a TableChart drawn from a TableView's state, whose parts
18291
+ * answer clicks. A header sorts (and its ▾ opens the column's menu); a cell opens the cell's menu; a
18292
+ * row number opens the row's menu; the corner ⋮ opens the table's menu (with "Show as" when the host
18293
+ * can draw a chart); the footer's ‹ › page. Each change goes through the view, whose `onChange`
18294
+ * redraws.
18295
+ *
18296
+ * Interaction needs a renderer whose shapes are DOM elements (SVG: `supportsSceneGraph`); anywhere
18297
+ * else it draws the same table without the clicks.
18298
+ */
18299
+ class TableViewChart extends TableChart {
18300
+ static GUTTER_W = 44;
18301
+ static MENU_GLYPH = '▾';
18302
+ static TABLE_GLYPH = '⋮';
18303
+ static HIGHLIGHT_FILL = '#fdeeb5';
18304
+
18305
+ /**
18306
+ * @param renderer
18307
+ * @param {TableView} view
18308
+ * @param {object} [options] title, nullText; onShowAs(choice) — draws a chart for the table menu's
18309
+ * "Show as"; showMenu(x, y, items) — opens a menu (PopupMenu.show by default)
18310
+ */
18311
+ constructor(renderer, view, options = {}) {
18312
+ super(renderer, view.rows, options);
18313
+ this.view = view;
18314
+ this.showMenu = options.showMenu ?? ((x, y, items) => PopupMenu.show(x, y, items));
18315
+ }
18316
+
18317
+ /** The height this draws: title, header, the page's rows and the footer. */
18318
+ static heightFor(view, { title } = {}) {
18319
+ const { top, bottom } = TableChart.margins(title);
18320
+ return top + (2 + view.pageRows().length) * TableChart.ROW_H + bottom;
18321
+ }
18322
+
18323
+ _render() {
18324
+ const { view } = this;
18325
+ const { title, nullText = '—' } = this.options;
18326
+ const { ROW_H, PAD_X } = TableChart;
18327
+ const box = ChartBox.fromRenderer(this.renderer, TableChart.margins(title));
18328
+ const interactive = this.renderer.supportsSceneGraph === true;
18329
+ const columns = view.visibleColumns().map(c => ({ key: c.name, label: c.name, align: c.kind === 'numeric' ? 'end' : 'start' }));
18330
+ const gutter = TableViewChart.GUTTER_W;
18331
+ const colW = columns.length > 0 ? (box.width - gutter) / columns.length : box.width - gutter;
18332
+ const x0 = box.x + gutter;
18333
+
18334
+ new ChartLabels({ title }).draw(this.renderer, { x: box.x, y: box.y, width: box.width, height: box.height });
18335
+
18336
+ // Header: the corner (table menu), then a header per column (sort; ▾ column menu).
18337
+ let top = box.y;
18338
+ this.renderer.rect(box.x, top, box.width, ROW_H, TableChart.HEADER_FILL, null);
18339
+ const corner = this.renderer.rect(box.x, top, gutter, ROW_H, TableChart.HEADER_FILL, null);
18340
+ const cornerGlyph = this._text(TableViewChart.TABLE_GLYPH, box.x + gutter / 2, top, { textAnchor: 'middle', fontWeight: 'bold', fill: '#555' });
18341
+ this._on(interactive, [corner, cornerGlyph], (e) => this.showMenu(e.clientX, e.clientY, view.tableMenu(this.options.onShowAs ?? null)), 'Table menu');
18342
+
18343
+ columns.forEach((c, i) => {
18344
+ const left = x0 + i * colW;
18345
+ const sorted = view.sort?.column === c.key ? (view.sort.direction === 'asc' ? ' ▲' : ' ▼') : '';
18346
+ // In words: an arrow-like mark here read as a second sort arrow.
18347
+ const filtered = view.filters.some(f => f.column === c.key) ? ' (filtered)' : '';
18348
+ const head = this.renderer.rect(left, top, colW, ROW_H, TableChart.HEADER_FILL, null);
18349
+ // The label leaves room at the right for the ▾.
18350
+ const label = this._cell(`${c.label}${sorted}${filtered}`, c, left, top, colW - 16, { fontWeight: 'bold', fill: '#333' });
18351
+ this._on(interactive, [head, label], () => view.toggleSort(c.key), 'Sort');
18352
+ const glyph = this._text(TableViewChart.MENU_GLYPH, left + colW - 8, top, { textAnchor: 'middle', fill: '#777' });
18353
+ this._on(interactive, [glyph], (e) => this.showMenu(e.clientX, e.clientY, view.columnMenu(c.key)), `${c.label} menu`);
18354
+ });
18355
+ top += ROW_H;
18356
+ this._rule(box.x, top, box.width);
18357
+
18358
+ // Rows: a number in the gutter (row menu), a cell per column (cell menu).
18359
+ view.pageRows().forEach(({ row, index }, r) => {
18360
+ if (view.isHighlighted(row)) this.renderer.rect(box.x, top, box.width, ROW_H, TableViewChart.HIGHLIGHT_FILL, null);
18361
+ else if (r % 2 === 1) this.renderer.rect(box.x, top, box.width, ROW_H, TableChart.STRIPE_FILL, null);
18362
+ const number = this._text(String(index + 1), box.x + gutter - PAD_X, top, { textAnchor: 'end', fill: '#999', fontSize: '11px' });
18363
+ this._on(interactive, [number], (e) => this.showMenu(e.clientX, e.clientY, view.rowMenu(row)), 'Row menu');
18364
+ columns.forEach((c, i) => {
18365
+ const left = x0 + i * colW;
18366
+ const hit = this.renderer.rect(left, top, colW, ROW_H, 'transparent', null);
18367
+ const text = this._cell(TableChart.cellText(row[c.key], nullText), c, left, top, colW, { fill: row[c.key] == null ? '#999' : '#222' });
18368
+ this._on(interactive, [hit, text], (e) => this.showMenu(e.clientX, e.clientY, view.cellMenu(row, c.key)), null);
18369
+ });
18370
+ top += ROW_H;
18371
+ this._rule(box.x, top, box.width);
18372
+ });
18373
+
18374
+ this._footer(box, top, interactive);
18375
+ }
18376
+
18377
+ /** "1–50 of 120 rows (filtered from 300: Region = West)", and ‹ › when there is more than a page. */
18378
+ _footer(box, top, interactive) {
18379
+ const { view } = this;
18380
+ const shown = view.pageRows();
18381
+ const total = view.filtered().length;
18382
+ const range = shown.length === 0 ? 'No rows' : `${shown[0].index + 1}–${shown.at(-1).index + 1} of ${total} row${total === 1 ? '' : 's'}`;
18383
+ const filters = view.filters.length > 0 ? ` (filtered from ${view.rows.length}: ${view.describeFilters()})` : '';
18384
+ this._text(`${range}${filters}`, box.x + TableChart.PAD_X, top, { textAnchor: 'start', fill: '#666', fontSize: '11px' });
18385
+ if (view.pageCount() > 1) {
18386
+ const right = box.x + box.width - TableChart.PAD_X;
18387
+ const previous = this._text('‹ previous', right - 70, top, { textAnchor: 'end', fill: view.page > 0 ? '#2b5fd9' : '#bbb', fontSize: '11px' });
18388
+ const next = this._text('next ›', right, top, { textAnchor: 'end', fill: view.page < view.pageCount() - 1 ? '#2b5fd9' : '#bbb', fontSize: '11px' });
18389
+ this._on(interactive, [previous], () => view.goToPage(view.page - 1), 'Previous page');
18390
+ this._on(interactive, [next], () => view.goToPage(view.page + 1), 'Next page');
18391
+ }
18392
+ }
18393
+
18394
+ _text(text, x, top, attrs) {
18395
+ return this.renderer.text(text, x, top + TableChart.ROW_H / 2, { verticalAlign: 'middle', fontSize: '12px', ...attrs });
18396
+ }
18397
+
18398
+ /** Wires a click to each element that can take one (SVG); a pointer cursor and a tooltip say so. */
18399
+ _on(interactive, elements, handler, title) {
18400
+ if (!interactive) return;
18401
+ for (const el of elements) {
18402
+ if (typeof el?.addEventListener !== 'function') continue;
18403
+ el.addEventListener('click', (event) => { event.stopPropagation?.(); handler(event); });
18404
+ if (el.style) el.style.cursor = 'pointer';
18405
+ if (title && typeof el.setAttribute === 'function' && el.ownerDocument) {
18406
+ const t = el.ownerDocument.createElementNS('http://www.w3.org/2000/svg', 'title');
18407
+ t.textContent = title;
18408
+ el.appendChild(t);
18409
+ }
18410
+ }
18411
+ }
18412
+ }
18413
+
18006
18414
  /**
18007
18415
  * RadarChart — one polygon per series across N shared axes, each axis scaled
18008
18416
  * independently by its own max value across all series.
@@ -22483,7 +22891,7 @@
22483
22891
  * Abstract base for "own a bounded collection, push one more item, drop the
22484
22892
  * oldest once past a cap" — the one behavior every streamable chart data
22485
22893
  * shape shares, however differently each stores its collection(s). See
22486
- * ARCHITECTURE.md's "Data windows — streaming" section for the concrete
22894
+ * documentation/ARCHITECTURE.md's "Data windows — streaming" section for the concrete
22487
22895
  * subtypes and why each exists as its own class rather than a branch here.
22488
22896
  */
22489
22897
  class BoundedWindow {
@@ -22607,7 +23015,7 @@
22607
23015
 
22608
23016
  /**
22609
23017
  * A nodes+edges pair — the shape behind `graphDiagram`/`graphDiagram3D`.
22610
- * Not a `CHART_CATALOG` entry (see ARCHITECTURE.md), so it isn't dispatched
23018
+ * Not a `CHART_CATALOG` entry (see documentation/ARCHITECTURE.md), so it isn't dispatched
22611
23019
  * through `createDataWindow`/`streamShape`; `RenderEngine.addGraphNode`
22612
23020
  * constructs it directly. Trimming drops the oldest nodes FIFO, then any
22613
23021
  * edge left dangling off a removed node — dropping nodes without this would
@@ -46928,7 +47336,7 @@
46928
47336
  * (github.com/bian-official/public, Apache-2.0) for the names control records
46929
47337
  * have and, where a pattern aggregates two creating terms (Monitor: evaluate
46930
47338
  * and initiate), the one its APIs use. Every value is BIAN's; `inferred` is
46931
- * kept for a value that is not. See BIAN_ALIGNMENT.md.
47339
+ * kept for a value that is not. See documentation/BIAN_ALIGNMENT.md.
46932
47340
  */
46933
47341
 
46934
47342
  /** One way a service domain behaves: what it does to its asset, and the record it keeps of it. */
@@ -53726,6 +54134,62 @@ ${theirs}` : theirs;
53726
54134
  this._renderChart(new TableChart(this.renderer, data, options));
53727
54135
  }
53728
54136
 
54137
+ /**
54138
+ * Shows result rows as an interactive table — sort, filter, hide columns, page, copy, through
54139
+ * menus on its cells, rows, columns and the table itself — whose table menu can show what it
54140
+ * holds as any chart type ChartChooser says fits ("Show as"); a chart drawn that way has a
54141
+ * "◂ Table" link back to the table as it was left. One output for a result, table or chart.
54142
+ *
54143
+ * The container's height follows what is drawn (the table's rows, or `chartHeight` for a chart),
54144
+ * so the host gives the container a width only. Interaction needs the SVG renderer; on canvas
54145
+ * the table is drawn without it.
54146
+ * @param {Array<object>} rows
54147
+ * @param {object} [options]
54148
+ * @param {Array<{name, kind?, measure?}>} [options.columns] hints (ChartChooser's)
54149
+ * @param {boolean} [options.complete] false when the rows are only part of the answer
54150
+ * @param {number} [options.pageSize] rows per page (50)
54151
+ * @param {object} [options.menus] the host's own items: { cell, row, column, table } → items
54152
+ * @param {string} [options.title]
54153
+ * @param {number} [options.chartHeight] a chart's height (360)
54154
+ * @param {boolean} [options.fitHeight] size the container to what is drawn (true)
54155
+ * @returns {TableView|null} the table's state, for the host to read or change
54156
+ */
54157
+ resultView(rows, options = {}) {
54158
+ if (!this.renderer) return null;
54159
+ const view = new TableView(rows, { ...options, onChange: () => this._drawResultTable(view, options) });
54160
+ this._drawResultTable(view, options);
54161
+ return view;
54162
+ }
54163
+
54164
+ _drawResultTable(view, options) {
54165
+ this._fitResultHeight(TableViewChart.heightFor(view, options), options);
54166
+ this._chart3D = false;
54167
+ this._chartState = { type: 'table', data: view.rows, options };
54168
+ this._renderChart(new TableViewChart(this.renderer, view, {
54169
+ ...options,
54170
+ onShowAs: (choice) => this._drawResultChart(view, choice, options),
54171
+ }));
54172
+ }
54173
+
54174
+ _drawResultChart(view, choice, options) {
54175
+ this._fitResultHeight(options.chartHeight ?? 360, options);
54176
+ this.renderChartType(choice.key, choice.data, { title: options.title ?? choice.reason });
54177
+ // The way back, drawn over the chart: the same table, as it was left.
54178
+ if (!this.renderer.supportsSceneGraph) return;
54179
+ const back = this.renderer.text('◂ Table', 8, 14, { fill: '#2b5fd9', fontSize: '12px', verticalAlign: 'middle' });
54180
+ back?.addEventListener?.('click', () => this._drawResultTable(view, options));
54181
+ if (back?.style) back.style.cursor = 'pointer';
54182
+ }
54183
+
54184
+ _fitResultHeight(height, options) {
54185
+ if (options.fitHeight === false || !this.container?.style) return;
54186
+ // min-height as well: in a flex layout (a notebook, a workbench pane) a height alone is
54187
+ // shrunk to fit, and the table drawn into the shrunk box lost its last rows.
54188
+ this.container.style.height = `${height}px`;
54189
+ this.container.style.minHeight = `${height}px`;
54190
+ this.fitToContainer();
54191
+ }
54192
+
53729
54193
  /**
53730
54194
  * Renders a waterfall chart — running-total bars, each floating from the
53731
54195
  * previous cumulative value to the new one (a "delta" bar), except
@@ -54050,7 +54514,7 @@ ${theirs}` : theirs;
54050
54514
  /**
54051
54515
  * Renders an arc gauge: a half circle (or any start angle and sweep) filled to the value.
54052
54516
  * @param {number|{value:number, target?:number}} value The value (0-100 unless options.min/max are given).
54053
- * @param {object} [options] The gauge options (see REFERENCE.md, Gauges), cx, cy, r, thickness, startAngle, sweep, cap.
54517
+ * @param {object} [options] The gauge options (see documentation/REFERENCE.md, Gauges), cx, cy, r, thickness, startAngle, sweep, cap.
54054
54518
  */
54055
54519
  arcGauge(value, options = {}) {
54056
54520
  this._renderGauge(ArcGauge, 'arcGauge', value, options);
@@ -54985,7 +55449,7 @@ ${theirs}` : theirs;
54985
55449
  /**
54986
55450
  * Appends one data point to a registered chart's data and re-renders it,
54987
55451
  * keeping at most `maxPoints` (oldest dropped first) — see
54988
- * REFERENCE.md's "Dynamically adding data points in a bounded window".
55452
+ * documentation/REFERENCE.md's "Dynamically adding data points in a bounded window".
54989
55453
  * Delegates the actual shape-aware append/trim to a `datawindow/`
54990
55454
  * class, selected via the chart's `CHART_CATALOG` `streamShape`; the
54991
55455
  * window instance is cached per `id` so repeated calls keep growing the
@@ -55139,7 +55603,7 @@ ${theirs}` : theirs;
55139
55603
  return this.chartOptions;
55140
55604
  }
55141
55605
 
55142
- /** Append a point to a bounded window — see REFERENCE.md's streaming section. */
55606
+ /** Append a point to a bounded window — see documentation/REFERENCE.md's streaming section. */
55143
55607
  addDataPoint(pointOrFactory, opts) {
55144
55608
  this.engine?.addDataPoint(this.chartId, pointOrFactory, opts);
55145
55609
  }
@@ -58472,7 +58936,7 @@ th { color: #555; font-weight: 500; }
58472
58936
  * a shell that may only ever host adapted panels. Call
58473
58937
  * `registerBuiltinParts(ws)` for the standard set, or register individually.
58474
58938
  *
58475
- * See WORKSPACE_ARCHITECTURE.md for the design.
58939
+ * See documentation/WORKSPACE_ARCHITECTURE.md for the design.
58476
58940
  */
58477
58941
 
58478
58942
  defineWorkspace();
@@ -58760,6 +59224,8 @@ th { color: #555; font-weight: 500; }
58760
59224
  exports.TREE_MODE_CATALOG = TREE_MODE_CATALOG;
58761
59225
  exports.TREE_SETTINGS_PROPERTIES = TREE_SETTINGS_PROPERTIES;
58762
59226
  exports.TableChart = TableChart;
59227
+ exports.TableView = TableView;
59228
+ exports.TableViewChart = TableViewChart;
58763
59229
  exports.TabsNode = TabsNode;
58764
59230
  exports.Template = Template;
58765
59231
  exports.TemplateCatalog = TemplateCatalog;