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 (
{chartDescription}