@peter.prib/renderengine 0.10.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/dist/renderengine-workspace.umd.js +4093 -2910
- package/dist/renderengine.umd.js +4093 -2910
- package/documentation/REFERENCE.md +3230 -3077
- package/package.json +2 -2
- 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 +74 -14
- package/src/charting/table/TableViewChart.js +4 -2
- package/src/index.js +2886 -2807
|
@@ -63,7 +63,8 @@ export class TableView {
|
|
|
63
63
|
this.copy = copy ?? TableView.clipboard;
|
|
64
64
|
this.chooser = chooser ?? new ChartChooser();
|
|
65
65
|
this.highlight = highlight;
|
|
66
|
-
|
|
66
|
+
/** The sort keys, most significant first: `{ column, direction: 'asc' | 'desc' }`. */
|
|
67
|
+
this.sorts = [];
|
|
67
68
|
this.filters = []; // { column, op, value }
|
|
68
69
|
this.hidden = new Set();
|
|
69
70
|
this.page = 0;
|
|
@@ -85,14 +86,36 @@ export class TableView {
|
|
|
85
86
|
: this.rows.filter(r => this.filters.every(f => FILTER_OPS[f.op](r[f.column], f.value)));
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
/** 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
|
+
*/
|
|
89
102
|
ordered() {
|
|
90
103
|
const rows = this.filtered();
|
|
91
|
-
if (this.
|
|
92
|
-
const { column, direction }
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 ');
|
|
96
119
|
}
|
|
97
120
|
|
|
98
121
|
pageCount() {
|
|
@@ -119,14 +142,41 @@ export class TableView {
|
|
|
119
142
|
this.onChange?.(this);
|
|
120
143
|
}
|
|
121
144
|
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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));
|
|
126
158
|
}
|
|
127
159
|
|
|
160
|
+
/** Sorts by `column` alone (null: unsorted). */
|
|
128
161
|
sortBy(column, direction) {
|
|
129
|
-
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 = [];
|
|
130
180
|
this.page = 0;
|
|
131
181
|
this.changed();
|
|
132
182
|
}
|
|
@@ -206,13 +256,23 @@ export class TableView {
|
|
|
206
256
|
}
|
|
207
257
|
|
|
208
258
|
columnMenu(column) {
|
|
209
|
-
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
|
+
] : [];
|
|
210
269
|
const values = [...new Set(this.filtered().map(r => r[column]))].sort(compareValues);
|
|
211
270
|
const listed = values.slice(0, TableView.MAX_FILTER_VALUES);
|
|
212
271
|
return [
|
|
213
272
|
{ label: 'Sort ascending', checked: sorted === 'asc', action: () => this.sortBy(column, 'asc') },
|
|
214
273
|
{ label: 'Sort descending', checked: sorted === 'desc', action: () => this.sortBy(column, 'desc') },
|
|
215
|
-
...
|
|
274
|
+
...further,
|
|
275
|
+
...(this.sorts.length > 0 ? [{ label: 'Clear sort', action: () => this.clearSort() }] : []),
|
|
216
276
|
{ separator: true },
|
|
217
277
|
{
|
|
218
278
|
label: 'Filter to',
|
|
@@ -58,13 +58,15 @@ export class TableViewChart extends TableChart {
|
|
|
58
58
|
|
|
59
59
|
columns.forEach((c, i) => {
|
|
60
60
|
const left = x0 + i * colW;
|
|
61
|
-
|
|
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 : ''}`;
|
|
62
64
|
// In words: an arrow-like mark here read as a second sort arrow.
|
|
63
65
|
const filtered = view.filters.some(f => f.column === c.key) ? ' (filtered)' : '';
|
|
64
66
|
const head = this.renderer.rect(left, top, colW, ROW_H, TableChart.HEADER_FILL, null);
|
|
65
67
|
// The label leaves room at the right for the ▾.
|
|
66
68
|
const label = this._cell(`${c.label}${sorted}${filtered}`, c, left, top, colW - 16, { fontWeight: 'bold', fill: '#333' });
|
|
67
|
-
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)');
|
|
68
70
|
const glyph = this._text(TableViewChart.MENU_GLYPH, left + colW - 8, top, { textAnchor: 'middle', fill: '#777' });
|
|
69
71
|
this._on(interactive, [glyph], (e) => this.showMenu(e.clientX, e.clientY, view.columnMenu(c.key)), `${c.label} menu`);
|
|
70
72
|
});
|