48 / 122 · Structured output
Chart
Area, line, and bars, with points landing one at a time as the series streams in.
Runs this week+34%
92Installation
npx shadcn@latest add "@assistant-ui/elements-chart"
Usage
import { Chart } from "@/components/elements/chart";
<Chart
label="Runs this week"
value="92"
delta="+34%"
points={points}
visibleCount={points.length}
variant="area"
/>Props
label*stringWhat the series measures.
value*stringThe headline figure, pre-formatted. The chart never formats numbers itself.
deltastringChange against the previous window. Renders green unless it starts with a minus sign.
points*number[]The full series. The scale is computed from all of it, so the axis does not jump while streaming.
visibleCount*numberHow many points have arrived.
variant"area" | "line" | "bars"Mark type. The last point stays emphasized in all three.
classNamestringExtra classes merged onto the root.
Source
chart.tsx"use client";
import { cn } from "@/lib/utils";
import { mono, paper } from "./surfaces";
export type ChartVariant = "area" | "line" | "bars";
const W = 300;
const H = 88;
const PAD = 6;
const scale = (points: readonly number[]) => {
const max = Math.max(...points, 1);
const min = Math.min(...points, 0);
const span = max - min || 1;
return (value: number) => H - PAD - ((value - min) / span) * (H - PAD * 2);
};
export function Chart({
label,
value,
delta,
points,
visibleCount,
variant = "area",
className,
}: {
label: string;
value: string;
delta?: string;
points: readonly number[];
visibleCount: number;
variant?: ChartVariant;
className?: string;
}) {
const shown = points.slice(0, Math.max(1, visibleCount));
const y = scale(points);
const step = points.length > 1 ? (W - PAD * 2) / (points.length - 1) : 0;
const x = (i: number) => PAD + i * step;
const coords = shown.map((p, i) => ({ x: x(i), y: y(p) }));
const line = coords.map((c) => `${c.x},${c.y}`).join(" ");
const last = coords.at(-1);
const area = last
? `M ${PAD},${H - PAD} ${coords.map((c) => `L ${c.x},${c.y}`).join(" ")} L ${last.x},${H - PAD} Z`
: "";
const lastIndex = shown.length - 1;
const falling = delta !== undefined && /^\s*[-−–]/.test(delta);
const rising = delta !== undefined && !falling;
return (
<div
className={cn(
paper,
"flex w-full max-w-sm flex-col gap-3 rounded-2xl p-4",
className,
)}
>
<div className="flex items-baseline justify-between">
<span className={cn(mono, "text-foreground/35")}>{label}</span>
{delta !== undefined && (
<span
className={cn(
mono,
"tabular-nums",
rising
? "text-emerald-600 dark:text-emerald-400"
: "text-red-600 dark:text-red-400",
)}
>
{delta}
</span>
)}
</div>
<span className="text-2xl font-medium tracking-tight tabular-nums">
{value}
</span>
<svg
viewBox={`0 0 ${W} ${H}`}
role="img"
aria-label={`${label}: ${value}`}
className="h-[88px] w-full overflow-visible"
preserveAspectRatio="none"
>
<line
x1="0"
x2={W}
y1={H - PAD}
y2={H - PAD}
className="stroke-foreground/[0.08]"
strokeWidth="1"
vectorEffect="non-scaling-stroke"
/>
{variant === "bars" ? (
shown.map((p, i) => {
const top = y(p);
const barWidth = Math.max(2, step * 0.55);
return (
<rect
key={i}
x={x(i) - barWidth / 2}
y={top}
width={barWidth}
height={Math.max(1, H - PAD - top)}
rx="1.5"
className={cn(
"fade-in animate-in fill-mode-both duration-300",
i === lastIndex
? "fill-blue-500 dark:fill-blue-400"
: "fill-foreground/25",
)}
style={{ animationDelay: `${i * 40}ms` }}
/>
);
})
) : (
<>
{variant === "area" && shown.length > 1 && (
<path
d={area}
className="fill-blue-500/12 dark:fill-blue-400/15"
/>
)}
<polyline
points={line}
fill="none"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
vectorEffect="non-scaling-stroke"
className="stroke-blue-500 dark:stroke-blue-400"
/>
{last && (
<circle
cx={last.x}
cy={last.y}
r="3"
className="fill-blue-500 dark:fill-blue-400"
/>
)}
</>
)}
</svg>
</div>
);
}