@peter.prib/renderengine 0.14.0 → 0.15.1

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 (113) hide show
  1. package/README.md +45 -19
  2. package/bin/renderengine.mjs +157 -0
  3. package/dist/renderengine-workspace.umd.js +49641 -39747
  4. package/dist/renderengine.umd.js +25366 -15650
  5. package/documentation/REFERENCE.md +206 -0
  6. package/examples/decision-workbench.html +26 -0
  7. package/package.json +7 -1
  8. package/src/CanvasRenderer.js +225 -200
  9. package/src/SVGRenderer.js +476 -454
  10. package/src/archimate.js +22 -2
  11. package/src/archimateArchiFormat.js +1735 -1701
  12. package/src/archimateArchive.js +36 -7
  13. package/src/archimateCSV.js +93 -37
  14. package/src/archimateDrawio.js +2 -0
  15. package/src/archimateExchangeFormat.js +3 -1
  16. package/src/chartCatalog.js +219 -0
  17. package/src/charting/Annotations.js +169 -0
  18. package/src/charting/ArchimateDiagramEditor.js +54 -10
  19. package/src/charting/Axis.js +217 -5
  20. package/src/charting/Chart.js +99 -9
  21. package/src/charting/Chart3D.js +3 -0
  22. package/src/charting/ChartBox.js +8 -1
  23. package/src/charting/ColorPalette.js +42 -4
  24. package/src/charting/DiagramEditor.js +174 -39
  25. package/src/charting/DiagramEditorCommands.js +28 -0
  26. package/src/charting/Edge.js +7 -0
  27. package/src/charting/Hierarchy.js +114 -0
  28. package/src/charting/InteractionManager.js +4 -0
  29. package/src/charting/ModelFileFormat.js +5 -1
  30. package/src/charting/SettingsManager.js +35 -0
  31. package/src/charting/ShapePaths.js +82 -10
  32. package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
  33. package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
  34. package/src/charting/charts/AlluvialDiagram.js +311 -0
  35. package/src/charting/charts/AreaChart.js +52 -15
  36. package/src/charting/charts/BarChart.js +38 -3
  37. package/src/charting/charts/BarChart3D.js +5 -3
  38. package/src/charting/charts/BarChartBase.js +26 -6
  39. package/src/charting/charts/BeeswarmChart.js +164 -0
  40. package/src/charting/charts/BoxPlotChart.js +23 -1
  41. package/src/charting/charts/CalendarHeatmapChart.js +2 -1
  42. package/src/charting/charts/CandlestickChart.js +172 -4
  43. package/src/charting/charts/ChordDiagram.js +105 -2
  44. package/src/charting/charts/CirclePackChart.js +81 -0
  45. package/src/charting/charts/ComboChart.js +233 -0
  46. package/src/charting/charts/ContourChart.js +310 -0
  47. package/src/charting/charts/DonutChart3D.js +13 -0
  48. package/src/charting/charts/DumbbellChart.js +217 -0
  49. package/src/charting/charts/EcdfChart.js +150 -0
  50. package/src/charting/charts/FunnelChart.js +4 -1
  51. package/src/charting/charts/GeoMapChart.js +132 -11
  52. package/src/charting/charts/GroupedBarChart.js +44 -3
  53. package/src/charting/charts/GroupedDistributionChart.js +26 -1
  54. package/src/charting/charts/HeatmapChart.js +2 -2
  55. package/src/charting/charts/HexbinChart.js +137 -0
  56. package/src/charting/charts/HierarchyChart.js +77 -3
  57. package/src/charting/charts/HorizonChart.js +105 -0
  58. package/src/charting/charts/KagiChart.js +167 -0
  59. package/src/charting/charts/LineChart.js +74 -18
  60. package/src/charting/charts/OhlcChart.js +79 -0
  61. package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
  62. package/src/charting/charts/PieChart.js +195 -17
  63. package/src/charting/charts/PieChart3D.js +137 -0
  64. package/src/charting/charts/PointAndFigureChart.js +198 -0
  65. package/src/charting/charts/PolarAreaChart.js +189 -0
  66. package/src/charting/charts/RadarChart.js +7 -1
  67. package/src/charting/charts/RadialBarChart.js +5 -2
  68. package/src/charting/charts/RenkoChart.js +178 -0
  69. package/src/charting/charts/RidgelineChart.js +113 -0
  70. package/src/charting/charts/SankeyDiagram.js +127 -4
  71. package/src/charting/charts/ScatterChart.js +13 -0
  72. package/src/charting/charts/ScatterMatrixChart.js +119 -0
  73. package/src/charting/charts/SlopegraphChart.js +205 -0
  74. package/src/charting/charts/SparklineTableChart.js +166 -0
  75. package/src/charting/charts/StackedBarChart.js +48 -5
  76. package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
  77. package/src/charting/charts/StreamGraphChart.js +10 -3
  78. package/src/charting/charts/SunburstChart.js +27 -7
  79. package/src/charting/charts/SurfaceChart3D.js +157 -0
  80. package/src/charting/charts/TreemapChart.js +28 -8
  81. package/src/charting/charts/ViolinChart.js +18 -0
  82. package/src/charting/datamodel/ArchimateDataLink.js +70 -0
  83. package/src/charting/datamodel/DataLineage.js +150 -0
  84. package/src/charting/datamodel/DataModel.js +83 -1
  85. package/src/charting/datamodel/ModelMapping.js +87 -0
  86. package/src/charting/datamodel/SqlDdl.js +110 -85
  87. package/src/charting/datamodel/index.js +3 -0
  88. package/src/charting/decision/DecisionWorkbench.js +295 -0
  89. package/src/charting/diagrams/ArchimateDiagram.js +25 -9
  90. package/src/charting/diagrams/ArchimateModel.js +1382 -1238
  91. package/src/charting/diagrams/GraphDiagram.js +1424 -1350
  92. package/src/charting/diagrams/SketchView.js +121 -0
  93. package/src/charting/diagrams/sketchBoxKinds.js +30 -0
  94. package/src/charting/modelTree/AnalysisPanel.js +101 -23
  95. package/src/charting/modelTree/NavigatorModel.js +410 -0
  96. package/src/charting/modelTree/NavigatorPanel.js +453 -0
  97. package/src/charting/pivot/PivotChart.js +109 -4
  98. package/src/charting/pivot/PivotGridChart.js +321 -33
  99. package/src/charting/pivot/PivotView.js +82 -0
  100. package/src/charting/report/TemplatedReport.js +547 -0
  101. package/src/charting/templates/BoxTemplate.js +28 -27
  102. package/src/charting/templates/PackageTemplate.js +26 -25
  103. package/src/charting/templates/SwimlaneTemplate.js +19 -18
  104. package/src/charting/templates/Template.js +219 -212
  105. package/src/cli.js +146 -0
  106. package/src/index.js +3370 -2995
  107. package/src/workspace/Workspace.js +126 -7
  108. package/src/workspace/archimateServices.js +4 -0
  109. package/src/workspace/index.js +3 -0
  110. package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
  111. package/src/workspace/parts/DecisionWorkbenchPart.js +58 -0
  112. package/src/workspace/parts/ViewsPart.js +196 -0
  113. 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
