import React, { useState, useMemo, useRef, useEffect } from 'react'; const NormalDistributionExplorer = () => { const [mean, setMean] = useState(0); const [stdDev, setStdDev] = useState(1); const [rawScore, setRawScore] = useState(0); const [showEmpiricalRule, setShowEmpiricalRule] = useState(false); const [selectedRegion, setSelectedRegion] = useState(null); const [focusedElement, setFocusedElement] = useState(null); // Refs for keyboard navigation const chartRef = useRef(null); const liveRegionRef = useRef(null); const empiricalRegionRefs = useRef([]); // Calculate z-score const zScore = useMemo(() => { if (stdDev === 0) return 0; return (rawScore - mean) / stdDev; }, [rawScore, mean, stdDev]); // Standard normal cumulative distribution function (approximation) const standardNormalCDF = (z) => { const t = 1.0 / (1.0 + 0.2316419 * Math.abs(z)); const d = 0.3989423 * Math.exp(-z * z / 2.0); let prob = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274)))); if (z > 0.0) { prob = 1.0 - prob; } return prob; }; // Calculate tail probabilities const tailProbabilities = useMemo(() => { const leftTail = standardNormalCDF(zScore); const rightTail = 1 - leftTail; return { leftTail: leftTail, rightTail: rightTail, leftTailPercent: (leftTail * 100).toFixed(2), rightTailPercent: (rightTail * 100).toFixed(2) }; }, [zScore]); // Normal distribution probability density function const normalPDF = (x, mu, sigma) => { return (1 / (sigma * Math.sqrt(2 * Math.PI))) * Math.exp(-0.5 * Math.pow((x - mu) / sigma, 2)); }; // Generate curve points const curvePoints = useMemo(() => { const points = []; const range = 4 * stdDev; const start = mean - range; const end = mean + range; const step = (end - start) / 200; for (let x = start; x <= end; x += step) { const y = normalPDF(x, mean, stdDev); points.push({ x, y }); } return points; }, [mean, stdDev]); // Calculate empirical rule percentages const empiricalRuleData = useMemo(() => { return [ { range: '68%', bounds: [mean - stdDev, mean + stdDev], color: 'rgba(59, 130, 246, 0.3)', description: 'Within 1 standard deviation', id: 'empirical-1-sigma' }, { range: '95%', bounds: [mean - 2*stdDev, mean + 2*stdDev], color: 'rgba(16, 185, 129, 0.2)', description: 'Within 2 standard deviations', id: 'empirical-2-sigma' }, { range: '99.7%', bounds: [mean - 3*stdDev, mean + 3*stdDev], color: 'rgba(245, 158, 11, 0.15)', description: 'Within 3 standard deviations', id: 'empirical-3-sigma' } ]; }, [mean, stdDev]); // SVG dimensions const width = 600; const height = 300; const margin = { top: 20, right: 50, bottom: 50, left: 50 }; const plotWidth = width - margin.left - margin.right; const plotHeight = height - margin.top - margin.bottom; // Scale functions const xMin = mean - 4 * stdDev; const xMax = mean + 4 * stdDev; const yMax = Math.max(...curvePoints.map(p => p.y)); const xScale = (x) => ((x - xMin) / (xMax - xMin)) * plotWidth; const yScale = (y) => plotHeight - ((y / yMax) * plotHeight); // Create path for the curve const curvePath = curvePoints.map((point, i) => `${i === 0 ? 'M' : 'L'} ${xScale(point.x)} ${yScale(point.y)}` ).join(' '); // Create filled areas for empirical rule const createFilledArea = (bounds) => { const filteredPoints = curvePoints.filter(p => p.x >= bounds[0] && p.x <= bounds[1]); if (filteredPoints.length === 0) return ''; let path = `M ${xScale(bounds[0])} ${yScale(0)}`; filteredPoints.forEach(point => { path += ` L ${xScale(point.x)} ${yScale(point.y)}`; }); path += ` L ${xScale(bounds[1])} ${yScale(0)} Z`; return path; }; // Announce changes to screen readers const announceChange = (message) => { if (liveRegionRef.current) { liveRegionRef.current.textContent = message; } }; // Handle parameter changes with announcements const handleMeanChange = (value) => { setMean(value); announceChange(`Mean changed to ${value}. Z-score is now ${((rawScore - value) / stdDev).toFixed(3)}`); }; const handleStdDevChange = (value) => { setStdDev(value); announceChange(`Standard deviation changed to ${value}. Z-score is now ${((rawScore - mean) / value).toFixed(3)}`); }; const handleRawScoreChange = (value) => { setRawScore(value); const newZScore = ((value - mean) / stdDev).toFixed(3); const leftTail = (standardNormalCDF((value - mean) / stdDev) * 100).toFixed(2); const rightTail = (100 - leftTail).toFixed(2); announceChange(`Raw score changed to ${value}. Z-score is ${newZScore}. Left tail probability is ${leftTail}%, right tail probability is ${rightTail}%`); }; // Keyboard navigation for empirical rule regions const handleRegionKeyDown = (event, index) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); setSelectedRegion(selectedRegion === index ? null : index); const region = empiricalRuleData[index]; announceChange(`${selectedRegion === index ? 'Deselected' : 'Selected'} ${region.range} region: ${region.description}`); } }; // Generate chart description for screen readers const chartDescription = useMemo(() => { const empiricalText = showEmpiricalRule ? `Empirical rule regions are visible showing 68%, 95%, and 99.7% of data within 1, 2, and 3 standard deviations respectively.` : ''; return `Normal distribution curve with mean ${mean} and standard deviation ${stdDev}. ` + `Raw score marker at ${rawScore.toFixed(1)} with z-score ${zScore.toFixed(3)}. ` + `Left tail (red shaded area) contains ${tailProbabilities.leftTailPercent}% of data. ` + `Right tail (green shaded area) contains ${tailProbabilities.rightTailPercent}% of data. ` + empiricalText; }, [mean, stdDev, rawScore, zScore, tailProbabilities, showEmpiricalRule]); return (
{/* Skip Navigation */} Skip to main controls

Interactive Normal Distribution Explorer

{/* Live Region for Screen Reader Announcements */}
{/* Instructions */}

How to Use:

{/* Two Column Layout */}
{/* Left Column - Controls */}
{/* Distribution Parameters */}
Distribution Parameters
handleMeanChange(parseFloat(e.target.value))} className="w-full h-3 bg-gray-200 rounded-lg appearance-none cursor-pointer focus:outline-none focus:ring-4 focus:ring-blue-300" aria-describedby="mean-description" />
Adjust the center point of the distribution (range: -5 to 5)
handleStdDevChange(parseFloat(e.target.value))} className="w-full h-3 bg-gray-200 rounded-lg appearance-none cursor-pointer focus:outline-none focus:ring-4 focus:ring-blue-300" aria-describedby="stddev-description" />
Adjust the spread of the distribution (range: 0.1 to 3)
{/* Z-Score Calculator */}
Z-Score Calculator
handleRawScoreChange(parseFloat(e.target.value))} className="w-full h-3 bg-gray-200 rounded-lg appearance-none cursor-pointer focus:outline-none focus:ring-4 focus:ring-blue-300" aria-describedby="rawscore-description" />
Select a value to see its z-score and tail probabilities

