104 lines
4.0 KiB
TypeScript
104 lines
4.0 KiB
TypeScript
import React, { useState } from "react";
|
|
|
|
const HistogramBuilderWidget: React.FC = () => {
|
|
const [mode, setMode] = useState<"count" | "percent">("count");
|
|
|
|
// Data: [60, 70), [70, 80), [80, 90), [90, 100)
|
|
const data = [
|
|
{ bin: "60-70", count: 4, label: "60s" },
|
|
{ bin: "70-80", count: 9, label: "70s" },
|
|
{ bin: "80-90", count: 6, label: "80s" },
|
|
{ bin: "90-100", count: 1, label: "90s" },
|
|
];
|
|
|
|
const total = data.reduce((acc, curr) => acc + curr.count, 0); // 20
|
|
|
|
const maxCount = Math.max(...data.map((d) => d.count));
|
|
const maxPercent = maxCount / total; // 0.45
|
|
|
|
return (
|
|
<div className="bg-white p-6 rounded-xl shadow-lg border border-slate-200">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h3 className="font-bold text-slate-700">Test Scores Distribution</h3>
|
|
<div className="flex bg-slate-100 p-1 rounded-lg">
|
|
<button
|
|
onClick={() => setMode("count")}
|
|
className={`px-4 py-1.5 text-sm font-bold rounded-md transition-all ${mode === "count" ? "bg-white shadow-sm text-indigo-600" : "text-slate-500 hover:text-slate-700"}`}
|
|
>
|
|
Frequency (Count)
|
|
</button>
|
|
<button
|
|
onClick={() => setMode("percent")}
|
|
className={`px-4 py-1.5 text-sm font-bold rounded-md transition-all ${mode === "percent" ? "bg-white shadow-sm text-rose-600" : "text-slate-500 hover:text-slate-700"}`}
|
|
>
|
|
Relative Freq (%)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative h-64 border-b-2 border-slate-200 flex items-end px-8 gap-1">
|
|
{/* Y Axis Labels */}
|
|
<div className="absolute left-0 top-0 bottom-0 flex flex-col justify-between text-xs font-mono text-slate-400 py-2">
|
|
<span>
|
|
{mode === "count"
|
|
? maxCount + 1
|
|
: ((maxPercent + 0.05) * 100).toFixed(0) + "%"}
|
|
</span>
|
|
<span>
|
|
{mode === "count"
|
|
? Math.round((maxCount + 1) / 2)
|
|
: (((maxPercent + 0.05) / 2) * 100).toFixed(0) + "%"}
|
|
</span>
|
|
<span>0</span>
|
|
</div>
|
|
|
|
{data.map((d, i) => {
|
|
// Normalize to max height of graph area roughly
|
|
// Actually map 0 to maxScale
|
|
const maxScale = mode === "count" ? maxCount + 1 : maxPercent + 0.05;
|
|
const val = mode === "count" ? d.count : d.count / total;
|
|
const hPercent = (val / maxScale) * 100;
|
|
|
|
return (
|
|
<div
|
|
key={i}
|
|
className="flex-1 flex flex-col justify-end group relative h-full"
|
|
>
|
|
{/* Tooltip */}
|
|
<div className="opacity-0 group-hover:opacity-100 absolute -top-10 left-1/2 -translate-x-1/2 bg-slate-800 text-white text-xs py-1 px-2 rounded pointer-events-none transition-opacity z-10 whitespace-nowrap">
|
|
{d.bin}:{" "}
|
|
{mode === "count"
|
|
? d.count
|
|
: `${((d.count / total) * 100).toFixed(0)}%`}
|
|
</div>
|
|
|
|
{/* Bar */}
|
|
<div
|
|
className={`w-full transition-all duration-500 rounded-t ${mode === "count" ? "bg-indigo-500 group-hover:bg-indigo-600" : "bg-rose-500 group-hover:bg-rose-600"}`}
|
|
style={{ height: `${hPercent}%` }}
|
|
></div>
|
|
|
|
{/* Bin Label */}
|
|
<div className="absolute -bottom-6 w-full text-center text-xs font-bold text-slate-500">
|
|
{d.label}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="mt-8 p-4 bg-slate-50 rounded-xl border border-slate-200">
|
|
<p className="text-sm text-slate-600">
|
|
<strong>Key Takeaway:</strong> Notice that the{" "}
|
|
<span className="font-bold text-slate-800">shape</span> of the
|
|
distribution stays exactly the same. Only the{" "}
|
|
<span className="font-bold text-slate-800">Y-axis scale</span>{" "}
|
|
changes.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default HistogramBuilderWidget;
|