+ }
@@ -45,7 +45,12 @@ export class StackedBarChart extends BarChartBase {
45
45
 
46
46
  _legendOptions() {
47
47
  return {
48
- items: this._normalized.seriesNames.map((name, i) => ({ label: name, color: this._palette.colorAt(i), dataIndex: i })),
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
+ })),
49
54
  itemHeight: 20, markerSize: 10,
50
55
  };
51
56
  }
@@ -56,11 +61,11 @@ export class StackedBarChart extends BarChartBase {
56
61
 
57
62
  _computeMaxValue() {
58
63
  if (this._isNormalized()) {
59
- return this._drawYAxis(100);
64
+ return this._isHorizontal() ? this._drawXAxis(100) : this._drawYAxis(100);
60
65
  }
61
66
  const { categories } = this._normalized;
62
- const rawMax = Math.max(0, ...categories.map(d => d.values.reduce((sum, val) => sum + Math.max(0, val), 0))) || 1;
63
- return this._drawYAxis(rawMax);
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);
64
69
  }
65
70
 
66
71
  _renderBars(maxStackValue) {
@@ -71,6 +76,37 @@ export class StackedBarChart extends BarChartBase {
71
76
  const labels = categories.map(d => d.label);
72
77
  const { bandWidth, margin } = this._bandScale(categories.length);
73
78
  const textAnchor = this.renderer.centerTextAnchor;
79
+
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
+
74
110
  const measure = (s) => this.renderer.measureText?.(s, { fontSize: '12px' }) ?? s.length * 12 * 0.55;
75
111
  const room = (bandWidth + margin) - 4;
76
112
  const cut = room >= measure('x'.repeat(6));
@@ -79,8 +115,9 @@ export class StackedBarChart extends BarChartBase {
79
115
  categories.forEach((d, i) => {
80
116
  let currentY = y + height;
81
117
  const barX = x + i * (bandWidth + margin);
82
- const colTotal = d.values.reduce((sum, val) => sum + Math.max(0, val ?? 0), 0);
118
+ const colTotal = d.values.reduce((sum, val, idx) => sum + (this._dataFilter.has(idx) ? 0 : Math.max(0, val ?? 0)), 0);
83
119
  d.values.forEach((value, j) => {
120
+ if (this._dataFilter.has(j)) return;
84
121
  if (value == null || value <= 0) return;
85
122
  const plotted = normalize ? (colTotal > 0 ? (value / colTotal) * 100 : 0) : value;
86
123
  const barHeight = (plotted / maxStackValue) * height;
@@ -90,6 +127,12 @@ export class StackedBarChart extends BarChartBase {
90
127
  this._datum(this.renderer.rect(barX, currentY, bandWidth, barHeight, this._palette.colorAt(j), null, {
91
128
  tooltip: `${d.label} - ${series}: ${tipVal}`,
92
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
+ }
93
136
  });
94
137
  if (i % labelStep === 0) {
95
138
  this.renderer.text(cut ? TableChart.fit(String(d.label), room, measure) : d.label, barX + bandWidth / 2, y + height + 20, { textAnchor });
@@ -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,20 +33,24 @@ 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
50
  const series = options.seriesLabels?.[j] ?? `Series ${j + 1}`;
51
+ const tipVal = normalize ? `${((rawVal / bar.colTotal) * 100).toFixed(1)}% (${rawVal})` : `${rawVal}`;
47
52
  this.drawBox(this.renderer, bar.x0, bar.x1, y0, y1, bar.z0, bar.z1, palette.colorAt(j), toScreen, {
48
- label: bar.d.x, series, x: bar.d.x, z: bar.d.z,
53
+ label: bar.d.x, series, x: bar.d.x, z: bar.d.z, tooltip: `${bar.d.x} / ${bar.d.z} — ${series}: ${tipVal}`,
49
54
  });
50
55
  }
51
56
  }
@@ -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,6 +103,7 @@ 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) }));
@@ -132,7 +134,12 @@ export class StreamGraphChart extends Chart {
132
134
  this.renderer.path(`M${x},${y} L${x},${y + height} L${x + width},${y + height}`, 'none', { color: '#999', width: 1 });
133
135
 
134
136
  if (isMultiline && showLegend) {
135
- 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
+ }));
136
143
  Legend.beside({ x, y, width }, this.renderer.width, {
137
144
  items: legendItems,
138
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._datum(this.renderer.path(path, color, { color: '#fff', width: 1 }, {
41
- tooltip: `${node.label}: ${this._formatNodeValue(node)}`,
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)' : ''}`,
42
51
  }), { label: node.label });
43
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];
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
+ }
@@ -5,18 +5,22 @@ import { HierarchyChart } from './HierarchyChart.js';
5
5
  /**
6
6
  * TreemapChart — rectangles nested/sized proportional to value (squarified
7
7
  * layout), the standard chart for "composition of a whole across a hierarchy".
8
+ * Supports breadcrumb drill-down.
8
9
  */
9
10
  export class TreemapChart extends HierarchyChart {
10
11
  static chartType = 'treemap';
11
12
 
12
13
  _render() {
13
- const data = this.data;
14
- const options = this.options;
14
+ const data = this._activeData();
15
+ const options = this.options || {};
15
16
  const box = ChartBox.fromRenderer(this.renderer);
16
- const { x = box.x, y = box.y, width = box.width, height = box.height, title } = options;
17
- const plotBox = { x, y, width, height };
17
+ let { x = box.x, y = box.y, width = box.width, height = box.height, title } = options;
18
18
 
19
- new ChartLabels({ title }).draw(this.renderer, plotBox);
19
+ new ChartLabels({ title }).draw(this.renderer, { x, y, width, height });
20
+
21
+ const crumbH = this._drawBreadcrumbs(this.renderer, { x, y, width });
22
+ y += crumbH;
23
+ height -= crumbH;
20
24
 
21
25
  const layout = Hierarchy.treemapLayout(data, x, y, width, height, 0, null, TreemapChart.HEADER_H);
22
26
  const textAnchor = this.renderer.centerTextAnchor;
@@ -31,16 +35,32 @@ export class TreemapChart extends HierarchyChart {
31
35
  tooltip: `${node.label}: ${this._formatNodeValue(node)}`,
32
36
  }), { label: node.label });
33
37
  if (rw > 30 && rh > 14) {
34
- this.renderer.text(node.label, rx + rw / 2, ry + rh / 2, {
38
+ const showVals = options.showValues && rh > 28;
39
+ const valText = typeof options.format === 'function' ? options.format(node.value) : this._formatNodeValue(node);
40
+ this.renderer.text(node.label, rx + rw / 2, ry + (showVals ? rh / 2 - 6 : rh / 2), {
35
41
  textAnchor, verticalAlign: 'middle', fontSize: '10px', fill: textColor, 'pointer-events': 'none',
36
42
  });
43
+ if (showVals) {
44
+ this.renderer.text(valText, rx + rw / 2, ry + rh / 2 + 6, {
45
+ textAnchor, verticalAlign: 'middle', fontSize: '9px', fill: textColor, opacity: 0.9, 'pointer-events': 'none',
46
+ });
47
+ }
37
48
  }
38
49
  } else {
39
50
  const headerH = Math.min(rh, TreemapChart.HEADER_H);
40
51
  this.renderer.rect(rx, ry, Math.max(0, rw), Math.max(0, rh), 'rgba(0,0,0,0.03)', { color: '#ddd', width: 1 });
41
- this._datum(this.renderer.rect(rx, ry, Math.max(0, rw), headerH, color, null, {
42
- tooltip: `${node.label}: ${this._formatNodeValue(node)}`,
52
+ const headerRect = this._datum(this.renderer.rect(rx, ry, Math.max(0, rw), headerH, color, null, {
53
+ cursor: options.drilldown !== false ? 'pointer' : 'default',
54
+ tooltip: `${node.label}: ${this._formatNodeValue(node)} (Click to drill down)`,
43
55
  }), { label: node.label });
56
+
57
+ if (options.drilldown !== false && typeof headerRect?.addEventListener === 'function') {
58
+ headerRect.addEventListener('click', (e) => {
59
+ e.stopPropagation();
60
+ this.drillTo(node);
61
+ });
62
+ }
63
+
44
64
  if (rw > 30) {
45
65
  this.renderer.text(node.label, rx + 4, ry + headerH / 2, {
46
66
  textAnchor: 'start', verticalAlign: 'middle', fontSize: '9px', fontWeight: 'bold', fill: textColor, 'pointer-events': 'none',
@@ -18,6 +18,24 @@ export class ViolinChart extends GroupedDistributionChart {
18
18
  const color = this._palette.colorAt(i);
19
19
  const curve = Statistics.kernelDensityEstimate(d.values, { min: this._domain.vMin, max: this._domain.vMax });
20
20
  const maxDensity = Math.max(...curve.map(p => p.density)) || 1;
21
+
22
+ if (this._isHorizontal()) {
23
+ const cy = cx;
24
+ const slotHeight = slotWidth;
25
+ const halfHeight = slotHeight * 0.4;
26
+ const topPts = curve.map(p => ({ px: this._toX(p.x), py: cy - (p.density / maxDensity) * halfHeight }));
27
+ const bottomPts = curve.map(p => ({ px: this._toX(p.x), py: cy + (p.density / maxDensity) * halfHeight }));
28
+ const path = 'M' + topPts.map(p => `${p.px},${p.py}`).join(' L')
29
+ + ' L' + [...bottomPts].reverse().map(p => `${p.px},${p.py}`).join(' L') + ' Z';
30
+ this._datum(this.renderer.path(path, color, { color: '#333', width: 1 }, {
31
+ tooltip: `${d.label}: n=${d.values.length}`,
32
+ }), { label: d.label, series: d.label });
33
+
34
+ const median = Statistics.quantile([...d.values].sort((a, b) => a - b), 0.5);
35
+ this.renderer.path(`M${this._toX(median)},${cy - halfHeight} L${this._toX(median)},${cy + halfHeight}`, 'none', { color: '#fff', width: 2 });
36
+ return;
37
+ }
38
+
21
39
  const halfWidth = slotWidth * 0.4;
22
40
 
23
41
  // Mirrored silhouette: right edge low-to-high value, then left