@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.
Files changed (124) hide show
  1. package/README.md +480 -302
  2. package/bin/renderengine.mjs +157 -0
  3. package/dist/renderengine-workspace.umd.js +49217 -37484
  4. package/dist/renderengine.umd.js +24899 -13093
  5. package/documentation/REFERENCE.md +68 -1
  6. package/package.json +13 -3
  7. package/src/CanvasRenderer.js +225 -200
  8. package/src/SVGRenderer.js +476 -454
  9. package/src/archimate.js +22 -2
  10. package/src/archimateArchiFormat.js +1735 -1701
  11. package/src/archimateArchive.js +36 -7
  12. package/src/archimateCSV.js +93 -37
  13. package/src/archimateDrawio.js +2 -0
  14. package/src/archimateExchangeFormat.js +3 -1
  15. package/src/chartCatalog.js +269 -3
  16. package/src/charting/Annotations.js +169 -0
  17. package/src/charting/ArchimateDiagramEditor.js +54 -10
  18. package/src/charting/Axis.js +217 -5
  19. package/src/charting/Chart.js +99 -9
  20. package/src/charting/Chart3D.js +7 -2
  21. package/src/charting/ChartBox.js +8 -1
  22. package/src/charting/ColorPalette.js +42 -4
  23. package/src/charting/DiagramEditor.js +174 -39
  24. package/src/charting/DiagramEditorCommands.js +28 -0
  25. package/src/charting/Edge.js +7 -0
  26. package/src/charting/Hierarchy.js +142 -0
  27. package/src/charting/InteractionManager.js +4 -0
  28. package/src/charting/ModelFileFormat.js +5 -1
  29. package/src/charting/SettingsManager.js +35 -0
  30. package/src/charting/ShapePaths.js +82 -10
  31. package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
  32. package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
  33. package/src/charting/ask/AskBar.js +388 -0
  34. package/src/charting/ask/AskView.js +400 -0
  35. package/src/charting/charts/AlluvialDiagram.js +311 -0
  36. package/src/charting/charts/AreaChart.js +69 -15
  37. package/src/charting/charts/BarChart.js +38 -3
  38. package/src/charting/charts/BarChart3D.js +6 -4
  39. package/src/charting/charts/BarChart3DOrbit.js +3 -1
  40. package/src/charting/charts/BarChartBase.js +26 -6
  41. package/src/charting/charts/BeeswarmChart.js +164 -0
  42. package/src/charting/charts/BoxPlotChart.js +25 -3
  43. package/src/charting/charts/BubbleChart.js +4 -1
  44. package/src/charting/charts/CalendarHeatmapChart.js +2 -1
  45. package/src/charting/charts/CandlestickChart.js +172 -4
  46. package/src/charting/charts/ChordDiagram.js +107 -4
  47. package/src/charting/charts/CirclePackChart.js +81 -0
  48. package/src/charting/charts/ComboChart.js +233 -0
  49. package/src/charting/charts/ContourChart.js +310 -0
  50. package/src/charting/charts/DensityChart.js +1 -1
  51. package/src/charting/charts/DonutChart3D.js +13 -0
  52. package/src/charting/charts/DumbbellChart.js +217 -0
  53. package/src/charting/charts/EcdfChart.js +150 -0
  54. package/src/charting/charts/FunnelChart.js +5 -2
  55. package/src/charting/charts/GeoMapChart.js +132 -11
  56. package/src/charting/charts/GroupedBarChart.js +44 -3
  57. package/src/charting/charts/GroupedDistributionChart.js +26 -1
  58. package/src/charting/charts/HeatmapChart.js +6 -3
  59. package/src/charting/charts/HexbinChart.js +137 -0
  60. package/src/charting/charts/HierarchyChart.js +77 -3
  61. package/src/charting/charts/HistogramChart.js +2 -2
  62. package/src/charting/charts/HorizonChart.js +105 -0
  63. package/src/charting/charts/IcicleChart.js +47 -0
  64. package/src/charting/charts/KagiChart.js +167 -0
  65. package/src/charting/charts/LineChart.js +74 -18
  66. package/src/charting/charts/LollipopChart.js +104 -0
  67. package/src/charting/charts/MarimekkoChart.js +75 -0
  68. package/src/charting/charts/OhlcChart.js +79 -0
  69. package/src/charting/charts/OutlineChart.js +10 -8
  70. package/src/charting/charts/ParallelCoordinatesChart.js +11 -3
  71. package/src/charting/charts/ParetoChart.js +92 -0
  72. package/src/charting/charts/PieChart.js +195 -17
  73. package/src/charting/charts/PieChart3D.js +137 -0
  74. package/src/charting/charts/PipeChart.js +5 -2
  75. package/src/charting/charts/PointAndFigureChart.js +198 -0
  76. package/src/charting/charts/PolarAreaChart.js +189 -0
  77. package/src/charting/charts/PopulationPyramidChart.js +4 -4
  78. package/src/charting/charts/RadarChart.js +12 -5
  79. package/src/charting/charts/RadialBarChart.js +6 -3
  80. package/src/charting/charts/RenkoChart.js +178 -0
  81. package/src/charting/charts/RidgelineChart.js +113 -0
  82. package/src/charting/charts/SankeyDiagram.js +129 -6
  83. package/src/charting/charts/ScatterChart.js +19 -3
  84. package/src/charting/charts/ScatterMatrixChart.js +119 -0
  85. package/src/charting/charts/SlopegraphChart.js +205 -0
  86. package/src/charting/charts/SparklineTableChart.js +166 -0
  87. package/src/charting/charts/StackedBarChart.js +125 -11
  88. package/src/charting/charts/StackedBarChart3DOrbit.js +14 -6
  89. package/src/charting/charts/StreamGraphChart.js +18 -3
  90. package/src/charting/charts/SunburstChart.js +28 -8
  91. package/src/charting/charts/SurfaceChart3D.js +157 -0
  92. package/src/charting/charts/TreemapChart.js +31 -11
  93. package/src/charting/charts/ViolinChart.js +20 -2
  94. package/src/charting/charts/WaterfallChart.js +2 -2
  95. package/src/charting/charts/WordCloudChart.js +2 -2
  96. package/src/charting/datamodel/ArchimateDataLink.js +70 -0
  97. package/src/charting/datamodel/DataLineage.js +150 -0
  98. package/src/charting/datamodel/DataModel.js +83 -1
  99. package/src/charting/datamodel/ModelMapping.js +87 -0
  100. package/src/charting/datamodel/SqlDdl.js +110 -85
  101. package/src/charting/datamodel/index.js +3 -0
  102. package/src/charting/decision/DecisionWorkbench.js +295 -0
  103. package/src/charting/diagrams/ArchimateDiagram.js +25 -9
  104. package/src/charting/diagrams/ArchimateModel.js +1382 -1238
  105. package/src/charting/diagrams/GraphDiagram.js +1424 -1350
  106. package/src/charting/diagrams/SketchView.js +121 -0
  107. package/src/charting/diagrams/sketchBoxKinds.js +30 -0
  108. package/src/charting/modelTree/AnalysisPanel.js +101 -23
  109. package/src/charting/modelTree/NavigatorModel.js +410 -0
  110. package/src/charting/modelTree/NavigatorPanel.js +453 -0
  111. package/src/charting/pivot/PivotChart.js +310 -13
  112. package/src/charting/pivot/PivotGridChart.js +322 -34
  113. package/src/charting/pivot/PivotView.js +626 -21
  114. package/src/charting/report/TemplatedReport.js +547 -0
  115. package/src/charting/templates/BoxTemplate.js +28 -27
  116. package/src/charting/templates/PackageTemplate.js +26 -25
  117. package/src/charting/templates/SwimlaneTemplate.js +19 -18
  118. package/src/charting/templates/Template.js +219 -212
  119. package/src/cli.js +146 -0
  120. package/src/index.js +3370 -2899
  121. package/src/workspace/Workspace.js +126 -7
  122. package/src/workspace/archimateServices.js +4 -0
  123. package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
  124. package/src/workspace/styles.js +36 -1
