127 lines
5.8 KiB
TypeScript
127 lines
5.8 KiB
TypeScript
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">x̄</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; |