- Migrate from vanilla JavaScript SPA to Next.js 16 with App Router - Add server-side rendering for all pages (Home, Compare, Rankings) - Create individual school pages with dynamic routing (/school/[urn]) - Implement Chart.js and Leaflet map integrations - Add comprehensive SEO with sitemap, robots.txt, and JSON-LD - Set up Docker multi-service architecture (PostgreSQL, FastAPI, Next.js) - Update CI/CD pipeline to build both backend and frontend images - Fix Dockerfile to include devDependencies for TypeScript compilation - Add Jest testing configuration - Implement performance optimizations (code splitting, caching) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
177 lines
4.1 KiB
TypeScript
177 lines
4.1 KiB
TypeScript
/**
|
|
* ComparisonChart Component
|
|
* Multi-school comparison chart using Chart.js
|
|
*/
|
|
|
|
'use client';
|
|
|
|
import { Line } from 'react-chartjs-2';
|
|
import {
|
|
Chart as ChartJS,
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LineElement,
|
|
Title,
|
|
Tooltip,
|
|
Legend,
|
|
ChartOptions,
|
|
} from 'chart.js';
|
|
import type { ComparisonData } from '@/lib/types';
|
|
import { CHART_COLORS } from '@/lib/utils';
|
|
|
|
// Register Chart.js components
|
|
ChartJS.register(
|
|
CategoryScale,
|
|
LinearScale,
|
|
PointElement,
|
|
LineElement,
|
|
Title,
|
|
Tooltip,
|
|
Legend
|
|
);
|
|
|
|
interface ComparisonChartProps {
|
|
comparisonData: Record<string, ComparisonData>;
|
|
metric: string;
|
|
metricLabel: string;
|
|
}
|
|
|
|
export function ComparisonChart({ comparisonData, metric, metricLabel }: ComparisonChartProps) {
|
|
// Get all schools and their data
|
|
const schools = Object.entries(comparisonData);
|
|
|
|
if (schools.length === 0) {
|
|
return <div>No data available</div>;
|
|
}
|
|
|
|
// Get years from first school (assuming all schools have same years)
|
|
const years = schools[0][1].yearly_data.map((d) => d.year).sort((a, b) => a - b);
|
|
|
|
// Create datasets for each school
|
|
const datasets = schools.map(([urn, data], index) => {
|
|
const schoolInfo = data.school_info;
|
|
const color = CHART_COLORS[index % CHART_COLORS.length];
|
|
|
|
return {
|
|
label: schoolInfo.school_name,
|
|
data: years.map((year) => {
|
|
const yearData = data.yearly_data.find((d) => d.year === year);
|
|
if (!yearData) return null;
|
|
return yearData[metric as keyof typeof yearData] as number | null;
|
|
}),
|
|
borderColor: color,
|
|
backgroundColor: color.replace('rgb', 'rgba').replace(')', ', 0.1)'),
|
|
tension: 0.3,
|
|
spanGaps: true,
|
|
};
|
|
});
|
|
|
|
const chartData = {
|
|
labels: years.map(String),
|
|
datasets,
|
|
};
|
|
|
|
// Determine if metric is a progress score or percentage
|
|
const isProgressScore = metric.includes('progress');
|
|
const isPercentage = metric.includes('pct') || metric.includes('rate');
|
|
|
|
const options: ChartOptions<'line'> = {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
interaction: {
|
|
mode: 'index' as const,
|
|
intersect: false,
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
position: 'top' as const,
|
|
labels: {
|
|
usePointStyle: true,
|
|
padding: 15,
|
|
font: {
|
|
size: 12,
|
|
},
|
|
},
|
|
},
|
|
title: {
|
|
display: true,
|
|
text: `${metricLabel} - Comparison`,
|
|
font: {
|
|
size: 16,
|
|
weight: 'bold',
|
|
},
|
|
padding: {
|
|
bottom: 20,
|
|
},
|
|
},
|
|
tooltip: {
|
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
padding: 12,
|
|
titleFont: {
|
|
size: 14,
|
|
},
|
|
bodyFont: {
|
|
size: 13,
|
|
},
|
|
callbacks: {
|
|
label: function (context) {
|
|
let label = context.dataset.label || '';
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
if (context.parsed.y !== null) {
|
|
if (isProgressScore) {
|
|
label += context.parsed.y.toFixed(1);
|
|
} else if (isPercentage) {
|
|
label += context.parsed.y.toFixed(1) + '%';
|
|
} else {
|
|
label += context.parsed.y.toFixed(1);
|
|
}
|
|
} else {
|
|
label += 'N/A';
|
|
}
|
|
return label;
|
|
},
|
|
},
|
|
},
|
|
},
|
|
scales: {
|
|
y: {
|
|
type: 'linear' as const,
|
|
display: true,
|
|
title: {
|
|
display: true,
|
|
text: isPercentage ? 'Percentage (%)' : isProgressScore ? 'Progress Score' : 'Value',
|
|
font: {
|
|
size: 12,
|
|
weight: 'bold',
|
|
},
|
|
},
|
|
...(isPercentage && {
|
|
min: 0,
|
|
max: 100,
|
|
}),
|
|
grid: {
|
|
color: 'rgba(0, 0, 0, 0.05)',
|
|
},
|
|
},
|
|
x: {
|
|
grid: {
|
|
display: false,
|
|
},
|
|
title: {
|
|
display: true,
|
|
text: 'Year',
|
|
font: {
|
|
size: 12,
|
|
weight: 'bold',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
return <Line data={chartData} options={options} />;
|
|
}
|