@@ -0,0 +1,166 @@
1
+ import { Axis } from '../Axis.js';
2
+ import { Chart } from '../Chart.js';
3
+ import { ChartBox, ChartLabels } from '../ChartBox.js';
4
+ import { ColorPalette } from '../ColorPalette.js';
5
+
6
+ /**
7
+ * SparklineTableChart — executive multi-metric table embedding inline
8
+ * sparklines, trend change badges, and bullet KPI performance bars per row.
9
+ *
10
+ * Data shape:
11
+ * Array<{
12
+ * label: string,
13
+ * value: number,
14
+ * target?: number,
15
+ * sparkline?: number[],
16
+ * delta?: number,
17
+ * status?: 'good'|'warn'|'bad'
18
+ * }>
19
+ */
20
+ export class SparklineTableChart extends Chart {
21
+ static chartType = 'sparklineTable';
22
+
23
+ _render() {
24
+ const data = this.data;
25
+ if (!Array.isArray(data) || !data.length) return;
26
+
27
+ const options = this.options;
28
+ const box = ChartBox.fromRenderer(this.renderer);
29
+ const {
30
+ x = box.x, y = box.y, width = box.width, height = box.height,
31
+ title, rowHeight = 44, colors,
32
+ } = options;
33
+ const plotBox = { x, y, width, height };
34
+
35
+ new ChartLabels({ title }).draw(this.renderer, plotBox);
36
+
37
+ const palette = new ColorPalette(colors);
38
+ const totalRows = data.length;
39
+ const headerH = 26;
40
+ const startY = y + headerH;
41
+
42
+ // Column widths
43
+ const colLabelW = Math.max(90, width * 0.22);
44
+ const colValueW = Math.max(65, width * 0.16);
45
+ const colTrendW = Math.max(65, width * 0.15);
46
+ const colSparkW = Math.max(100, width * 0.24);
47
+ const colBulletW = Math.max(80, width - colLabelW - colValueW - colTrendW - colSparkW);
48
+
49
+ const colLabelX = x;
50
+ const colValueX = colLabelX + colLabelW;
51
+ const colTrendX = colValueX + colValueW;
52
+ const colSparkX = colTrendX + colTrendW;
53
+ const colBulletX = colSparkX + colSparkW;
54
+
55
+ // Table Header
56
+ this.renderer.rect(x, y, width, headerH, 'rgba(0,0,0,0.03)', { color: '#e0e0e0', width: 1 });
57
+ const textH = y + headerH / 2;
58
+ this.renderer.text('Metric / Entity', colLabelX + 8, textH, { textAnchor: 'start', verticalAlign: 'middle', fontSize: '10px', fontWeight: 'bold', fill: '#666' });
59
+ this.renderer.text('Current', colValueX + colValueW - 8, textH, { textAnchor: 'end', verticalAlign: 'middle', fontSize: '10px', fontWeight: 'bold', fill: '#666' });
60
+ this.renderer.text('Change', colTrendX + colTrendW / 2, textH, { textAnchor: this.renderer.centerTextAnchor, verticalAlign: 'middle', fontSize: '10px', fontWeight: 'bold', fill: '#666' });
61
+ this.renderer.text('History', colSparkX + colSparkW / 2, textH, { textAnchor: this.renderer.centerTextAnchor, verticalAlign: 'middle', fontSize: '10px', fontWeight: 'bold', fill: '#666' });
62
+ this.renderer.text('Target Performance', colBulletX + 8, textH, { textAnchor: 'start', verticalAlign: 'middle', fontSize: '10px', fontWeight: 'bold', fill: '#666' });
63
+
64
+ // Global max for bullet bars
65
+ const allTargetsAndValues = data.flatMap(d => {
66
+ const v = Number(d.value ?? d.current);
67
+ const t = Number(d.target ?? v);
68
+ return [Number.isFinite(v) ? v : 0, Number.isFinite(t) ? t : 0];
69
+ });
70
+ const maxBulletVal = Math.max(...allTargetsAndValues, 1);
71
+
72
+ data.forEach((row, i) => {
73
+ const rowY = startY + i * rowHeight;
74
+ if (rowY + rowHeight > y + height) return;
75
+
76
+ // Zebra striping
77
+ const bg = i % 2 === 0 ? '#ffffff' : 'rgba(0,0,0,0.015)';
78
+ this.renderer.rect(x, rowY, width, rowHeight, bg, { color: '#f0f0f0', width: 1 });
79
+ const midY = rowY + rowHeight / 2;
80
+
81
+ // 1. Label
82
+ const label = String(row.label ?? row.metric ?? row.kpi ?? row.name ?? `Metric ${i + 1}`);
83
+ this.renderer.text(label, colLabelX + 8, midY, {
84
+ textAnchor: 'start', verticalAlign: 'middle', fontSize: '12px', fontWeight: '600', fill: '#222',
85
+ });
86
+
87
+ // 2. Current Value
88
+ const rawVal = row.value ?? row.current;
89
+ const valStr = rawVal != null ? (typeof rawVal === 'string' ? rawVal : Axis.formatValue(rawVal)) : '';
90
+ this.renderer.text(valStr, colValueX + colValueW - 8, midY, {
91
+ textAnchor: 'end', verticalAlign: 'middle', fontSize: '12px', fontWeight: 'bold', fill: '#111',
92
+ });
93
+
94
+ // 3. Trend Delta Badge
95
+ const history = Array.isArray(row.sparkline) ? row.sparkline : (Array.isArray(row.trend) ? row.trend : (Array.isArray(row.history) ? row.history : null));
96
+ const delta = row.delta ?? (typeof row.change === 'number' ? row.change : null) ?? (history?.length > 1 ? history[history.length - 1] - history[0] : null);
97
+ const deltaStr = typeof row.change === 'string' ? row.change : (delta != null ? `${delta >= 0 ? '+' : ''}${Axis.formatValue(delta)}` : null);
98
+ if (deltaStr != null) {
99
+ const isUp = delta != null ? delta >= 0 : !deltaStr.startsWith('-');
100
+ const badgeColor = isUp ? '#27ae60' : '#e74c3c';
101
+ const badgeBg = isUp ? 'rgba(39, 174, 96, 0.12)' : 'rgba(231, 76, 60, 0.12)';
102
+ const badgeW = 54, badgeH = 18;
103
+ const badgeX = colTrendX + (colTrendW - badgeW) / 2;
104
+ const badgeY = midY - badgeH / 2;
105
+
106
+ this.renderer.rect(badgeX, badgeY, badgeW, badgeH, badgeBg, { color: badgeColor, width: 1 }, { rx: 3, ry: 3 });
107
+ this.renderer.text(deltaStr, colTrendX + colTrendW / 2, midY, {
108
+ textAnchor: this.renderer.centerTextAnchor, verticalAlign: 'middle', fontSize: '10px', fontWeight: 'bold', fill: badgeColor,
109
+ });
110
+ }
111
+
112
+ // 4. Inline Sparkline
113
+ if (history && history.length > 1) {
114
+ const sPadX = 10, sPadY = 8;
115
+ const sw = colSparkW - sPadX * 2;
116
+ const sh = rowHeight - sPadY * 2;
117
+ const sx = colSparkX + sPadX;
118
+ const sy = rowY + sPadY;
119
+
120
+ const sMin = Math.min(...history);
121
+ const sMax = Math.max(...history);
122
+ const sRange = (sMax - sMin) || 1;
123
+ const sStep = sw / (history.length - 1);
124
+
125
+ const pts = history.map((v, idx) => ({
126
+ px: sx + idx * sStep,
127
+ py: sy + sh - ((v - sMin) / sRange) * sh,
128
+ }));
129
+ const sparkD = 'M' + pts.map(p => `${p.px.toFixed(1)},${p.py.toFixed(1)}`).join(' L');
130
+ const sparkColor = palette.colorAt(i);
131
+ this.renderer.path(sparkD, 'none', { color: sparkColor, width: 1.6, 'stroke-linecap': 'round' });
132
+
133
+ // Endpoint accent dot
134
+ const last = pts[pts.length - 1];
135
+ this.renderer.circle(last.px, last.py, 2.5, sparkColor, { color: '#ffffff', width: 1 });
136
+ }
137
+
138
+ // 5. Bullet Performance Bar
139
+ const bPadX = 8;
140
+ const bw = Math.max(10, colBulletW - bPadX * 2);
141
+ const bh = 14;
142
+ const bx = colBulletX + bPadX;
143
+ const by = midY - bh / 2;
144
+
145
+ // Range background track
146
+ this.renderer.rect(bx, by, bw, bh, '#e8ecef', null, { rx: 2, ry: 2 });
147
+ // Qualitative 70% threshold band
148
+ this.renderer.rect(bx, by, bw * 0.7, bh, '#d5dbdb', null);
149
+
150
+ // Performance value bar
151
+ const numVal = Number(row.value ?? row.current);
152
+ const valRatio = Number.isFinite(numVal) ? Math.min(1, Math.max(0, numVal / maxBulletVal)) : 0.5;
153
+ const barW = Math.max(2, valRatio * bw);
154
+ const barColor = row.status === 'bad' || row.status === 'Attention' ? '#e74c3c' : (row.status === 'warn' ? '#f39c12' : '#2c3e50');
155
+ this.renderer.rect(bx, by + 3, barW, bh - 6, barColor, null);
156
+
157
+ // Target marker line
158
+ const numTarget = Number(row.target);
159
+ if (Number.isFinite(numTarget)) {
160
+ const targetRatio = Math.min(1, Math.max(0, numTarget / maxBulletVal));
161
+ const targetX = bx + targetRatio * bw;
162
+ this.renderer.path(`M${targetX},${by - 2} L${targetX},${by + bh + 2}`, 'none', { color: '#e74c3c', width: 2.2 });
163
+ }
164
+ });
165
+ }
166
+ }
@@ -1,35 +1,149 @@
1
1
  import { Axis } from '../Axis.js';
