87 lines
4.4 KiB
TypeScript
87 lines
4.4 KiB
TypeScript
import React, { useState } from 'react';
|
|
|
|
const BoxPlotAnatomyWidget: React.FC = () => {
|
|
const [q1, setQ1] = useState(20);
|
|
const [q3, setQ3] = useState(60);
|
|
const [med, setMed] = useState(40);
|
|
const min = 10;
|
|
const max = 90;
|
|
|
|
// Enforce constraints
|
|
const handleMedChange = (val: number) => {
|
|
setMed(Math.max(q1, Math.min(q3, val)));
|
|
};
|
|
const handleQ1Change = (val: number) => {
|
|
const newQ1 = Math.min(val, med);
|
|
setQ1(Math.max(min, newQ1));
|
|
};
|
|
const handleQ3Change = (val: number) => {
|
|
const newQ3 = Math.max(val, med);
|
|
setQ3(Math.min(max, newQ3));
|
|
};
|
|
|
|
const scale = (val: number) => ((val) / 100) * 100; // 0-100 domain mapped to %
|
|
|
|
return (
|
|
<div className="bg-white p-6 rounded-xl shadow-lg border border-slate-200 select-none">
|
|
<div className="relative h-40 mt-8 mb-4">
|
|
{/* Axis Line */}
|
|
<div className="absolute top-1/2 left-[10%] right-[10%] h-0.5 bg-slate-300 -translate-y-1/2"></div>
|
|
|
|
{/* Range Line (Whiskers) */}
|
|
<div className="absolute top-1/2 bg-slate-800 h-0.5 -translate-y-1/2 transition-all"
|
|
style={{ left: `${scale(min)}%`, right: `${100 - scale(max)}%` }}></div>
|
|
|
|
{/* Endpoints (Min/Max) */}
|
|
<div className="absolute top-1/2 h-4 w-0.5 bg-slate-800 -translate-y-1/2" style={{ left: `${scale(min)}%` }}>
|
|
<span className="absolute -top-6 left-1/2 -translate-x-1/2 text-xs font-bold text-slate-500">Min</span>
|
|
</div>
|
|
<div className="absolute top-1/2 h-4 w-0.5 bg-slate-800 -translate-y-1/2" style={{ left: `${scale(max)}%` }}>
|
|
<span className="absolute -top-6 left-1/2 -translate-x-1/2 text-xs font-bold text-slate-500">Max</span>
|
|
</div>
|
|
|
|
{/* The Box */}
|
|
<div className="absolute top-1/2 -translate-y-1/2 h-16 bg-amber-100 border-2 border-amber-500 rounded-sm transition-all"
|
|
style={{ left: `${scale(q1)}%`, width: `${scale(q3) - scale(q1)}%` }}>
|
|
|
|
{/* Median Line */}
|
|
<div className="absolute top-0 bottom-0 w-1 bg-amber-600 left-[50%] -translate-x-1/2 transition-all"
|
|
style={{ left: `${((med - q1) / (q3 - q1)) * 100}%` }}>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Labels below for Q1, Med, Q3 */}
|
|
<div className="absolute top-[70%] text-xs font-bold text-amber-700 -translate-x-1/2 transition-all" style={{ left: `${scale(q1)}%` }}>Q1</div>
|
|
<div className="absolute top-[70%] text-xs font-bold text-amber-900 -translate-x-1/2 transition-all" style={{ left: `${scale(med)}%` }}>Median</div>
|
|
<div className="absolute top-[70%] text-xs font-bold text-amber-700 -translate-x-1/2 transition-all" style={{ left: `${scale(q3)}%` }}>Q3</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-3 gap-6 mt-8">
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-400 uppercase">Q1 (25th %)</label>
|
|
<input type="range" min={min} max={max} value={q1} onChange={e => handleQ1Change(parseInt(e.target.value))}
|
|
className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-amber-500"/>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-400 uppercase">Median (50th %)</label>
|
|
<input type="range" min={min} max={max} value={med} onChange={e => handleMedChange(parseInt(e.target.value))}
|
|
className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-amber-700"/>
|
|
</div>
|
|
<div>
|
|
<label className="text-xs font-bold text-slate-400 uppercase">Q3 (75th %)</label>
|
|
<input type="range" min={min} max={max} value={q3} onChange={e => handleQ3Change(parseInt(e.target.value))}
|
|
className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer accent-amber-500"/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 p-4 bg-amber-50 border border-amber-100 rounded-lg text-center">
|
|
<div className="text-sm font-mono text-amber-900">
|
|
IQR (Box Width) = <span className="font-bold">{q3 - q1}</span>
|
|
</div>
|
|
<p className="text-xs text-amber-700/70 mt-1">The middle 50% of data lies inside the box.</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BoxPlotAnatomyWidget; |