A typed Sparkline component
components/sparkline.tsx
import { useId } from "react";
type SparklineProps = {
data: number[];
width?: number;
height?: number;
/** Any CSS color. Leave it out to use the text color, so a text-emerald-400 class works. */
color?: string;
/** Soft gradient under the line */
fill?: boolean;
/** Dot on the last point */
dot?: boolean;
/** Dashed line at the average */
mean?: boolean;
/** Read out by screen readers, e.g. "Revenue, last 30 days, up 12%". Without it the chart is hidden from them. */
label?: string;
className?: string;
};
export function Sparkline({
data,
width = 120,
height = 32,
color,
fill = true,
dot = true,
mean = false,
label,
className,
}: SparklineProps) {
const id = "spark" + useId().replace(/[^a-zA-Z0-9_-]/g, "");
if (data.length < 2) return null;
const pad = 3;
const min = Math.min(...data);
const max = Math.max(...data);
const span = max - min || 1; // a flat series draws a flat line, not NaN
const y = (v: number) => height - pad - ((v - min) / span) * (height - pad * 2);
const pts = data.map((v, i) => [(i / (data.length - 1)) * width, y(v)] as const);
const line = pts.map(([px, py], i) => `${i ? "L" : "M"}${px.toFixed(1)} ${py.toFixed(1)}`).join("");
const [lx, ly] = pts[pts.length - 1];
const avg = data.reduce((s, v) => s + v, 0) / data.length;
return (
<svg
viewBox={`0 0 ${width} ${height}`}
width={width}
height={height}
className={className}
style={{ color, overflow: "visible" }}
role={label ? "img" : undefined}
aria-label={label}
aria-hidden={label ? undefined : true}
>
{fill && (
<>
<defs>
<linearGradient id={id} x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="currentColor" stopOpacity={0.3} />
<stop offset="1" stopColor="currentColor" stopOpacity={0} />
</linearGradient>
</defs>
<path d={`${line}L${width} ${height}L0 ${height}Z`} fill={`url(#${id})`} />
</>
)}
{mean && (
<line x1={0} x2={width} y1={y(avg)} y2={y(avg)} stroke="currentColor" strokeOpacity={0.4} strokeDasharray="2 3" vectorEffect="non-scaling-stroke" />
)}
<path
d={line}
fill="none"
stroke="currentColor"
strokeWidth={1.5}
strokeLinejoin="round"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
/>
{dot && <circle cx={lx} cy={ly} r={2.5} fill="currentColor" />}
</svg>
);
}