2
+ import { Legend } from '../Legend.js';
2
3
  import { BarChartBase } from './BarChartBase.js';
4
+ import { TableChart } from './TableChart.js';
3
5
 
4
6
  /**
5
7
  * StackedBarChart — vertical stacked bar chart.
8
+ *
9
+ * Accepts either:
10
+ * - Multi-series `Array<{name: string, values: Array<{label: string, value: number}>}>` (the same
11
+ * shape as `GroupedBarChart`, `LineChart` and `StackedAreaChart`), or
12
+ * - Flat per-category stacks `Array<{label: string, values: number[]}>` with optional
13
+ * `options.seriesLabels`.
14
+ *
15
+ * Draws a numeric Y axis (`_drawYAxis`), optional series legend (`legend: true`), and records each
16
+ * segment with `this._datum()` so `onDatumClick` / `PivotChart` drilling works on every segment.
6
17
  */
7
18
  export class StackedBarChart extends BarChartBase {
8
19
  static chartType = 'stackedBar';
9
20
 
21
+ static _normalize(data, seriesLabels = null) {
22
+ if (!Array.isArray(data) || data.length === 0) return { categories: [], seriesNames: [] };
23
+ if (Array.isArray(data[0]?.values) && typeof data[0].values[0] === 'object' && data[0].values[0] !== null) {
24
+ const labels = [...new Set(data.flatMap(s => s.values.map(v => v.label)))];
25
+ const maps = data.map(s => new Map(s.values.map(v => [v.label, v.value])));
26
+ return {
27
+ seriesNames: data.map((s, i) => s.name ?? seriesLabels?.[i] ?? `Series ${i + 1}`),
28
+ categories: labels.map(label => ({ label, values: maps.map(m => m.get(label) ?? 0) })),
29
+ };
30
+ }
31
+ const maxLen = Math.max(0, ...data.map(d => d.values?.length ?? 0));
32
+ return {
33
+ seriesNames: Array.from({ length: maxLen }, (_, i) => seriesLabels?.[i] ?? `Series ${i + 1}`),
34
+ categories: data.map(d => ({ label: d.label, values: (d.values ?? []).map(v => v ?? 0) })),
35
+ };
36
+ }
37
+
38
+ _setup() {
39
+ this._normalized = StackedBarChart._normalize(this.data, this.options.seriesLabels);
40
+ super._setup();
41
+ if (this.options.legend && this.options.width === undefined && this._normalized.seriesNames.length > 0) {
42
+ this._plotBox.width = Legend.plotWidthBeside(this._plotBox, this.renderer.width, this._legendOptions());
43
+ }
44
+ }
45
+
46
+ _legendOptions() {
47
+ return {
48
+ items: this._normalized.seriesNames.map((name, i) => ({
49
+ label: name,
50
+ color: this._palette.colorAt(i),
51
+ hidden: this._dataFilter.has(i),
52
+ dataIndex: i,
53
+ })),
54
+ itemHeight: 20, markerSize: 10,
55
+ };
56
+ }
57
+
58
+ _isNormalized() {
59
+ return Boolean(this.options.normalize || this.options.stackMode === 'percent');
60
+ }
61
+
10
62
  _computeMaxValue() {
11
- const data = this.data;
12
- return Math.max(...data.map(d => d.values.reduce((sum, val) => sum + val, 0)));
63
+ if (this._isNormalized()) {
64
+ return this._isHorizontal() ? this._drawXAxis(100) : this._drawYAxis(100);
65
+ }
66
+ const { categories } = this._normalized;
67
+ const rawMax = Math.max(0, ...categories.map(d => d.values.reduce((sum, val, idx) => sum + (this._dataFilter.has(idx) ? 0 : Math.max(0, val)), 0))) || 1;
68
+ return this._isHorizontal() ? this._drawXAxis(rawMax) : this._drawYAxis(rawMax);
13
69
  }
14
70
 
15
71
  _renderBars(maxStackValue) {
16
- const data = this.data;
72
+ const { categories, seriesNames } = this._normalized;
73
+ if (categories.length === 0) return;
17
74
  const { x, y, width, height } = this._plotBox;
18
- const labelStep = Axis.categoryLabelStep(data.map(d => d.label), width);
19
- const { bandWidth, margin } = this._bandScale(data.length);
75
+ const normalize = this._isNormalized();
76
+ const labels = categories.map(d => d.label);
77
+ const { bandWidth, margin } = this._bandScale(categories.length);
20
78
  const textAnchor = this.renderer.centerTextAnchor;
21
79
 
22
- data.forEach((d, i) => {
80
+ if (this._isHorizontal()) {
81
+ categories.forEach((d, i) => {
82
+ let currentX = x;
83
+ const barY = y + i * (bandWidth + margin);
84
+ const colTotal = d.values.reduce((sum, val, idx) => sum + (this._dataFilter.has(idx) ? 0 : Math.max(0, val ?? 0)), 0);
85
+ d.values.forEach((value, j) => {
86
+ if (this._dataFilter.has(j)) return;
87
+ if (value == null || value <= 0) return;
88
+ const plotted = normalize ? (colTotal > 0 ? (value / colTotal) * 100 : 0) : value;
89
+ const barWidth = (plotted / maxStackValue) * width;
90
+ const series = seriesNames[j] ?? `Series ${j + 1}`;
91
+ const tipVal = normalize ? `${plotted.toFixed(1)}% (${value})` : `${value}`;
92
+ this._datum(this.renderer.rect(currentX, barY, barWidth, bandWidth, this._palette.colorAt(j), null, {
93
+ tooltip: `${d.label} - ${series}: ${tipVal}`,
94
+ }), { label: d.label, series });
95
+ if (this.options.showValues && barWidth > 20 && bandWidth > 12) {
96
+ const valText = typeof this.options.format === 'function' ? this.options.format(value) : (normalize ? `${plotted.toFixed(0)}%` : String(value));
97
+ this.renderer.text(valText, currentX + barWidth / 2, barY + bandWidth / 2, {
98
+ textAnchor: 'middle', verticalAlign: 'middle', fontSize: '9px', fill: '#ffffff', 'pointer-events': 'none',
99
+ });
100
+ }
101
+ currentX += barWidth;
102
+ });
103
+ this.renderer.text(d.label, x - 8, barY + bandWidth / 2, {
104
+ textAnchor: 'end', verticalAlign: 'middle', fontSize: '11px', fill: '#333',
105
+ });
106
+ });
107
+ return;
108
+ }
109
+
110
+ const measure = (s) => this.renderer.measureText?.(s, { fontSize: '12px' }) ?? s.length * 12 * 0.55;
111
+ const room = (bandWidth + margin) - 4;
112
+ const cut = room >= measure('x'.repeat(6));
113
+ const labelStep = cut ? 1 : Axis.categoryLabelStep(labels, width);
114
+
115
+ categories.forEach((d, i) => {
23
116
  let currentY = y + height;
24
117
  const barX = x + i * (bandWidth + margin);
118
+ const colTotal = d.values.reduce((sum, val, idx) => sum + (this._dataFilter.has(idx) ? 0 : Math.max(0, val ?? 0)), 0);
25
119
  d.values.forEach((value, j) => {
26
- const barHeight = (value / maxStackValue) * height;
120
+ if (this._dataFilter.has(j)) return;
121
+ if (value == null || value <= 0) return;
122
+ const plotted = normalize ? (colTotal > 0 ? (value / colTotal) * 100 : 0) : value;
123
+ const barHeight = (plotted / maxStackValue) * height;
27
124
  currentY -= barHeight;
28
- this.renderer.rect(barX, currentY, bandWidth, barHeight, this._palette.colorAt(j), null, {
29
- tooltip: `${d.label} - Series ${j + 1}: ${value}`,
30
- });
125
+ const series = seriesNames[j] ?? `Series ${j + 1}`;
126
+ const tipVal = normalize ? `${plotted.toFixed(1)}% (${value})` : `${value}`;
127
+ this._datum(this.renderer.rect(barX, currentY, bandWidth, barHeight, this._palette.colorAt(j), null, {
128
+ tooltip: `${d.label} - ${series}: ${tipVal}`,
129
+ }), { label: d.label, series });
130
+ if (this.options.showValues && barHeight > 14 && bandWidth > 18) {
131
+ const valText = typeof this.options.format === 'function' ? this.options.format(value) : (normalize ? `${plotted.toFixed(0)}%` : String(value));
132
+ this.renderer.text(valText, barX + bandWidth / 2, currentY + barHeight / 2, {
133
+ textAnchor: 'middle', verticalAlign: 'middle', fontSize: '9px', fill: '#ffffff', 'pointer-events': 'none',
134
+ });
135
+ }
31
136
  });
32
- if (i % labelStep === 0) this.renderer.text(d.label, barX + bandWidth / 2, y + height + 20, { textAnchor });
137
+ if (i % labelStep === 0) {
138
+ this.renderer.text(cut ? TableChart.fit(String(d.label), room, measure) : d.label, barX + bandWidth / 2, y + height + 20, { textAnchor });
139
+ }
33
140
  });
34
141
  }
142
+
143
+ _afterBars() {
144
+ const { x, y, width } = this._plotBox;
145
+ if (this.options.legend && this._normalized.seriesNames.length > 0) {
146
+ Legend.beside({ x, y, width }, this.renderer.width, this._legendOptions()).draw(this.renderer);
147
+ }
148
+ }
35
149
  }
@@ -16,7 +16,8 @@ export class StackedBarChart3DOrbit extends Chart3D {
16
16
  const { x: bx, y: by, width, height } = box;
17
17
 
18
18
  const { xLabels, zLabels, xIndex, zIndex } = this.bucketGrid(data);
19
- const stackMax = Math.max(...data.map(d => d.values.reduce((s, v) => s + v, 0))) || 1;
19
+ const normalize = Boolean(options.normalize || options.stackMode === 'percent');
20
+ const stackMax = normalize ? 100 : (Math.max(...data.map(d => d.values.reduce((s, v) => s + v, 0))) || 1);
20
21
  const palette = new ColorPalette(colors);
21
22
 
22
23
  const cellW = 2 / Math.max(1, xLabels.length);
@@ -32,24 +33,31 @@ export class StackedBarChart3DOrbit extends Chart3D {
32
33
  const xCenter = -1 + cellW * (xi + 0.5);
33
34
  const zCenter = -1 + cellD * (zi + 0.5);
34
35
  const hw = cellW * barFrac / 2, hd = cellD * barFrac / 2;
35
- const stackTotal = d.values.reduce((s, v) => s + v, 0) / stackMax;
36
+ const colTotal = d.values.reduce((s, v) => s + Math.max(0, v), 0) || 1;
37
+ const stackTotal = normalize ? 1 : colTotal / stackMax;
36
38
  const { depth } = this.project(xCenter, stackTotal / 2, zCenter);
37
- return { d, x0: xCenter - hw, x1: xCenter + hw, z0: zCenter - hd, z1: zCenter + hd, depth };
39
+ return { d, colTotal, x0: xCenter - hw, x1: xCenter + hw, z0: zCenter - hd, z1: zCenter + hd, depth };
38
40
  }).sort((a, b) => b.depth - a.depth);
39
41
 
40
42
  for (const bar of bars) {
41
43
  let yCursor = 0;
42
44
  for (let j = 0; j < bar.d.values.length; j++) {
43
- const segHeight = bar.d.values[j] / stackMax;
45
+ const rawVal = bar.d.values[j] ?? 0;
46
+ const plotted = normalize ? (rawVal / bar.colTotal) * 100 : rawVal;
47
+ const segHeight = plotted / stackMax;
44
48
  const y0 = yCursor, y1 = yCursor + segHeight;
45
49
  yCursor = y1;
46
- this.drawBox(this.renderer, bar.x0, bar.x1, y0, y1, bar.z0, bar.z1, palette.colorAt(j), toScreen);
50
+ const series = options.seriesLabels?.[j] ?? `Series ${j + 1}`;
51
+ const tipVal = normalize ? `${((rawVal / bar.colTotal) * 100).toFixed(1)}% (${rawVal})` : `${rawVal}`;
52
+ this.drawBox(this.renderer, bar.x0, bar.x1, y0, y1, bar.z0, bar.z1, palette.colorAt(j), toScreen, {
53
+ label: bar.d.x, series, x: bar.d.x, z: bar.d.z, tooltip: `${bar.d.x} / ${bar.d.z} — ${series}: ${tipVal}`,
54
+ });
47
55
  }
48
56
  }
49
57
 
50
58
  const maxSegments = Math.max(...data.map(d => d.values.length));
51
59
  new Legend({
52
- items: Array.from({ length: maxSegments }, (_, j) => ({ label: `Series ${j + 1}`, color: palette.colorAt(j) })),
60
+ items: Array.from({ length: maxSegments }, (_, j) => ({ label: options.seriesLabels?.[j] ?? `Series ${j + 1}`, color: palette.colorAt(j) })),
53
61
  x: bx + width - 100, y: by + 5, itemHeight: 18, fontSize: '11px', marker: 'circle',
54
62
  }).draw(this.renderer);
55
63
  }
@@ -69,7 +69,8 @@ export class StreamGraphChart extends Chart {
69
69
 
70
70
  // Compute totals per point and max extent for scaling.
71
71
  const totals = new Array(pointCount).fill(0);
72
- series.forEach(s => {
72
+ series.forEach((s, si) => {
73
+ if (this._dataFilter.has(si)) return;
73
74
  s.values.forEach((d, i) => { totals[i] += d.value; });
74
75
  });
75
76
  const maxExtent = Math.max(...totals.map(v => Math.abs(v))) || 1;
@@ -81,7 +82,7 @@ export class StreamGraphChart extends Chart {
81
82
  const total = totals[i];
82
83
  let y0 = -total / 2;
83
84
  series.forEach((s, si) => {
84
- const v = s.values[i].value;
85
+ const v = this._dataFilter.has(si) ? 0 : s.values[i].value;
85
86
  bands[si][i] = { y0, y1: y0 + v };
86
87
  y0 += v;
87
88
  });
@@ -102,12 +103,21 @@ export class StreamGraphChart extends Chart {
102
103
 
103
104
  // Draw layers bottom-up so overlaps look correct.
104
105
  series.forEach((s, si) => {
106
+ if (this._dataFilter.has(si)) return;
105
107
  const color = palette.colorAt(si);
106
108
  const topPts = bands[si].map((b, i) => ({ x: x + i * pointMargin, y: toY(b.y1) }));
107
109
  const bottomPts = bands[si].map((b, i) => ({ x: x + i * pointMargin, y: toY(b.y0) }));
108
110
  const path = StreamGraphChart.areaPath(topPts, bottomPts, smooth, tension);
109
111
  const { r, g, b: bl } = palette.hexToRgb(color);
110
112
  this.renderer.path(path, `rgba(${r},${g},${bl},0.75)`, { color: color, width: 1 }, { tooltip: s.name });
113
+ if (typeof this.renderer.circle === 'function') {
114
+ topPts.forEach((p, i) => {
115
+ const d = s.values[i];
116
+ this._datum(this.renderer.circle(p.x, p.y, 3, color, { color: '#fff', width: 1 }, {
117
+ tooltip: `${isMultiline ? s.name + ': ' : ''}${d.label}: ${d.value}`,
118
+ }), { label: d.label, series: isMultiline ? s.name : null });
119
+ });
120
+ }
111
121
  });
112
122
 
113
123
  // X-axis labels.
@@ -124,7 +134,12 @@ export class StreamGraphChart extends Chart {
124
134
  this.renderer.path(`M${x},${y} L${x},${y + height} L${x + width},${y + height}`, 'none', { color: '#999', width: 1 });
125
135
 
126
136
  if (isMultiline && showLegend) {
127
- const legendItems = series.map((s, i) => ({ label: s.name, color: palette.colorAt(i) }));
137
+ const legendItems = series.map((s, i) => ({
138
+ label: s.name,
139
+ color: palette.colorAt(i),
140
+ hidden: this._dataFilter.has(i),
141
+ dataIndex: i,
142
+ }));
128
143
  Legend.beside({ x, y, width }, this.renderer.width, {
129
144
  items: legendItems,
130
145
  itemHeight: 20, markerSize: 12,
@@ -6,16 +6,17 @@ import { HierarchyChart } from './HierarchyChart.js';
6
6
  * SunburstChart — the same hierarchical composition a treemap shows as nested
7
7
  * rectangles, drawn instead as nested rings (one ring per depth), each ring's
8
8
  * arcs sized by value like a pie chart.
9
+ * Supports breadcrumb drill-down.
9
10
  */
10
11
  export class SunburstChart extends HierarchyChart {
11
12
  static chartType = 'sunburst';
12
13
 
13
14
  _render() {
14
- const data = this.data;
15
- const options = this.options;
15
+ const data = this._activeData();
16
+ const options = this.options || {};
16
17
  const w = this.renderer?.width ?? 640;
17
18
  const h = this.renderer?.height ?? 400;
18
- const {
19
+ let {
19
20
  cx = w / 2, cy = h / 2 + 10,
20
21
  radius = Math.min(w, h) / 2 - 60,
21
22
  innerRadiusRatio = 0,
@@ -26,6 +27,12 @@ export class SunburstChart extends HierarchyChart {
26
27
  this.renderer.text(title, cx, cy - radius - 20, { textAnchor: 'middle', fontSize: '18px', fill: '#333' });
27
28
  }
28
29
 
30
+ const crumbH = this._drawBreadcrumbs(this.renderer, { x: 20, y: 10, width: w - 40 });
31
+ if (crumbH > 0) {
32
+ cy += crumbH / 2;
33
+ radius -= crumbH / 3;
34
+ }
35
+
29
36
  const maxDepth = Math.max(1, ...data.map(n => Hierarchy.depth(n)));
30
37
  const innerHole = radius * innerRadiusRatio;
31
38
  const ringWidth = (radius - innerHole) / maxDepth;
@@ -35,23 +42,36 @@ export class SunburstChart extends HierarchyChart {
35
42
  const { node, cx: ecx, cy: ecy, rInner, rOuter, startAngle, endAngle, depth, topAncestor } = entry;
36
43
  const color = this._colorForNode(topAncestor, depth);
37
44
  const textColor = this._textColorForNode(topAncestor, depth);
45
+ const hasChildren = Boolean(node.children?.length);
38
46
 
39
47
  const path = ShapePaths.donutSlicePath(ecx, ecy, rOuter, rInner, startAngle, endAngle);
40
- this.renderer.path(path, color, { color: '#fff', width: 1 }, {
41
- tooltip: `${node.label}: ${this._formatNodeValue(node)}`,
42
- });
48
+ const arcEl = this._datum(this.renderer.path(path, color, { color: '#fff', width: 1 }, {
49
+ cursor: (hasChildren && options.drilldown !== false) ? 'pointer' : 'default',
50
+ tooltip: `${node.label}: ${this._formatNodeValue(node)}${hasChildren ? ' (Click to drill down)' : ''}`,
51
+ }), { label: node.label });
52
+
53
+ if (hasChildren && options.drilldown !== false && typeof arcEl?.addEventListener === 'function') {
54
+ arcEl.addEventListener('click', (e) => {
55
+ e.stopPropagation();
56
+ this.drillTo(node);
57
+ });
58
+ }
59
+
60
+ const midAngle = (startAngle + endAngle) / 2;
61
+ const hoverEls = [arcEl];
43
62
 
44
63
  if (endAngle - startAngle > 0.35) {
45
- const midAngle = (startAngle + endAngle) / 2;
46
64
  const midR = (rInner + rOuter) / 2;
47
65
  const labelX = ecx + midR * Math.cos(midAngle);
48
66
  const labelY = ecy + midR * Math.sin(midAngle);
49
67
  const upright = Math.cos(midAngle) >= 0;
50
68
  const rotation = upright ? midAngle : midAngle + Math.PI;
51
- this.renderer.text(node.label, labelX, labelY, {
69
+ const txtEl = this.renderer.text(node.label, labelX, labelY, {
52
70
  textAnchor: 'middle', verticalAlign: 'middle', fontSize: '9px', fill: textColor, rotation, 'pointer-events': 'none',
53
71
  });
72
+ hoverEls.push(txtEl);
54
73
  }
74
+ this._wireRadialHover(hoverEls, midAngle, { offset: options.hoverOffset ?? 8 });
55
75
  });
56
76
  }
57
77
  }
@@ -0,0 +1,157 @@
1
+ import { Chart3D } from '../Chart3D.js';
2
+ import { ColorPalette } from '../ColorPalette.js';
3
+
4
+ /**
5
+ * SurfaceChart3D — 3D `(x, z) -> value` surface mesh rendered through the real 4×4
6
+ * `OrbitCamera` (drag to rotate, wheel to zoom).
7
+ *
8
+ * Accepts either:
9
+ * - `Array<{x: string, z: string, value: number}>` (same flat grid contract as `barChart3DOrbit` and `heatmap`)
10
+ * - `{ xLabels: string[], zLabels: string[], values: number[][] }`
11
+ */
12
+ export class SurfaceChart3D extends Chart3D {
13
+ static chartType = 'surface3D';
14
+
15
+ _render() {
16
+ const data = this.data;
17
+ const options = this.options || {};
18
+ const {
19
+ title,
20
+ colorScale = ['#313695', '#4575b4', '#74add1', '#fee090', '#f46d43', '#d73027'],
21
+ wireframe = true,
22
+ wireframeColor = 'rgba(255,255,255,0.45)',
23
+ initialTheta = -0.6,
24
+ initialPhi = 0.45,
25
+ initialDistance = 2.35,
26
+ } = options;
27
+
28
+ this.seedCamera({ theta: initialTheta, phi: initialPhi, distance: initialDistance });
29
+ const { cx, toScreen, box } = this._viewport();
30
+ const { y: by } = box;
31
+
32
+ if (title) {
33
+ this.renderer.text(title, cx, by - 20, { textAnchor: 'middle', fontSize: '18px', fill: '#333' });
34
+ }
35
+
36
+ const { xLabels, zLabels, grid } = this._normalizeGrid(data);
37
+ const nx = xLabels.length;
38
+ const nz = zLabels.length;
39
+ if (nx < 2 || nz < 2) return;
40
+
41
+ let minVal = Infinity;
42
+ let maxVal = -Infinity;
43
+ for (let ix = 0; ix < nx; ix++) {
44
+ for (let iz = 0; iz < nz; iz++) {
45
+ const v = grid[ix][iz];
46
+ if (v < minVal) minVal = v;
47
+ if (v > maxVal) maxVal = v;
48
+ }
49
+ }
50
+ const span = (maxVal - minVal) || 1;
51
+
52
+ const normX = (ix) => nx === 1 ? 0 : (ix / (nx - 1)) * 1.6 - 0.8;
53
+ const normZ = (iz) => nz === 1 ? 0 : (iz / (nz - 1)) * 1.6 - 0.8;
54
+ const normY = (v) => -0.55 + ((v - minVal) / span) * 1.15;
55
+
56
+ // Draw floor base frame
57
+ const floor = [
58
+ toScreen(-0.8, -0.55, -0.8),
59
+ toScreen(0.8, -0.55, -0.8),
60
+ toScreen(0.8, -0.55, 0.8),
61
+ toScreen(-0.8, -0.55, 0.8),
62
+ ];
63
+ this.renderer.path(
64
+ `M${floor[0].x},${floor[0].y} L${floor[1].x},${floor[1].y} L${floor[2].x},${floor[2].y} L${floor[3].x},${floor[3].y} Z`,
65
+ 'rgba(241, 245, 249, 0.55)',
66
+ { color: '#cbd5e1', width: 1 }
67
+ );
68
+
69
+ // Build surface quads for each (ix, iz) -> (ix+1, iz+1) cell
70
+ const quads = [];
71
+ for (let ix = 0; ix < nx - 1; ix++) {
72
+ for (let iz = 0; iz < nz - 1; iz++) {
73
+ const v00 = grid[ix][iz];
74
+ const v10 = grid[ix + 1][iz];
75
+ const v11 = grid[ix + 1][iz + 1];
76
+ const v01 = grid[ix][iz + 1];
77
+ const avgVal = (v00 + v10 + v11 + v01) / 4;
78
+ const t = Math.max(0, Math.min(1, (avgVal - minVal) / span));
79
+
80
+ const p00 = toScreen(normX(ix), normY(v00), normZ(iz));
81
+ const p10 = toScreen(normX(ix + 1), normY(v10), normZ(iz));
82
+ const p11 = toScreen(normX(ix + 1), normY(v11), normZ(iz + 1));
83
+ const p01 = toScreen(normX(ix), normY(v01), normZ(iz + 1));
84
+
85
+ // Directional slope shading based on finite differences along x and z
86
+ const dxSlope = (v10 + v11 - v00 - v01) / (2 * span);
87
+ const dzSlope = (v01 + v11 - v00 - v10) / (2 * span);
88
+ const lightShade = Math.max(-0.22, Math.min(0.22, dxSlope * 0.25 - dzSlope * 0.18));
89
+ const baseColor = this._sampleGradient(colorScale, t);
90
+ const shadedColor = ColorPalette.lighten(baseColor, lightShade);
91
+
92
+ const depth = (p00.depth + p10.depth + p11.depth + p01.depth) / 4;
93
+ quads.push({
94
+ pts: [p00, p10, p11, p01],
95
+ color: shadedColor,
96
+ depth,
97
+ label: `${xLabels[ix]}–${xLabels[ix + 1]}`,
98
+ series: `${zLabels[iz]}–${zLabels[iz + 1]}`,
99
+ avgVal,
100
+ });
101
+ }
102
+ }
103
+
104
+ // Painter's sort: furthest (most negative depth) first
105
+ quads.sort((a, b) => a.depth - b.depth);
106
+
107
+ const stroke = wireframe ? { color: wireframeColor, width: 0.8 } : null;
108
+ for (const q of quads) {
109
+ const [p0, p1, p2, p3] = q.pts;
110
+ const d = `M${p0.x},${p0.y} L${p1.x},${p1.y} L${p2.x},${p2.y} L${p3.x},${p3.y} Z`;
111
+ const el = this._datum(this.renderer.path(d, q.color, stroke, {
112
+ tooltip: `${q.label} / ${q.series}: ${q.avgVal.toFixed(2)}`,
113
+ }), { label: q.label, series: q.series });
114
+ this._wireHoverTransform([el], { dx: 0, dy: -4 });
115
+ }
116
+
117
+ // Draw X and Z floor tick labels
118
+ const xStep = Math.max(1, Math.ceil(nx / 6));
119
+ for (let ix = 0; ix < nx; ix += xStep) {
120
+ const lp = toScreen(normX(ix), -0.58, 0.92);
121
+ this.renderer.text(String(xLabels[ix]), lp.x, lp.y, { textAnchor: 'middle', fontSize: '10px', fill: '#475569' });
122
+ }
123
+ const zStep = Math.max(1, Math.ceil(nz / 6));
124
+ for (let iz = 0; iz < nz; iz += zStep) {
125
+ const lp = toScreen(0.92, -0.58, normZ(iz));
126
+ this.renderer.text(String(zLabels[iz]), lp.x, lp.y, { textAnchor: 'middle', fontSize: '10px', fill: '#475569' });
127
+ }
128
+ }
129
+
130
+ _normalizeGrid(data) {
131
+ if (data && !Array.isArray(data) && Array.isArray(data.values)) {
132
+ const xLabels = data.xLabels || data.values.map((_, i) => String(i));
133
+ const zLabels = data.zLabels || (data.values[0] || []).map((_, j) => String(j));
134
+ return { xLabels, zLabels, grid: data.values };
135
+ }
136
+ const rows = Array.isArray(data) ? data : [];
137
+ const { xLabels, zLabels, xIndex, zIndex } = this.bucketGrid(rows);
138
+ const grid = Array.from({ length: xLabels.length }, () => new Array(zLabels.length).fill(0));
139
+ for (const r of rows) {
140
+ const ix = xIndex.get(r.x);
141
+ const iz = zIndex.get(r.z);
142
+ if (ix != null && iz != null) {
143
+ grid[ix][iz] = Number(r.value) || 0;
144
+ }
145
+ }
146
+ return { xLabels, zLabels, grid };
147
+ }
148
+
149
+ _sampleGradient(stops, t) {
150
+ if (!Array.isArray(stops) || stops.length === 0) return '#4f8ef7';
151
+ if (stops.length === 1) return stops[0];
152
+ const scaled = Math.max(0, Math.min(1, t)) * (stops.length - 1);
153
+ const idx = Math.min(stops.length - 2, Math.floor(scaled));
154
+ const localT = scaled - idx;
155
+ return ColorPalette.interpolate(stops[idx], stops[idx + 1], localT);
156
+ }
157
+ }