neon-ui
componentautoscale-chartsource: ok · hit
AutoscaleChart
Autoscaling compute units over time against the min/max bounds, with a scrubable readout.
npx shadcn add "https://ui.neon.com/r/autoscale-chart.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumchartsrecharts
Registry dependencies
utilshttps://ui.neon.com/r/skeleton.jsonhttps://ui.neon.com/r/neon-tokens.jsonhttps://ui.neon.com/r/chart.json
Files1 · source cached 10/13/2026, 9:55:25 AM
src/components/autoscale-chart/autoscale-chart.tsx
"use client";
import { useId } from "react";
import type { ComponentProps } from "react";
import {
Area,
AreaChart,
ReferenceArea,
ReferenceLine,
XAxis,
YAxis,
} from "recharts";
import type { ChartConfig } from "@/components/ui/chart";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
} from "@/components/ui/chart";
import { Skeleton } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
export interface AutoscalePoint {
/** Short time label, e.g. "12:00". */
label: string;
/** Compute units in use at this moment. */
cu: number;
}
const HEADROOM = 1.08;
const NICE_STEPS = [1, 2, 2.5, 5, 10] as const;
/**
* Rounds the top of the scale to a readable number. Left at peak x 1.08
* the axis reads 4.32, which is a number nobody asked for.
*/
const niceCeiling = (peak: number) => {
const raw = peak * HEADROOM;
if (raw <= 0) {
return 1;
}
const magnitude = 10 ** Math.floor(Math.log10(raw));
const normalized = raw / magnitude;
const step = NICE_STEPS.find((candidate) => normalized <= candidate) ?? 10;
return step * magnitude;
};
const formatCu = (cu: number) => {
const rounded = Math.round(cu * 100) / 100;
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(2);
};
export type AutoscaleChartProps = Omit<ComponentProps<"div">, "children"> & {
/** Compute-unit readings over time, oldest first. */
data: AutoscalePoint[];
/** Autoscale lower bound (CU); the compute never scales below it. */
min: number;
/** Autoscale upper bound (CU); the compute never scales above it. */
max: number;
/** Panel heading. */
title?: string;
isLoading?: boolean;
};
/* ─────────────────────────────────────────────────────────
* AUTOSCALE STORYBOARD
*
* envelope the band between min and max is drawn first,
* so the reading is always seen against the
* room it has to move in
* bounds dashed lines with mono labels at both edges;
* a ceiling you can't see is a ceiling you'll
* hit without knowing
* scrub Recharts' accessibility layer gives the plot
* focus and arrow keys; the tooltip names the
* moment and its CU
* ───────────────────────────────────────────────────────── */
const cuTooltipFormatter = (
value: unknown,
_name: unknown,
entry: { color?: string }
) => (
<>
<span
aria-hidden="true"
className="size-2.5 shrink-0 translate-y-[1px] rounded-[2px]"
style={{ background: String(entry?.color ?? "var(--primary)") }}
/>
<div className="flex flex-1 items-center justify-between gap-3 leading-none">
<span className="text-muted-foreground">Compute</span>
<span className="font-medium font-mono text-foreground tabular-nums">
{formatCu(Number(value))}
<span className="ml-1 font-normal text-muted-foreground/70">CU</span>
</span>
</div>
</>
);
export const AutoscaleChart = ({
className,
data,
isLoading = false,
max,
min,
title = "Autoscaling",
...props
}: AutoscaleChartProps) => {
const gradientId = `autoscale-fill-${useId().replaceAll(":", "")}`;
if (isLoading) {
return (
<div
className={cn(
"rounded-lg border border-border/60 bg-card p-4",
className
)}
data-slot="autoscale-chart"
{...props}
>
<Skeleton className="h-4 w-28" />
<Skeleton className="mt-4 h-[150px] w-full" />
</div>
);
}
const current = data.at(-1)?.cu ?? 0;
const ceiling = niceCeiling(Math.max(max, ...data.map((point) => point.cu)));
const config: ChartConfig = {
cu: { color: "var(--primary)", label: "Compute units" },
};
return (
<div
className={cn(
"rounded-lg border border-border/60 bg-card p-4",
className
)}
data-slot="autoscale-chart"
{...props}
>
<header className="mb-3 flex items-baseline justify-between gap-3">
<span className="font-mono text-muted-foreground text-xs">{title}</span>
<span className="flex items-baseline gap-1.5 font-mono tabular-nums">
<span className="font-medium text-foreground text-sm">
{formatCu(current)}
</span>
<span className="text-muted-foreground text-xs">CU</span>
<span className="text-muted-foreground/50 text-[10px]">
/ {formatCu(min)}–{formatCu(max)}
</span>
</span>
</header>
<ChartContainer className="aspect-auto h-[170px] w-full" config={config}>
<AreaChart
accessibilityLayer
data={data}
margin={{ left: 4, right: 76, top: 4 }}
>
<defs>
<linearGradient id={gradientId} x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stopColor="var(--primary)" stopOpacity={0.2} />
<stop offset="100%" stopColor="var(--primary)" stopOpacity={0} />
</linearGradient>
</defs>
{/* The band the compute may move within. */}
<ReferenceArea
fill="var(--muted-foreground)"
fillOpacity={0.06}
ifOverflow="extendDomain"
y1={min}
y2={max}
/>
<ReferenceLine
label={{
className: "fill-muted-foreground/80 font-mono text-[10px]",
position: "right",
value: `max ${formatCu(max)} CU`,
}}
stroke="var(--border)"
strokeDasharray="3 3"
y={max}
/>
<ReferenceLine
label={{
className: "fill-muted-foreground/80 font-mono text-[10px]",
position: "right",
value: `min ${formatCu(min)} CU`,
}}
stroke="var(--border)"
strokeDasharray="3 3"
y={min}
/>
<XAxis
axisLine={false}
dataKey="label"
interval="preserveStartEnd"
minTickGap={24}
tickLine={false}
tickMargin={8}
/>
<YAxis
axisLine={false}
domain={[0, ceiling]}
tickFormatter={formatCu}
tickLine={false}
width={36}
/>
<ChartTooltip
content={<ChartTooltipContent formatter={cuTooltipFormatter} />}
cursor={{ stroke: "var(--border)", strokeDasharray: "2 3" }}
/>
<Area
dataKey="cu"
fill={`url(#${gradientId})`}
stroke="var(--primary)"
strokeWidth={1.75}
type="monotone"
/>
</AreaChart>
</ChartContainer>
</div>
);
};