feat(lessons): add lessons from client db
This commit is contained in:
166
src/components/lessons/SamplingVisualizerWidget.tsx
Normal file
166
src/components/lessons/SamplingVisualizerWidget.tsx
Normal file
@ -0,0 +1,166 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user