Chart states
Loading, empty and error versions of any chart.
Overtime
September 2026 · hourly-paid staff
Loading
Loading
Engineering logged the most overtime, 118 hours, during the payroll migration; Product logged the least, 9 hours.
Loading chart
Most overtime
chart-states.tsx
"use client";
import { useState } from "react";
import { LuFilterX, LuRefreshCw, LuUsers } from "react-icons/lu";
import {
AvatarLabel,
BarChart,
Button,
ChartState,
EmptyState,
MetricCard,
Segment,
SegmentedControl,
Skeleton,
} from "@nexera-ui/react";
// Sample data
interface Person {
id: string;
name: string;
initials: string;
role: string;
team: string;
email: string;
location: string;
status: "Active" | "On leave" | "Remote" | "Invited";
}
const PEOPLE: Person[] = [
{
id: "p1",
name: "Ayesha Khan",
initials: "AK",
role: "Senior product designer",
team: "Design",
email: "ayesha.khan@northwind.io",
location: "London",
status: "Active",
},
{
id: "p2",
name: "Sara Malik",
initials: "SM",
role: "Payroll specialist",
team: "People Ops",
email: "sara.malik@northwind.io",
location: "Lisbon",
status: "On leave",
},
{
id: "p3",
name: "Ahmed Raza",
initials: "AR",
role: "Engineering manager",
team: "Engineering",
email: "ahmed.raza@northwind.io",
location: "Lahore",
status: "Remote",
},
{
id: "p4",
name: "Mia Jensen",
initials: "MJ",
role: "Product manager",
team: "Product",
email: "mia.jensen@northwind.io",
location: "Copenhagen",
status: "Active",
},
{
id: "p5",
name: "Luis Ortega",
initials: "LO",
role: "Frontend engineer",
team: "Engineering",
email: "luis.ortega@northwind.io",
location: "Madrid",
status: "Active",
},
{
id: "p6",
name: "Yuki Tanaka",
initials: "YT",
role: "Recruiter",
team: "People Ops",
email: "yuki.tanaka@northwind.io",
location: "Tokyo",
status: "Active",
},
{
id: "p7",
name: "Chloe Martin",
initials: "CM",
role: "Data analyst",
team: "Finance",
email: "chloe.martin@northwind.io",
location: "Paris",
status: "Remote",
},
{
id: "p8",
name: "Omar Haddad",
initials: "OH",
role: "Support lead",
team: "Customer",
email: "omar.haddad@northwind.io",
location: "Dubai",
status: "Invited",
},
];
const TEAMS = [
"Design",
"Engineering",
"Finance",
"People Ops",
"Product",
"Customer",
] as const;
type Status = "ready" | "loading" | "empty" | "error";
const OVERTIME = [42, 118, 16, 24, 9, 37]; // hours in September, in TEAMS order
/** Chart states: one switch moves a dashboard panel between ready, loading, empty and error, using the matching placeholder. */
export default function ChartStates() {
const [status, setStatus] = useState<Status>("loading");
const retry = (
<Button
variant="secondary"
size="sm"
leadingIcon={<LuRefreshCw />}
onPress={() => {
setStatus("ready");
}}
>
Try again
</Button>
);
const clear = (
<Button
variant="secondary"
size="sm"
leadingIcon={<LuFilterX />}
onPress={() => {
setStatus("ready");
}}
>
Clear filters
</Button>
);
const chartState =
status === "loading"
? { status: "loading" as const }
: status === "empty"
? {
status: "empty" as const,
title: "No overtime logged",
description: "Nobody in the selected teams logged overtime in September.",
action: clear,
}
: status === "error"
? {
status: "error" as const,
title: "Couldn't load overtime",
description: "The time-tracking service did not answer. Your data is safe.",
action: retry,
}
: undefined;
return (
<div className="@container bg-page px-4 py-8 @3xl:px-8">
<div className="mx-auto grid max-w-6xl gap-6">
<header className="flex flex-wrap items-end justify-between gap-4">
<div className="grid gap-1">
<h2 className="text-heading-h2 text-primary">Overtime</h2>
<p className="text-body-default text-secondary">September 2026 · hourly-paid staff</p>
</div>
<SegmentedControl
aria-label="Preview state"
selectedKeys={[status]}
onSelectionChange={(keys) => {
setStatus(([...keys][0] ?? "ready") as Status);
}}
>
<Segment id="ready" label="Ready" />
<Segment id="loading" label="Loading" />
<Segment id="empty" label="Empty" />
<Segment id="error" label="Error" />
</SegmentedControl>
</header>
<div className="grid gap-4 @xl:grid-cols-2">
<MetricCard
type="trend"
label="Overtime hours"
value={status === "ready" ? "246 h" : status === "empty" ? "0 h" : "–"}
delta="-8%"
trend="down-good"
caption="vs August"
isLoading={status === "loading"}
/>
<MetricCard
type="simple"
label="Overtime cost"
value={status === "ready" ? "$9,840" : status === "empty" ? "$0" : "–"}
caption="Paid at 1.5× the hourly rate"
isLoading={status === "loading"}
/>
</div>
<div className="grid gap-4 @5xl:grid-cols-[3fr_2fr]">
<BarChart
title="Overtime by team"
description="Hours in September"
summary="Engineering logged the most overtime, 118 hours, during the payroll migration; Product logged the least, 9 hours."
categories={[...TEAMS]}
series={[{ name: "Hours", data: OVERTIME }]}
valueLabels="all"
{...(chartState === undefined ? {} : { state: chartState })}
/>
<section
aria-labelledby="overtime-people"
aria-busy={status === "loading"}
className="grid content-start gap-3 rounded-lg border border-default bg-surface p-4 @3xl:p-6"
>
<h3 id="overtime-people" className="text-heading-h4 text-primary">
Most overtime
</h3>
{status === "loading" ? (
<div className="grid gap-4">
<Skeleton type="avatar-row" />
<Skeleton type="avatar-row" />
<Skeleton type="avatar-row" />
</div>
) : status === "empty" ? (
<EmptyState
icon={<LuUsers />}
title="Nobody to show"
description="People appear here once they log overtime."
headingLevel={4}
/>
) : status === "error" ? (
<ChartState
status="error"
title="Couldn't load people"
description="Try again in a moment."
action={retry}
isEmbedded
/>
) : (
<ul className="grid gap-3">
{[PEOPLE[4], PEOPLE[7], PEOPLE[6]].map((person, i) =>
person ? (
<li key={person.id} className="flex items-center justify-between gap-3">
<AvatarLabel name={person.name} subtitle={person.team} size="md" />
<span className="text-body-strong text-primary tabular-nums">
{[21, 17, 12][i]} h
</span>
</li>
) : null,
)}
</ul>
)}
</section>
</div>
</div>
</div>
);
}
More dashboards and analytics blocks
- KPI overviewFour headline metrics with trend and change.
- Sales dashboardRevenue over time against target.
- People analyticsHeadcount by team, type and tenure.
- Recruitment funnelApplicants at each stage and the drop-off between.
- Engagement survey resultseNPS score and check-in patterns across the week.
- Budget varianceHow the budget moved from plan to actual.
- Skills comparisonOne person against the team average.
- Live status boardReal-time counts with tiny trends.