Z-Score = {zScore.toFixed(3)}

{zScore > 0 ? `${zScore.toFixed(3)} standard deviations above the mean` : zScore < 0 ? `${Math.abs(zScore).toFixed(3)} standard deviations below the mean` : 'Exactly at the mean'}

Left Tail (P(X ≤ {rawScore.toFixed(1)}))

{tailProbabilities.leftTailPercent}%

Probability of values less than or equal to raw score (red shaded area)

Right Tail (P(X ≥ {rawScore.toFixed(1)}))

{tailProbabilities.rightTailPercent}%

Probability of values greater than or equal to raw score (green shaded area)

Verification: {tailProbabilities.leftTailPercent}% + {tailProbabilities.rightTailPercent}% = {(parseFloat(tailProbabilities.leftTailPercent) + parseFloat(tailProbabilities.rightTailPercent)).toFixed(2)}%

{/* Empirical Rule Toggle */}
Display Options
Toggle to display regions showing 68%, 95%, and 99.7% of data within 1, 2, and 3 standard deviations
{/* Empirical Rule Legend */} {showEmpiricalRule && (
Empirical Rule Regions {empiricalRuleData.map((region, index) => (
empiricalRegionRefs.current[index] = el} className={`p-3 rounded-lg border-2 cursor-pointer transition-all focus:outline-none focus:ring-4 focus:ring-blue-300 ${ selectedRegion === index ? 'border-gray-600 shadow-lg ring-2 ring-blue-400' : 'border-gray-300' }`} style={{ backgroundColor: region.color }} onClick={() => { setSelectedRegion(selectedRegion === index ? null : index); announceChange(`${selectedRegion === index ? 'Deselected' : 'Selected'} ${region.range} region: ${region.description}`); }} onKeyDown={(e) => handleRegionKeyDown(e, index)} tabIndex="0" role="button" aria-pressed={selectedRegion === index} aria-describedby={`${region.id}-description`} >

{region.range}

{region.description}

Range: [{(region.bounds[0]).toFixed(2)}, {(region.bounds[1]).toFixed(2)}]

{selectedRegion === index && (

Selected - Press Enter or Space to deselect

)}
))}
)}
{/* Right Column - Chart */}

