@peter.prib/renderengine 0.9.0 → 0.12.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 +5 -5
- package/dist/renderengine-workspace.umd.js +4109 -2919
- package/dist/renderengine.umd.js +4107 -2917
- package/{REFERENCE.md → documentation/REFERENCE.md} +3230 -3076
- package/package.json +3 -3
- package/src/bian.js +1 -1
- package/src/chartCatalog.js +1 -1
- package/src/charting/Chart.js +25 -0
- package/src/charting/ChartChooser.js +5 -4
- package/src/charting/Legend.js +16 -0
- package/src/charting/charts/BarChart.js +1 -1
- package/src/charting/charts/GroupedBarChart.js +38 -16
- package/src/charting/charts/LineChart.js +9 -8
- package/src/charting/charts/PieChart.js +2 -2
- package/src/charting/charts/TableChart.js +16 -3
- package/src/charting/pivot/PivotChart.js +50 -0
- package/src/charting/pivot/PivotGridChart.js +191 -0
- package/src/charting/pivot/PivotView.js +731 -0
- package/src/charting/table/TableView.js +80 -15
- package/src/charting/table/TableViewChart.js +7 -3
- package/src/datawindow/BoundedWindow.js +1 -1
- package/src/datawindow/GraphDataWindow.js +1 -1
- package/src/index.js +2886 -2807
- package/src/kernelExpr.js +2 -2
- package/src/workspace/index.js +1 -1
- package/src/workspace/parts/ChartPart.js +1 -1
- package/src/workspace.js +1 -1
|
@@ -51,8 +51,9 @@ export class TableView {
|
|
|
51
51
|
* @param {object} [options.menus] the host's own items: { cell, row, column, table } → items
|
|
52
52
|
* @param {function} [options.copy] writes text to the clipboard (navigator.clipboard by default)
|
|
53
53
|
* @param {ChartChooser} [options.chooser]
|
|
54
|
+
* @param {function} [options.highlight] row → true for a row drawn highlighted (a host's selection)
|
|
54
55
|
*/
|
|
55
|
-
constructor(rows, { columns = null, complete = true, pageSize = 50, onChange = null, menus = {}, copy = null, chooser = null } = {}) {
|
|
56
|
+
constructor(rows, { columns = null, complete = true, pageSize = 50, onChange = null, menus = {}, copy = null, chooser = null, highlight = null } = {}) {
|
|
56
57
|
this.rows = rows;
|
|
57
58
|
this.columns = ChartChooser.describe(rows, columns);
|
|
58
59
|
this.complete = complete;
|
|
@@ -61,7 +62,9 @@ export class TableView {
|
|
|
61
62
|
this.menus = menus;
|
|
62
63
|
this.copy = copy ?? TableView.clipboard;
|
|
63
64
|
this.chooser = chooser ?? new ChartChooser();
|
|
64
|
-
this.
|
|
65
|
+
this.highlight = highlight;
|
|
66
|
+
/** The sort keys, most significant first: `{ column, direction: 'asc' | 'desc' }`. */
|
|
67
|
+
this.sorts = [];
|
|
65
68
|
this.filters = []; // { column, op, value }
|
|
66
69
|
this.hidden = new Set();
|
|
67
70
|
this.page = 0;
|
|
@@ -75,19 +78,44 @@ export class TableView {
|
|
|
75
78
|
|
|
76
79
|
visibleColumns() { return this.columns.filter(c => !this.hidden.has(c.name)); }
|
|
77
80
|
|
|
81
|
+
/** Whether a row is drawn highlighted: the host's `highlight` says (its selection, say). */
|
|
82
|
+
isHighlighted(row) { return this.highlight?.(row) === true; }
|
|
83
|
+
|
|
78
84
|
filtered() {
|
|
79
85
|
return this.filters.length === 0 ? this.rows
|
|
80
86
|
: this.rows.filter(r => this.filters.every(f => FILTER_OPS[f.op](r[f.column], f.value)));
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
/** The
|
|
89
|
+
/** The first sort key, or null — the one a single-column sort has. */
|
|
90
|
+
get sort() { return this.sorts[0] ?? null; }
|
|
91
|
+
|
|
92
|
+
/** Where `column` is among the sort keys: `{ direction, rank }` (rank from 1), or null. */
|
|
93
|
+
sortOf(column) {
|
|
94
|
+
const at = this.sorts.findIndex(s => s.column === column);
|
|
95
|
+
return at < 0 ? null : { direction: this.sorts[at].direction, rank: at + 1 };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The filtered rows in sort order (a copy): by the first key, then the next among equals, and so
|
|
100
|
+
* on; rows equal on every key keep their order.
|
|
101
|
+
*/
|
|
84
102
|
ordered() {
|
|
85
103
|
const rows = this.filtered();
|
|
86
|
-
if (this.
|
|
87
|
-
const { column, direction }
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
104
|
+
if (this.sorts.length === 0) return rows;
|
|
105
|
+
const keys = this.sorts.map(({ column, direction }) => ({ column, sign: direction === 'desc' ? -1 : 1 }));
|
|
106
|
+
return [...rows].sort((a, b) => {
|
|
107
|
+
for (const { column, sign } of keys) {
|
|
108
|
+
// Nulls stay last whichever way: a reader looks for values, not for blanks.
|
|
109
|
+
const c = a[column] == null || b[column] == null ? compareValues(a[column], b[column]) : sign * compareValues(a[column], b[column]);
|
|
110
|
+
if (c !== 0) return c;
|
|
111
|
+
}
|
|
112
|
+
return 0;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** "Region ▲, then Revenue ▼", or '' when unsorted. */
|
|
117
|
+
describeSort() {
|
|
118
|
+
return this.sorts.map(s => `${s.column} ${s.direction === 'asc' ? '▲' : '▼'}`).join(', then ');
|
|
91
119
|
}
|
|
92
120
|
|
|
93
121
|
pageCount() {
|
|
@@ -114,14 +142,41 @@ export class TableView {
|
|
|
114
142
|
this.onChange?.(this);
|
|
115
143
|
}
|
|
116
144
|
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
145
|
+
/**
|
|
146
|
+
* A header click: unsorted → ascending → descending → unsorted, by this column alone. With
|
|
147
|
+
* `{ then: true }` (a shift-click) the column is a further key instead: added after the others,
|
|
148
|
+
* or, if it is one already, cycled in its place — the other keys stay.
|
|
149
|
+
*/
|
|
150
|
+
toggleSort(column, { then = false } = {}) {
|
|
151
|
+
const next = (d) => (d === null ? 'asc' : d === 'asc' ? 'desc' : null);
|
|
152
|
+
if (then) {
|
|
153
|
+
this.thenSortBy(column, next(this.sortOf(column)?.direction ?? null));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const alone = this.sorts.length === 1 && this.sorts[0].column === column ? this.sorts[0].direction : null;
|
|
157
|
+
this.sortBy(column, next(alone));
|
|
121
158
|
}
|
|
122
159
|
|
|
160
|
+
/** Sorts by `column` alone (null: unsorted). */
|
|
123
161
|
sortBy(column, direction) {
|
|
124
|
-
this.
|
|
162
|
+
this.sorts = direction === null ? [] : [{ column, direction }];
|
|
163
|
+
this.page = 0;
|
|
164
|
+
this.changed();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** `column` as a further sort key: its direction changed in place, appended if new, removed with null. */
|
|
168
|
+
thenSortBy(column, direction) {
|
|
169
|
+
const at = this.sorts.findIndex(s => s.column === column);
|
|
170
|
+
if (direction === null) this.sorts = this.sorts.filter(s => s.column !== column);
|
|
171
|
+
else if (at >= 0) this.sorts = this.sorts.map(s => (s.column === column ? { column, direction } : s));
|
|
172
|
+
else this.sorts = [...this.sorts, { column, direction }];
|
|
173
|
+
this.page = 0;
|
|
174
|
+
this.changed();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Unsorted: rows in the order given. */
|
|
178
|
+
clearSort() {
|
|
179
|
+
this.sorts = [];
|
|
125
180
|
this.page = 0;
|
|
126
181
|
this.changed();
|
|
127
182
|
}
|
|
@@ -201,13 +256,23 @@ export class TableView {
|
|
|
201
256
|
}
|
|
202
257
|
|
|
203
258
|
columnMenu(column) {
|
|
204
|
-
const
|
|
259
|
+
const key = this.sortOf(column);
|
|
260
|
+
const alone = this.sorts.length === 1 && key !== null;
|
|
261
|
+
const sorted = alone ? key.direction : null;
|
|
262
|
+
// A further key: offered once something else is sorted (so this column is never the only key
|
|
263
|
+
// here); ticked where this column already is one.
|
|
264
|
+
const further = this.sorts.some(s => s.column !== column) ? [
|
|
265
|
+
{ label: 'Then sort ascending', checked: key?.direction === 'asc', action: () => this.thenSortBy(column, 'asc') },
|
|
266
|
+
{ label: 'Then sort descending', checked: key?.direction === 'desc', action: () => this.thenSortBy(column, 'desc') },
|
|
267
|
+
...(key !== null ? [{ label: 'Stop sorting by this column', action: () => this.thenSortBy(column, null) }] : []),
|
|
268
|
+
] : [];
|
|
205
269
|
const values = [...new Set(this.filtered().map(r => r[column]))].sort(compareValues);
|
|
206
270
|
const listed = values.slice(0, TableView.MAX_FILTER_VALUES);
|
|
207
271
|
return [
|
|
208
272
|
{ label: 'Sort ascending', checked: sorted === 'asc', action: () => this.sortBy(column, 'asc') },
|
|
209
273
|
{ label: 'Sort descending', checked: sorted === 'desc', action: () => this.sortBy(column, 'desc') },
|
|
210
|
-
...
|
|
274
|
+
...further,
|
|
275
|
+
...(this.sorts.length > 0 ? [{ label: 'Clear sort', action: () => this.clearSort() }] : []),
|
|
211
276
|
{ separator: true },
|
|
212
277
|
{
|
|
213
278
|
label: 'Filter to',
|
|
@@ -16,6 +16,7 @@ export class TableViewChart extends TableChart {
|
|
|
16
16
|
static GUTTER_W = 44;
|
|
17
17
|
static MENU_GLYPH = '▾';
|
|
18
18
|
static TABLE_GLYPH = '⋮';
|
|
19
|
+
static HIGHLIGHT_FILL = '#fdeeb5';
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* @param renderer
|
|
@@ -57,13 +58,15 @@ export class TableViewChart extends TableChart {
|
|
|
57
58
|
|
|
58
59
|
columns.forEach((c, i) => {
|
|
59
60
|
const left = x0 + i * colW;
|
|
60
|
-
|
|
61
|
+
// ▲ / ▼, and with several keys their order: "Region ▲1", "Revenue ▼2".
|
|
62
|
+
const key = view.sortOf(c.key);
|
|
63
|
+
const sorted = key === null ? '' : ` ${key.direction === 'asc' ? '▲' : '▼'}${view.sorts.length > 1 ? key.rank : ''}`;
|
|
61
64
|
// In words: an arrow-like mark here read as a second sort arrow.
|
|
62
65
|
const filtered = view.filters.some(f => f.column === c.key) ? ' (filtered)' : '';
|
|
63
66
|
const head = this.renderer.rect(left, top, colW, ROW_H, TableChart.HEADER_FILL, null);
|
|
64
67
|
// The label leaves room at the right for the ▾.
|
|
65
68
|
const label = this._cell(`${c.label}${sorted}${filtered}`, c, left, top, colW - 16, { fontWeight: 'bold', fill: '#333' });
|
|
66
|
-
this._on(interactive, [head, label], () => view.toggleSort(c.key), 'Sort');
|
|
69
|
+
this._on(interactive, [head, label], (e) => view.toggleSort(c.key, { then: e?.shiftKey === true }), 'Sort (shift-click: then sort by)');
|
|
67
70
|
const glyph = this._text(TableViewChart.MENU_GLYPH, left + colW - 8, top, { textAnchor: 'middle', fill: '#777' });
|
|
68
71
|
this._on(interactive, [glyph], (e) => this.showMenu(e.clientX, e.clientY, view.columnMenu(c.key)), `${c.label} menu`);
|
|
69
72
|
});
|
|
@@ -72,7 +75,8 @@ export class TableViewChart extends TableChart {
|
|
|
72
75
|
|
|
73
76
|
// Rows: a number in the gutter (row menu), a cell per column (cell menu).
|
|
74
77
|
view.pageRows().forEach(({ row, index }, r) => {
|
|
75
|
-
if (
|
|
78
|
+
if (view.isHighlighted(row)) this.renderer.rect(box.x, top, box.width, ROW_H, TableViewChart.HIGHLIGHT_FILL, null);
|
|
79
|
+
else if (r % 2 === 1) this.renderer.rect(box.x, top, box.width, ROW_H, TableChart.STRIPE_FILL, null);
|
|
76
80
|
const number = this._text(String(index + 1), box.x + gutter - PAD_X, top, { textAnchor: 'end', fill: '#999', fontSize: '11px' });
|
|
77
81
|
this._on(interactive, [number], (e) => this.showMenu(e.clientX, e.clientY, view.rowMenu(row)), 'Row menu');
|
|
78
82
|
columns.forEach((c, i) => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Abstract base for "own a bounded collection, push one more item, drop the
|
|
3
3
|
* oldest once past a cap" — the one behavior every streamable chart data
|
|
4
4
|
* shape shares, however differently each stores its collection(s). See
|
|
5
|
-
* ARCHITECTURE.md's "Data windows — streaming" section for the concrete
|
|
5
|
+
* documentation/ARCHITECTURE.md's "Data windows — streaming" section for the concrete
|
|
6
6
|
* subtypes and why each exists as its own class rather than a branch here.
|
|
7
7
|
*/
|
|
8
8
|
export class BoundedWindow {
|
|
@@ -2,7 +2,7 @@ import { BoundedWindow } from './BoundedWindow.js';
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* A nodes+edges pair — the shape behind `graphDiagram`/`graphDiagram3D`.
|
|
5
|
-
* Not a `CHART_CATALOG` entry (see ARCHITECTURE.md), so it isn't dispatched
|
|
5
|
+
* Not a `CHART_CATALOG` entry (see documentation/ARCHITECTURE.md), so it isn't dispatched
|
|
6
6
|
* through `createDataWindow`/`streamShape`; `RenderEngine.addGraphNode`
|
|
7
7
|
* constructs it directly. Trimming drops the oldest nodes FIFO, then any
|
|
8
8
|
* edge left dangling off a removed node — dropping nodes without this would
|