@peter.prib/renderengine 0.13.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 +480 -302
- package/bin/renderengine.mjs +157 -0
- package/dist/renderengine-workspace.umd.js +49217 -37484
- package/dist/renderengine.umd.js +24899 -13093
- package/documentation/REFERENCE.md +68 -1
- package/package.json +13 -3
- 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 +269 -3
- 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 +7 -2
- 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 +142 -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/ask/AskBar.js +388 -0
- package/src/charting/ask/AskView.js +400 -0
- package/src/charting/charts/AlluvialDiagram.js +311 -0
- package/src/charting/charts/AreaChart.js +69 -15
- package/src/charting/charts/BarChart.js +38 -3
- package/src/charting/charts/BarChart3D.js +6 -4
- package/src/charting/charts/BarChart3DOrbit.js +3 -1
- package/src/charting/charts/BarChartBase.js +26 -6
- package/src/charting/charts/BeeswarmChart.js +164 -0
- package/src/charting/charts/BoxPlotChart.js +25 -3
- package/src/charting/charts/BubbleChart.js +4 -1
- package/src/charting/charts/CalendarHeatmapChart.js +2 -1
- package/src/charting/charts/CandlestickChart.js +172 -4
- package/src/charting/charts/ChordDiagram.js +107 -4
- 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/DensityChart.js +1 -1
- 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 +5 -2
- 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 +6 -3
- package/src/charting/charts/HexbinChart.js +137 -0
- package/src/charting/charts/HierarchyChart.js +77 -3
- package/src/charting/charts/HistogramChart.js +2 -2
- package/src/charting/charts/HorizonChart.js +105 -0
- package/src/charting/charts/IcicleChart.js +47 -0
- package/src/charting/charts/KagiChart.js +167 -0
- package/src/charting/charts/LineChart.js +74 -18
- package/src/charting/charts/LollipopChart.js +104 -0
- package/src/charting/charts/MarimekkoChart.js +75 -0
- package/src/charting/charts/OhlcChart.js +79 -0
- package/src/charting/charts/OutlineChart.js +10 -8
- package/src/charting/charts/ParallelCoordinatesChart.js +11 -3
- package/src/charting/charts/ParetoChart.js +92 -0
- package/src/charting/charts/PieChart.js +195 -17
- package/src/charting/charts/PieChart3D.js +137 -0
- package/src/charting/charts/PipeChart.js +5 -2
- package/src/charting/charts/PointAndFigureChart.js +198 -0
- package/src/charting/charts/PolarAreaChart.js +189 -0
- package/src/charting/charts/PopulationPyramidChart.js +4 -4
- package/src/charting/charts/RadarChart.js +12 -5
- package/src/charting/charts/RadialBarChart.js +6 -3
- package/src/charting/charts/RenkoChart.js +178 -0
- package/src/charting/charts/RidgelineChart.js +113 -0
- package/src/charting/charts/SankeyDiagram.js +129 -6
- package/src/charting/charts/ScatterChart.js +19 -3
- 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 +125 -11
- package/src/charting/charts/StackedBarChart3DOrbit.js +14 -6
- package/src/charting/charts/StreamGraphChart.js +18 -3
- package/src/charting/charts/SunburstChart.js +28 -8
- package/src/charting/charts/SurfaceChart3D.js +157 -0
- package/src/charting/charts/TreemapChart.js +31 -11
- package/src/charting/charts/ViolinChart.js +20 -2
- package/src/charting/charts/WaterfallChart.js +2 -2
- package/src/charting/charts/WordCloudChart.js +2 -2
- 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 +310 -13
- package/src/charting/pivot/PivotGridChart.js +322 -34
- package/src/charting/pivot/PivotView.js +626 -21
- 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 -2899
- 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
|
@@ -15,16 +15,210 @@ export class PivotChart {
|
|
|
15
15
|
* @param {string} [options.type] a catalog key to prefer while it fits (else ChartChooser's best)
|
|
16
16
|
* @param {ChartChooser} [options.chooser]
|
|
17
17
|
*/
|
|
18
|
-
constructor(view, { type = null, chooser = null } = {}) {
|
|
18
|
+
constructor(view, { type = null, chooser = null, facetBy = null } = {}) {
|
|
19
19
|
this.view = view;
|
|
20
20
|
this.type = type;
|
|
21
|
+
this.facetBy = facetBy;
|
|
21
22
|
this.chooser = chooser ?? new ChartChooser();
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
/** The charts that fit what the grid shows, best first. The grid itself is the table. */
|
|
25
26
|
choices() {
|
|
26
|
-
const
|
|
27
|
-
|
|
27
|
+
const chartData = this.view.chartData();
|
|
28
|
+
const { rows, columns, complete } = chartData;
|
|
29
|
+
const base = this.chooser.choose(rows, columns, { complete }).filter(c => c.key !== 'table');
|
|
30
|
+
const seen = new Set(base.map(c => c.key));
|
|
31
|
+
const extra = this._cubeChoices(chartData).filter(c => !seen.has(c.key) && this.chooser.registry.get(c.key));
|
|
32
|
+
return [...base, ...extra];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Additional chart types suited to a cube's hierarchy, series, and matrix structure beyond the
|
|
37
|
+
* general flat-table defaults in `ChartChooser`.
|
|
38
|
+
*/
|
|
39
|
+
_cubeChoices({ rows, columns, complete, hierarchy, rowTotals = null, primaryMeasure = null }) {
|
|
40
|
+
if (!rows || rows.length === 0) return [];
|
|
41
|
+
const categories = columns.filter(c => c.kind !== 'numeric');
|
|
42
|
+
const numbers = columns.filter(c => c.kind === 'numeric');
|
|
43
|
+
if (categories.length !== 1 || numbers.length === 0) return [];
|
|
44
|
+
const category = categories[0];
|
|
45
|
+
const add = (key, reason, data, options) => ({
|
|
46
|
+
key,
|
|
47
|
+
label: this.chooser.registry.get(key)?.label ?? key,
|
|
48
|
+
reason,
|
|
49
|
+
data,
|
|
50
|
+
...(options ? { options } : {}),
|
|
51
|
+
});
|
|
52
|
+
const out = [];
|
|
53
|
+
|
|
54
|
+
if (numbers.length === 1) {
|
|
55
|
+
const number = numbers[0];
|
|
56
|
+
const points = rows.filter(r => r[number.name] != null).map(r => ({
|
|
57
|
+
label: String(r[category.name]),
|
|
58
|
+
value: r[number.name],
|
|
59
|
+
}));
|
|
60
|
+
if (points.length < 2) return out;
|
|
61
|
+
this._addSingleSeriesChoices(out, add, category, number, points, complete, hierarchy, false);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Multi-series (2+ numeric series from column-axis members or multiple measures).
|
|
66
|
+
const gaps = rows.some(r => numbers.some(n => r[n.name] == null));
|
|
67
|
+
if (gaps) return out;
|
|
68
|
+
|
|
69
|
+
const names = numbers.map(n => n.name).join(', ');
|
|
70
|
+
const seriesData = numbers.map(n => ({
|
|
71
|
+
name: n.name,
|
|
72
|
+
values: rows.map(r => ({ label: String(r[category.name]), value: r[n.name] })),
|
|
73
|
+
}));
|
|
74
|
+
const allNonNeg = rows.every(r => numbers.every(n => r[n.name] >= 0));
|
|
75
|
+
const additiveNonNeg = allNonNeg && numbers.every(n => n.measure !== 'semi-additive' && n.measure !== 'non-additive');
|
|
76
|
+
|
|
77
|
+
if (additiveNonNeg) {
|
|
78
|
+
out.push(add('stackedBar', `${names} stacked for each ${category.name}`, seriesData));
|
|
79
|
+
out.push(add('marimekko', `${names} share and total across ${category.name}, as a Marimekko mosaic`, seriesData));
|
|
80
|
+
}
|
|
81
|
+
out.push(add('lollipop', `${names} for each ${category.name}, as stems and dots`, seriesData));
|
|
82
|
+
if (allNonNeg && rows.length >= 2) {
|
|
83
|
+
out.push(add('area', `${names} across ${category.name}, filled`, seriesData));
|
|
84
|
+
}
|
|
85
|
+
if (additiveNonNeg && rows.length >= 2) {
|
|
86
|
+
out.push(add('stackedArea', `${names} stacked across ${category.name}`, seriesData));
|
|
87
|
+
out.push(add('streamGraph', `${names} flowing across ${category.name}`, seriesData));
|
|
88
|
+
}
|
|
89
|
+
if (allNonNeg && rows.length >= 2) {
|
|
90
|
+
out.push(add('pipe', `${names} tapering across ${category.name}`, seriesData.map(s => ({
|
|
91
|
+
name: s.name,
|
|
92
|
+
values: s.values.map(v => ({ label: v.label, value: v.value, metric: v.value })),
|
|
93
|
+
}))));
|
|
94
|
+
}
|
|
95
|
+
const heatData = rows.flatMap(r => numbers.map(n => ({
|
|
96
|
+
x: String(r[category.name]),
|
|
97
|
+
y: n.name,
|
|
98
|
+
value: r[n.name],
|
|
99
|
+
})));
|
|
100
|
+
out.push(add('heatmap', `${names} for each ${category.name}, as a colour grid`, heatData));
|
|
101
|
+
if (allNonNeg && rows.length >= 3 && rows.length <= 12) {
|
|
102
|
+
out.push(add('radar', `${names} across ${category.name} axes`, seriesData.map(s => ({
|
|
103
|
+
name: s.name,
|
|
104
|
+
values: s.values.map(v => ({ axis: v.label, value: v.value })),
|
|
105
|
+
}))));
|
|
106
|
+
}
|
|
107
|
+
if (rows.length >= 2) {
|
|
108
|
+
out.push(add('parallelCoordinates', `${names} across parallel axes for each ${category.name}`,
|
|
109
|
+
rows.map(r => ({
|
|
110
|
+
label: String(r[category.name]),
|
|
111
|
+
category: String(r[category.name]),
|
|
112
|
+
...Object.fromEntries(numbers.map(n => [n.name, r[n.name]])),
|
|
113
|
+
})),
|
|
114
|
+
{ dimensions: numbers.map(n => n.name) }));
|
|
115
|
+
if (allNonNeg) {
|
|
116
|
+
out.push(add('populationPyramid', `${numbers[0].name} vs ${numbers[1].name} for each ${category.name}`,
|
|
117
|
+
rows.map(r => ({ label: String(r[category.name]), left: r[numbers[0].name], right: r[numbers[1].name] })),
|
|
118
|
+
{ leftLabel: numbers[0].name, rightLabel: numbers[1].name, showValues: true }));
|
|
119
|
+
out.push(add('dumbbell', `${numbers[0].name} to ${numbers[1].name} gap for each ${category.name}`,
|
|
120
|
+
rows.map(r => ({ label: String(r[category.name]), start: r[numbers[0].name], end: r[numbers[1].name] })),
|
|
121
|
+
{ startLabel: numbers[0].name, endLabel: numbers[1].name, showDelta: true }));
|
|
122
|
+
out.push(add('slopegraph', `${names} trajectory across ${category.name}`,
|
|
123
|
+
rows.map(r => ({ label: String(r[category.name]), values: numbers.map(n => r[n.name]) })),
|
|
124
|
+
{ stageLabels: numbers.map(n => n.name) }));
|
|
125
|
+
const flowLinks = rows.flatMap(r => numbers
|
|
126
|
+
.filter(n => r[n.name] > 0)
|
|
127
|
+
.map(n => ({ source: String(r[category.name]), target: n.name, value: r[n.name] })));
|
|
128
|
+
if (flowLinks.length > 0) {
|
|
129
|
+
out.push(add('sankey', `flow from ${category.name} to ${names}`, { links: flowLinks }));
|
|
130
|
+
out.push(add('chord', `chords between ${category.name} and ${names}`, { links: flowLinks }));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (rows.length >= 3) {
|
|
134
|
+
const distData = numbers.map(n => ({ label: n.name, values: rows.map(r => r[n.name]) }));
|
|
135
|
+
out.push(add('boxPlot', `distribution of ${category.name} values for each of ${names}`, distData));
|
|
136
|
+
out.push(add('density', `density curve of ${category.name} values for each of ${names}`, distData));
|
|
137
|
+
out.push(add('violin', `violin distribution of ${category.name} values for each of ${names}`, distData));
|
|
138
|
+
out.push(add('ridgeline', `ridgeline joy plot of ${category.name} values for each of ${names}`, distData));
|
|
139
|
+
out.push(add('beeswarm', `beeswarm raincloud plot of ${category.name} values for each of ${names}`, distData));
|
|
140
|
+
}
|
|
141
|
+
out.push(add('scatter', `${numbers[0].name} against ${numbers[1].name} for each ${category.name}`,
|
|
142
|
+
rows.map(r => ({ x: r[numbers[0].name], y: r[numbers[1].name], label: String(r[category.name]) }))));
|
|
143
|
+
if (numbers.length >= 3 || (rowTotals && rowTotals.length === rows.length)) {
|
|
144
|
+
out.push(add('bubble', `${numbers[0].name} and ${numbers[1].name} sized by ${numbers[2]?.name ?? (primaryMeasure?.name || 'Total')}`,
|
|
145
|
+
rows.map((r, idx) => ({
|
|
146
|
+
x: r[numbers[0].name],
|
|
147
|
+
y: r[numbers[1].name],
|
|
148
|
+
z: Math.max(1, numbers[2] ? r[numbers[2].name] : rowTotals[idx].value),
|
|
149
|
+
label: String(r[category.name]),
|
|
150
|
+
}))));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (allNonNeg) {
|
|
154
|
+
out.push(add('bar3DOrbit', `${names} for each ${category.name}, in 3D`,
|
|
155
|
+
rows.flatMap(r => numbers.map(n => ({ x: String(r[category.name]), z: n.name, value: r[n.name] })))));
|
|
156
|
+
if (rows.length >= 2 && numbers.length >= 2) {
|
|
157
|
+
out.push(add('surface3D', `3D surface mesh of ${names} across ${category.name}`,
|
|
158
|
+
rows.flatMap(r => numbers.map(n => ({ x: String(r[category.name]), z: n.name, value: r[n.name] })))));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (additiveNonNeg) {
|
|
162
|
+
out.push(add('stackedBar3DOrbit', `${names} stacked for each ${category.name}, in 3D`,
|
|
163
|
+
rows.map(r => ({ x: String(r[category.name]), z: category.name, values: numbers.map(n => r[n.name]) })),
|
|
164
|
+
{ seriesLabels: numbers.map(n => n.name) }));
|
|
165
|
+
}
|
|
166
|
+
if (rowTotals && rowTotals.length >= 2 && primaryMeasure) {
|
|
167
|
+
this._addSingleSeriesChoices(out, add, category, primaryMeasure, rowTotals, complete, hierarchy, true);
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_addSingleSeriesChoices(out, add, category, number, points, complete, hierarchy, includeBase = false) {
|
|
173
|
+
const nonNeg = points.every(d => d.value >= 0);
|
|
174
|
+
if (includeBase) {
|
|
175
|
+
out.push(add('bar', `total ${number.name} for each ${category.name}`, points));
|
|
176
|
+
if (nonNeg && complete && number.measure === 'additive' && points.length <= 8) {
|
|
177
|
+
out.push(add('pie', `share of total ${number.name} across ${category.name}`, points, { exploded: 'max', hoverExplode: true }));
|
|
178
|
+
out.push(add('donut', `share of total ${number.name} across ${category.name}, as a ring`, points, { exploded: 'max', hoverExplode: true, showTotal: true }));
|
|
179
|
+
out.push(add('pie3D', `share of total ${number.name} across ${category.name}, in 3D`, points, { exploded: 'max', hoverExplode: true }));
|
|
180
|
+
out.push(add('donut3D', `share of total ${number.name} across ${category.name}, as a 3D ring`, points, { exploded: 'max', hoverExplode: true }));
|
|
181
|
+
}
|
|
182
|
+
} else if (nonNeg && complete && number.measure === 'additive' && points.length <= 8) {
|
|
183
|
+
out.push(add('pie3D', `share of total ${number.name} across ${category.name}, in 3D`, points, { exploded: 'max', hoverExplode: true }));
|
|
184
|
+
out.push(add('donut3D', `share of total ${number.name} across ${category.name}, as a 3D ring`, points, { exploded: 'max', hoverExplode: true }));
|
|
185
|
+
}
|
|
186
|
+
if (hierarchy) {
|
|
187
|
+
out.push(add('treemap', `${number.name} across the ${category.name} hierarchy`, hierarchy.tree));
|
|
188
|
+
out.push(add('sunburst', `${number.name} across the ${category.name} hierarchy, in rings`, hierarchy.tree));
|
|
189
|
+
out.push(add('circlePack', `${number.name} across the ${category.name} hierarchy, as packed circles`, hierarchy.tree));
|
|
190
|
+
out.push(add('icicle', `${number.name} across the ${category.name} hierarchy, as an icicle partition`, hierarchy.tree));
|
|
191
|
+
out.push(add('outline', `${number.name} across the ${category.name} hierarchy, as an outline`, hierarchy.tree));
|
|
192
|
+
}
|
|
193
|
+
if (number.measure === 'additive') {
|
|
194
|
+
out.push(add('waterfall', `how each ${category.name} adds to ${number.name}`, points));
|
|
195
|
+
if (nonNeg && complete) {
|
|
196
|
+
out.push(add('pareto', `cumulative share of ${number.name} across ${category.name}`, points));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (!includeBase) {
|
|
200
|
+
out.push(add('lollipop', `${number.name} for each ${category.name}, as stems and dots`, points));
|
|
201
|
+
if (nonNeg && points.length >= 2) {
|
|
202
|
+
out.push(add('pipe', `${number.name} tapering across ${category.name}`,
|
|
203
|
+
points.map(d => ({ label: d.label, value: d.value, metric: d.value }))));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (nonNeg && points.length <= 12) {
|
|
207
|
+
out.push(add('funnel', `${number.name} across ${category.name} stages`, points));
|
|
208
|
+
out.push(add('radialBar', `${number.name} for each ${category.name}, around a circle`, points));
|
|
209
|
+
out.push(add('polarArea', `${number.name} for each ${category.name}, as a 3D Nightingale Rose`, points, { extrude3D: true, exploded: 'max', hoverExplode: true }));
|
|
210
|
+
if (!includeBase && points.length >= 3) {
|
|
211
|
+
out.push(add('radar', `${number.name} across ${category.name} axes`, points.map(d => ({ axis: d.label, value: d.value }))));
|
|
212
|
+
}
|
|
213
|
+
out.push(add('wordCloud', `${category.name} sized by ${number.name}`, points.map(d => ({ text: d.label, value: d.value }))));
|
|
214
|
+
out.push(add('bar3D', `${number.name} for each ${category.name}, in 3D`, points));
|
|
215
|
+
}
|
|
216
|
+
if (points.length >= 3) {
|
|
217
|
+
const sample = points.map(d => d.value);
|
|
218
|
+
out.push(add('histogram', `frequency distribution of ${number.name} across ${category.name}`, sample));
|
|
219
|
+
out.push(add('qq', `normal Q-Q plot of ${number.name} across ${category.name}`, sample));
|
|
220
|
+
out.push(add('lag', `lag-1 autocorrelation of ${number.name} across ${category.name}`, sample));
|
|
221
|
+
}
|
|
28
222
|
}
|
|
29
223
|
|
|
30
224
|
/** The chart to draw: the one asked for while it still fits, else the best; null when none fits. */
|
|
@@ -33,18 +227,121 @@ export class PivotChart {
|
|
|
33
227
|
return choices.find(c => c.key === this.type) ?? choices[0] ?? null;
|
|
34
228
|
}
|
|
35
229
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
230
|
+
/** Sets the preferred chart type key (e.g. `'stackedBar'`, `'line'`, `'bar'`, `'pareto'`) and redraws if attached. */
|
|
231
|
+
setType(type) {
|
|
232
|
+
this.type = type;
|
|
233
|
+
this.redraw?.();
|
|
234
|
+
return this.chosen();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Sets or clears the facet dimension for small-multiples grids. */
|
|
238
|
+
setFacetBy(dimension) {
|
|
239
|
+
this.facetBy = dimension;
|
|
240
|
+
this.redraw?.();
|
|
241
|
+
return this.facetBy;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Dimensions available to facet small-multiples charts by. */
|
|
245
|
+
facetChoices() {
|
|
246
|
+
const pool = [...(this.view.pageAxis?.map(p => p.dimension) ?? []), ...this.view.rowAxis, ...this.view.columnAxis];
|
|
247
|
+
return Array.from(new Set(pool));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Splits chart data into small-multiple facets by `facetDimension`, calculating unified global scale bounds.
|
|
252
|
+
* @param {string} [facetDimension]
|
|
253
|
+
* @returns {{ facetDimension: string, facets: Array<object>, globalMax: number, globalMin: number }|null}
|
|
254
|
+
*/
|
|
255
|
+
facetData(facetDimension = this.facetBy) {
|
|
256
|
+
if (!facetDimension) return null;
|
|
257
|
+
const chartData = this.view.chartData();
|
|
258
|
+
const { rows, columns, complete } = chartData;
|
|
259
|
+
if (!rows || rows.length === 0) return null;
|
|
260
|
+
|
|
261
|
+
// Group rows by facetDimension
|
|
262
|
+
const groups = new Map();
|
|
263
|
+
rows.forEach(r => {
|
|
264
|
+
const key = r[facetDimension] ?? 'All';
|
|
265
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
266
|
+
groups.get(key).push(r);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (groups.size <= 1) return null;
|
|
270
|
+
|
|
271
|
+
const numCols = columns.filter(c => c.kind === 'numeric').map(c => c.name);
|
|
272
|
+
let globalMax = -Infinity;
|
|
273
|
+
let globalMin = Infinity;
|
|
274
|
+
rows.forEach(r => {
|
|
275
|
+
numCols.forEach(col => {
|
|
276
|
+
const val = r[col];
|
|
277
|
+
if (typeof val === 'number' && Number.isFinite(val)) {
|
|
278
|
+
if (val > globalMax) globalMax = val;
|
|
279
|
+
if (val < globalMin) globalMin = val;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const chosenChart = this.chosen();
|
|
285
|
+
const baseKey = chosenChart?.key ?? 'bar';
|
|
286
|
+
|
|
287
|
+
const facets = [];
|
|
288
|
+
groups.forEach((groupRows, facetValue) => {
|
|
289
|
+
const subChart = this.chooser.choose(groupRows, columns, { complete }).find(c => c.key === baseKey)
|
|
290
|
+
?? this.chooser.choose(groupRows, columns, { complete })[0];
|
|
291
|
+
facets.push({
|
|
292
|
+
key: facetValue,
|
|
293
|
+
label: String(facetValue),
|
|
294
|
+
chartKey: subChart?.key ?? baseKey,
|
|
295
|
+
data: subChart?.data ?? groupRows,
|
|
296
|
+
options: { ...(subChart?.options || {}), maxValue: globalMax, minValue: globalMin },
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
facetDimension,
|
|
302
|
+
facets,
|
|
303
|
+
globalMax: Number.isFinite(globalMax) ? globalMax : 1,
|
|
304
|
+
globalMin: Number.isFinite(globalMin) ? globalMin : 0,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** The chart's menu: "Chart as" each chart that fits (plus quick top-level items), the one showing ticked. */
|
|
309
|
+
menu(redraw = () => this.redraw?.()) {
|
|
38
310
|
const current = this.chosen()?.key;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
311
|
+
const all = this.choices();
|
|
312
|
+
const toItem = (c) => ({
|
|
313
|
+
label: `${c.label}${c.key === current ? ' ✓' : ''}`,
|
|
314
|
+
action: () => { this.type = c.key; redraw(); },
|
|
315
|
+
});
|
|
316
|
+
const quickKeys = new Set([
|
|
317
|
+
'groupedBar', 'stackedBar', 'bar', 'line', 'area', 'stackedArea',
|
|
318
|
+
'pareto', 'waterfall', 'marimekko', 'lollipop', 'heatmap', 'radar',
|
|
319
|
+
'treemap', 'sunburst', 'icicle', 'sankey', 'pie', 'donut', 'pie3D', 'donut3D', 'polarArea', 'surface3D',
|
|
320
|
+
]);
|
|
321
|
+
const quickItems = all.filter(c => quickKeys.has(c.key)).map(toItem);
|
|
322
|
+
|
|
323
|
+
const facetDims = this.facetChoices();
|
|
324
|
+
const facetItems = [
|
|
325
|
+
{ label: `(None)${!this.facetBy ? ' ✓' : ''}`, action: () => { this.setFacetBy(null); redraw(); } },
|
|
326
|
+
...facetDims.map(d => ({
|
|
327
|
+
label: `${d}${this.facetBy === d ? ' ✓' : ''}`,
|
|
328
|
+
action: () => { this.setFacetBy(d); redraw(); },
|
|
44
329
|
})),
|
|
45
|
-
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
return [
|
|
333
|
+
{
|
|
334
|
+
label: 'Chart as',
|
|
335
|
+
items: all.map(toItem),
|
|
336
|
+
},
|
|
337
|
+
...(facetDims.length > 0 ? [{
|
|
338
|
+
label: 'Facet by (Small Multiples)',
|
|
339
|
+
items: facetItems,
|
|
340
|
+
}] : []),
|
|
341
|
+
...(quickItems.length > 0 ? [{ separator: true }, ...quickItems] : []),
|
|
342
|
+
];
|
|
46
343
|
}
|
|
47
344
|
|
|
48
|
-
/** A click on a charted value: drill the grid at its member. */
|
|
49
|
-
click(datum) { return this.view.drillFrom(datum.label); }
|
|
345
|
+
/** A click on a charted value: drill the grid at its member (or its series' column member). */
|
|
346
|
+
click(datum) { return this.view.drillFrom(datum.label, datum.series ?? null); }
|
|
50
347
|
}
|