Skip to main content

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:

  1. Install Highcharts and dependencies
  2. Implement base chart wrapper component
  3. Create 2-3 priority charts for student dashboard
  4. Test on real data
  5. Gather user feedback
  6. Iterate and expand chart library

Document Version: 1.0 Last Updated: December 13, 2025