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(); 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( ` `, ` ` ); code = code.replace( ` {(pathData) => ( {(pathData) => ( )} `, ` /> )} ` ); 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);