visualize_dataset / src /components /stats-panel.tsx
mishig's picture
mishig HF Staff
refactor: update component definitions to function syntax
3ed0c9a
"use client";
import type {
DatasetDisplayInfo,
EpisodeLengthStats,
CameraInfo,
} from "@/app/[org]/[dataset]/[episode]/fetch-data";
interface StatsPanelProps {
datasetInfo: DatasetDisplayInfo;
episodeLengthStats: EpisodeLengthStats | null;
loading: boolean;
}
function formatTotalTime(totalFrames: number, fps: number): string {
const totalSec = totalFrames / fps;
const hours = Math.floor(totalSec / 3600);
const minutes = Math.floor((totalSec % 3600) / 60);
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}
/** SVG bar chart for the episode-length histogram */
function EpisodeLengthHistogram({
data,
}: {
data: { binLabel: string; count: number }[];
}) {
if (data.length === 0) return null;
const maxCount = Math.max(...data.map((d) => d.count));
if (maxCount === 0) return null;
const totalWidth = 560;
const gap = Math.max(1, Math.min(3, Math.floor(60 / data.length)));
const barWidth = Math.max(
4,
Math.floor((totalWidth - gap * data.length) / data.length),
);
const chartHeight = 150;
const labelHeight = 30;
const topPad = 16;
const svgWidth = data.length * (barWidth + gap);
const labelStep = Math.max(1, Math.ceil(data.length / 10));
return (
<div className="overflow-x-auto">
<svg
width={svgWidth}
height={topPad + chartHeight + labelHeight}
className="block"
aria-label="Episode length distribution histogram"
>
{data.map((bin, i) => {
const barH = Math.max(1, (bin.count / maxCount) * chartHeight);
const x = i * (barWidth + gap);
const y = topPad + chartHeight - barH;
return (
<g key={i}>
<title>{`${bin.binLabel}: ${bin.count} episode${bin.count !== 1 ? "s" : ""}`}</title>
<rect
x={x}
y={y}
width={barWidth}
height={barH}
className="fill-orange-500/80 hover:fill-orange-400 transition-colors"
rx={Math.min(2, barWidth / 4)}
/>
{bin.count > 0 && barWidth >= 8 && (
<text
x={x + barWidth / 2}
y={y - 3}
textAnchor="middle"
className="fill-slate-400"
fontSize={Math.min(10, barWidth - 1)}
>
{bin.count}
</text>
)}
</g>
);
})}
{data.map((bin, idx) => {
const isFirst = idx === 0;
const isLast = idx === data.length - 1;
if (!isFirst && !isLast && idx % labelStep !== 0) return null;
const label = bin.binLabel.split("–")[0];
return (
<text
key={idx}
x={idx * (barWidth + gap) + barWidth / 2}
y={topPad + chartHeight + 14}
textAnchor="middle"
className="fill-slate-400"
fontSize={9}
>
{label}s
</text>
);
})}
</svg>
</div>
);
}
function Card({ label, value }: { label: string; value: string | number }) {
return (
<div className="bg-slate-800/60 rounded-lg p-4 border border-slate-700">
<p className="text-xs text-slate-400 uppercase tracking-wide">{label}</p>
<p className="text-xl font-bold tabular-nums mt-1">{value}</p>
</div>
);
}
function StatsPanel({
datasetInfo,
episodeLengthStats,
loading,
}: StatsPanelProps) {
const els = episodeLengthStats;
return (
<div className="max-w-4xl mx-auto py-6 space-y-8">
<div>
<h2 className="text-xl text-slate-100">
<span className="font-bold">Dataset Statistics:</span>{" "}
<span className="font-normal text-slate-400">
{datasetInfo.repoId}
</span>
</h2>
</div>
{/* Overview cards */}
<div className="grid grid-cols-3 gap-4">
<Card label="Robot Type" value={datasetInfo.robot_type ?? "unknown"} />
<Card label="Dataset Version" value={datasetInfo.codebase_version} />
<Card label="Tasks" value={datasetInfo.total_tasks} />
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card
label="Total Frames"
value={datasetInfo.total_frames.toLocaleString()}
/>
<Card
label="Total Episodes"
value={datasetInfo.total_episodes.toLocaleString()}
/>
<Card label="FPS" value={datasetInfo.fps} />
<Card
label="Total Recording Time"
value={formatTotalTime(datasetInfo.total_frames, datasetInfo.fps)}
/>
</div>
{/* Camera resolutions */}
{datasetInfo.cameras.length > 0 && (
<div className="bg-slate-800/60 rounded-lg p-5 border border-slate-700">
<h3 className="text-sm font-semibold text-slate-200 mb-3">
Camera Resolutions
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{datasetInfo.cameras.map((cam: CameraInfo) => (
<div key={cam.name} className="bg-slate-900/50 rounded-md p-3">
<p
className="text-xs text-slate-400 mb-1 truncate"
title={cam.name}
>
{cam.name}
</p>
<p className="text-base font-bold tabular-nums">
{cam.width}×{cam.height}
</p>
</div>
))}
</div>
</div>
)}
{/* Loading spinner for async stats */}
{loading && (
<div className="flex items-center gap-2 text-slate-400 text-sm py-4">
<svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" fill="none">
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
Computing episode statistics…
</div>
)}
{/* Episode length section */}
{els && (
<>
<div className="bg-slate-800/60 rounded-lg p-5 border border-slate-700">
<h3 className="text-sm font-semibold text-slate-200 mb-4">
Episode Lengths
</h3>
<div className="grid grid-cols-3 md:grid-cols-5 gap-4 mb-4">
<Card
label="Shortest"
value={`${els.shortestEpisodes[0]?.lengthSeconds ?? "–"}s`}
/>
<Card
label="Longest"
value={`${els.longestEpisodes[els.longestEpisodes.length - 1]?.lengthSeconds ?? "–"}s`}
/>
<Card label="Mean" value={`${els.meanEpisodeLength}s`} />
<Card label="Median" value={`${els.medianEpisodeLength}s`} />
<Card label="Std Dev" value={`${els.stdEpisodeLength}s`} />
</div>
</div>
{els.episodeLengthHistogram.length > 0 && (
<div className="bg-slate-800/60 rounded-lg p-5 border border-slate-700">
<h3 className="text-sm font-semibold text-slate-200 mb-4">
Episode Length Distribution
<span className="text-xs text-slate-500 ml-2 font-normal">
{els.episodeLengthHistogram.length} bin
{els.episodeLengthHistogram.length !== 1 ? "s" : ""}
</span>
</h3>
<EpisodeLengthHistogram data={els.episodeLengthHistogram} />
</div>
)}
</>
)}
</div>
);
}
export default StatsPanel;