Sparkline stat
A stat with a plain SVG sparkline, two date labels and one brand mark on the last point.
npx shadcn@latest add https://ui.goodtabs.co/r/sparkline-stat.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/sparkline-stat
Weekly installs
4,218
A rising series with its first and last date.
Falling and flat
Median load
1.1s
Open issues
37
Each line scales to its own range, so shape is what you read.
Usage
import { SparklineStat } from "@/components/ui/sparkline-stat"
<SparklineStat label="Weekly installs" value="4,218" data={[310, 340, 420, 560]} from="Aug 18" to="Nov 3" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | What the number measures. | |
| value | ReactNode | The number, already formatted. | |
| data | number[] | The series, oldest first. At least two points. | |
| from | string | Label under the left end of the line. | |
| to | string | Label under the right end of the line. | |
| unit | string | "" | Unit word used in the text alternative. |
Source
sparkline-stat/sparkline-stat.tsx
import * as React from "react";
import { cn } from "@/lib/utils";
type SparklineStatProps = Omit<React.ComponentProps<"div">, "children"> & {
/** What the number measures. */
label: string;
/** The number, already formatted. */
value: React.ReactNode;
/** The series, oldest first. Needs at least two points. */
data: number[];
/** Label for the first point, for example "Oct 6". */
from: string;
/** Label for the last point, for example "Nov 5". */
to: string;
/** Unit word used in the text alternative, for example "visits". */
unit?: string;
};
const W = 100;
const H = 40;
const PAD = 3;
/**
* A stat with a plain SVG sparkline. The series is scaled to its own min and max, so the line shows shape, not size.
* The last point is the one brand mark.
*/
export function SparklineStat({ label, value, data, from, to, unit = "", className, ...props }: SparklineStatProps) {
const min = Math.min(...data);
const max = Math.max(...data);
const span = max - min || 1;
const last = data.length - 1;
const x = (i: number) => (i / last) * W;
const y = (v: number) => H - PAD - ((v - min) / span) * (H - PAD * 2);
const points = data.map((v, i) => `${x(i).toFixed(2)},${y(v).toFixed(2)}`);
const line = `M${points.join("L")}`;
const area = `${line}L${W},${H}L0,${H}Z`;
const text = `${label}: from ${data[0]} to ${data[last]}${unit ? ` ${unit}` : ""} between ${from} and ${to}, low ${min}, high ${max}.`;
return (
<div
data-slot="sparkline-stat"
className={cn("flex w-full flex-col gap-3 rounded-lg border bg-background p-4", className)}
{...props}
>
<div className="flex flex-col gap-1">
<p className="m-0 text-sm text-muted-foreground">{label}</p>
<p className="m-0 text-2xl font-medium tracking-tight tabular-nums">{value}</p>
</div>
<div className="relative h-10 w-full">
<svg
role="img"
aria-label={text}
viewBox={`0 0 ${W} ${H}`}
preserveAspectRatio="none"
className="absolute inset-0 size-full overflow-visible text-foreground"
>
<line
x1="0"
x2={W}
y1={H - 0.5}
y2={H - 0.5}
stroke="currentColor"
strokeOpacity="0.14"
vectorEffect="non-scaling-stroke"
/>
<path d={area} fill="currentColor" fillOpacity="0.07" />
<path
d={line}
fill="none"
stroke="currentColor"
strokeOpacity="0.8"
strokeWidth="1.5"
strokeLinejoin="round"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
/>
</svg>
<span
aria-hidden="true"
className="absolute size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-brand ring-2 ring-background"
style={{ left: "100%", top: `${(y(data[last]) / H) * 100}%` }}
/>
</div>
<div aria-hidden="true" className="flex justify-between font-mono text-xs text-muted-foreground">
<span>{from}</span>
<span>{to}</span>
</div>
</div>
);
}
For AI agents
Hand your agent this page, the markdown at /components/sparkline-stat/prompt.md, or the Copy AI prompt button above.
When to use
- A headline number where the shape of the last few weeks matters
- Dashboards that need a trend without axes and gridlines
When not to use
- Reading exact values: use a full chart with axes
- Comparing several series on one scale
Agent notes
- Server component. The SVG has role img and an aria-label with the first, last, low and high values
- Each series is scaled to its own min and max, so two sparklines are not comparable by height
- Stretches to the width of its container; the line uses non-scaling strokes so it stays 1.5px
- The brand dot on the last point is the only coloured mark
The maths
- A point's x is i / (n - 1) × 100 and its y is 40 - 3 - (v - min) / (max - min) × 34, so the series fills the box with 3 units of padding.
- The area is the line closed along the baseline at 7 percent foreground, so the 1.5px stroke stays the loudest mark.