Data Visualization Strategy
Highcharts Integration & Charting Patterns
Document Date: December 13, 2025
1. Overview
1.1 Why Data Visualization Matters
Current Gap: STEMBlock.ai currently uses only progress bars and static metrics. Students, coaches, and parents need richer insights to understand:
- Performance trends over time
- Strengths and weaknesses across skills
- Progress toward goals
- Comparative performance (peer benchmarks)
- Time spent on different activities
Expected Impact:
- +30% user retention through increased engagement
- +40% coach efficiency with visual performance summaries
- +25% parent satisfaction with clearer progress reports
- Better learning outcomes through data-driven insights
1.2 Highcharts Selection Rationale
Why Highcharts:
- ✅ Comprehensive chart types (30+ options)
- ✅ Excellent accessibility support (WCAG 2.1 AA compliant)
- ✅ Mobile-responsive by default
- ✅ Strong TypeScript support
- ✅ Export capabilities (PNG, PDF, SVG)
- ✅ Animation and interaction options
- ✅ Extensive documentation
- ✅ Active community and support
License:
- Free for non-commercial use
- Commercial license required for production ($490/year single developer)
Alternatives Considered:
- Chart.js - Simpler but less features
- D3.js - Powerful but steeper learning curve
- Recharts - React-specific but limited chart types
- Victory - Good but smaller ecosystem
2. Highcharts Setup & Configuration
2.1 Installation
npm install highcharts highcharts-react-official
npm install --save-dev @types/highcharts
# Optional modules for advanced features
npm install highcharts/modules/exporting
npm install highcharts/modules/export-data
npm install highcharts/modules/accessibility
2.2 Global Theme Configuration
// /lib/highcharts-config.ts
import Highcharts from 'highcharts'
import HighchartsExporting from 'highcharts/modules/exporting'
import HighchartsExportData from 'highcharts/modules/export-data'
import HighchartsAccessibility from 'highcharts/modules/accessibility'
// Initialize modules
if (typeof window !== 'undefined') {
HighchartsExporting(Highcharts)
HighchartsExportData(Highcharts)
HighchartsAccessibility(Highcharts)
}
// STEMBlock.ai Brand Theme
export const stemBlockTheme: Highcharts.Options = {
// Color palette matching design system
colors: [
'#2E7CF6', // stem-blue-500 (Primary)
'#8B5CF6', // stem-purple-500 (Secondary)
'#10B981', // stem-green-500 (Success)
'#F59E0B', // stem-orange-500 (Warning)
'#14B8A6', // stem-teal-500 (Accent)
'#EF4444', // stem-red-500 (Error)
'#60A5FA', // stem-blue-400 (Light variant)
'#C084FC', // stem-purple-400 (Light variant)
],
// Chart styling
chart: {
backgroundColor: 'transparent',
style: {
fontFamily: 'Inter, system-ui, sans-serif',
fontSize: '14px',
},
spacingTop: 20,
spacingBottom: 20,
spacingLeft: 10,
spacingRight: 10,
},
// Title styling
title: {
style: {
fontFamily: 'Poppins, system-ui, sans-serif',
fontWeight: '700',
fontSize: '20px',
color: '#1E293B', // slate-900
},
align: 'left',
},
// Subtitle styling
subtitle: {
style: {
fontFamily: 'Inter, system-ui, sans-serif',
fontSize: '14px',
color: '#64748B', // slate-500
},
align: 'left',
},
// X-Axis styling
xAxis: {
gridLineColor: '#E2E8F0', // slate-200
lineColor: '#CBD5E1', // slate-300
tickColor: '#CBD5E1',
labels: {
style: {
color: '#64748B', // slate-500
fontSize: '12px',
fontWeight: '500',
},
},
title: {
style: {
color: '#475569', // slate-600
fontSize: '13px',
fontWeight: '600',
},
},
},
// Y-Axis styling
yAxis: {
gridLineColor: '#E2E8F0',
lineColor: '#CBD5E1',
tickColor: '#CBD5E1',
labels: {
style: {
color: '#64748B',
fontSize: '12px',
fontWeight: '500',
},
},
title: {
style: {
color: '#475569',
fontSize: '13px',
fontWeight: '600',
},
},
},
// Legend styling
legend: {
backgroundColor: '#F8FAFC', // slate-50
borderColor: '#E2E8F0',
borderWidth: 1,
borderRadius: 8,
itemStyle: {
color: '#475569',
fontSize: '14px',
fontWeight: '500',
},
itemHoverStyle: {
color: '#1E293B',
},
itemHiddenStyle: {
color: '#CBD5E1',
},
},
// Plot options
plotOptions: {
series: {
borderRadius: 4,
animation: {
duration: 800,
easing: 'easeOutCubic',
},
dataLabels: {
style: {
fontSize: '12px',
fontWeight: '600',
textOutline: 'none',
color: '#1E293B',
},
},
marker: {
lineWidth: 2,
lineColor: '#FFFFFF',
radius: 5,
states: {
hover: {
radius: 7,
lineWidth: 3,
},
},
},
},
column: {
borderRadius: 6,
groupPadding: 0.15,
pointPadding: 0.05,
},
bar: {
borderRadius: 6,
groupPadding: 0.15,
pointPadding: 0.05,
},
pie: {
borderRadius: 4,
dataLabels: {
distance: 20,
style: {
fontSize: '13px',
fontWeight: '600',
},
},
},
area: {
fillOpacity: 0.2,
lineWidth: 2,
threshold: null,
},
},
// Tooltip styling
tooltip: {
backgroundColor: '#FFFFFF',
borderColor: '#CBD5E1',
borderRadius: 8,
borderWidth: 1,
shadow: {
color: 'rgba(0, 0, 0, 0.1)',
offsetX: 0,
offsetY: 2,
opacity: 0.5,
width: 4,
},
style: {
color: '#1E293B',
fontSize: '14px',
},
padding: 12,
useHTML: true,
},
// Credits (hide by default)
credits: {
enabled: false,
},
// Exporting options
exporting: {
enabled: true,
buttons: {
contextButton: {
menuItems: [
'viewFullscreen',
'separator',
'downloadPNG',
'downloadJPEG',
'downloadPDF',
'downloadSVG',
'separator',
'downloadCSV',
'downloadXLS',
],
theme: {
fill: '#F8FAFC',
stroke: '#E2E8F0',
states: {
hover: {
fill: '#EFF6FF',
stroke: '#2E7CF6',
},
},
},
},
},
},
// Accessibility
accessibility: {
enabled: true,
keyboardNavigation: {
enabled: true,
},
announceNewData: {
enabled: true,
},
},
// Responsive rules
responsive: {
rules: [
{
condition: {
maxWidth: 640, // Mobile
},
chartOptions: {
legend: {
layout: 'horizontal',
align: 'center',
verticalAlign: 'bottom',
},
yAxis: {
labels: {
align: 'left',
x: 0,
y: -2,
},
title: {
text: null,
},
},
subtitle: {
text: null,
},
},
},
],
},
}
// Apply theme globally
Highcharts.setOptions(stemBlockTheme)
export default Highcharts
3. Chart Components Library
3.1 Base Chart Wrapper
// /components/charts/ChartWrapper.tsx
import { useRef, useEffect } from 'react'
import Highcharts from 'highcharts'
import HighchartsReact from 'highcharts-react-official'
import { Card } from '@/components/ui/Card'
import { Button } from '@/components/ui/Button'
interface ChartWrapperProps {
options: Highcharts.Options
title?: string
description?: string
loading?: boolean
error?: string
onRefresh?: () => void
className?: string
}
export function ChartWrapper({
options,
title,
description,
loading = false,
error,
onRefresh,
className,
}: ChartWrapperProps) {
const chartRef = useRef<HighchartsReact.RefObject>(null)
// Handle window resize for responsiveness
useEffect(() => {
const handleResize = () => {
chartRef.current?.chart?.reflow()
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return (
<Card className={className} padding="lg">
{/* Header */}
{(title || description) && (
<div className="mb-6 flex items-start justify-between">
<div>
{title && (
<h3 className="text-xl font-display font-bold text-slate-900 mb-1">
{title}
</h3>
)}
{description && (
<p className="text-sm text-slate-600">{description}</p>
)}
</div>
{onRefresh && (
<Button
variant="ghost"
size="sm"
onClick={onRefresh}
leftIcon={<RefreshIcon className="w-4 h-4" />}
>
Refresh
</Button>
)}
</div>
)}
{/* Chart Content */}
{loading ? (
<div className="h-80 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin w-8 h-8 border-4 border-stem-blue-500 border-t-transparent rounded-full mx-auto mb-4" />
<p className="text-slate-600">Loading chart...</p>
</div>
</div>
) : error ? (
<div className="h-80 flex items-center justify-center">
<div className="text-center">
<div className="text-5xl mb-4">📊</div>
<p className="text-slate-600 mb-4">{error}</p>
{onRefresh && (
<Button variant="secondary" onClick={onRefresh}>
Try Again
</Button>
)}
</div>
</div>
) : (
<HighchartsReact
highcharts={Highcharts}
options={options}
ref={chartRef}
/>
)}
</Card>
)
}
function RefreshIcon({ className }: { className?: string }) {
return (
<svg className={className} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
)
}
3.2 Score Trend Line Chart
// /components/charts/ScoreTrendChart.tsx
import Highcharts from '@/lib/highcharts-config'
import { ChartWrapper } from './ChartWrapper'
interface ScoreData {
date: string
score: number
maxScore: number
assignmentTitle: string
}
interface ScoreTrendChartProps {
data: ScoreData[]
targetScore?: number
loading?: boolean
}
export function ScoreTrendChart({
data,
targetScore = 80,
loading = false,
}: ScoreTrendChartProps) {
// Convert scores to percentages
const percentages = data.map(d => ({
y: Math.round((d.score / d.maxScore) * 100),
name: d.assignmentTitle,
date: new Date(d.date).toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
}),
}))
const options: Highcharts.Options = {
chart: {
type: 'line',
height: 350,
},
title: {
text: undefined, // Using ChartWrapper title
},
xAxis: {
categories: percentages.map(p => p.date),
crosshair: true,
},
yAxis: {
min: 0,
max: 100,
title: {
text: 'Score (%)',
},
plotLines: [
{
value: targetScore,
color: '#10B981', // stem-green-500
dashStyle: 'Dash',
width: 2,
label: {
text: `Target: ${targetScore}%`,
align: 'right',
style: {
color: '#10B981',
fontWeight: '600',
},
},
zIndex: 5,
},
],
},
tooltip: {
formatter: function() {
const point = this.point as any
return `
<div class="p-2">
<div class="font-bold text-slate-900 mb-1">${point.name}</div>
<div class="text-sm text-slate-600">${this.x}</div>
<div class="text-lg font-bold text-stem-blue-600 mt-1">
${this.y}%
</div>
</div>
`
},
useHTML: true,
},
series: [
{
name: 'Your Scores',
type: 'line',
data: percentages,
color: '#2E7CF6', // stem-blue-500
marker: {
symbol: 'circle',
radius: 6,
fillColor: '#2E7CF6',
},
lineWidth: 3,
states: {
hover: {
lineWidth: 4,
},
},
},
],
legend: {
enabled: false,
},
}
return (
<ChartWrapper
options={options}
title="Score Trends"
description="Track your performance over time"
loading={loading}
/>
)
}
3.3 Skills Radar Chart
// /components/charts/SkillsRadarChart.tsx
import Highcharts from '@/lib/highcharts-config'
import { ChartWrapper } from './ChartWrapper'
interface SkillsData {
[skillName: string]: number
}
interface SkillsRadarChartProps {
currentSkills: SkillsData
averageSkills?: SkillsData
loading?: boolean
}
export function SkillsRadarChart({
currentSkills,
averageSkills,
loading = false,
}: SkillsRadarChartProps) {
const categories = Object.keys(currentSkills)
const currentValues = Object.values(currentSkills)
const averageValues = averageSkills ? Object.values(averageSkills) : null
const options: Highcharts.Options = {
chart: {
polar: true,
type: 'area',
height: 400,
},
title: {
text: undefined,
},
pane: {
size: '80%',
},
xAxis: {
categories,
tickmarkPlacement: 'on',
lineWidth: 0,
labels: {
style: {
fontSize: '13px',
fontWeight: '600',
color: '#475569',
},
},
},
yAxis: {
gridLineInterpolation: 'polygon',
lineWidth: 0,
min: 0,
max: 100,
tickInterval: 20,
labels: {
format: '{value}%',
},
},
tooltip: {
formatter: function() {
return `
<div class="p-2">
<div class="font-bold text-slate-900 mb-1">${this.x}</div>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full" style="background-color: ${this.color}"></div>
<span class="text-sm font-semibold">${this.series.name}:</span>
<span class="text-lg font-bold text-stem-blue-600">${this.y}%</span>
</div>
</div>
`
},
useHTML: true,
},
series: [
{
name: 'Your Skills',
type: 'area',
data: currentValues,
color: '#2E7CF6',
fillOpacity: 0.25,
lineWidth: 2,
pointPlacement: 'on',
},
...(averageValues
? [
{
name: 'Class Average',
type: 'area' as const,
data: averageValues,
color: '#8B5CF6',
fillOpacity: 0.1,
lineWidth: 2,
dashStyle: 'Dash' as const,
pointPlacement: 'on' as const,
},
]
: []),
],
legend: {
align: 'center',
verticalAlign: 'bottom',
layout: 'horizontal',
},
}
return (
<ChartWrapper
options={options}
title="Skills Assessment"
description="Compare your skills across different areas"
loading={loading}
/>
)
}
3.4 Assignment Completion Column Chart
// /components/charts/AssignmentCompletionChart.tsx
import Highcharts from '@/lib/highcharts-config'
import { ChartWrapper } from './ChartWrapper'
interface ClassData {
className: string
completed: number
inProgress: number
notStarted: number
}
interface AssignmentCompletionChartProps {
data: ClassData[]
loading?: boolean
}
export function AssignmentCompletionChart({
data,
loading = false,
}: AssignmentCompletionChartProps) {
const options: Highcharts.Options = {
chart: {
type: 'column',
height: 350,
},
title: {
text: undefined,
},
xAxis: {
categories: data.map(d => d.className),
},
yAxis: {
min: 0,
title: {
text: 'Number of Assignments',
},
stackLabels: {
enabled: true,
style: {
fontWeight: '600',
color: '#1E293B',
},
},
},
tooltip: {
formatter: function() {
return `
<div class="p-2">
<div class="font-bold text-slate-900 mb-2">${this.x}</div>
<div class="flex items-center gap-2 mb-1">
<div class="w-3 h-3 rounded" style="background-color: ${this.color}"></div>
<span class="text-sm">${this.series.name}:</span>
<span class="font-bold">${this.y}</span>
</div>
<div class="text-xs text-slate-500 mt-2">
Total: ${this.point.stackTotal}
</div>
</div>
`
},
useHTML: true,
},
plotOptions: {
column: {
stacking: 'normal',
dataLabels: {
enabled: true,
color: '#FFFFFF',
},
},
},
series: [
{
name: 'Completed',
type: 'column',
data: data.map(d => d.completed),
color: '#10B981', // stem-green-500
},
{
name: 'In Progress',
type: 'column',
data: data.map(d => d.inProgress),
color: '#F59E0B', // stem-orange-500
},
{
name: 'Not Started',
type: 'column',
data: data.map(d => d.notStarted),
color: '#CBD5E1', // slate-300
},
],
}
return (
<ChartWrapper
options={options}
title="Assignment Progress by Class"
description="Track completion status across all classes"
loading={loading}
/>
)
}
3.5 Time Spent Pie Chart
// /components/charts/TimeSpentChart.tsx
import Highcharts from '@/lib/highcharts-config'
import { ChartWrapper } from './ChartWrapper'
interface TimeData {
category: string
hours: number
color?: string
}
interface TimeSpentChartProps {
data: TimeData[]
loading?: boolean
}
export function TimeSpentChart({ data, loading = false }: TimeSpentChartProps) {
const totalHours = data.reduce((sum, item) => sum + item.hours, 0)
const options: Highcharts.Options = {
chart: {
type: 'pie',
height: 400,
},
title: {
text: undefined,
},
tooltip: {
formatter: function() {
const percentage = ((this.y || 0) / totalHours * 100).toFixed(1)
return `
<div class="p-2">
<div class="font-bold text-slate-900 mb-2">${this.point.name}</div>
<div class="flex items-center gap-2 mb-1">
<div class="w-3 h-3 rounded-full" style="background-color: ${this.color}"></div>
<span class="text-lg font-bold">${this.y} hours</span>
</div>
<div class="text-sm text-slate-600">
${percentage}% of total time
</div>
</div>
`
},
useHTML: true,
},
plotOptions: {
pie: {
innerSize: '50%', // Donut chart
dataLabels: {
enabled: true,
format: '{point.name}<br/>{point.percentage:.1f}%',
distance: 20,
style: {
fontSize: '13px',
fontWeight: '600',
},
},
showInLegend: true,
},
},
series: [
{
name: 'Time',
type: 'pie',
data: data.map(item => ({
name: item.category,
y: item.hours,
color: item.color,
})),
},
],
legend: {
align: 'right',
verticalAlign: 'middle',
layout: 'vertical',
},
}
return (
<ChartWrapper
options={options}
title="Time Spent by Activity"
description={`Total: ${totalHours} hours this month`}
loading={loading}
/>
)
}
3.6 Performance Heatmap
// /components/charts/PerformanceHeatmap.tsx
import Highcharts from '@/lib/highcharts-config'
import HighchartsHeatmap from 'highcharts/modules/heatmap'
import { ChartWrapper } from './ChartWrapper'
// Initialize heatmap module
if (typeof window !== 'undefined') {
HighchartsHeatmap(Highcharts)
}
interface HeatmapData {
week: string
skill: string
score: number
}
interface PerformanceHeatmapProps {
data: HeatmapData[]
loading?: boolean
}
export function PerformanceHeatmap({
data,
loading = false,
}: PerformanceHeatmapProps) {
// Get unique weeks and skills
const weeks = [...new Set(data.map(d => d.week))].sort()
const skills = [...new Set(data.map(d => d.skill))]
// Convert data to heatmap format [x, y, value]
const heatmapData = data.map(d => [
weeks.indexOf(d.week),
skills.indexOf(d.skill),
d.score,
])
const options: Highcharts.Options = {
chart: {
type: 'heatmap',
height: 400,
},
title: {
text: undefined,
},
xAxis: {
categories: weeks,
title: {
text: 'Week',
},
},
yAxis: {
categories: skills,
title: {
text: null,
},
},
colorAxis: {
min: 0,
max: 100,
stops: [
[0, '#FEE2E2'], // Red (low)
[0.5, '#FED7AA'], // Orange (medium)
[0.75, '#D1FAE5'], // Green light (good)
[1, '#10B981'], // Green (excellent)
],
labels: {
format: '{value}%',
},
},
tooltip: {
formatter: function() {
const point = this.point as any
return `
<div class="p-2">
<div class="font-bold text-slate-900 mb-1">${skills[point.y]}</div>
<div class="text-sm text-slate-600 mb-1">${weeks[point.x]}</div>
<div class="text-lg font-bold" style="color: ${this.color}">
${point.value}%
</div>
</div>
`
},
useHTML: true,
},
series: [
{
name: 'Performance',
type: 'heatmap',
data: heatmapData as any,
borderWidth: 2,
borderColor: '#FFFFFF',
dataLabels: {
enabled: true,
color: '#1E293B',
format: '{point.value}',
},
},
],
}
return (
<ChartWrapper
options={options}
title="Weekly Performance Heatmap"
description="Visualize your progress across skills over time"
loading={loading}
/>
)
}
4. Dashboard Integration Examples
4.1 Enhanced Student Dashboard
// /app/student/dashboard/page.tsx - Add analytics section
'use client'
import { useState, useEffect } from 'react'
import { useAuth } from '@/lib/auth-context'
import { Navigation } from '@/components/Navigation'
import { Container } from '@/components/ui/Container'
import { Grid } from '@/components/ui/Grid'
// Import chart components
import { ScoreTrendChart } from '@/components/charts/ScoreTrendChart'
import { SkillsRadarChart } from '@/components/charts/SkillsRadarChart'
import { TimeSpentChart } from '@/components/charts/TimeSpentChart'
import { AssignmentCompletionChart } from '@/components/charts/AssignmentCompletionChart'
export default function StudentDashboard() {
const { user } = useAuth()
const [submissions, setSubmissions] = useState([])
const [loading, setLoading] = useState(true)
// Fetch data
useEffect(() => {
async function loadData() {
const data = await fetchSubmissions()
setSubmissions(data)
setLoading(false)
}
loadData()
}, [])
// Prepare chart data
const scoreData = submissions
.filter(s => s.evaluation)
.map(s => ({
date: s.submittedAt,
score: s.evaluation.overallScore,
maxScore: s.assignment.maxScore,
assignmentTitle: s.assignment.title,
}))
const skillsData = {
'Problem Solving': 85,
'Creativity': 92,
'Technical Skills': 78,
'Documentation': 88,
'Collaboration': 90,
'Time Management': 75,
}
const timeData = [
{ category: 'Coding', hours: 12, color: '#2E7CF6' },
{ category: 'Documentation', hours: 5, color: '#8B5CF6' },
{ category: 'Testing', hours: 3, color: '#10B981' },
{ category: 'Research', hours: 6, color: '#F59E0B' },
]
return (
<div className="min-h-screen bg-gradient-to-br from-stem-blue-50 via-stem-purple-50 to-stem-teal-50">
<Navigation />
<main>
<Container size="xl" padding>
{/* Welcome Section */}
<section className="py-8">
<h1 className="text-4xl font-display font-bold text-slate-900 mb-2">
Welcome back, {user?.firstName}! 👋
</h1>
<p className="text-lg text-slate-600">
Here's your learning journey at a glance
</p>
</section>
{/* Performance Analytics Section */}
{submissions.length >= 3 && (
<section className="mb-12">
<div className="mb-6">
<h2 className="text-2xl font-display font-bold text-slate-900 mb-2">
📊 Your Performance Analytics
</h2>
<p className="text-slate-600">
Track your progress and identify areas for improvement
</p>
</div>
<Grid cols={2} gap="lg" responsive className="mb-6">
<ScoreTrendChart
data={scoreData}
targetScore={80}
loading={loading}
/>
<SkillsRadarChart
currentSkills={skillsData}
loading={loading}
/>
</Grid>
<Grid cols={2} gap="lg" responsive>
<TimeSpentChart data={timeData} loading={loading} />
<AssignmentCompletionChart
data={[
{
className: 'Robotics 101',
completed: 8,
inProgress: 2,
notStarted: 1,
},
{
className: 'Coding Basics',
completed: 12,
inProgress: 1,
notStarted: 0,
},
]}
loading={loading}
/>
</Grid>
</section>
)}
{/* Rest of dashboard content */}
{/* ... existing sections ... */}
</Container>
</main>
</div>
)
}
4.2 Parent Report Dashboard
// /app/parent/children/[studentId]/report/page.tsx
import { PerformanceHeatmap } from '@/components/charts/PerformanceHeatmap'
import { ScoreTrendChart } from '@/components/charts/ScoreTrendChart'
import { SkillsRadarChart } from '@/components/charts/SkillsRadarChart'
export default function StudentReport({ params }: { params: { studentId: string } }) {
// Fetch student data
const studentData = useStudentReport(params.studentId)
return (
<Container size="xl" padding>
<section className="mb-12">
<h1 className="text-3xl font-display font-bold text-slate-900 mb-2">
{studentData.student.name}'s Progress Report
</h1>
<p className="text-slate-600">
{new Date().toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
</p>
</section>
{/* Executive Summary */}
<section className="mb-12">
<Card padding="lg" className="border-2 border-stem-blue-200">
<h2 className="text-xl font-display font-bold text-slate-900 mb-4">
Executive Summary
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<p className="text-sm text-slate-600 mb-1">Overall Performance</p>
<p className="text-3xl font-bold text-stem-green-600">87%</p>
<p className="text-xs text-slate-500 mt-1">↑ 5% from last month</p>
</div>
<div>
<p className="text-sm text-slate-600 mb-1">Assignments Completed</p>
<p className="text-3xl font-bold text-stem-blue-600">24/28</p>
<p className="text-xs text-slate-500 mt-1">86% completion rate</p>
</div>
<div>
<p className="text-sm text-slate-600 mb-1">Time Invested</p>
<p className="text-3xl font-bold text-stem-purple-600">42 hrs</p>
<p className="text-xs text-slate-500 mt-1">This month</p>
</div>
</div>
</Card>
</section>
{/* Detailed Analytics */}
<Grid cols={1} gap="lg">
<ScoreTrendChart
data={studentData.scores}
targetScore={80}
/>
<SkillsRadarChart
currentSkills={studentData.skills}
averageSkills={studentData.classAverageSkills}
/>
<PerformanceHeatmap data={studentData.weeklyPerformance} />
</Grid>
{/* Recommendations Section */}
<section className="mt-12">
<Card padding="lg" className="border-2 border-stem-purple-200">
<h2 className="text-xl font-display font-bold text-slate-900 mb-4">
💡 Recommendations
</h2>
<ul className="space-y-3">
<li className="flex items-start gap-3">
<div className="text-2xl">🎯</div>
<div>
<p className="font-semibold text-slate-900">
Focus on Technical Skills
</p>
<p className="text-sm text-slate-600">
{studentData.student.name} could benefit from additional practice
in coding fundamentals. Consider our weekend workshops.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<div className="text-2xl">⭐</div>
<div>
<p className="font-semibold text-slate-900">
Excellent Creativity
</p>
<p className="text-sm text-slate-600">
Outstanding performance in creative problem-solving! Encourage
participation in design challenges.
</p>
</div>
</li>
</ul>
</Card>
</section>
</Container>
)
}
5. Performance Optimization
5.1 Lazy Loading Charts
// Dynamic import for heavy chart components
import dynamic from 'next/dynamic'
const ScoreTrendChart = dynamic(
() => import('@/components/charts/ScoreTrendChart').then(mod => mod.ScoreTrendChart),
{
loading: () => (
<Card className="h-96 flex items-center justify-center">
<LoadingState type="spinner" message="Loading chart..." />
</Card>
),
ssr: false, // Don't render on server
}
)
5.2 Data Memoization
import { useMemo } from 'react'
export function StudentAnalytics({ submissions }) {
// Memoize expensive calculations
const chartData = useMemo(() => {
return submissions
.filter(s => s.evaluation)
.map(s => ({
date: s.submittedAt,
score: (s.evaluation.overallScore / s.assignment.maxScore) * 100,
title: s.assignment.title,
}))
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
}, [submissions])
return <ScoreTrendChart data={chartData} />
}
5.3 Chart Updates Without Re-render
// Update chart data without full re-render
const chartRef = useRef<HighchartsReact.RefObject>(null)
const updateChartData = (newData: number[]) => {
if (chartRef.current?.chart) {
chartRef.current.chart.series[0].setData(newData, true) // redraw: true
}
}
6. Accessibility for Charts
6.1 Screen Reader Support
// Enhanced accessibility options
const accessibleOptions: Highcharts.Options = {
accessibility: {
enabled: true,
description: 'Line chart showing score trends over 6 assignments',
keyboardNavigation: {
enabled: true,
},
point: {
valueDescriptionFormat: '{index}. {xDescription}, {value}%',
},
series: {
descriptionFormatter: function(series) {
return `${series.name}. Line chart with ${series.points.length} data points.`
},
},
},
// Add table fallback for screen readers
exporting: {
showTable: true,
tableCaption: 'Data table for score trends',
},
}
6.2 Data Table Fallback
// Provide accessible data table alongside chart
export function AccessibleChart({ data, chartComponent: ChartComponent }) {
const [showTable, setShowTable] = useState(false)
return (
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-bold">Score Trends</h3>
<button
onClick={() => setShowTable(!showTable)}
className="text-sm text-stem-blue-600 hover:underline"
aria-expanded={showTable}
aria-controls="data-table"
>
{showTable ? 'Hide' : 'Show'} Data Table
</button>
</div>
<ChartComponent data={data} />
{showTable && (
<div id="data-table" className="mt-6 overflow-x-auto">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-2 text-left text-sm font-semibold">Date</th>
<th className="px-4 py-2 text-left text-sm font-semibold">Assignment</th>
<th className="px-4 py-2 text-left text-sm font-semibold">Score</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
{data.map((row, idx) => (
<tr key={idx}>
<td className="px-4 py-2 text-sm">{row.date}</td>
<td className="px-4 py-2 text-sm">{row.assignmentTitle}</td>
<td className="px-4 py-2 text-sm font-semibold">{row.score}%</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)
}
7. Testing Strategy
7.1 Visual Regression Testing
// __tests__/charts/ScoreTrendChart.visual.test.tsx
import { render } from '@testing-library/react'
import { axe } from 'jest-axe'
import { ScoreTrendChart } from '@/components/charts/ScoreTrendChart'
describe('ScoreTrendChart Visual Tests', () => {
const mockData = [
{ date: '2025-01-01', score: 85, maxScore: 100, assignmentTitle: 'Assignment 1' },
{ date: '2025-01-15', score: 90, maxScore: 100, assignmentTitle: 'Assignment 2' },
]
it('renders correctly', () => {
const { container } = render(<ScoreTrendChart data={mockData} />)
expect(container).toMatchSnapshot()
})
it('has no accessibility violations', async () => {
const { container } = render(<ScoreTrendChart data={mockData} />)
const results = await axe(container)
expect(results).toHaveNoViolations()
})
it('renders with loading state', () => {
const { container } = render(<ScoreTrendChart data={[]} loading />)
expect(container.textContent).toContain('Loading chart')
})
})
Summary
This data visualization strategy provides:
- ✅ Comprehensive Highcharts setup with STEMBlock.ai theme
- ✅ 6 reusable chart components (line, radar, column, pie, heatmap)
- ✅ Dashboard integration examples
- ✅ Performance optimization techniques
- ✅ Full accessibility support
- ✅ Testing guidelines
Expected Impact:
- +30% user retention through better engagement
- +40% coach efficiency with visual summaries
- +25% parent satisfaction with progress insights
Next Steps:
- Install Highcharts and dependencies
- Implement base chart wrapper component
- Create 2-3 priority charts for student dashboard
- Test on real data
- Gather user feedback
- Iterate and expand chart library
Document Version: 1.0 Last Updated: December 13, 2025