Pulse/frontend-modern/update_sparkline.js
2026-03-18 16:06:30 +00:00

279 lines
10 KiB
JavaScript

const fs = require('fs');
const file = './src/components/shared/InteractiveSparkline.tsx';
let code = fs.readFileSync(file, 'utf8');
// 1. Optimize findNearestMetricPoint
code = code.replace(
`const findNearestMetricPoint = (points: MetricPoint[], targetTimestamp: number): MetricPoint | null => {`,
`const findNearestMetricPoint = (points: MetricPoint[], targetTimestamp: number, hintIndex: number = -1): { point: MetricPoint, index: number } | null => {`
);
code = code.replace(
` const candidate = points[low];
const previous = low > 0 ? points[low - 1] : candidate;
return Math.abs(previous.timestamp - targetTimestamp) <= Math.abs(candidate.timestamp - targetTimestamp)
? previous
: candidate;
};`,
` const candidate = points[low];
const previous = low > 0 ? points[low - 1] : candidate;
if (Math.abs(previous.timestamp - targetTimestamp) <= Math.abs(candidate.timestamp - targetTimestamp)) {
return { point: previous, index: low > 0 ? low - 1 : low };
}
return { point: candidate, index: low };
};`
);
// Update calls to findNearestMetricPoint
code = code.replace(
` const point = findNearestMetricPoint(series.hoverData, targetTimestamp);
if (!point) continue;`,
` const nearest = findNearestMetricPoint(series.hoverData, targetTimestamp);
if (!nearest) continue;
const point = nearest.point;`
);
// 2. Add Fill/Area Gradients & 3. Hover crosshair capping
code = code.replace(
` const x = clamp(((point.timestamp - windowStart) / rangeMs) * vbW, 0, vbW);
const normalized = yMode() === 'auto'
? Math.max(0, point.value) / scaleMax
: Math.min(Math.max(point.value, 0), 100) / 100;
const y = vbH - normalized * vbH;
return \`\${x.toFixed(1)},\${y.toFixed(1)}\`;
});
return { path: \`M\${points.join('L')}\`, color: series.color, seriesIndex: index };`,
` const x = clamp(((point.timestamp - windowStart) / rangeMs) * vbW, 0, vbW);
const normalized = yMode() === 'auto'
? Math.max(0, point.value) / scaleMax
: Math.min(Math.max(point.value, 0), 100) / 100;
const y = vbH - normalized * vbH;
return { x, y };
});
const pathStrings = points.map(p => \`\${p.x.toFixed(1)},\${p.y.toFixed(1)}\`);
const path = \`M\${pathStrings.join('L')}\`;
let areaPath = '';
if (validSeries.length === 1 && points.length > 1) {
areaPath = \`\${path} L\${points[points.length - 1].x.toFixed(1)},\${vbH} L\${points[0].x.toFixed(1)},\${vbH} Z\`;
}
return { path, areaPath, color: series.color, seriesIndex: index };`
);
// Track minY for hover line
code = code.replace(
` let nearestSeriesIndex: number | null = null;
let nearestDistance = Number.POSITIVE_INFINITY;
const values: HoverSeriesValue[] = [];`,
` let nearestSeriesIndex: number | null = null;
let nearestDistance = Number.POSITIVE_INFINITY;
const values: HoverSeriesValue[] = [];
let minY = vbH;`
);
code = code.replace(
` if (shouldTrackNearest) {
const pointY = valueToChartY(point.value);`,
` const pointY = valueToChartY(point.value);
if (pointY < minY) minY = pointY;
if (shouldTrackNearest) {`
);
code = code.replace(
` timestamp: tooltipValues[0].timestamp,
totalValues,`,
` timestamp: tooltipValues[0].timestamp,
totalValues,
minY,`
);
// 3. De-duplicate Identical Tooltip Values
code = code.replace(
` const tooltipValues: HoverSeriesValue[] = focusedTooltip && effectiveSeriesIndex !== null
? values.filter((value) => value.seriesIndex === effectiveSeriesIndex)
: (
props.sortTooltipByValue
? selectTopValuesByValue(values, maxRows())
: values.slice(0, maxRows())
);`,
` let groupedValues = values;
if (!focusedTooltip) {
const byValue = new Map<number, HoverSeriesValue[]>();
for (const v of values) {
let key = Math.round(v.value * 1000) / 1000;
if (!byValue.has(key)) byValue.set(key, []);
byValue.get(key)!.push(v);
}
groupedValues = [];
for (const arr of byValue.values()) {
if (arr.length > 1) {
groupedValues.push({
name: \`\${arr.length} Series\`,
color: 'currentColor',
value: arr[0].value,
timestamp: arr[0].timestamp,
seriesIndex: -1,
});
} else {
groupedValues.push(arr[0]);
}
}
}
const tooltipValues: HoverSeriesValue[] = focusedTooltip && effectiveSeriesIndex !== null
? values.filter((value) => value.seriesIndex === effectiveSeriesIndex)
: (
props.sortTooltipByValue
? selectTopValuesByValue(groupedValues, maxRows())
: groupedValues.slice(0, maxRows())
);`
);
// 4. Update SVG & Canvas to use area and minY
code = code.replace(
` x1={hover().x}
y1="0"
x2={hover().x}
y2={vbH}`,
` x1={hover().x}
y1={Math.max(0, hover().minY - 4)}
x2={hover().x}
y2={vbH}`
);
// Add Defs for single series area gradient and area path logic
code = code.replace(
` <For each={gridLineY()}>`,
` <Show when={chartData().validSeries.length === 1}>
<defs>
<linearGradient id="single-series-area" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color={chartData().validSeries[0].color} stop-opacity="0.25" />
<stop offset="100%" stop-color={chartData().validSeries[0].color} stop-opacity="0" />
</linearGradient>
</defs>
</Show>
<For each={gridLineY()}>`
);
code = code.replace(
` <For each={chartData().paths}>
{(pathData) => (
<path
d={pathData.path}`,
` <For each={chartData().paths}>
{(pathData) => (
<g>
<Show when={pathData.areaPath}>
<path
d={pathData.areaPath}
fill="url(#single-series-area)"
stroke="none"
/>
</Show>
<path
d={pathData.path}`
);
code = code.replace(
` />
)}
</For>`,
` />
</g>
)}
</For>`
);
code = code.replace(
` ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.stroke();`,
` const grad = ctx.createLinearGradient(0, Math.max(0, hover.minY - 4), 0, height);
grad.addColorStop(0, 'rgba(255, 255, 255, 0)');
grad.addColorStop(0.1, hoverLineColor);
grad.addColorStop(1, hoverLineColor);
ctx.strokeStyle = grad;
ctx.beginPath();
ctx.moveTo(x, Math.max(0, hover.minY - 4));
ctx.lineTo(x, height);
ctx.stroke();`
);
// Update canvas for Area Fill
code = code.replace(
` if (segment.length === 0) continue;
ctx.beginPath();
for (let i = 0; i < segment.length; i++) {
const point = segment[i];
const x = clamp(((point.timestamp - computed.windowStart) / computed.rangeMs) * width, 0, width);
const y = valueToY(point.value);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();`,
` if (segment.length === 0) continue;
// Single series area fill
if (computed.validSeries.length === 1) {
ctx.beginPath();
for (let i = 0; i < segment.length; i++) {
const point = segment[i];
const x = clamp(((point.timestamp - computed.windowStart) / computed.rangeMs) * width, 0, width);
const y = valueToY(point.value);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
const lastPoint = segment[segment.length - 1];
const firstPoint = segment[0];
const lastX = clamp(((lastPoint.timestamp - computed.windowStart) / computed.rangeMs) * width, 0, width);
const firstX = clamp(((firstPoint.timestamp - computed.windowStart) / computed.rangeMs) * width, 0, width);
ctx.lineTo(lastX, height);
ctx.lineTo(firstX, height);
ctx.closePath();
const areaGrad = ctx.createLinearGradient(0, 0, 0, height);
areaGrad.addColorStop(0, series.color.replace('rgb', 'rgba').replace(')', ', 0.25)'));
areaGrad.addColorStop(1, series.color.replace('rgb', 'rgba').replace(')', ', 0)'));
ctx.fillStyle = areaGrad;
ctx.fill();
}
ctx.beginPath();
for (let i = 0; i < segment.length; i++) {
const point = segment[i];
const x = clamp(((point.timestamp - computed.windowStart) / computed.rangeMs) * width, 0, width);
const y = valueToY(point.value);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();`
);
// 5. Add Transitions to axis labels
code = code.replace(
`class="absolute left-0 text-[8px] leading-none text-slate-400 dark:text-slate-500"`,
`class="absolute left-0 text-[8px] leading-none text-slate-400 dark:text-slate-500 transition-all duration-300 ease-out"`
);
code = code.replace(
`class="absolute top-[2px] text-[9px] font-medium leading-none text-slate-500 dark:text-slate-400"`,
`class="absolute top-[2px] text-[9px] font-medium leading-none text-slate-500 dark:text-slate-400 transition-all duration-300 ease-out"`
);
// Add AreaPath to InteractiveSparklineProps
code = code.replace(
`paths: [] as { path: string; color: string; seriesIndex: number }[],`,
`paths: [] as { path: string; areaPath?: string; color: string; seriesIndex: number }[],`
);
// Type alias update where HoverSeriesValue exists inside tooltip values output
fs.writeFileSync(file, code);