@peter.prib/renderengine 0.14.0 → 0.15.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 +45 -19
- package/bin/renderengine.mjs +157 -0
- package/dist/renderengine-workspace.umd.js +49391 -39748
- package/dist/renderengine.umd.js +25366 -15650
- package/documentation/REFERENCE.md +38 -0
- package/package.json +6 -1
- package/src/CanvasRenderer.js +225 -200
- package/src/SVGRenderer.js +476 -454
- package/src/archimate.js +22 -2
- package/src/archimateArchiFormat.js +1735 -1701
- package/src/archimateArchive.js +36 -7
- package/src/archimateCSV.js +93 -37
- package/src/archimateDrawio.js +2 -0
- package/src/archimateExchangeFormat.js +3 -1
- package/src/chartCatalog.js +219 -0
- package/src/charting/Annotations.js +169 -0
- package/src/charting/ArchimateDiagramEditor.js +54 -10
- package/src/charting/Axis.js +217 -5
- package/src/charting/Chart.js +99 -9
- package/src/charting/Chart3D.js +3 -0
- package/src/charting/ChartBox.js +8 -1
- package/src/charting/ColorPalette.js +42 -4
- package/src/charting/DiagramEditor.js +174 -39
- package/src/charting/DiagramEditorCommands.js +28 -0
- package/src/charting/Edge.js +7 -0
- package/src/charting/Hierarchy.js +114 -0
- package/src/charting/InteractionManager.js +4 -0
- package/src/charting/ModelFileFormat.js +5 -1
- package/src/charting/SettingsManager.js +35 -0
- package/src/charting/ShapePaths.js +82 -10
- package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
- package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
- package/src/charting/charts/AlluvialDiagram.js +311 -0
- package/src/charting/charts/AreaChart.js +52 -15
- package/src/charting/charts/BarChart.js +38 -3
- package/src/charting/charts/BarChart3D.js +5 -3
- package/src/charting/charts/BarChartBase.js +26 -6
- package/src/charting/charts/BeeswarmChart.js +164 -0
- package/src/charting/charts/BoxPlotChart.js +23 -1
- package/src/charting/charts/CalendarHeatmapChart.js +2 -1
- package/src/charting/charts/CandlestickChart.js +172 -4
- package/src/charting/charts/ChordDiagram.js +105 -2
- package/src/charting/charts/CirclePackChart.js +81 -0
- package/src/charting/charts/ComboChart.js +233 -0
- package/src/charting/charts/ContourChart.js +310 -0
- package/src/charting/charts/DonutChart3D.js +13 -0
- package/src/charting/charts/DumbbellChart.js +217 -0
- package/src/charting/charts/EcdfChart.js +150 -0
- package/src/charting/charts/FunnelChart.js +4 -1
- package/src/charting/charts/GeoMapChart.js +132 -11
- package/src/charting/charts/GroupedBarChart.js +44 -3
- package/src/charting/charts/GroupedDistributionChart.js +26 -1
- package/src/charting/charts/HeatmapChart.js +2 -2
- package/src/charting/charts/HexbinChart.js +137 -0
- package/src/charting/charts/HierarchyChart.js +77 -3
- package/src/charting/charts/HorizonChart.js +105 -0
- package/src/charting/charts/KagiChart.js +167 -0
- package/src/charting/charts/LineChart.js +74 -18
- package/src/charting/charts/OhlcChart.js +79 -0
- package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
- package/src/charting/charts/PieChart.js +195 -17
- package/src/charting/charts/PieChart3D.js +137 -0
- package/src/charting/charts/PointAndFigureChart.js +198 -0
- package/src/charting/charts/PolarAreaChart.js +189 -0
- package/src/charting/charts/RadarChart.js +7 -1
- package/src/charting/charts/RadialBarChart.js +5 -2
- package/src/charting/charts/RenkoChart.js +178 -0
- package/src/charting/charts/RidgelineChart.js +113 -0
- package/src/charting/charts/SankeyDiagram.js +127 -4
- package/src/charting/charts/ScatterChart.js +13 -0
- package/src/charting/charts/ScatterMatrixChart.js +119 -0
- package/src/charting/charts/SlopegraphChart.js +205 -0
- package/src/charting/charts/SparklineTableChart.js +166 -0
- package/src/charting/charts/StackedBarChart.js +48 -5
- package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
- package/src/charting/charts/StreamGraphChart.js +10 -3
- package/src/charting/charts/SunburstChart.js +27 -7
- package/src/charting/charts/SurfaceChart3D.js +157 -0
- package/src/charting/charts/TreemapChart.js +28 -8
- package/src/charting/charts/ViolinChart.js +18 -0
- package/src/charting/datamodel/ArchimateDataLink.js +70 -0
- package/src/charting/datamodel/DataLineage.js +150 -0
- package/src/charting/datamodel/DataModel.js +83 -1
- package/src/charting/datamodel/ModelMapping.js +87 -0
- package/src/charting/datamodel/SqlDdl.js +110 -85
- package/src/charting/datamodel/index.js +3 -0
- package/src/charting/decision/DecisionWorkbench.js +295 -0
- package/src/charting/diagrams/ArchimateDiagram.js +25 -9
- package/src/charting/diagrams/ArchimateModel.js +1382 -1238
- package/src/charting/diagrams/GraphDiagram.js +1424 -1350
- package/src/charting/diagrams/SketchView.js +121 -0
- package/src/charting/diagrams/sketchBoxKinds.js +30 -0
- package/src/charting/modelTree/AnalysisPanel.js +101 -23
- package/src/charting/modelTree/NavigatorModel.js +410 -0
- package/src/charting/modelTree/NavigatorPanel.js +453 -0
- package/src/charting/pivot/PivotChart.js +109 -4
- package/src/charting/pivot/PivotGridChart.js +321 -33
- package/src/charting/pivot/PivotView.js +82 -0
- package/src/charting/report/TemplatedReport.js +547 -0
- package/src/charting/templates/BoxTemplate.js +28 -27
- package/src/charting/templates/PackageTemplate.js +26 -25
- package/src/charting/templates/SwimlaneTemplate.js +19 -18
- package/src/charting/templates/Template.js +219 -212
- package/src/cli.js +146 -0
- package/src/index.js +3370 -2995
- package/src/workspace/Workspace.js +126 -7
- package/src/workspace/archimateServices.js +4 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
- package/src/workspace/styles.js +36 -1
|
@@ -25,6 +25,20 @@ export class PivotGridChart extends TableViewChart {
|
|
|
25
25
|
static EXPANDED = '▾';
|
|
26
26
|
static COLLAPSED = '▸';
|
|
27
27
|
|
|
28
|
+
constructor(renderer, view, options = {}) {
|
|
29
|
+
super(renderer, view, options);
|
|
30
|
+
this.pinnedHeaders = options.pinnedHeaders !== false;
|
|
31
|
+
this.virtualize = options.virtualize !== false;
|
|
32
|
+
this.virtualBuffer = options.virtualBuffer ?? 5;
|
|
33
|
+
this._renderedRange = null;
|
|
34
|
+
this._scrollLeft = 0;
|
|
35
|
+
this._scrollTop = 0;
|
|
36
|
+
this._scrollHandler = null;
|
|
37
|
+
this._scrollContainer = null;
|
|
38
|
+
this._rowShapesStartIndex = null;
|
|
39
|
+
this._rowCtx = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
28
42
|
/** Lines the layout strip takes, when the view's layout is the host's to change. */
|
|
29
43
|
static layoutLines(view) { return view.onLayout ? 2 : 0; }
|
|
30
44
|
|
|
@@ -56,6 +70,58 @@ export class PivotGridChart extends TableViewChart {
|
|
|
56
70
|
return width;
|
|
57
71
|
}
|
|
58
72
|
|
|
73
|
+
_rule(x, y, width) {
|
|
74
|
+
return this.renderer.path(`M${x},${y} L${x + width},${y}`, 'none', TableChart.RULE);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_isVirtualizable() {
|
|
78
|
+
if (this.options.virtualize === false) return false;
|
|
79
|
+
if (!this.renderer?.supportsSceneGraph) return false;
|
|
80
|
+
const totalRows = this.view.visibleRows().length;
|
|
81
|
+
if (this.options.virtualize === true) return totalRows > 0;
|
|
82
|
+
const container = this.renderer.container;
|
|
83
|
+
if (container && container.clientHeight > 0) {
|
|
84
|
+
const fullHeight = PivotGridChart.heightFor(this.view, this.options);
|
|
85
|
+
return container.clientHeight < fullHeight && totalRows > 20;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
_getVisibleRowRange(scrollTop, viewportH) {
|
|
91
|
+
const totalRows = this.view.visibleRows().length;
|
|
92
|
+
const relativeY = Math.max(0, scrollTop - (this._rowTopStart || 0));
|
|
93
|
+
const rawStart = Math.floor(relativeY / TableChart.ROW_H);
|
|
94
|
+
const start = Math.max(0, rawStart - this.virtualBuffer);
|
|
95
|
+
const rawEnd = Math.ceil((relativeY + viewportH) / TableChart.ROW_H);
|
|
96
|
+
const end = Math.min(totalRows, rawEnd + this.virtualBuffer);
|
|
97
|
+
return { start, end };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get renderedRowRange() {
|
|
101
|
+
return this._renderedRange ? { ...this._renderedRange } : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
scrollTo(scrollTop, scrollLeft = 0) {
|
|
105
|
+
if (this._scrollContainer) {
|
|
106
|
+
this._scrollContainer.scrollTop = scrollTop;
|
|
107
|
+
this._scrollContainer.scrollLeft = scrollLeft;
|
|
108
|
+
}
|
|
109
|
+
this._handleScroll(scrollLeft, scrollTop);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
scrollToRow(index) {
|
|
113
|
+
const targetY = (this._rowTopStart || 0) + index * TableChart.ROW_H;
|
|
114
|
+
this.scrollTo(targetY, this._scrollLeft || 0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
destroy() {
|
|
118
|
+
if (this._scrollHandler && this._scrollContainer) {
|
|
119
|
+
this._scrollContainer.removeEventListener('scroll', this._scrollHandler);
|
|
120
|
+
this._scrollHandler = null;
|
|
121
|
+
this._scrollContainer = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
59
125
|
_render() {
|
|
60
126
|
const { view } = this;
|
|
61
127
|
const { title, nullText = '—' } = this.options;
|
|
@@ -71,17 +137,50 @@ export class PivotGridChart extends TableViewChart {
|
|
|
71
137
|
|
|
72
138
|
new ChartLabels({ title }).draw(this.renderer, { x: box.x, y: box.y, width: box.width, height: box.height });
|
|
73
139
|
|
|
140
|
+
const inGroup = (group, shapeEl) => {
|
|
141
|
+
if (group && typeof group.appendChild === 'function' && shapeEl && shapeEl.nodeType) {
|
|
142
|
+
group.appendChild(shapeEl);
|
|
143
|
+
}
|
|
144
|
+
return shapeEl;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const createGroup = (className) => {
|
|
148
|
+
if (interactive && typeof this.renderer.group === 'function') {
|
|
149
|
+
return this.renderer.group({ class: className });
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const bodyGroup = createGroup('pivot-body');
|
|
155
|
+
const bodyRowsGroup = createGroup('pivot-body-rows');
|
|
156
|
+
const colHeaderGroup = createGroup('pivot-col-headers');
|
|
157
|
+
const rowHeaderGroup = createGroup('pivot-row-headers');
|
|
158
|
+
const rowHeadersContentGroup = createGroup('pivot-row-headers-content');
|
|
159
|
+
const cornerGroup = createGroup('pivot-corner');
|
|
160
|
+
const freezeLinesGroup = createGroup('pivot-freeze-lines');
|
|
161
|
+
|
|
162
|
+
this._colHeaderGroup = colHeaderGroup;
|
|
163
|
+
this._rowHeaderGroup = rowHeaderGroup;
|
|
164
|
+
this._cornerGroup = cornerGroup;
|
|
165
|
+
this._bodyGroup = bodyGroup;
|
|
166
|
+
this._bodyRowsGroup = bodyRowsGroup;
|
|
167
|
+
this._rowHeadersContentGroup = rowHeadersContentGroup;
|
|
168
|
+
this._freezeLinesGroup = freezeLinesGroup;
|
|
169
|
+
|
|
74
170
|
// The corner: the grid's menu, and what each axis holds.
|
|
75
171
|
let top = box.y;
|
|
76
|
-
if (view.onLayout) top = this._layoutStrip(box, top, interactive);
|
|
77
|
-
const
|
|
78
|
-
const
|
|
172
|
+
if (view.onLayout) top = this._layoutStrip(box, top, interactive, colHeaderGroup);
|
|
173
|
+
const headerTop = top;
|
|
174
|
+
const headerHeight = lines.length * ROW_H;
|
|
175
|
+
|
|
176
|
+
const corner = inGroup(cornerGroup, this.renderer.rect(box.x, top, sideW, headerHeight, TableChart.HEADER_FILL, null));
|
|
177
|
+
const glyph = inGroup(cornerGroup, this._text(TableViewChart.TABLE_GLYPH, box.x + PAD_X + 2, top, { textAnchor: 'middle', fontWeight: 'bold', fill: '#555' }));
|
|
79
178
|
this._on(interactive, [corner, glyph], (e) => this.showMenu(e.clientX, e.clientY, view.gridMenu()), 'Grid menu');
|
|
80
179
|
if (view.columnAxis.length > 0 && lines.length > 1) {
|
|
81
|
-
this._cell(view.columnAxis.join(' › '), end, box.x + 16, top, sideW - 16, { fill: '#666', fontSize: '11px' });
|
|
180
|
+
inGroup(cornerGroup, this._cell(view.columnAxis.join(' › '), end, box.x + 16, top, sideW - 16, { fill: '#666', fontSize: '11px' }));
|
|
82
181
|
}
|
|
83
182
|
if (view.rowAxis.length > 0) {
|
|
84
|
-
this._cell(view.rowAxis.join(' › '), {}, box.x + 12, top + (lines.length - 1) * ROW_H, sideW - 12, { fontWeight: 'bold', fill: '#333' });
|
|
183
|
+
inGroup(cornerGroup, this._cell(view.rowAxis.join(' › '), {}, box.x + 12, top + (lines.length - 1) * ROW_H, sideW - 12, { fontWeight: 'bold', fill: '#333' }));
|
|
85
184
|
}
|
|
86
185
|
|
|
87
186
|
// The column headers: a line per level, a member spanning the columns beneath it.
|
|
@@ -89,46 +188,223 @@ export class PivotGridChart extends TableViewChart {
|
|
|
89
188
|
let left = x0;
|
|
90
189
|
for (const cell of line) {
|
|
91
190
|
const w = cell.span * colW;
|
|
92
|
-
this.renderer.rect(left, top, w, ROW_H, TableChart.HEADER_FILL, null);
|
|
93
|
-
this._member('columns', cell.node, cell.text, left, top, w, interactive, { fontWeight: 'bold', fill: '#333' });
|
|
191
|
+
inGroup(colHeaderGroup, this.renderer.rect(left, top, w, ROW_H, TableChart.HEADER_FILL, null));
|
|
192
|
+
this._member('columns', cell.node, cell.text, left, top, w, interactive, { fontWeight: 'bold', fill: '#333' }, colHeaderGroup);
|
|
94
193
|
left += w;
|
|
95
194
|
}
|
|
96
195
|
top += ROW_H;
|
|
97
|
-
this._rule(box.x, top, box.width);
|
|
196
|
+
inGroup(colHeaderGroup, this._rule(box.x, top, box.width));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
this._rowTopStart = top;
|
|
200
|
+
this._sideW = sideW;
|
|
201
|
+
this._colW = colW;
|
|
202
|
+
this._x0 = x0;
|
|
203
|
+
this._box = box;
|
|
204
|
+
|
|
205
|
+
if (interactive && freezeLinesGroup) {
|
|
206
|
+
this._colDivider = inGroup(freezeLinesGroup, this.renderer.path(`M${box.x},${top} L${box.x + box.width},${top}`, 'none', { color: '#c5cde0', width: 1.5 }));
|
|
207
|
+
const fullHeight = PivotGridChart.heightFor(view, this.options);
|
|
208
|
+
this._rowDivider = inGroup(freezeLinesGroup, this.renderer.path(`M${x0},${headerTop} L${x0},${box.y + fullHeight}`, 'none', { color: '#c5cde0', width: 1.5 }));
|
|
98
209
|
}
|
|
99
210
|
|
|
100
211
|
// The rows: a member (indented, with its ▸ / ▾) and its cells; members with members below are totals.
|
|
101
212
|
const cuts = view.lights && view.lights !== 'rules' ? view.thirds() : null; // once per drawing, not per cell
|
|
102
|
-
|
|
213
|
+
const colMaxValues = this.options.dataBars ? columns.map(c => {
|
|
214
|
+
let max = 0;
|
|
215
|
+
view.visibleRows().forEach(({ node }) => {
|
|
216
|
+
const isTotal = node.children.size > 0 || node.path.length === 0;
|
|
217
|
+
if (!isTotal) {
|
|
218
|
+
const r = view.cell(node, c.node);
|
|
219
|
+
const val = r?.[c.measure];
|
|
220
|
+
if (typeof val === 'number' && Number.isFinite(val) && val > max) max = val;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
return max;
|
|
224
|
+
}) : null;
|
|
225
|
+
|
|
226
|
+
const allRows = view.visibleRows();
|
|
227
|
+
const totalRows = allRows.length;
|
|
228
|
+
const isVirtual = this._isVirtualizable();
|
|
229
|
+
this.isVirtual = isVirtual;
|
|
230
|
+
|
|
231
|
+
let startRow = 0;
|
|
232
|
+
let endRow = totalRows;
|
|
233
|
+
|
|
234
|
+
if (isVirtual) {
|
|
235
|
+
const container = this.renderer.container;
|
|
236
|
+
const viewportH = this.options.viewportHeight || container?.clientHeight || 400;
|
|
237
|
+
const range = this._getVisibleRowRange(this._scrollTop, viewportH);
|
|
238
|
+
startRow = range.start;
|
|
239
|
+
endRow = range.end;
|
|
240
|
+
this._renderedRange = range;
|
|
241
|
+
} else {
|
|
242
|
+
this._renderedRange = { start: 0, end: totalRows };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this._rowShapesStartIndex = Array.isArray(this.renderer?.shapes) ? this.renderer.shapes.length : null;
|
|
246
|
+
this._rowCtx = {
|
|
247
|
+
cuts, colMaxValues, columns, box, sideW, colW, x0, end, interactive, nullText,
|
|
248
|
+
bodyGroup: bodyRowsGroup || bodyGroup,
|
|
249
|
+
rowHeaderGroup: rowHeadersContentGroup || rowHeaderGroup,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
this._renderRowsSlice(startRow, endRow, this._rowCtx);
|
|
253
|
+
|
|
254
|
+
let notesTop = this._rowTopStart + totalRows * ROW_H;
|
|
255
|
+
if (view.unplaced > 0) {
|
|
256
|
+
inGroup(bodyGroup, this._text(`${view.unplaced} row(s) total an outer level but not an inner one, and are not shown`, box.x + PAD_X, notesTop,
|
|
257
|
+
{ textAnchor: 'start', fill: '#888', fontSize: '11px' }));
|
|
258
|
+
notesTop += ROW_H;
|
|
259
|
+
}
|
|
260
|
+
const legend = view.lightsLegend();
|
|
261
|
+
if (legend) inGroup(bodyGroup, this._text(legend, box.x + PAD_X, notesTop, { textAnchor: 'start', fill: '#666', fontSize: '11px' }));
|
|
262
|
+
|
|
263
|
+
this._wireScrollListener();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
_renderRowsSlice(startRow, endRow, ctx) {
|
|
267
|
+
const { view } = this;
|
|
268
|
+
const { ROW_H } = TableChart;
|
|
269
|
+
const { cuts, colMaxValues, columns, box, sideW, colW, x0, end, interactive, nullText, bodyGroup, rowHeaderGroup } = ctx;
|
|
270
|
+
const inGroup = (group, shapeEl) => {
|
|
271
|
+
if (group && typeof group.appendChild === 'function' && shapeEl && shapeEl.nodeType) {
|
|
272
|
+
group.appendChild(shapeEl);
|
|
273
|
+
}
|
|
274
|
+
return shapeEl;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const allRows = view.visibleRows();
|
|
278
|
+
for (let r = startRow; r < endRow; r++) {
|
|
279
|
+
const item = allRows[r];
|
|
280
|
+
if (!item) continue;
|
|
281
|
+
const { node, depth } = item;
|
|
282
|
+
let top = this._rowTopStart + r * ROW_H;
|
|
103
283
|
const total = node.children.size > 0 || node.path.length === 0;
|
|
104
|
-
|
|
105
|
-
|
|
284
|
+
|
|
285
|
+
if (total) inGroup(bodyGroup, this.renderer.rect(box.x, top, box.width, ROW_H, PivotGridChart.TOTAL_FILL, null));
|
|
286
|
+
else if (r % 2 === 1) inGroup(bodyGroup, this.renderer.rect(box.x, top, box.width, ROW_H, TableChart.STRIPE_FILL, null));
|
|
287
|
+
|
|
288
|
+
if (rowHeaderGroup && interactive) {
|
|
289
|
+
const headerFill = total ? PivotGridChart.TOTAL_FILL : (r % 2 === 1 ? TableChart.STRIPE_FILL : '#ffffff');
|
|
290
|
+
inGroup(rowHeaderGroup, this.renderer.rect(box.x, top, sideW, ROW_H, headerFill, null));
|
|
291
|
+
}
|
|
292
|
+
|
|
106
293
|
const indent = node.path.length === 0 ? 0 : depth * PivotGridChart.INDENT;
|
|
107
294
|
const weight = total ? { fontWeight: 'bold' } : {};
|
|
108
|
-
this._member('rows', node.path.length === 0 ? null : node, PivotView.label(node), box.x + indent, top, sideW - indent, interactive, { fill: '#222', ...weight });
|
|
295
|
+
this._member('rows', node.path.length === 0 ? null : node, PivotView.label(node), box.x + indent, top, sideW - indent, interactive, { fill: '#222', ...weight }, rowHeaderGroup);
|
|
109
296
|
columns.forEach((c, i) => {
|
|
110
297
|
const lit = view.cellFill(node, c, cuts);
|
|
111
|
-
if (lit) this.renderer.rect(x0 + i * colW + 1, top + 1, colW - 2, ROW_H - 2, lit, null);
|
|
298
|
+
if (lit) inGroup(bodyGroup, this.renderer.rect(x0 + i * colW + 1, top + 1, colW - 2, ROW_H - 2, lit, null));
|
|
112
299
|
const row = view.cell(node, c.node);
|
|
300
|
+
if (colMaxValues && !total) {
|
|
301
|
+
const val = row?.[c.measure];
|
|
302
|
+
const colMax = colMaxValues[i];
|
|
303
|
+
if (typeof val === 'number' && Number.isFinite(val) && val > 0 && colMax > 0) {
|
|
304
|
+
const barW = Math.max(2, (val / colMax) * (colW - 8));
|
|
305
|
+
const barColor = typeof this.options.dataBars === 'string' ? this.options.dataBars : (this.options.dataBarColor || 'rgba(66, 133, 244, 0.22)');
|
|
306
|
+
inGroup(bodyGroup, this.renderer.rect(x0 + i * colW + 4, top + 4, barW, ROW_H - 8, barColor, null));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
113
309
|
const text = view.cellText ? view.cellText(node, c, nullText) : (row === null ? '' : view.valueText(c.measure, row[c.measure], nullText));
|
|
114
|
-
const shown = this._cell(text, end, x0 + i * colW, top, colW, { fill: row?.[c.measure] == null ? '#999' : '#222', ...weight });
|
|
310
|
+
const shown = inGroup(bodyGroup, this._cell(text, end, x0 + i * colW, top, colW, { fill: row?.[c.measure] == null ? '#999' : '#222', ...weight }));
|
|
115
311
|
// A cell with a value opens its menu (drill-through) where the host gives one.
|
|
116
312
|
if (row !== null && view.cellHasMenu()) {
|
|
117
|
-
const hit = this.renderer.rect(x0 + i * colW, top, colW, ROW_H, 'transparent', null);
|
|
313
|
+
const hit = inGroup(bodyGroup, this.renderer.rect(x0 + i * colW, top, colW, ROW_H, 'transparent', null));
|
|
118
314
|
this._on(interactive, [hit, shown], (e) => this.showMenu(e.clientX, e.clientY, view.cellMenu(node, c)), 'Cell menu');
|
|
119
315
|
}
|
|
120
316
|
});
|
|
121
317
|
top += ROW_H;
|
|
122
|
-
this._rule(box.x, top, box.width);
|
|
123
|
-
}
|
|
318
|
+
inGroup(bodyGroup, this._rule(box.x, top, box.width));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
124
321
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
322
|
+
_updateVirtualSlice(start, end) {
|
|
323
|
+
if (!this._rowCtx) return;
|
|
324
|
+
const { bodyGroup, rowHeaderGroup } = this._rowCtx;
|
|
325
|
+
|
|
326
|
+
if (bodyGroup && typeof bodyGroup.replaceChildren === 'function') {
|
|
327
|
+
bodyGroup.replaceChildren();
|
|
328
|
+
} else if (bodyGroup && bodyGroup.firstChild) {
|
|
329
|
+
while (bodyGroup.firstChild) bodyGroup.removeChild(bodyGroup.firstChild);
|
|
330
|
+
}
|
|
331
|
+
if (rowHeaderGroup && typeof rowHeaderGroup.replaceChildren === 'function') {
|
|
332
|
+
rowHeaderGroup.replaceChildren();
|
|
333
|
+
} else if (rowHeaderGroup && rowHeaderGroup.firstChild) {
|
|
334
|
+
while (rowHeaderGroup.firstChild) rowHeaderGroup.removeChild(rowHeaderGroup.firstChild);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (Array.isArray(this.renderer?.shapes) && this._rowShapesStartIndex != null) {
|
|
338
|
+
this.renderer.shapes.length = this._rowShapesStartIndex;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
this._renderRowsSlice(start, end, this._rowCtx);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
_handleScroll(scrollLeft, scrollTop) {
|
|
345
|
+
this._scrollLeft = scrollLeft;
|
|
346
|
+
this._scrollTop = scrollTop;
|
|
347
|
+
|
|
348
|
+
if (this.options.pinnedHeaders !== false && this.renderer?.supportsSceneGraph) {
|
|
349
|
+
if (this._colHeaderGroup?.setAttribute) {
|
|
350
|
+
this._colHeaderGroup.setAttribute('transform', `translate(0, ${scrollTop})`);
|
|
351
|
+
}
|
|
352
|
+
if (this._rowHeaderGroup?.setAttribute) {
|
|
353
|
+
this._rowHeaderGroup.setAttribute('transform', `translate(${scrollLeft}, 0)`);
|
|
354
|
+
}
|
|
355
|
+
if (this._cornerGroup?.setAttribute) {
|
|
356
|
+
this._cornerGroup.setAttribute('transform', `translate(${scrollLeft}, ${scrollTop})`);
|
|
357
|
+
}
|
|
358
|
+
if (this._colDivider?.setAttribute) {
|
|
359
|
+
this._colDivider.setAttribute('transform', `translate(0, ${scrollTop})`);
|
|
360
|
+
}
|
|
361
|
+
if (this._rowDivider?.setAttribute) {
|
|
362
|
+
this._rowDivider.setAttribute('transform', `translate(${scrollLeft}, 0)`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (this._rowHeaderGroup?.style) {
|
|
366
|
+
this._rowHeaderGroup.style.filter = scrollLeft > 0 ? 'drop-shadow(2px 0 3px rgba(0,0,0,0.12))' : '';
|
|
367
|
+
}
|
|
368
|
+
if (this._colHeaderGroup?.style) {
|
|
369
|
+
this._colHeaderGroup.style.filter = scrollTop > 0 ? 'drop-shadow(0 2px 3px rgba(0,0,0,0.12))' : '';
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (this._isVirtualizable()) {
|
|
374
|
+
const container = this.renderer.container;
|
|
375
|
+
const viewportH = this.options.viewportHeight || container?.clientHeight || 400;
|
|
376
|
+
const newRange = this._getVisibleRowRange(scrollTop, viewportH);
|
|
377
|
+
if (!this._renderedRange || newRange.start !== this._renderedRange.start || newRange.end !== this._renderedRange.end) {
|
|
378
|
+
this._renderedRange = newRange;
|
|
379
|
+
this._updateVirtualSlice(newRange.start, newRange.end);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (typeof this.options.onScroll === 'function') {
|
|
384
|
+
this.options.onScroll({ scrollLeft, scrollTop, range: this._renderedRange });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
_wireScrollListener() {
|
|
389
|
+
const container = this.renderer.container;
|
|
390
|
+
if (!container || typeof container.addEventListener !== 'function') return;
|
|
391
|
+
|
|
392
|
+
if (this._scrollHandler && this._scrollContainer) {
|
|
393
|
+
this._scrollContainer.removeEventListener('scroll', this._scrollHandler);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
this._scrollContainer = container;
|
|
397
|
+
this._scrollHandler = () => {
|
|
398
|
+
const scrollLeft = container.scrollLeft || 0;
|
|
399
|
+
const scrollTop = container.scrollTop || 0;
|
|
400
|
+
this._handleScroll(scrollLeft, scrollTop);
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
container.addEventListener('scroll', this._scrollHandler);
|
|
404
|
+
|
|
405
|
+
if (container.scrollLeft || container.scrollTop) {
|
|
406
|
+
this._handleScroll(container.scrollLeft || 0, container.scrollTop || 0);
|
|
129
407
|
}
|
|
130
|
-
const legend = view.lightsLegend();
|
|
131
|
-
if (legend) this._text(legend, box.x + PAD_X, top, { textAnchor: 'start', fill: '#666', fontSize: '11px' });
|
|
132
408
|
}
|
|
133
409
|
|
|
134
410
|
/**
|
|
@@ -136,8 +412,14 @@ export class PivotGridChart extends TableViewChart {
|
|
|
136
412
|
* member, or All) and "Add" (the dimensions not placed). Each dimension is a chip; a click opens
|
|
137
413
|
* its layout menu. Returns where the grid starts.
|
|
138
414
|
*/
|
|
139
|
-
_layoutStrip(box, top, interactive) {
|
|
415
|
+
_layoutStrip(box, top, interactive, colHeaderGroup = null) {
|
|
140
416
|
const { view } = this;
|
|
417
|
+
const inGroup = (shapeEl) => {
|
|
418
|
+
if (colHeaderGroup && typeof colHeaderGroup.appendChild === 'function' && shapeEl && shapeEl.nodeType) {
|
|
419
|
+
colHeaderGroup.appendChild(shapeEl);
|
|
420
|
+
}
|
|
421
|
+
return shapeEl;
|
|
422
|
+
};
|
|
141
423
|
const zones = [
|
|
142
424
|
[['Rows', view.rowAxis.map(d => [d, d + view.memberFilterText(d)])], ['Columns', view.columnAxis.map(d => [d, d + view.memberFilterText(d)])]],
|
|
143
425
|
[['Filters', view.pageAxis.map(p => [p.dimension, `${p.dimension}: ${p.value === null ? 'All' : TableChart.cellText(p.value)}`])],
|
|
@@ -147,17 +429,17 @@ export class PivotGridChart extends TableViewChart {
|
|
|
147
429
|
for (const line of zones) {
|
|
148
430
|
let x = box.x + TableChart.PAD_X;
|
|
149
431
|
for (const [name, chips] of line) {
|
|
150
|
-
this._text(name, x, top, { textAnchor: 'start', fill: '#666', fontSize: '11px', fontWeight: 'bold' });
|
|
432
|
+
inGroup(this._text(name, x, top, { textAnchor: 'start', fill: '#666', fontSize: '11px', fontWeight: 'bold' }));
|
|
151
433
|
x += measure(name) + 8;
|
|
152
434
|
if (chips.length === 0) {
|
|
153
|
-
this._text('—', x, top, { textAnchor: 'start', fill: '#aaa' });
|
|
435
|
+
inGroup(this._text('—', x, top, { textAnchor: 'start', fill: '#aaa' }));
|
|
154
436
|
x += 16;
|
|
155
437
|
}
|
|
156
438
|
for (const [dimension, text] of chips) {
|
|
157
439
|
const label = `${text} ▾`;
|
|
158
440
|
const w = measure(label) + 14;
|
|
159
|
-
const chip = this.renderer.rect(x, top + 3, w, TableChart.ROW_H - 6, PivotGridChart.TOTAL_FILL, { color: '#c5cde0', width: 1 });
|
|
160
|
-
const t = this._text(label, x + 7, top, { textAnchor: 'start', fill: '#2b3f66' });
|
|
441
|
+
const chip = inGroup(this.renderer.rect(x, top + 3, w, TableChart.ROW_H - 6, PivotGridChart.TOTAL_FILL, { color: '#c5cde0', width: 1 }));
|
|
442
|
+
const t = inGroup(this._text(label, x + 7, top, { textAnchor: 'start', fill: '#2b3f66' }));
|
|
161
443
|
this._on(interactive, [chip, t], (e) => this.showMenu(e.clientX, e.clientY, view.layoutMenu(dimension)), `${dimension}: move, filter or remove`);
|
|
162
444
|
x += w + 6;
|
|
163
445
|
}
|
|
@@ -172,20 +454,26 @@ export class PivotGridChart extends TableViewChart {
|
|
|
172
454
|
* A member's header: ▸ / ▾ when it can collapse onto its total (a click toggles it), then its label
|
|
173
455
|
* (a click opens its menu). Without a member (a total's or a blank header) just the text.
|
|
174
456
|
*/
|
|
175
|
-
_member(axis, node, text, left, top, width, interactive, attrs) {
|
|
457
|
+
_member(axis, node, text, left, top, width, interactive, attrs, group = null) {
|
|
176
458
|
const { PAD_X } = TableChart;
|
|
459
|
+
const inGroup = (shapeEl) => {
|
|
460
|
+
if (group && typeof group.appendChild === 'function' && shapeEl && shapeEl.nodeType) {
|
|
461
|
+
group.appendChild(shapeEl);
|
|
462
|
+
}
|
|
463
|
+
return shapeEl;
|
|
464
|
+
};
|
|
177
465
|
if (node === null) {
|
|
178
|
-
this._cell(text, {}, left, top, width, attrs);
|
|
179
|
-
return;
|
|
466
|
+
return inGroup(this._cell(text, {}, left, top, width, attrs));
|
|
180
467
|
}
|
|
181
468
|
// Room for the ▸ / ▾ on every member, so a level down is indented whether or not it has one.
|
|
182
469
|
const labelLeft = left + 14;
|
|
183
470
|
if (this.view.collapsible(node)) {
|
|
184
471
|
const sign = this.view.isCollapsed(axis, node) ? PivotGridChart.COLLAPSED : PivotGridChart.EXPANDED;
|
|
185
|
-
const toggle = this._text(sign, left + PAD_X + 3, top, { textAnchor: 'middle', fill: '#555' });
|
|
472
|
+
const toggle = inGroup(this._text(sign, left + PAD_X + 3, top, { textAnchor: 'middle', fill: '#555' }));
|
|
186
473
|
this._on(interactive, [toggle], () => this.view.toggle(axis, node), this.view.isCollapsed(axis, node) ? 'Expand' : 'Collapse');
|
|
187
474
|
}
|
|
188
|
-
const label = this._cell(text, {}, labelLeft, top, width - (labelLeft - left), attrs);
|
|
475
|
+
const label = inGroup(this._cell(text, {}, labelLeft, top, width - (labelLeft - left), attrs));
|
|
189
476
|
this._on(interactive, [label], (e) => this.showMenu(e.clientX, e.clientY, this.view.memberMenu(axis, node)), `${text} menu`);
|
|
477
|
+
return label;
|
|
190
478
|
}
|
|
191
479
|
}
|
|
@@ -746,6 +746,88 @@ export class PivotView {
|
|
|
746
746
|
return layout;
|
|
747
747
|
}
|
|
748
748
|
|
|
749
|
+
/**
|
|
750
|
+
* The layout keeping only the specified `values` for `dimension` (multi-selection slice & dice).
|
|
751
|
+
* @param {string} dimension
|
|
752
|
+
* @param {Array<any>|any} values
|
|
753
|
+
* @returns {object} layout
|
|
754
|
+
*/
|
|
755
|
+
withMemberKeep(dimension, values) {
|
|
756
|
+
const keep = Array.isArray(values) ? values : [values];
|
|
757
|
+
const layout = this.layout();
|
|
758
|
+
layout.members = [...layout.members.filter(m => m.dimension !== dimension), { dimension, keep }];
|
|
759
|
+
return layout;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* The layout excluding the specified `values` for `dimension`.
|
|
764
|
+
* @param {string} dimension
|
|
765
|
+
* @param {Array<any>|any} values
|
|
766
|
+
* @returns {object} layout
|
|
767
|
+
*/
|
|
768
|
+
withMemberRemove(dimension, values) {
|
|
769
|
+
const removeList = Array.isArray(values) ? values : [values];
|
|
770
|
+
const layout = this.layout();
|
|
771
|
+
const current = layout.members.find(m => m.dimension === dimension);
|
|
772
|
+
const others = layout.members.filter(m => m.dimension !== dimension);
|
|
773
|
+
const combined = Array.from(new Set([...(current?.remove ?? []), ...removeList]));
|
|
774
|
+
layout.members = [...others, { dimension, remove: combined }];
|
|
775
|
+
return layout;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Returns unique member values for a given dimension across facts or rows.
|
|
780
|
+
* @param {string} dimension
|
|
781
|
+
* @returns {Array<any>}
|
|
782
|
+
*/
|
|
783
|
+
uniqueMembersOf(dimension) {
|
|
784
|
+
const pool = this.facts ?? this.rows;
|
|
785
|
+
const set = new Set();
|
|
786
|
+
pool.forEach(r => {
|
|
787
|
+
const val = r[dimension];
|
|
788
|
+
if (val != null) set.add(val);
|
|
789
|
+
});
|
|
790
|
+
return Array.from(set);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Opens a multi-member slice & dice filter dialog for `dimension`.
|
|
795
|
+
* @param {string} dimension
|
|
796
|
+
*/
|
|
797
|
+
showMemberFilterDialog(dimension) {
|
|
798
|
+
if (!this.onLayout || !dimension) return;
|
|
799
|
+
const all = this.uniqueMembersOf(dimension);
|
|
800
|
+
if (!all.length) return;
|
|
801
|
+
const currentFilter = this.memberFilterOf(dimension);
|
|
802
|
+
const selected = currentFilter
|
|
803
|
+
? (currentFilter.keep ?? all.filter(m => !currentFilter.remove?.includes(m)))
|
|
804
|
+
: all;
|
|
805
|
+
|
|
806
|
+
if (typeof document !== 'undefined') {
|
|
807
|
+
import('../FormDialog.js').then(({ FormDialog }) => {
|
|
808
|
+
new FormDialog({
|
|
809
|
+
title: `Filter ${dimension}`,
|
|
810
|
+
fields: [
|
|
811
|
+
{
|
|
812
|
+
key: 'members',
|
|
813
|
+
label: 'Members to include',
|
|
814
|
+
type: 'checklist',
|
|
815
|
+
choices: all.map(m => [m, TableChart.cellText(m)]),
|
|
816
|
+
value: selected,
|
|
817
|
+
},
|
|
818
|
+
],
|
|
819
|
+
onApply: ({ members }) => {
|
|
820
|
+
if (!members || members.length === all.length) {
|
|
821
|
+
this.onLayout(this.allMembers(dimension));
|
|
822
|
+
} else {
|
|
823
|
+
this.onLayout(this.withMemberKeep(dimension, members));
|
|
824
|
+
}
|
|
825
|
+
},
|
|
826
|
+
}).open();
|
|
827
|
+
}).catch(() => {});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
749
831
|
/** A member's keep-or-remove items, the host's through onLayout; none for a NULL member (no filter holds one). */
|
|
750
832
|
_memberFilterItems(dimension, node) {
|
|
751
833
|
if (!this.onLayout || !dimension) return [];
|