@maxhealth.tech/prefab 0.3.13 → 0.3.15
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/CHANGELOG.md +762 -739
- package/dist/a2ui.min.js +1 -1
- package/dist/auto/chart.d.ts +1 -1
- package/dist/auto/chart.js +1 -1
- package/dist/auto/comparison.d.ts +1 -1
- package/dist/auto/comparison.js +1 -1
- package/dist/auto/form.d.ts +1 -1
- package/dist/auto/form.js +1 -1
- package/dist/auto/index.d.ts +1 -1
- package/dist/auto/index.js +1 -1
- package/dist/auto/metrics.d.ts +1 -1
- package/dist/auto/metrics.js +1 -1
- package/dist/auto/progress.d.ts +1 -1
- package/dist/auto/progress.js +1 -1
- package/dist/auto/timeline.d.ts +1 -1
- package/dist/auto/timeline.js +1 -1
- package/dist/core/guards.d.ts +8 -0
- package/dist/core/guards.d.ts.map +1 -0
- package/dist/core/guards.js +29 -0
- package/dist/core/guards.js.map +1 -0
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/renderer/actions.d.ts +14 -2
- package/dist/renderer/actions.d.ts.map +1 -1
- package/dist/renderer/actions.js +134 -164
- package/dist/renderer/actions.js.map +1 -1
- package/dist/renderer/bridge.d.ts.map +1 -1
- package/dist/renderer/bridge.js +9 -2
- package/dist/renderer/bridge.js.map +1 -1
- package/dist/renderer/components/charts/cartesian.d.ts +4 -0
- package/dist/renderer/components/charts/cartesian.d.ts.map +1 -0
- package/dist/renderer/components/charts/cartesian.js +189 -0
- package/dist/renderer/components/charts/cartesian.js.map +1 -0
- package/dist/renderer/components/{chart-helpers.d.ts → charts/helpers.d.ts} +5 -6
- package/dist/renderer/components/charts/helpers.d.ts.map +1 -0
- package/dist/renderer/components/{chart-helpers.js → charts/helpers.js} +8 -8
- package/dist/renderer/components/charts/helpers.js.map +1 -0
- package/dist/renderer/components/charts/index.d.ts +2 -0
- package/dist/renderer/components/charts/index.d.ts.map +1 -0
- package/dist/renderer/components/charts/index.js +15 -0
- package/dist/renderer/components/charts/index.js.map +1 -0
- package/dist/renderer/components/charts/polar.d.ts +5 -0
- package/dist/renderer/components/charts/polar.d.ts.map +1 -0
- package/dist/renderer/components/charts/polar.js +189 -0
- package/dist/renderer/components/charts/polar.js.map +1 -0
- package/dist/renderer/components/charts/props.d.ts +30 -0
- package/dist/renderer/components/charts/props.d.ts.map +1 -0
- package/dist/renderer/components/charts/props.js +67 -0
- package/dist/renderer/components/charts/props.js.map +1 -0
- package/dist/renderer/components/charts/scatter.d.ts +4 -0
- package/dist/renderer/components/charts/scatter.d.ts.map +1 -0
- package/dist/renderer/components/charts/scatter.js +107 -0
- package/dist/renderer/components/charts/scatter.js.map +1 -0
- package/dist/renderer/components/{chart-tooltip.d.ts → charts/tooltip.d.ts} +3 -4
- package/dist/renderer/components/charts/tooltip.d.ts.map +1 -0
- package/dist/renderer/components/{chart-tooltip.js → charts/tooltip.js} +11 -5
- package/dist/renderer/components/charts/tooltip.js.map +1 -0
- package/dist/renderer/components/choice.d.ts.map +1 -1
- package/dist/renderer/components/choice.js.map +1 -1
- package/dist/renderer/components/data.d.ts.map +1 -1
- package/dist/renderer/components/data.js.map +1 -1
- package/dist/renderer/components/form.d.ts.map +1 -1
- package/dist/renderer/components/form.js.map +1 -1
- package/dist/renderer/components/index.js +1 -1
- package/dist/renderer/components/index.js.map +1 -1
- package/dist/renderer/components/layout.d.ts.map +1 -1
- package/dist/renderer/components/layout.js +39 -24
- package/dist/renderer/components/layout.js.map +1 -1
- package/dist/renderer/components/media.d.ts.map +1 -1
- package/dist/renderer/components/media.js.map +1 -1
- package/dist/renderer/engine.js.map +1 -1
- package/dist/renderer/index.d.ts +1 -0
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js.map +1 -1
- package/dist/renderer/node-guard.d.ts +3 -0
- package/dist/renderer/node-guard.d.ts.map +1 -0
- package/dist/renderer/node-guard.js +16 -0
- package/dist/renderer/node-guard.js.map +1 -0
- package/dist/renderer.auto.min.js +10 -10
- package/dist/renderer.min.js +10 -10
- package/package.json +2 -2
- package/dist/renderer/components/chart-helpers.d.ts.map +0 -1
- package/dist/renderer/components/chart-helpers.js.map +0 -1
- package/dist/renderer/components/chart-tooltip.d.ts.map +0 -1
- package/dist/renderer/components/chart-tooltip.js.map +0 -1
- package/dist/renderer/components/charts.d.ts +0 -9
- package/dist/renderer/components/charts.d.ts.map +0 -1
- package/dist/renderer/components/charts.js +0 -593
- package/dist/renderer/components/charts.js.map +0 -1
|
@@ -1,593 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Chart component renderers — BarChart, LineChart, AreaChart, PieChart,
|
|
3
|
-
* ScatterChart, RadialChart, Histogram.
|
|
4
|
-
*
|
|
5
|
-
* Charts render as SVG using simple built-in drawing. Shared axis/SVG/format
|
|
6
|
-
* helpers live in [chart-helpers.ts]; tooltip hit-zones in [chart-tooltip.ts].
|
|
7
|
-
*/
|
|
8
|
-
import { registerComponent, resolveValue, el } from '../engine.js';
|
|
9
|
-
import { createTooltip, addBarTooltipZones, addLineTooltipZones, showTooltipAt } from './chart-tooltip.js';
|
|
10
|
-
import { COLORS, AXIS_COLOR, GRID_COLOR, AXIS_FONT, chartLayout, createSvg, svgEl, svgText, polar, arcPath, drawYAxis, drawYAxisRight, drawXAxisLabels, drawBaseline, applyPipeFormat, resolveValueFormat, makeTooltipFormatter, addLegend, } from './chart-helpers.js';
|
|
11
|
-
import { stringifyValue } from '../../core/stringify.js';
|
|
12
|
-
export function registerChartComponents() {
|
|
13
|
-
registerComponent('BarChart', renderBarChart);
|
|
14
|
-
registerComponent('LineChart', renderLineChart);
|
|
15
|
-
registerComponent('AreaChart', renderLineChart); // Same renderer, different fill
|
|
16
|
-
registerComponent('PieChart', renderPieChart);
|
|
17
|
-
registerComponent('RadarChart', renderRadarChart);
|
|
18
|
-
registerComponent('ScatterChart', renderScatterChart);
|
|
19
|
-
registerComponent('RadialChart', renderRadialChart);
|
|
20
|
-
registerComponent('Histogram', renderHistogram);
|
|
21
|
-
}
|
|
22
|
-
// ── BarChart ─────────────────────────────────────────────────────────────────
|
|
23
|
-
function renderBarChart(node, ctx) {
|
|
24
|
-
const wrapper = el('div', 'pf-chart pf-bar-chart');
|
|
25
|
-
const data = resolveValue(node.data, ctx) ?? [];
|
|
26
|
-
const series = node.series ?? [];
|
|
27
|
-
const height = node.height ?? 300;
|
|
28
|
-
if (data.length === 0 || series.length === 0) {
|
|
29
|
-
wrapper.textContent = 'No chart data';
|
|
30
|
-
return wrapper;
|
|
31
|
-
}
|
|
32
|
-
const showYAxis = node.showYAxis !== false;
|
|
33
|
-
const showGrid = node.showGrid === true;
|
|
34
|
-
const showYAxisRight = node.showYAxisRight === true;
|
|
35
|
-
const showTooltipProp = node.showTooltip !== false;
|
|
36
|
-
const xAxisKey = node.xAxis;
|
|
37
|
-
const tooltipXKey = node.tooltipXKey;
|
|
38
|
-
const xAxisFormat = node.xAxisFormat;
|
|
39
|
-
const tooltipXFormat = node.tooltipXFormat;
|
|
40
|
-
const leftSeries = series.filter(s => s.yAxisId !== 'right');
|
|
41
|
-
const rightSeries = series.filter(s => s.yAxisId === 'right');
|
|
42
|
-
const hasRight = showYAxisRight && rightSeries.length > 0;
|
|
43
|
-
const leftMax = leftSeries.length > 0
|
|
44
|
-
? Math.max(...data.flatMap(d => leftSeries.map(s => Number(d[s.dataKey] ?? 0))), 1)
|
|
45
|
-
: 1;
|
|
46
|
-
const rightMax = hasRight
|
|
47
|
-
? Math.max(...data.flatMap(d => rightSeries.map(s => Number(d[s.dataKey] ?? 0))), 1)
|
|
48
|
-
: 1;
|
|
49
|
-
const w = 400;
|
|
50
|
-
const layout = chartLayout(w, height, showYAxis, hasRight);
|
|
51
|
-
const svg = createSvg(w, height, 'Bar');
|
|
52
|
-
// Axes + grid (behind bars)
|
|
53
|
-
if (showYAxis)
|
|
54
|
-
drawYAxis(svg, layout, leftMax, showGrid, resolveValueFormat(node));
|
|
55
|
-
if (hasRight)
|
|
56
|
-
drawYAxisRight(svg, layout, rightMax, node.yAxisRightFormat);
|
|
57
|
-
drawBaseline(svg, layout);
|
|
58
|
-
// Bars — use percentage-based X within the plot area
|
|
59
|
-
const barGroupWidth = layout.plotWidth / data.length;
|
|
60
|
-
const barWidth = barGroupWidth / (series.length + 1);
|
|
61
|
-
for (let di = 0; di < data.length; di++) {
|
|
62
|
-
for (let si = 0; si < series.length; si++) {
|
|
63
|
-
const s = series[si];
|
|
64
|
-
const raw = data[di][s.dataKey];
|
|
65
|
-
if (raw === null || raw === undefined)
|
|
66
|
-
continue; // skip null bars
|
|
67
|
-
const isRight = s.yAxisId === 'right';
|
|
68
|
-
const max = isRight ? rightMax : leftMax;
|
|
69
|
-
const val = Number(raw);
|
|
70
|
-
const h = Math.max(0, (val / max) * layout.plotHeight);
|
|
71
|
-
const x = layout.plotLeft + di * barGroupWidth + si * barWidth + barWidth / 2;
|
|
72
|
-
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
73
|
-
rect.setAttribute('x', String(x));
|
|
74
|
-
rect.setAttribute('y', String(layout.plotBottom - h));
|
|
75
|
-
rect.setAttribute('width', String(barWidth * 0.8));
|
|
76
|
-
rect.setAttribute('height', String(h));
|
|
77
|
-
rect.setAttribute('fill', s.color ?? COLORS[si % COLORS.length]);
|
|
78
|
-
rect.setAttribute('rx', '2');
|
|
79
|
-
svg.appendChild(rect);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
// X-axis labels
|
|
83
|
-
if (xAxisKey) {
|
|
84
|
-
const axisLabelFmt = xAxisFormat ? (v) => applyPipeFormat(v, xAxisFormat, ctx) : undefined;
|
|
85
|
-
drawXAxisLabels(svg, data, xAxisKey, (i) => {
|
|
86
|
-
return layout.plotLeft + i * barGroupWidth + barGroupWidth / 2;
|
|
87
|
-
}, layout.plotBottom, axisLabelFmt);
|
|
88
|
-
}
|
|
89
|
-
// Tooltip hit-zones (on top of bars)
|
|
90
|
-
if (showTooltipProp) {
|
|
91
|
-
const ttCtx = createTooltip(wrapper, svg);
|
|
92
|
-
const fmt = makeTooltipFormatter(ctx, resolveValueFormat(node), node.yAxisRightFormat);
|
|
93
|
-
const ttLabelFmt = tooltipXFormat ? (v) => applyPipeFormat(v, tooltipXFormat, ctx) : undefined;
|
|
94
|
-
addBarTooltipZones(ttCtx, svg, data, series, layout, tooltipXKey ?? xAxisKey, fmt, ttLabelFmt);
|
|
95
|
-
}
|
|
96
|
-
addLegend(wrapper, series, node.showLegend);
|
|
97
|
-
wrapper.appendChild(svg);
|
|
98
|
-
return wrapper;
|
|
99
|
-
}
|
|
100
|
-
// ── LineChart / AreaChart ────────────────────────────────────────────────────
|
|
101
|
-
function renderLineChart(node, ctx) {
|
|
102
|
-
const wrapper = el('div', `pf-chart pf-${node.type.toLowerCase()}-chart`);
|
|
103
|
-
const data = resolveValue(node.data, ctx) ?? [];
|
|
104
|
-
const allSeries = node.series ?? [];
|
|
105
|
-
const height = node.height ?? 300;
|
|
106
|
-
if (data.length === 0 || allSeries.length === 0) {
|
|
107
|
-
wrapper.textContent = 'No chart data';
|
|
108
|
-
return wrapper;
|
|
109
|
-
}
|
|
110
|
-
const showYAxis = node.showYAxis !== false;
|
|
111
|
-
const showGrid = node.showGrid === true;
|
|
112
|
-
const showYAxisRight = node.showYAxisRight === true;
|
|
113
|
-
const showTooltipProp = node.showTooltip !== false;
|
|
114
|
-
const xAxisKey = node.xAxis;
|
|
115
|
-
const tooltipXKey = node.tooltipXKey;
|
|
116
|
-
const xAxisFormat = node.xAxisFormat;
|
|
117
|
-
const tooltipXFormat = node.tooltipXFormat;
|
|
118
|
-
// Split series by axis
|
|
119
|
-
const leftSeries = allSeries.filter(s => s.yAxisId !== 'right');
|
|
120
|
-
const rightSeries = allSeries.filter(s => s.yAxisId === 'right');
|
|
121
|
-
const hasRight = showYAxisRight && rightSeries.length > 0;
|
|
122
|
-
// Compute max for each axis independently
|
|
123
|
-
const leftMax = leftSeries.length > 0
|
|
124
|
-
? Math.max(...data.flatMap(d => leftSeries.map(s => Number(d[s.dataKey] ?? 0))), 1)
|
|
125
|
-
: 1;
|
|
126
|
-
const rightMax = hasRight
|
|
127
|
-
? Math.max(...data.flatMap(d => rightSeries.map(s => Number(d[s.dataKey] ?? 0))), 1)
|
|
128
|
-
: 1;
|
|
129
|
-
const w = 400;
|
|
130
|
-
const layout = chartLayout(w, height, showYAxis, hasRight);
|
|
131
|
-
const svg = createSvg(w, height, node.type === 'AreaChart' ? 'Area' : 'Line');
|
|
132
|
-
const isArea = node.type === 'AreaChart';
|
|
133
|
-
// Draw grid + axes (behind data)
|
|
134
|
-
if (showYAxis)
|
|
135
|
-
drawYAxis(svg, layout, leftMax, showGrid, resolveValueFormat(node));
|
|
136
|
-
if (hasRight)
|
|
137
|
-
drawYAxisRight(svg, layout, rightMax, node.yAxisRightFormat);
|
|
138
|
-
drawBaseline(svg, layout);
|
|
139
|
-
// Draw series (with null-gap handling)
|
|
140
|
-
const dotGroups = []; // one array of dots per series
|
|
141
|
-
for (let si = 0; si < allSeries.length; si++) {
|
|
142
|
-
const s = allSeries[si];
|
|
143
|
-
const isRight = s.yAxisId === 'right';
|
|
144
|
-
const max = isRight ? rightMax : leftMax;
|
|
145
|
-
const color = s.color ?? COLORS[si % COLORS.length];
|
|
146
|
-
const points = data.map((d, i) => {
|
|
147
|
-
const raw = d[s.dataKey];
|
|
148
|
-
const isNull = raw === null || raw === undefined;
|
|
149
|
-
const x = data.length === 1
|
|
150
|
-
? (layout.plotLeft + layout.plotRight) / 2
|
|
151
|
-
: layout.plotLeft + (i / (data.length - 1)) * layout.plotWidth;
|
|
152
|
-
const y = isNull ? layout.plotBottom : layout.plotBottom - (Number(raw) / max) * layout.plotHeight;
|
|
153
|
-
return { x, y, isNull };
|
|
154
|
-
});
|
|
155
|
-
// Area fill (skip null segments)
|
|
156
|
-
if (isArea) {
|
|
157
|
-
let seg = [];
|
|
158
|
-
const flushArea = () => {
|
|
159
|
-
if (seg.length < 2) {
|
|
160
|
-
seg = [];
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
const d = `M ${seg[0].x},${layout.plotBottom} ` +
|
|
164
|
-
seg.map(p => `L ${p.x},${p.y}`).join(' ') +
|
|
165
|
-
` L ${seg[seg.length - 1].x},${layout.plotBottom} Z`;
|
|
166
|
-
const area = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
167
|
-
area.setAttribute('d', d);
|
|
168
|
-
area.setAttribute('fill', color);
|
|
169
|
-
area.setAttribute('opacity', '0.15');
|
|
170
|
-
svg.appendChild(area);
|
|
171
|
-
seg = [];
|
|
172
|
-
};
|
|
173
|
-
for (const p of points) {
|
|
174
|
-
if (p.isNull) {
|
|
175
|
-
flushArea();
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
seg.push(p);
|
|
179
|
-
}
|
|
180
|
-
flushArea();
|
|
181
|
-
}
|
|
182
|
-
// Line path with gap handling: break into segments on null
|
|
183
|
-
let linePath = '';
|
|
184
|
-
for (const p of points) {
|
|
185
|
-
if (p.isNull) { /* gap — next valid point starts a new M */
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
// Check if previous point was null → start new segment
|
|
189
|
-
const idx = points.indexOf(p);
|
|
190
|
-
const prevNull = idx === 0 || points[idx - 1].isNull;
|
|
191
|
-
linePath += `${prevNull ? 'M' : 'L'} ${p.x},${p.y} `;
|
|
192
|
-
}
|
|
193
|
-
if (linePath) {
|
|
194
|
-
const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
195
|
-
line.setAttribute('d', linePath.trim());
|
|
196
|
-
line.setAttribute('fill', 'none');
|
|
197
|
-
line.setAttribute('stroke', color);
|
|
198
|
-
line.setAttribute('stroke-width', '2');
|
|
199
|
-
if (isRight)
|
|
200
|
-
line.setAttribute('stroke-dasharray', '6 3');
|
|
201
|
-
svg.appendChild(line);
|
|
202
|
-
}
|
|
203
|
-
// Data-point dots (hidden by default, shown on hover)
|
|
204
|
-
const seriesDots = [];
|
|
205
|
-
for (const p of points) {
|
|
206
|
-
const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
207
|
-
dot.setAttribute('cx', String(p.x));
|
|
208
|
-
dot.setAttribute('cy', String(p.y));
|
|
209
|
-
dot.setAttribute('r', '4');
|
|
210
|
-
dot.setAttribute('fill', color);
|
|
211
|
-
dot.setAttribute('class', 'pf-data-dot');
|
|
212
|
-
dot.setAttribute('data-visible', 'false');
|
|
213
|
-
dot.style.pointerEvents = 'none';
|
|
214
|
-
if (p.isNull)
|
|
215
|
-
dot.style.display = 'none';
|
|
216
|
-
svg.appendChild(dot);
|
|
217
|
-
seriesDots.push(dot);
|
|
218
|
-
}
|
|
219
|
-
dotGroups.push(seriesDots);
|
|
220
|
-
}
|
|
221
|
-
// Crosshair line (hidden by default)
|
|
222
|
-
const crosshair = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
223
|
-
crosshair.setAttribute('class', 'pf-crosshair');
|
|
224
|
-
crosshair.setAttribute('x1', String(layout.plotLeft));
|
|
225
|
-
crosshair.setAttribute('x2', String(layout.plotLeft));
|
|
226
|
-
crosshair.setAttribute('y1', String(layout.plotTop));
|
|
227
|
-
crosshair.setAttribute('y2', String(layout.plotBottom));
|
|
228
|
-
crosshair.setAttribute('stroke', AXIS_COLOR);
|
|
229
|
-
crosshair.setAttribute('stroke-width', '1');
|
|
230
|
-
crosshair.setAttribute('stroke-dasharray', '3 3');
|
|
231
|
-
crosshair.setAttribute('opacity', '0');
|
|
232
|
-
svg.appendChild(crosshair);
|
|
233
|
-
// X-axis labels
|
|
234
|
-
if (xAxisKey) {
|
|
235
|
-
const axisLabelFmt = xAxisFormat ? (v) => applyPipeFormat(v, xAxisFormat, ctx) : undefined;
|
|
236
|
-
drawXAxisLabels(svg, data, xAxisKey, (i) => {
|
|
237
|
-
return data.length === 1
|
|
238
|
-
? (layout.plotLeft + layout.plotRight) / 2
|
|
239
|
-
: layout.plotLeft + (i / (data.length - 1)) * layout.plotWidth;
|
|
240
|
-
}, layout.plotBottom, axisLabelFmt);
|
|
241
|
-
}
|
|
242
|
-
// Tooltip hit-zones (on top of lines)
|
|
243
|
-
if (showTooltipProp) {
|
|
244
|
-
const ttCtx = createTooltip(wrapper, svg);
|
|
245
|
-
const fmt = makeTooltipFormatter(ctx, resolveValueFormat(node), node.yAxisRightFormat);
|
|
246
|
-
const ttLabelFmt = tooltipXFormat ? (v) => applyPipeFormat(v, tooltipXFormat, ctx) : undefined;
|
|
247
|
-
addLineTooltipZones(ttCtx, svg, data, allSeries, layout, tooltipXKey ?? xAxisKey, fmt, crosshair, dotGroups, ttLabelFmt);
|
|
248
|
-
}
|
|
249
|
-
addLegend(wrapper, allSeries, node.showLegend);
|
|
250
|
-
wrapper.appendChild(svg);
|
|
251
|
-
return wrapper;
|
|
252
|
-
}
|
|
253
|
-
// ── PieChart ─────────────────────────────────────────────────────────────────
|
|
254
|
-
function renderPieChart(node, ctx) {
|
|
255
|
-
const wrapper = el('div', 'pf-chart pf-pie-chart');
|
|
256
|
-
const data = resolveValue(node.data, ctx) ?? [];
|
|
257
|
-
const seriesIn = node.series ?? [];
|
|
258
|
-
const height = node.height ?? 300;
|
|
259
|
-
const size = Math.min(height, 300);
|
|
260
|
-
const showTooltipProp = node.showTooltip !== false;
|
|
261
|
-
// Value field: upstream `dataKey`, falling back to series[0] (legacy series-based input).
|
|
262
|
-
const key = node.dataKey ?? seriesIn[0]?.dataKey;
|
|
263
|
-
// Slice-label field: upstream `nameKey`, falling back to tooltipXKey / xAxis (legacy).
|
|
264
|
-
const nameKey = node.nameKey
|
|
265
|
-
?? node.tooltipXKey
|
|
266
|
-
?? node.xAxis;
|
|
267
|
-
// Synthesize a single series for the legend/tooltip when only dataKey/nameKey is given.
|
|
268
|
-
const series = seriesIn.length > 0
|
|
269
|
-
? seriesIn
|
|
270
|
-
: (key ? [{ dataKey: key, label: key }] : []);
|
|
271
|
-
if (data.length === 0 || !key) {
|
|
272
|
-
wrapper.textContent = 'No chart data';
|
|
273
|
-
return wrapper;
|
|
274
|
-
}
|
|
275
|
-
const svg = createSvg(size, size, 'Pie');
|
|
276
|
-
const cx = size / 2;
|
|
277
|
-
const cy = size / 2;
|
|
278
|
-
const r = size / 2 - 10;
|
|
279
|
-
const tooltipXFormat = node.tooltipXFormat;
|
|
280
|
-
const valueFmt = resolveValueFormat(node);
|
|
281
|
-
const fmtValue = (v) => valueFmt ? applyPipeFormat(v, valueFmt, ctx) : String(v);
|
|
282
|
-
const values = data.map(d => Number(d[key] ?? 0));
|
|
283
|
-
const total = values.reduce((a, b) => a + b, 0);
|
|
284
|
-
if (total === 0) {
|
|
285
|
-
wrapper.textContent = 'No chart data';
|
|
286
|
-
wrapper.appendChild(svg);
|
|
287
|
-
return wrapper;
|
|
288
|
-
}
|
|
289
|
-
const ttCtx = showTooltipProp ? createTooltip(wrapper, svg) : undefined;
|
|
290
|
-
let startAngle = -Math.PI / 2;
|
|
291
|
-
for (let i = 0; i < values.length; i++) {
|
|
292
|
-
const angle = (values[i] / total) * 2 * Math.PI;
|
|
293
|
-
const endAngle = startAngle + angle;
|
|
294
|
-
const largeArc = angle > Math.PI ? 1 : 0;
|
|
295
|
-
const x1 = cx + r * Math.cos(startAngle);
|
|
296
|
-
const y1 = cy + r * Math.sin(startAngle);
|
|
297
|
-
const x2 = cx + r * Math.cos(endAngle);
|
|
298
|
-
const y2 = cy + r * Math.sin(endAngle);
|
|
299
|
-
const color = COLORS[i % COLORS.length];
|
|
300
|
-
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
301
|
-
path.setAttribute('d', `M ${cx},${cy} L ${x1},${y1} A ${r},${r} 0 ${largeArc} 1 ${x2},${y2} Z`);
|
|
302
|
-
path.setAttribute('fill', color);
|
|
303
|
-
path.style.cursor = 'default';
|
|
304
|
-
if (ttCtx) {
|
|
305
|
-
const rawSlice = nameKey ? data[i][nameKey] : undefined;
|
|
306
|
-
const sliceLabel = rawSlice != null
|
|
307
|
-
? (tooltipXFormat ? applyPipeFormat(rawSlice, tooltipXFormat, ctx) : stringifyValue(rawSlice))
|
|
308
|
-
: `Slice ${i + 1}`;
|
|
309
|
-
const pct = `${((values[i] / total) * 100).toFixed(1)}%`;
|
|
310
|
-
const midAngle = startAngle + angle / 2;
|
|
311
|
-
const tipX = cx + (r * 0.6) * Math.cos(midAngle);
|
|
312
|
-
path.addEventListener('mouseenter', () => {
|
|
313
|
-
showTooltipAt(ttCtx, tipX, cy, sliceLabel, [
|
|
314
|
-
{ label: series[0].label ?? key, value: `${fmtValue(values[i])} (${pct})`, color },
|
|
315
|
-
]);
|
|
316
|
-
});
|
|
317
|
-
path.addEventListener('mouseleave', () => {
|
|
318
|
-
ttCtx.tooltip.classList.remove('pf-visible');
|
|
319
|
-
});
|
|
320
|
-
path.addEventListener('touchstart', () => {
|
|
321
|
-
showTooltipAt(ttCtx, tipX, cy, sliceLabel, [
|
|
322
|
-
{ label: series[0].label ?? key, value: `${fmtValue(values[i])} (${pct})`, color },
|
|
323
|
-
]);
|
|
324
|
-
}, { passive: true });
|
|
325
|
-
path.addEventListener('touchend', () => {
|
|
326
|
-
ttCtx.tooltip.classList.remove('pf-visible');
|
|
327
|
-
}, { passive: true });
|
|
328
|
-
}
|
|
329
|
-
svg.appendChild(path);
|
|
330
|
-
startAngle = endAngle;
|
|
331
|
-
}
|
|
332
|
-
addLegend(wrapper, series, node.showLegend);
|
|
333
|
-
wrapper.appendChild(svg);
|
|
334
|
-
return wrapper;
|
|
335
|
-
}
|
|
336
|
-
// ── ScatterChart ─────────────────────────────────────────────────────────────
|
|
337
|
-
function renderScatterChart(node, ctx) {
|
|
338
|
-
const wrapper = el('div', 'pf-chart pf-scatter-chart');
|
|
339
|
-
const data = resolveValue(node.data, ctx) ?? [];
|
|
340
|
-
const xKey = node.xAxis;
|
|
341
|
-
const yKey = node.yAxis;
|
|
342
|
-
const zKey = node.zAxis;
|
|
343
|
-
const series = node.series ?? [];
|
|
344
|
-
const height = node.height ?? 300;
|
|
345
|
-
if (data.length === 0 || !xKey || !yKey) {
|
|
346
|
-
wrapper.textContent = 'No chart data';
|
|
347
|
-
return wrapper;
|
|
348
|
-
}
|
|
349
|
-
const showGrid = node.showGrid !== false;
|
|
350
|
-
const showTooltipProp = node.showTooltip !== false;
|
|
351
|
-
const color = series[0]?.color ?? COLORS[0];
|
|
352
|
-
const valueFmt = resolveValueFormat(node);
|
|
353
|
-
const fmt = (v) => valueFmt ? applyPipeFormat(v, valueFmt, ctx) : String(Math.round(v * 100) / 100);
|
|
354
|
-
const xs = data.map(d => Number(d[xKey] ?? 0));
|
|
355
|
-
const ys = data.map(d => Number(d[yKey] ?? 0));
|
|
356
|
-
const xMin = Math.min(...xs), xMax = Math.max(...xs);
|
|
357
|
-
const yMin = Math.min(...ys), yMax = Math.max(...ys);
|
|
358
|
-
const xRange = (xMax - xMin) || 1;
|
|
359
|
-
const yRange = (yMax - yMin) || 1;
|
|
360
|
-
const zs = zKey ? data.map(d => Number(d[zKey] ?? 0)) : [];
|
|
361
|
-
const zMax = zKey ? Math.max(...zs, 1) : 1;
|
|
362
|
-
const W = 400;
|
|
363
|
-
const layout = chartLayout(W, height, true);
|
|
364
|
-
const svg = createSvg(W, height, 'Scatter');
|
|
365
|
-
const sx = (v) => layout.plotLeft + ((v - xMin) / xRange) * layout.plotWidth;
|
|
366
|
-
const sy = (v) => layout.plotBottom - ((v - yMin) / yRange) * layout.plotHeight;
|
|
367
|
-
// Y axis ticks across the actual [min, max] range (+ optional grid).
|
|
368
|
-
const Y_TICKS = 4;
|
|
369
|
-
for (let t = 0; t <= Y_TICKS; t++) {
|
|
370
|
-
const val = yMin + (yRange * t) / Y_TICKS;
|
|
371
|
-
const y = sy(val);
|
|
372
|
-
svg.appendChild(svgText({ x: layout.plotLeft - 6, y: y + 3, 'text-anchor': 'end', 'font-size': AXIS_FONT, fill: AXIS_COLOR }, fmt(val)));
|
|
373
|
-
if (showGrid && t > 0) {
|
|
374
|
-
svg.appendChild(svgEl('line', {
|
|
375
|
-
x1: layout.plotLeft, y1: y, x2: layout.plotRight, y2: y,
|
|
376
|
-
stroke: GRID_COLOR, 'stroke-width': 1, 'stroke-dasharray': '4 3',
|
|
377
|
-
}));
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
drawBaseline(svg, layout);
|
|
381
|
-
// X axis: min / mid / max labels.
|
|
382
|
-
for (const xv of [xMin, (xMin + xMax) / 2, xMax]) {
|
|
383
|
-
svg.appendChild(svgText({ x: sx(xv), y: layout.plotBottom + 14, 'text-anchor': 'middle', 'font-size': AXIS_FONT, fill: AXIS_COLOR }, String(Math.round(xv * 100) / 100)));
|
|
384
|
-
}
|
|
385
|
-
const ttCtx = showTooltipProp ? createTooltip(wrapper, svg) : undefined;
|
|
386
|
-
for (let i = 0; i < data.length; i++) {
|
|
387
|
-
const px = sx(xs[i]);
|
|
388
|
-
const py = sy(ys[i]);
|
|
389
|
-
// Bubble radius from z (sqrt → area-proportional); fixed dot otherwise.
|
|
390
|
-
const rad = zKey ? 4 + Math.sqrt(zs[i] / zMax) * 14 : 4;
|
|
391
|
-
const dot = svgEl('circle', { cx: px, cy: py, r: rad, fill: color, 'fill-opacity': zKey ? 0.6 : 0.85 });
|
|
392
|
-
if (ttCtx) {
|
|
393
|
-
const entries = [
|
|
394
|
-
{ label: xKey, value: fmt(xs[i]), color },
|
|
395
|
-
{ label: yKey, value: fmt(ys[i]), color },
|
|
396
|
-
...(zKey ? [{ label: zKey, value: fmt(zs[i]), color }] : []),
|
|
397
|
-
];
|
|
398
|
-
const title = series[0]?.label ?? 'Point';
|
|
399
|
-
dot.addEventListener('mouseenter', () => showTooltipAt(ttCtx, px, py, title, entries));
|
|
400
|
-
dot.addEventListener('mouseleave', () => ttCtx.tooltip.classList.remove('pf-visible'));
|
|
401
|
-
dot.addEventListener('touchstart', () => showTooltipAt(ttCtx, px, py, title, entries), { passive: true });
|
|
402
|
-
dot.addEventListener('touchend', () => ttCtx.tooltip.classList.remove('pf-visible'), { passive: true });
|
|
403
|
-
}
|
|
404
|
-
svg.appendChild(dot);
|
|
405
|
-
}
|
|
406
|
-
addLegend(wrapper, series, node.showLegend);
|
|
407
|
-
wrapper.appendChild(svg);
|
|
408
|
-
return wrapper;
|
|
409
|
-
}
|
|
410
|
-
// ── RadialChart ──────────────────────────────────────────────────────────────
|
|
411
|
-
function renderRadialChart(node, ctx) {
|
|
412
|
-
const wrapper = el('div', 'pf-chart pf-radial-chart');
|
|
413
|
-
const data = resolveValue(node.data, ctx) ?? [];
|
|
414
|
-
const seriesIn = node.series ?? [];
|
|
415
|
-
const dataKey = node.dataKey ?? seriesIn[0]?.dataKey;
|
|
416
|
-
const nameKey = node.nameKey
|
|
417
|
-
?? node.tooltipXKey
|
|
418
|
-
?? node.xAxis;
|
|
419
|
-
const height = node.height ?? 300;
|
|
420
|
-
if (data.length === 0 || !dataKey) {
|
|
421
|
-
wrapper.textContent = 'No chart data';
|
|
422
|
-
return wrapper;
|
|
423
|
-
}
|
|
424
|
-
const size = Math.min(height, 300);
|
|
425
|
-
const cx = size / 2;
|
|
426
|
-
const cy = size / 2;
|
|
427
|
-
const outer = size / 2 - 10;
|
|
428
|
-
const inner = Math.max(0, Math.min(node.innerRadius ?? 30, outer - 10));
|
|
429
|
-
const startAngle = node.startAngle ?? 180;
|
|
430
|
-
const endAngle = node.endAngle ?? 0;
|
|
431
|
-
const showTooltipProp = node.showTooltip !== false;
|
|
432
|
-
const values = data.map(d => Number(d[dataKey] ?? 0));
|
|
433
|
-
const max = Math.max(...values, 1);
|
|
434
|
-
const svg = createSvg(size, size, 'Radial');
|
|
435
|
-
const band = (outer - inner) / data.length;
|
|
436
|
-
const thickness = Math.max(2, band * 0.7);
|
|
437
|
-
const valueFmt = resolveValueFormat(node);
|
|
438
|
-
const fmt = (v) => valueFmt ? applyPipeFormat(v, valueFmt, ctx) : String(v);
|
|
439
|
-
const ttCtx = showTooltipProp ? createTooltip(wrapper, svg) : undefined;
|
|
440
|
-
const legendSeries = [];
|
|
441
|
-
for (let i = 0; i < data.length; i++) {
|
|
442
|
-
// First row is the outermost ring.
|
|
443
|
-
const rMid = outer - band * i - band / 2;
|
|
444
|
-
const color = seriesIn[i]?.color ?? COLORS[i % COLORS.length];
|
|
445
|
-
const frac = values[i] / max;
|
|
446
|
-
const valueEnd = startAngle + frac * (endAngle - startAngle);
|
|
447
|
-
const rawName = nameKey ? data[i][nameKey] : null;
|
|
448
|
-
const name = rawName != null ? stringifyValue(rawName) : (seriesIn[i]?.label ?? dataKey);
|
|
449
|
-
legendSeries.push({ dataKey: name || dataKey, label: name || dataKey, color });
|
|
450
|
-
// Muted full-range track.
|
|
451
|
-
svg.appendChild(svgEl('path', {
|
|
452
|
-
d: arcPath(cx, cy, rMid, startAngle, endAngle),
|
|
453
|
-
fill: 'none', stroke: GRID_COLOR, 'stroke-width': thickness, 'stroke-linecap': 'round',
|
|
454
|
-
}));
|
|
455
|
-
// Coloured value arc.
|
|
456
|
-
const arc = svgEl('path', {
|
|
457
|
-
d: arcPath(cx, cy, rMid, startAngle, valueEnd),
|
|
458
|
-
fill: 'none', stroke: color, 'stroke-width': thickness, 'stroke-linecap': 'round',
|
|
459
|
-
});
|
|
460
|
-
if (ttCtx) {
|
|
461
|
-
const mid = polar(cx, cy, rMid, (startAngle + valueEnd) / 2);
|
|
462
|
-
const entries = [{ label: dataKey, value: fmt(values[i]), color }];
|
|
463
|
-
const title = name || dataKey;
|
|
464
|
-
arc.addEventListener('mouseenter', () => showTooltipAt(ttCtx, mid.x, mid.y, title, entries));
|
|
465
|
-
arc.addEventListener('mouseleave', () => ttCtx.tooltip.classList.remove('pf-visible'));
|
|
466
|
-
arc.addEventListener('touchstart', () => showTooltipAt(ttCtx, mid.x, mid.y, title, entries), { passive: true });
|
|
467
|
-
arc.addEventListener('touchend', () => ttCtx.tooltip.classList.remove('pf-visible'), { passive: true });
|
|
468
|
-
}
|
|
469
|
-
svg.appendChild(arc);
|
|
470
|
-
}
|
|
471
|
-
addLegend(wrapper, legendSeries, node.showLegend);
|
|
472
|
-
wrapper.appendChild(svg);
|
|
473
|
-
return wrapper;
|
|
474
|
-
}
|
|
475
|
-
// ── RadarChart ───────────────────────────────────────────────────────────────
|
|
476
|
-
function renderRadarChart(node, ctx) {
|
|
477
|
-
const wrapper = el('div', 'pf-chart pf-radar-chart');
|
|
478
|
-
const data = resolveValue(node.data, ctx) ?? [];
|
|
479
|
-
const series = node.series ?? [];
|
|
480
|
-
const axisKey = node.axisKey ?? node.xAxis;
|
|
481
|
-
const height = node.height ?? 300;
|
|
482
|
-
if (data.length === 0 || series.length === 0) {
|
|
483
|
-
wrapper.textContent = 'No chart data';
|
|
484
|
-
return wrapper;
|
|
485
|
-
}
|
|
486
|
-
const filled = node.filled !== false;
|
|
487
|
-
const showDots = node.showDots === true;
|
|
488
|
-
const showGrid = node.showGrid !== false;
|
|
489
|
-
const showTooltipProp = node.showTooltip !== false;
|
|
490
|
-
const size = Math.min(height, 320);
|
|
491
|
-
const cx = size / 2;
|
|
492
|
-
const cy = size / 2;
|
|
493
|
-
const R = size / 2 - 24; // leave room for spoke labels
|
|
494
|
-
const N = data.length;
|
|
495
|
-
const max = Math.max(...data.flatMap(d => series.map(s => Number(d[s.dataKey] ?? 0))), 1);
|
|
496
|
-
// Spokes start at the top and go clockwise.
|
|
497
|
-
const angleAt = (i) => 90 - (360 * i) / N;
|
|
498
|
-
const ptStr = (pts) => pts.map(p => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');
|
|
499
|
-
const svg = createSvg(size, size, 'Radar');
|
|
500
|
-
// Polar grid: concentric rings + spokes.
|
|
501
|
-
if (showGrid) {
|
|
502
|
-
const rings = 4;
|
|
503
|
-
for (let r = 1; r <= rings; r++) {
|
|
504
|
-
const rr = (R * r) / rings;
|
|
505
|
-
const ring = Array.from({ length: N }, (_unused, i) => polar(cx, cy, rr, angleAt(i)));
|
|
506
|
-
svg.appendChild(svgEl('polygon', { points: ptStr(ring), fill: 'none', stroke: GRID_COLOR, 'stroke-width': 1 }));
|
|
507
|
-
}
|
|
508
|
-
for (let i = 0; i < N; i++) {
|
|
509
|
-
const p = polar(cx, cy, R, angleAt(i));
|
|
510
|
-
svg.appendChild(svgEl('line', { x1: cx, y1: cy, x2: p.x, y2: p.y, stroke: GRID_COLOR, 'stroke-width': 1 }));
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
// Spoke labels.
|
|
514
|
-
for (let i = 0; i < N; i++) {
|
|
515
|
-
const p = polar(cx, cy, R + 12, angleAt(i));
|
|
516
|
-
const raw = axisKey ? data[i][axisKey] : null;
|
|
517
|
-
svg.appendChild(svgText({ x: p.x, y: p.y + 3, 'text-anchor': 'middle', 'font-size': AXIS_FONT, fill: AXIS_COLOR }, raw != null ? String(raw) : ''));
|
|
518
|
-
}
|
|
519
|
-
const ttCtx = showTooltipProp ? createTooltip(wrapper, svg) : undefined;
|
|
520
|
-
const valueFmt = resolveValueFormat(node);
|
|
521
|
-
const fmt = (v) => valueFmt ? applyPipeFormat(v, valueFmt, ctx) : String(v);
|
|
522
|
-
// One polygon per series.
|
|
523
|
-
for (let si = 0; si < series.length; si++) {
|
|
524
|
-
const s = series[si];
|
|
525
|
-
const color = s.color ?? COLORS[si % COLORS.length];
|
|
526
|
-
const pts = data.map((d, i) => polar(cx, cy, (Number(d[s.dataKey] ?? 0) / max) * R, angleAt(i)));
|
|
527
|
-
svg.appendChild(svgEl('polygon', {
|
|
528
|
-
points: ptStr(pts),
|
|
529
|
-
fill: filled ? color : 'none', 'fill-opacity': filled ? 0.2 : 0,
|
|
530
|
-
stroke: color, 'stroke-width': 2,
|
|
531
|
-
}));
|
|
532
|
-
if (showDots || ttCtx) {
|
|
533
|
-
for (let i = 0; i < pts.length; i++) {
|
|
534
|
-
const dot = svgEl('circle', {
|
|
535
|
-
cx: pts[i].x, cy: pts[i].y, r: showDots ? 3 : 6,
|
|
536
|
-
fill: showDots ? color : 'transparent', 'fill-opacity': showDots ? 1 : 0,
|
|
537
|
-
});
|
|
538
|
-
if (ttCtx) {
|
|
539
|
-
const raw = axisKey ? data[i][axisKey] : null;
|
|
540
|
-
const title = raw != null ? stringifyValue(raw) : (s.label ?? s.dataKey);
|
|
541
|
-
const entries = [{ label: s.label ?? s.dataKey, value: fmt(Number(data[i][s.dataKey] ?? 0)), color }];
|
|
542
|
-
dot.addEventListener('mouseenter', () => showTooltipAt(ttCtx, pts[i].x, pts[i].y, title, entries));
|
|
543
|
-
dot.addEventListener('mouseleave', () => ttCtx.tooltip.classList.remove('pf-visible'));
|
|
544
|
-
dot.addEventListener('touchstart', () => showTooltipAt(ttCtx, pts[i].x, pts[i].y, title, entries), { passive: true });
|
|
545
|
-
dot.addEventListener('touchend', () => ttCtx.tooltip.classList.remove('pf-visible'), { passive: true });
|
|
546
|
-
}
|
|
547
|
-
svg.appendChild(dot);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
addLegend(wrapper, series, node.showLegend);
|
|
552
|
-
wrapper.appendChild(svg);
|
|
553
|
-
return wrapper;
|
|
554
|
-
}
|
|
555
|
-
// ── Histogram ────────────────────────────────────────────────────────────────
|
|
556
|
-
function renderHistogram(node, ctx) {
|
|
557
|
-
const wrapper = el('div', 'pf-chart pf-histogram');
|
|
558
|
-
const rawData = resolveValue(node.data, ctx);
|
|
559
|
-
if (!rawData || rawData.length === 0) {
|
|
560
|
-
wrapper.textContent = 'No data';
|
|
561
|
-
return wrapper;
|
|
562
|
-
}
|
|
563
|
-
const data = rawData;
|
|
564
|
-
const binCount = node.bins ?? 10;
|
|
565
|
-
const height = node.height ?? 200;
|
|
566
|
-
const color = node.color ?? COLORS[0];
|
|
567
|
-
const min = Math.min(...data);
|
|
568
|
-
const max = Math.max(...data);
|
|
569
|
-
const binWidth = (max - min) / binCount || 1;
|
|
570
|
-
const bins = new Array(binCount).fill(0);
|
|
571
|
-
for (const v of data) {
|
|
572
|
-
const idx = Math.min(Math.floor((v - min) / binWidth), binCount - 1);
|
|
573
|
-
bins[idx]++;
|
|
574
|
-
}
|
|
575
|
-
const maxBin = Math.max(...bins);
|
|
576
|
-
const W = 300;
|
|
577
|
-
const H = height;
|
|
578
|
-
const barW = W / binCount;
|
|
579
|
-
const svg = createSvg(W, H, 'Histogram');
|
|
580
|
-
for (let i = 0; i < binCount; i++) {
|
|
581
|
-
const barH = maxBin > 0 ? (bins[i] / maxBin) * H : 0;
|
|
582
|
-
const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
583
|
-
rect.setAttribute('x', String(i * barW));
|
|
584
|
-
rect.setAttribute('y', String(H - barH));
|
|
585
|
-
rect.setAttribute('width', String(barW - 1));
|
|
586
|
-
rect.setAttribute('height', String(barH));
|
|
587
|
-
rect.setAttribute('fill', color);
|
|
588
|
-
svg.appendChild(rect);
|
|
589
|
-
}
|
|
590
|
-
wrapper.appendChild(svg);
|
|
591
|
-
return wrapper;
|
|
592
|
-
}
|
|
593
|
-
//# sourceMappingURL=charts.js.map
|