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

166 lines
6.8 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { RefreshCw } from 'lucide-react';
interface Dot {
id: number;
type: 'red' | 'blue';
x: number;
y: number;
}
const SamplingVisualizerWidget: React.FC = () => {
const [population, setPopulation] = useState<Dot[]>([]);
const [sample, setSample] = useState<number[]>([]); // IDs of selected dots
const [mode, setMode] = useState<'none' | 'random' | 'biased'>('none');
// Generate population on mount
useEffect(() => {
const dots: Dot[] = [];
for (let i = 0; i < 100; i++) {
// Biased distribution: Reds cluster in top-right
const isClustered = i < 40; // 40% Red
let x, y;
if (isClustered) {
// Cluster Reds (Type A) in top right (50-100, 0-50)
x = 50 + Math.random() * 50;
y = Math.random() * 50;
} else {
// Blues scattered everywhere else, but mostly bottom/left
// To make it simple, just uniform random, but if we hit the "Red Zone" we retry or accept overlap
// Let's force Blues to be mostly Bottom or Left
if (Math.random() > 0.5) {
x = Math.random() * 50; // Left half
y = Math.random() * 100;
} else {
x = 50 + Math.random() * 50; // Right half
y = 50 + Math.random() * 50; // Bottom right
}
}
dots.push({
id: i,
type: isClustered ? 'red' : 'blue',
x,
y
});
}
setPopulation(dots);
}, []);
const takeRandomSample = () => {
const indices: number[] = [];
const pool = [...population];
// Pick 10 random
for (let i = 0; i < 10; i++) {
const idx = Math.floor(Math.random() * pool.length);
indices.push(pool[idx].id);
pool.splice(idx, 1);
}
setSample(indices);
setMode('random');
};
const takeBiasedSample = () => {
// Simulate "Convenience": Pick from top-right (the Red cluster)
// Find dots with x > 50 and y < 50
const candidates = population.filter(d => d.x > 50 && d.y < 50);
// Take 10 from there
const selected = candidates.slice(0, 10).map(d => d.id);
setSample(selected);
setMode('biased');
};
// Stats
const sampleDots = population.filter(d => sample.includes(d.id));
const sampleRedCount = sampleDots.filter(d => d.type === 'red').length;
const samplePercent = sampleDots.length > 0 ? (sampleRedCount / sampleDots.length) * 100 : 0;
const truePercent = 40; // Hardcoded based on generation logic
return (
<div className="bg-white p-6 rounded-xl shadow-lg border border-slate-200">
<div className="flex flex-col md:flex-row gap-8">
<div className="flex-1">
<div className="relative h-64 bg-slate-50 rounded-lg border border-slate-200 overflow-hidden mb-4">
{population.map(dot => {
const isSelected = sample.includes(dot.id);
const isRed = dot.type === 'red';
return (
<div
key={dot.id}
className={`absolute w-3 h-3 rounded-full transition-all duration-500 ${
isSelected
? 'ring-4 ring-offset-1 z-10 scale-125'
: 'opacity-40 scale-75'
} ${
isRed ? 'bg-rose-500' : 'bg-indigo-500'
} ${
isSelected && isRed ? 'ring-rose-200' : ''
} ${
isSelected && !isRed ? 'ring-indigo-200' : ''
}`}
style={{ left: `${dot.x}%`, top: `${dot.y}%` }}
/>
);
})}
{/* Labels for Bias Zone */}
<div className="absolute top-2 right-2 text-xs font-bold text-rose-300 uppercase pointer-events-none">
Cluster Zone
</div>
</div>
<p className="text-xs text-center text-slate-400">Population: 100 individuals (40% Red)</p>
</div>
<div className="w-full md:w-1/3 flex flex-col justify-center space-y-4">
<div className="space-y-2">
<button
onClick={takeRandomSample}
className="w-full py-3 px-4 bg-emerald-100 hover:bg-emerald-200 text-emerald-800 rounded-lg font-bold transition-colors flex items-center justify-center gap-2"
>
<RefreshCw className="w-4 h-4" /> Random Sample
</button>
<button
onClick={takeBiasedSample}
className="w-full py-3 px-4 bg-amber-100 hover:bg-amber-200 text-amber-800 rounded-lg font-bold transition-colors"
>
Convenience Sample
</button>
</div>
<div className={`p-4 rounded-xl border ${mode === 'none' ? 'border-slate-100 bg-slate-50' : 'bg-white border-slate-200'}`}>
<h4 className="text-xs font-bold text-slate-400 uppercase mb-2">Sample Result (n=10)</h4>
{mode === 'none' ? (
<p className="text-sm text-slate-500 italic">Select a method...</p>
) : (
<div>
<div className="flex justify-between items-end mb-1">
<span className="text-slate-600 font-medium">Estimated Red %</span>
<span className={`text-2xl font-bold ${Math.abs(samplePercent - truePercent) > 15 ? 'text-rose-600' : 'text-emerald-600'}`}>
{samplePercent}%
</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2 mb-2">
<div
className={`h-2 rounded-full transition-all duration-500 ${Math.abs(samplePercent - truePercent) > 15 ? 'bg-rose-500' : 'bg-emerald-500'}`}
style={{ width: `${samplePercent}%` }}
></div>
</div>
<p className="text-xs text-slate-400 text-right">True Population: 40%</p>
{mode === 'biased' && (
<p className="mt-2 text-xs font-bold text-amber-600 bg-amber-50 p-2 rounded">
Bias Alert: Selecting only from the "easy to reach" cluster overestimates the Red group.
</p>
)}
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default SamplingVisualizerWidget;