153 lines
5.2 KiB
TypeScript
153 lines
5.2 KiB
TypeScript
import { useEffect, useRef } from 'react';
|
|
import './FitnessChart.css';
|
|
|
|
interface FitnessChartProps {
|
|
fitnessHistory: number[];
|
|
maxPoints?: number;
|
|
}
|
|
|
|
export default function FitnessChart({ fitnessHistory, maxPoints = 500 }: FitnessChartProps) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current;
|
|
if (!canvas) return;
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
const width = canvas.width;
|
|
const height = canvas.height;
|
|
|
|
// Clear canvas
|
|
ctx.fillStyle = '#0f0f1e';
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
if (fitnessHistory.length < 2) return;
|
|
|
|
const maxMSE = 255 * 255 * 3;
|
|
|
|
// Convert fitness (error) to similarity percentage
|
|
const similarityHistory = fitnessHistory.map(fitness =>
|
|
Math.max(0, Math.min(100, ((maxMSE - fitness) / maxMSE) * 100))
|
|
);
|
|
|
|
// ALL-TIME scale: fixed 0-100%
|
|
const allTimeMin = 0;
|
|
const allTimeMax = 100;
|
|
const allTimeRange = 100;
|
|
|
|
// RECENT scale: auto-scaled to recent data
|
|
const recentHistory = similarityHistory.slice(-maxPoints);
|
|
const recentMin = Math.min(...recentHistory);
|
|
const recentMax = Math.max(...recentHistory);
|
|
const recentRange = recentMax - recentMin || 1;
|
|
|
|
// Draw grid lines and left axis (0-100%)
|
|
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
|
|
ctx.lineWidth = 1;
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.4)';
|
|
ctx.font = '10px Inter, sans-serif';
|
|
ctx.textAlign = 'left';
|
|
|
|
for (let i = 0; i <= 5; i++) {
|
|
const y = (height / 5) * i;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, y);
|
|
ctx.lineTo(width, y);
|
|
ctx.stroke();
|
|
|
|
// Left axis labels (0-100%)
|
|
const value = allTimeMax - (allTimeRange / 5) * i;
|
|
ctx.fillText(value.toFixed(0) + '%', 5, y - 2);
|
|
}
|
|
|
|
// LAYER 1: All-time history (0-100% scale, faded)
|
|
const downsampleRate = Math.max(1, Math.floor(similarityHistory.length / 100));
|
|
const downsampledHistory = similarityHistory.filter((_, i) => i % downsampleRate === 0);
|
|
|
|
if (downsampledHistory.length >= 2) {
|
|
ctx.strokeStyle = 'rgba(102, 126, 234, 0.2)';
|
|
ctx.lineWidth = 1.5;
|
|
ctx.beginPath();
|
|
|
|
downsampledHistory.forEach((similarity, index) => {
|
|
const x = (index / (downsampledHistory.length - 1)) * width;
|
|
const normalizedSimilarity = (similarity - allTimeMin) / allTimeRange;
|
|
const y = height - normalizedSimilarity * height;
|
|
|
|
if (index === 0) {
|
|
ctx.moveTo(x, y);
|
|
} else {
|
|
ctx.lineTo(x, y);
|
|
}
|
|
});
|
|
|
|
ctx.stroke();
|
|
}
|
|
|
|
// LAYER 2: Recent progress (auto-scaled, bright)
|
|
if (recentHistory.length >= 2) {
|
|
ctx.strokeStyle = '#667eea';
|
|
ctx.lineWidth = 2.5;
|
|
ctx.beginPath();
|
|
|
|
recentHistory.forEach((similarity, index) => {
|
|
const x = (index / (recentHistory.length - 1)) * width;
|
|
const normalizedSimilarity = (similarity - recentMin) / recentRange;
|
|
const y = height - normalizedSimilarity * height;
|
|
|
|
if (index === 0) {
|
|
ctx.moveTo(x, y);
|
|
} else {
|
|
ctx.lineTo(x, y);
|
|
}
|
|
});
|
|
|
|
ctx.stroke();
|
|
|
|
// Gradient fill under recent line
|
|
const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
|
gradient.addColorStop(0, 'rgba(102, 126, 234, 0.25)');
|
|
gradient.addColorStop(1, 'rgba(102, 126, 234, 0)');
|
|
|
|
ctx.fillStyle = gradient;
|
|
ctx.beginPath();
|
|
|
|
recentHistory.forEach((similarity, index) => {
|
|
const x = (index / (recentHistory.length - 1)) * width;
|
|
const normalizedSimilarity = (similarity - recentMin) / recentRange;
|
|
const y = height - normalizedSimilarity * height;
|
|
|
|
if (index === 0) {
|
|
ctx.moveTo(x, y);
|
|
} else {
|
|
ctx.lineTo(x, y);
|
|
}
|
|
});
|
|
|
|
ctx.lineTo(width, height);
|
|
ctx.lineTo(0, height);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
|
|
// Right axis labels (recent auto-scale)
|
|
ctx.fillStyle = 'rgba(102, 126, 234, 0.7)';
|
|
ctx.textAlign = 'right';
|
|
|
|
for (let i = 0; i <= 5; i++) {
|
|
const y = (height / 5) * i;
|
|
const value = recentMax - (recentRange / 5) * i;
|
|
ctx.fillText(value.toFixed(2) + '%', width - 5, y - 2);
|
|
}
|
|
}, [fitnessHistory, maxPoints]);
|
|
|
|
return (
|
|
<div className="fitness-chart-container">
|
|
<h3 className="chart-title">Similarity Progress</h3>
|
|
<canvas ref={canvasRef} width={600} height={200} className="fitness-chart" />
|
|
</div>
|
|
);
|
|
}
|