Normal Distribution Curve

Normal Distribution Curve Visualization {chartDescription} {/* Left tail shading */} {(() => { const leftTailPoints = curvePoints.filter(p => p.x <= rawScore); if (leftTailPoints.length === 0) return null; let path = `M ${xScale(xMin)} ${yScale(0)}`; leftTailPoints.forEach(point => { path += ` L ${xScale(point.x)} ${yScale(point.y)}`; }); path += ` L ${xScale(rawScore)} ${yScale(0)} Z`; return ( ); })()} {/* Right tail shading */} {(() => { const rightTailPoints = curvePoints.filter(p => p.x >= rawScore); if (rightTailPoints.length === 0) return null; let path = `M ${xScale(rawScore)} ${yScale(0)}`; rightTailPoints.forEach(point => { path += ` L ${xScale(point.x)} ${yScale(point.y)}`; }); path += ` L ${xScale(xMax)} ${yScale(0)} Z`; return ( ); })()} {/* Empirical rule regions */} {showEmpiricalRule && empiricalRuleData.map((region, index) => ( { setSelectedRegion(selectedRegion === index ? null : index); announceChange(`${selectedRegion === index ? 'Deselected' : 'Selected'} ${region.range} region: ${region.description}`); }} aria-label={`${region.range} empirical rule region: ${region.description}. Range ${region.bounds[0].toFixed(2)} to ${region.bounds[1].toFixed(2)}. ${selectedRegion === index ? 'Currently selected.' : 'Click to select.'}`} role="button" tabIndex="0" onKeyDown={(e) => handleRegionKeyDown(e, index)} /> ))} {/* Main curve */} {/* Raw score line */} {/* Raw score point */} {/* X-axis */} {/* Y-axis */} {/* X-axis labels */} {[-3, -2, -1, 0, 1, 2, 3].map(z => { const xVal = mean + z * stdDev; return ( {xVal.toFixed(1)} z={z} ); })} {/* Axis labels */} Value (X) Probability Density
{/* Chart Summary for Screen Readers */}

Chart Summary

{chartDescription}

); }; export default NormalDistributionExplorer;