Files
edbridge-scholars/src/components/lessons/DataModifierWidget.tsx
2026-03-01 20:24:14 +06:00

127 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react';
const DataModifierWidget: React.FC = () => {
const initialData = [10, 12, 13, 15, 16, 18, 20];
const [data, setData] = useState<number[]>(initialData);
const calculateStats = (arr: number[]) => {
const sorted = [...arr].sort((a, b) => a - b);
const sum = sorted.reduce((a, b) => a + b, 0);
const mean = sum / sorted.length;
const min = sorted[0];
const max = sorted[sorted.length - 1];
const range = max - min;
// Median
const mid = Math.floor(sorted.length / 2);
const median = sorted.length % 2 !== 0
? sorted[mid]
: (sorted[mid - 1] + sorted[mid]) / 2;
// SD (Population)
const variance = sorted.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / sorted.length;
const sd = Math.sqrt(variance);
return { mean, median, range, sd, sorted };
};
const stats = calculateStats(data);
// Operations
const reset = () => setData(initialData);
const addConstant = (k: number) => {
setData(prev => prev.map(n => n + k));
};
const multiplyConstant = (k: number) => {
setData(prev => prev.map(n => n * k));
};
const addOutlier = (val: number) => {
setData(prev => [...prev, val]);
};
// Visual scaling
const minDisplay = Math.min(0, ...data) - 5;
const maxDisplay = Math.max(Math.max(...data), 100) + 10;
const getX = (val: number) => ((val - minDisplay) / (maxDisplay - minDisplay)) * 100;
return (
<div className="bg-white p-6 rounded-xl shadow-lg border border-slate-200">
<div className="flex flex-col md:flex-row gap-6">
{/* Controls */}
<div className="w-full md:w-1/3 space-y-3">
<h4 className="font-bold text-slate-700 mb-2">Apply Transformation</h4>
<button onClick={() => addConstant(5)} className="w-full py-2 px-4 bg-amber-100 hover:bg-amber-200 text-amber-900 rounded-lg font-bold text-sm text-left transition-colors">
+ Add 5 (Shift Right)
</button>
<button onClick={() => multiplyConstant(2)} className="w-full py-2 px-4 bg-amber-100 hover:bg-amber-200 text-amber-900 rounded-lg font-bold text-sm text-left transition-colors">
× Multiply by 2 (Scale)
</button>
<button onClick={() => addOutlier(80)} className="w-full py-2 px-4 bg-rose-100 hover:bg-rose-200 text-rose-900 rounded-lg font-bold text-sm text-left transition-colors border border-rose-200">
Add Outlier (80)
</button>
<button onClick={reset} className="w-full py-2 px-4 bg-slate-100 hover:bg-slate-200 text-slate-600 rounded-lg font-bold text-sm text-left transition-colors mt-4">
Reset Data
</button>
</div>
{/* Visualization */}
<div className="flex-1">
{/* Stats Panel */}
<div className="grid grid-cols-4 gap-2 mb-6 text-center">
<div className="p-2 bg-slate-50 border border-slate-200 rounded">
<div className="text-xs uppercase font-bold text-slate-500">Mean</div>
<div className="font-mono font-bold text-lg text-indigo-600">{stats.mean.toFixed(1)}</div>
</div>
<div className="p-2 bg-slate-50 border border-slate-200 rounded">
<div className="text-xs uppercase font-bold text-slate-500">Median</div>
<div className="font-mono font-bold text-lg text-emerald-600">{stats.median.toFixed(1)}</div>
</div>
<div className="p-2 bg-slate-50 border border-slate-200 rounded">
<div className="text-xs uppercase font-bold text-slate-500">Range</div>
<div className="font-mono font-bold text-lg text-slate-700">{stats.range.toFixed(0)}</div>
</div>
<div className="p-2 bg-slate-50 border border-slate-200 rounded">
<div className="text-xs uppercase font-bold text-slate-500">SD</div>
<div className="font-mono font-bold text-lg text-slate-700">{stats.sd.toFixed(1)}</div>
</div>
</div>
{/* Dot Plot */}
<div className="h-32 relative border-b border-slate-300">
{stats.sorted.map((val, idx) => (
<div
key={idx}
className="absolute w-4 h-4 rounded-full bg-indigo-500 shadow-sm border border-white transform -translate-x-1/2"
style={{ left: `${getX(val)}%`, bottom: '10px' }}
title={`Value: ${val}`}
/>
))}
{/* Mean Marker */}
<div className="absolute top-0 bottom-0 w-0.5 bg-indigo-300 border-l border-dashed border-indigo-500 opacity-60" style={{ left: `${getX(stats.mean)}%` }}>
<span className="absolute -top-6 left-1/2 -translate-x-1/2 text-xs font-bold text-indigo-600 bg-white px-1 rounded shadow-sm"></span>
</div>
{/* Median Marker */}
<div className="absolute top-0 bottom-0 w-0.5 bg-emerald-300 border-l border-dashed border-emerald-500 opacity-60" style={{ left: `${getX(stats.median)}%` }}>
<span className="absolute -bottom-6 left-1/2 -translate-x-1/2 text-xs font-bold text-emerald-600 bg-white px-1 rounded shadow-sm">M</span>
</div>
</div>
<div className="mt-8 text-sm text-slate-500 leading-relaxed bg-slate-50 p-3 rounded">
{data.length > 7 ? (
<span className="text-rose-600 font-bold">Notice how the Mean is pulled towards the outlier, while the Median barely moves!</span>
) : (
"Experiment with adding constants and multipliers to see which stats change."
)}
</div>
</div>
</div>
</div>
);
};
export default DataModifierWidget;