@peter.prib/renderengine 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -19
- package/bin/renderengine.mjs +157 -0
- package/dist/renderengine-workspace.umd.js +49391 -39748
- package/dist/renderengine.umd.js +25366 -15650
- package/documentation/REFERENCE.md +38 -0
- package/package.json +6 -1
- package/src/CanvasRenderer.js +225 -200
- package/src/SVGRenderer.js +476 -454
- package/src/archimate.js +22 -2
- package/src/archimateArchiFormat.js +1735 -1701
- package/src/archimateArchive.js +36 -7
- package/src/archimateCSV.js +93 -37
- package/src/archimateDrawio.js +2 -0
- package/src/archimateExchangeFormat.js +3 -1
- package/src/chartCatalog.js +219 -0
- package/src/charting/Annotations.js +169 -0
- package/src/charting/ArchimateDiagramEditor.js +54 -10
- package/src/charting/Axis.js +217 -5
- package/src/charting/Chart.js +99 -9
- package/src/charting/Chart3D.js +3 -0
- package/src/charting/ChartBox.js +8 -1
- package/src/charting/ColorPalette.js +42 -4
- package/src/charting/DiagramEditor.js +174 -39
- package/src/charting/DiagramEditorCommands.js +28 -0
- package/src/charting/Edge.js +7 -0
- package/src/charting/Hierarchy.js +114 -0
- package/src/charting/InteractionManager.js +4 -0
- package/src/charting/ModelFileFormat.js +5 -1
- package/src/charting/SettingsManager.js +35 -0
- package/src/charting/ShapePaths.js +82 -10
- package/src/charting/archimate/ArchimateCsvFileFormat.js +51 -12
- package/src/charting/archimate/ArchimateElementTemplate.js +235 -231
- package/src/charting/charts/AlluvialDiagram.js +311 -0
- package/src/charting/charts/AreaChart.js +52 -15
- package/src/charting/charts/BarChart.js +38 -3
- package/src/charting/charts/BarChart3D.js +5 -3
- package/src/charting/charts/BarChartBase.js +26 -6
- package/src/charting/charts/BeeswarmChart.js +164 -0
- package/src/charting/charts/BoxPlotChart.js +23 -1
- package/src/charting/charts/CalendarHeatmapChart.js +2 -1
- package/src/charting/charts/CandlestickChart.js +172 -4
- package/src/charting/charts/ChordDiagram.js +105 -2
- package/src/charting/charts/CirclePackChart.js +81 -0
- package/src/charting/charts/ComboChart.js +233 -0
- package/src/charting/charts/ContourChart.js +310 -0
- package/src/charting/charts/DonutChart3D.js +13 -0
- package/src/charting/charts/DumbbellChart.js +217 -0
- package/src/charting/charts/EcdfChart.js +150 -0
- package/src/charting/charts/FunnelChart.js +4 -1
- package/src/charting/charts/GeoMapChart.js +132 -11
- package/src/charting/charts/GroupedBarChart.js +44 -3
- package/src/charting/charts/GroupedDistributionChart.js +26 -1
- package/src/charting/charts/HeatmapChart.js +2 -2
- package/src/charting/charts/HexbinChart.js +137 -0
- package/src/charting/charts/HierarchyChart.js +77 -3
- package/src/charting/charts/HorizonChart.js +105 -0
- package/src/charting/charts/KagiChart.js +167 -0
- package/src/charting/charts/LineChart.js +74 -18
- package/src/charting/charts/OhlcChart.js +79 -0
- package/src/charting/charts/ParallelCoordinatesChart.js +9 -1
- package/src/charting/charts/PieChart.js +195 -17
- package/src/charting/charts/PieChart3D.js +137 -0
- package/src/charting/charts/PointAndFigureChart.js +198 -0
- package/src/charting/charts/PolarAreaChart.js +189 -0
- package/src/charting/charts/RadarChart.js +7 -1
- package/src/charting/charts/RadialBarChart.js +5 -2
- package/src/charting/charts/RenkoChart.js +178 -0
- package/src/charting/charts/RidgelineChart.js +113 -0
- package/src/charting/charts/SankeyDiagram.js +127 -4
- package/src/charting/charts/ScatterChart.js +13 -0
- package/src/charting/charts/ScatterMatrixChart.js +119 -0
- package/src/charting/charts/SlopegraphChart.js +205 -0
- package/src/charting/charts/SparklineTableChart.js +166 -0
- package/src/charting/charts/StackedBarChart.js +48 -5
- package/src/charting/charts/StackedBarChart3DOrbit.js +10 -5
- package/src/charting/charts/StreamGraphChart.js +10 -3
- package/src/charting/charts/SunburstChart.js +27 -7
- package/src/charting/charts/SurfaceChart3D.js +157 -0
- package/src/charting/charts/TreemapChart.js +28 -8
- package/src/charting/charts/ViolinChart.js +18 -0
- package/src/charting/datamodel/ArchimateDataLink.js +70 -0
- package/src/charting/datamodel/DataLineage.js +150 -0
- package/src/charting/datamodel/DataModel.js +83 -1
- package/src/charting/datamodel/ModelMapping.js +87 -0
- package/src/charting/datamodel/SqlDdl.js +110 -85
- package/src/charting/datamodel/index.js +3 -0
- package/src/charting/decision/DecisionWorkbench.js +295 -0
- package/src/charting/diagrams/ArchimateDiagram.js +25 -9
- package/src/charting/diagrams/ArchimateModel.js +1382 -1238
- package/src/charting/diagrams/GraphDiagram.js +1424 -1350
- package/src/charting/diagrams/SketchView.js +121 -0
- package/src/charting/diagrams/sketchBoxKinds.js +30 -0
- package/src/charting/modelTree/AnalysisPanel.js +101 -23
- package/src/charting/modelTree/NavigatorModel.js +410 -0
- package/src/charting/modelTree/NavigatorPanel.js +453 -0
- package/src/charting/pivot/PivotChart.js +109 -4
- package/src/charting/pivot/PivotGridChart.js +321 -33
- package/src/charting/pivot/PivotView.js +82 -0
- package/src/charting/report/TemplatedReport.js +547 -0
- package/src/charting/templates/BoxTemplate.js +28 -27
- package/src/charting/templates/PackageTemplate.js +26 -25
- package/src/charting/templates/SwimlaneTemplate.js +19 -18
- package/src/charting/templates/Template.js +219 -212
- package/src/cli.js +146 -0
- package/src/index.js +3370 -2995
- package/src/workspace/Workspace.js +126 -7
- package/src/workspace/archimateServices.js +4 -0
- package/src/workspace/parts/ArchimateCanvasPart.js +1 -1
- package/src/workspace/styles.js +36 -1
|
@@ -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) => ({
|
|
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
|
|
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
|
|
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
|
|
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) => ({
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
14
|
-
const options = this.options;
|
|
14
|
+
const data = this._activeData();
|
|
15
|
+
const options = this.options || {};
|
|
15
16
|
const box = ChartBox.fromRenderer(this.renderer);
|
|
16
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|