mememechez's picture
Deploy final cleaned source code
ca28016
raw
history blame
9.84 kB
import React, { useRef, useEffect, useState } from "react";
interface Trace {
data: number[];
color?: string; // e.g. "#ffe066", "#ff00ff", "#00ffe7"
label?: string;
}
interface Marker {
x: number; // index in data
y: number; // value
label?: string;
color?: string;
}
interface Overlay {
text: string;
color?: string;
x?: number;
y?: number;
fontSize?: number;
}
interface NeonAnalyzerChartProps {
traces: Trace[];
width?: number;
height?: number;
overlays?: Overlay[];
markers?: Marker[];
gridColor?: string;
bgColor?: string;
showLegend?: boolean;
yMin?: number;
yMax?: number;
jobName?: string;
}
const defaultColors = ["#ffe066", "#ff00ff", "#00ffe7", "#39ff14", "#ffffff"];
// --- Trace color presets ---
const colorPresets = [
["#ffe066", "#ff00ff", "#00ffe7", "#39ff14", "#ffffff"], // Neon
["#ffb300", "#1e88e5", "#43a047", "#e53935", "#8e24aa"], // Pro VNA
["#00e5ff", "#ff4081", "#ffd600", "#00c853", "#d500f9"], // Modern
];
export default function NeonAnalyzerChart({
traces,
width = 800,
height = 400,
overlays = [],
markers = [],
gridColor = "#00ffe7",
bgColor = "#0a0f1c",
showLegend = true,
yMin = 0,
yMax = 100,
jobName,
}: NeonAnalyzerChartProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [zoom, setZoom] = useState(1);
const [pan, setPan] = useState(0);
const [isPanning, setIsPanning] = useState(false);
const [panStartX, setPanStartX] = useState<number | null>(null);
const [panVelocity, setPanVelocity] = useState(0);
const [lastPanTime, setLastPanTime] = useState<number | null>(null);
const [userPanned, setUserPanned] = useState(false);
const [draggedMarker, setDraggedMarker] = useState<number | null>(null);
const [measureMarkers, setMeasureMarkers] = useState<{ x: number; y: number }[]>([]);
// Auto-pan to latest data if not manually panned
useEffect(() => {
if (!userPanned && traces[0]?.data.length > 0) {
setPan(1);
}
}, [traces, userPanned]);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Enable shadow for glow effects
ctx.shadowBlur = 5;
ctx.shadowColor = '#00ffe7';
// Clear canvas with dark background
ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, width, height);
// Draw grid
const drawGrid = () => {
ctx.strokeStyle = '#00ffe733';
ctx.lineWidth = 0.5;
// Vertical grid lines
for (let x = 50; x < width - 20; x += 50) {
ctx.beginPath();
ctx.moveTo(x, 20);
ctx.lineTo(x, height - 50);
ctx.stroke();
}
// Horizontal grid lines
for (let y = 20; y < height - 50; y += 50) {
ctx.beginPath();
ctx.moveTo(50, y);
ctx.lineTo(width - 20, y);
ctx.stroke();
}
};
// Draw glowing axes
const drawAxes = () => {
ctx.strokeStyle = '#00ffe7';
ctx.lineWidth = 2;
ctx.shadowBlur = 10;
ctx.beginPath();
ctx.moveTo(50, 20);
ctx.lineTo(50, height - 50);
ctx.lineTo(width - 20, height - 50);
ctx.stroke();
ctx.shadowBlur = 5;
};
// Draw y-axis labels with cyberpunk style
const drawYLabels = () => {
ctx.fillStyle = '#00ffe7';
ctx.font = '12px "Share Tech Mono", monospace';
ctx.textAlign = 'right';
const step = (yMax - yMin) / 5;
for (let i = 0; i <= 5; i++) {
const y = height - 50 - (i / 5) * (height - 70);
const value = yMin + i * step;
ctx.fillText(value.toFixed(1), 45, y + 4);
}
};
// Draw x-axis labels
const drawXLabels = () => {
ctx.fillStyle = '#00ffe7';
ctx.font = '12px "Share Tech Mono", monospace';
ctx.textAlign = 'center';
const maxEpochs = Math.max(...traces.map(t => t.data.length));
const step = Math.ceil(maxEpochs / 10);
for (let i = 0; i <= maxEpochs; i += step) {
const x = 50 + (i / maxEpochs) * (width - 70);
ctx.fillText(i.toString(), x, height - 35);
}
};
// Draw traces with glow effect
const drawTraces = () => {
traces.forEach(trace => {
if (!trace.data.length) return;
ctx.strokeStyle = trace.color || defaultColors[Math.floor(Math.random() * defaultColors.length)];
ctx.lineWidth = 2;
ctx.shadowColor = trace.color || defaultColors[Math.floor(Math.random() * defaultColors.length)];
ctx.shadowBlur = 10;
ctx.beginPath();
const xScale = (width - 70) / (trace.data.length - 1);
const yScale = (height - 70) / (yMax - yMin);
trace.data.forEach((value, i) => {
const x = 50 + i * xScale;
const y = height - 50 - (value - yMin) * yScale;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
});
};
// Draw legend with cyberpunk style
const drawLegend = () => {
if (!showLegend) return;
const legendX = width - 150;
const legendY = 30;
const lineLength = 20;
traces.forEach((trace, i) => {
const y = legendY + i * 25;
// Draw line with glow
ctx.strokeStyle = trace.color || defaultColors[Math.floor(Math.random() * defaultColors.length)];
ctx.shadowColor = trace.color || defaultColors[Math.floor(Math.random() * defaultColors.length)];
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.moveTo(legendX, y);
ctx.lineTo(legendX + lineLength, y);
ctx.stroke();
// Draw label
ctx.shadowBlur = 0;
ctx.fillStyle = trace.color || defaultColors[Math.floor(Math.random() * defaultColors.length)];
ctx.font = '14px "Share Tech Mono", monospace';
ctx.textAlign = 'left';
ctx.fillText(trace.label || `Trace ${i + 1}`, legendX + lineLength + 10, y + 4);
});
};
// Draw overlays with glow effect
const drawOverlays = () => {
overlays.forEach(overlay => {
ctx.fillStyle = overlay.color || '#ff0000';
ctx.shadowColor = overlay.color || '#ff0000';
ctx.shadowBlur = 8;
ctx.font = `${overlay.fontSize || 24}px "Share Tech Mono", monospace`;
ctx.fillText(overlay.text, overlay.x || 0, overlay.y || 0);
});
};
// Draw markers
[...markers, ...measureMarkers].forEach(marker => {
ctx.fillStyle = marker.color || '#ff0000';
ctx.beginPath();
ctx.arc(marker.x, marker.y, 4, 0, Math.PI * 2);
ctx.fill();
});
// Execute all drawing functions
drawGrid();
drawAxes();
drawYLabels();
drawXLabels();
drawTraces();
drawLegend();
drawOverlays();
}, [traces, width, height, overlays, markers, showLegend, yMin, yMax]);
const handleMouseDown = (e: React.MouseEvent) => {
const rect = (e.target as HTMLCanvasElement).getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Check if clicking near any existing markers
const n = traces[0]?.data.length || 0;
const windowSize = Math.floor(n / zoom);
const startIdx = Math.floor((n - windowSize) * pan);
const xScale = (width - 70) / (windowSize - 1);
for (let i = 0; i < measureMarkers.length; i++) {
const marker = measureMarkers[i];
const markerScreenX = 50 + (marker.x - startIdx) * xScale;
if (Math.abs(x - markerScreenX) < 10) {
setDraggedMarker(i);
return;
}
}
// Otherwise start panning
setIsPanning(true);
setPanStartX(e.clientX);
setUserPanned(true);
};
const handleMouseUp = () => {
setIsPanning(false);
setPanStartX(null);
setPanVelocity(0);
setDraggedMarker(null);
};
const handleMouseMove = (e: React.MouseEvent) => {
if (draggedMarker !== null) {
// Drag marker horizontally
const rect = (e.target as HTMLCanvasElement).getBoundingClientRect();
const x = e.clientX - rect.left;
const n = traces[0]?.data.length || 0;
const windowSize = Math.floor(n / zoom);
const startIdx = Math.floor((n - windowSize) * pan);
let epoch = Math.round(((x - 50) / (width - 70)) * (windowSize - 1)) + startIdx;
epoch = Math.max(startIdx, Math.min(startIdx + windowSize - 1, epoch));
const y = traces[0]?.data[epoch] ?? 0;
setMeasureMarkers(prev => prev.map((m, i) => i === draggedMarker ? { x: epoch, y } : m));
setUserPanned(true);
return;
}
if (!isPanning || panStartX === null) return;
const dx = e.clientX - panStartX;
setPan(prev => {
let next = Math.max(0, Math.min(1, prev - dx / width / zoom));
setPanVelocity(dx / ((Date.now() - (lastPanTime || Date.now())) || 1));
setLastPanTime(Date.now());
setUserPanned(true);
return next;
});
setPanStartX(e.clientX);
};
// Inertial scrolling
useEffect(() => {
if (!isPanning && Math.abs(panVelocity) > 0.001) {
const raf = requestAnimationFrame(() => {
setPan(prev => {
let next = Math.max(0, Math.min(1, prev - panVelocity * 0.05));
if (next === 0 || next === 1) setPanVelocity(0);
else setPanVelocity(panVelocity * 0.92);
return next;
});
});
return () => cancelAnimationFrame(raf);
}
}, [isPanning, panVelocity]);
return (
<canvas
ref={canvasRef}
width={width}
height={height}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseUp}
style={{
width: '100%',
height: '100%',
background: bgColor,
borderRadius: '8px',
boxShadow: '0 0 20px rgba(0, 255, 231, 0.2)'
}}
/>
);
}