Charts
Sparkline
A word-sized line chart that shows the shape of a series next to a number or inside a sentence.
- Built on
<svg>vector-effectcolor-mix()- Client JavaScript
- None
- Works without React
- Yes, as HTML and CSS
- Source, gzipped
- 2.2 kB
- Dependencies
- React
Try its options
Loading its options.
Code
import { Sparkline } from "@/components/equip/sparkline";
const requests = [3120, 3380, 2950, 3410, 3870, 2210, 1980, 3560, 3920, 4105, 3990, 4380, 2640, 4712];
export default function Demo() {
return (
<div style={{ display: "grid", gap: 4 }}>
<span style={{ fontSize: "var(--eq-text-s)", color: "var(--eq-ink-2)" }}>Requests, last 14 days</span>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<span style={{ fontSize: "var(--eq-text-l)", fontWeight: "var(--eq-weight-strong)", fontVariantNumeric: "tabular-nums" }}>
4,712
</span>
<Sparkline values={requests} label="Requests, last 14 days" width={112} height={28} area showLast />
</div>
</div>
);
}Your versionchange its options in Look, This part
HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:4px">
<span style="font-size:var(--eq-text-s);color:var(--eq-ink-2)">Requests, last 14 days</span>
<div style="display:flex;align-items:center;gap:12px">
<span style="font-size:var(--eq-text-l);font-weight:var(--eq-weight-strong);font-variant-numeric:tabular-nums">4,712</span>
<span class="eq-sparkline"><svg class="eq-sparkline-svg" role="img" aria-label="Requests, last 14 days: lowest 1,980, highest 4,712, latest 4,712" width="112" height="28" viewBox="0 0 112 28"><path class="eq-sparkline-area" d="M2.5 15.9 L10.73 13.71 L18.96 17.33 L27.19 13.46 L35.42 9.59 L43.65 23.56 L51.88 25.5 L60.12 12.2 L68.35 9.17 L76.58 7.61 L84.81 8.58 L93.04 5.3 L101.27 19.94 L109.5 2.5 L109.5 28 L2.5 28 Z" /><path class="eq-sparkline-line" d="M2.5 15.9 L10.73 13.71 L18.96 17.33 L27.19 13.46 L35.42 9.59 L43.65 23.56 L51.88 25.5 L60.12 12.2 L68.35 9.17 L76.58 7.61 L84.81 8.58 L93.04 5.3 L101.27 19.94 L109.5 2.5" /><circle class="eq-sparkline-dot" cx="109.5" cy="2.5" r="2" /></svg><span class="eq-sparkline-data">Values, oldest first: 3,120, 3,380, 2,950, 3,410, 3,870, 2,210, 1,980, 3,560, 3,920, 4,105, 3,990, 4,380, 2,640, 4,712</span></span>
</div>
</div>Examples
Inside a sentence
The root is a span, so it sits in running text; the line takes the colour of the text around it.
p95 latency on eu-west-1 Values, oldest first: 182, 176, 190, 210, 244, 231, 198, 187, 179, 171 is back to 171 ms after the morning spike.
Code
import { Sparkline } from "@/components/equip/sparkline";
export default function Example() {
return (
<p style={{ margin: 0, fontSize: "var(--eq-text-m)", lineHeight: 1.6 }}>
p95 latency on eu-west-1{" "}
<Sparkline values={[182, 176, 190, 210, 244, 231, 198, 187, 179, 171]} label="p95 latency in milliseconds, last 10 hours" />{" "}
is back to 171 ms after the morning spike.
</p>
);
}HTMLworks in any framework, with the part's CSS
<p style="margin:0;font-size:var(--eq-text-m);line-height:1.6">p95 latency on eu-west-1 <span class="eq-sparkline"><svg class="eq-sparkline-svg" role="img" aria-label="p95 latency in milliseconds, last 10 hours: lowest 171, highest 244, latest 171" width="96" height="24" viewBox="0 0 96 24"><path class="eq-sparkline-line" d="M2.5 18.64 L12.61 20.2 L22.72 16.55 L32.83 11.35 L42.94 2.5 L53.06 5.88 L63.17 14.47 L73.28 17.34 L83.39 19.42 L93.5 21.5" /></svg><span class="eq-sparkline-data">Values, oldest first: 182, 176, 190, 210, 244, 231, 198, 187, 179, 171</span></span> is back to 171 ms after the morning spike.</p>Fixed scale and tone
Pass min and max so rows compare on one scale, and set color on the part to give it a signal tone.
Code
import { Sparkline } from "@/components/equip/sparkline";
const rows = [
{ name: "api-gateway", values: [41, 44, 39, 47, 52, 49, 55, 58], tone: "var(--eq-ink)" },
{ name: "billing-worker", values: [62, 68, 74, 79, 83, 88, 91, 94], tone: "var(--eq-danger)" },
{ name: "search-indexer", values: [22, 20, 25, 23, 21, 24, 22, 23], tone: "var(--eq-ink)" },
];
export default function Example() {
return (
<div style={{ display: "grid", gridTemplateColumns: "auto auto", gap: "8px 16px", alignItems: "center", fontSize: "var(--eq-text-s)" }}>
{rows.map((row) => (
<div key={row.name} style={{ display: "contents" }}>
<span>{row.name}</span>
<Sparkline values={row.values} label={`${row.name} CPU percent, last 8 hours`} min={0} max={100} showLast style={{ color: row.tone }} />
</div>
))}
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;grid-template-columns:auto auto;gap:8px 16px;align-items:center;font-size:var(--eq-text-s)">
<div style="display:contents"><span>api-gateway</span><span style="color:var(--eq-ink)" class="eq-sparkline"><svg class="eq-sparkline-svg" role="img" aria-label="api-gateway CPU percent, last 8 hours: lowest 39, highest 58, latest 58" width="96" height="24" viewBox="0 0 96 24"><path class="eq-sparkline-line" d="M2.5 13.71 L15.5 13.14 L28.5 14.09 L41.5 12.57 L54.5 11.62 L67.5 12.19 L80.5 11.05 L93.5 10.48" /><circle class="eq-sparkline-dot" cx="93.5" cy="10.48" r="2" /></svg><span class="eq-sparkline-data">Values, oldest first: 41, 44, 39, 47, 52, 49, 55, 58</span></span></div>
<div style="display:contents"><span>billing-worker</span><span style="color:var(--eq-danger)" class="eq-sparkline"><svg class="eq-sparkline-svg" role="img" aria-label="billing-worker CPU percent, last 8 hours: lowest 62, highest 94, latest 94" width="96" height="24" viewBox="0 0 96 24"><path class="eq-sparkline-line" d="M2.5 9.72 L15.5 8.58 L28.5 7.44 L41.5 6.49 L54.5 5.73 L67.5 4.78 L80.5 4.21 L93.5 3.64" /><circle class="eq-sparkline-dot" cx="93.5" cy="3.64" r="2" /></svg><span class="eq-sparkline-data">Values, oldest first: 62, 68, 74, 79, 83, 88, 91, 94</span></span></div>
<div style="display:contents"><span>search-indexer</span><span style="color:var(--eq-ink)" class="eq-sparkline"><svg class="eq-sparkline-svg" role="img" aria-label="search-indexer CPU percent, last 8 hours: lowest 20, highest 25, latest 23" width="96" height="24" viewBox="0 0 96 24"><path class="eq-sparkline-line" d="M2.5 17.32 L15.5 17.7 L28.5 16.75 L41.5 17.13 L54.5 17.51 L67.5 16.94 L80.5 17.32 L93.5 17.13" /><circle class="eq-sparkline-dot" cx="93.5" cy="17.13" r="2" /></svg><span class="eq-sparkline-data">Values, oldest first: 22, 20, 25, 23, 21, 24, 22, 23</span></span></div>
</div>Empty, single and flat
No data draws a dashed rule, one value draws a dot, and a flat series sits on the middle line.
Code
import { Sparkline } from "@/components/equip/sparkline";
export default function Example() {
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: 24, alignItems: "center", fontSize: "var(--eq-text-s)", color: "var(--eq-ink-2)" }}>
<span style={{ display: "grid", gap: 4 }}>
New region
<Sparkline values={[]} label="Requests in ap-south-2" />
</span>
<span style={{ display: "grid", gap: 4 }}>
First day
<Sparkline values={[1240]} label="Sign-ups" />
</span>
<span style={{ display: "grid", gap: 4 }}>
Steady
<Sparkline values={[12, 12, 12, 12, 12, 12]} label="Replicas" area />
</span>
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:flex;flex-wrap:wrap;gap:24px;align-items:center;font-size:var(--eq-text-s);color:var(--eq-ink-2)">
<span style="display:grid;gap:4px">New region<span class="eq-sparkline"><svg class="eq-sparkline-svg" role="img" aria-label="Requests in ap-south-2: no data" width="96" height="24" viewBox="0 0 96 24"><line class="eq-sparkline-empty" x1="2.5" x2="93.5" y1="12" y2="12" /></svg></span></span>
<span style="display:grid;gap:4px">First day<span class="eq-sparkline"><svg class="eq-sparkline-svg" role="img" aria-label="Sign-ups: 1,240" width="96" height="24" viewBox="0 0 96 24"><circle class="eq-sparkline-dot" cx="48" cy="12" r="2" /></svg></span></span>
<span style="display:grid;gap:4px">Steady<span class="eq-sparkline"><svg class="eq-sparkline-svg" role="img" aria-label="Replicas: lowest 12, highest 12, latest 12" width="96" height="24" viewBox="0 0 96 24"><path class="eq-sparkline-area" d="M2.5 12 L20.7 12 L38.9 12 L57.1 12 L75.3 12 L93.5 12 L93.5 24 L2.5 24 Z" /><path class="eq-sparkline-line" d="M2.5 12 L20.7 12 L38.9 12 L57.1 12 L75.3 12 L93.5 12" /></svg><span class="eq-sparkline-data">Values, oldest first: 12, 12, 12, 12, 12, 12</span></span></span>
</div>Fluid width
fluid fills the container and stretches the line to it at a fixed height; the stroke and the dot keep their size.
Code
import { Sparkline } from "@/components/equip/sparkline";
export default function Example() {
return (
<div style={{ display: "grid", gap: 8, width: "100%", maxWidth: 360, fontSize: "var(--eq-text-s)" }}>
<span style={{ color: "var(--eq-ink-2)" }}>Checkout conversion, last 30 days</span>
<Sparkline
fluid
height={48}
area
showLast
min={0}
label="Checkout conversion percent, last 30 days"
values={[3.1, 3.4, 3.2, 3.6, 3.5, 3.9, 4.1, 3.8, 4.0, 4.3, 4.2, 4.6, 4.4, 4.8, 4.7, 5.0, 4.9, 5.2, 5.1, 5.4, 5.3, 5.1, 5.5, 5.6, 5.4, 5.8, 5.7, 5.9, 6.1, 6.0]}
/>
</div>
);
}HTMLworks in any framework, with the part's CSS
<div style="display:grid;gap:8px;width:100%;max-width:360px;font-size:var(--eq-text-s)">
<span style="color:var(--eq-ink-2)">Checkout conversion, last 30 days</span>
<span class="eq-sparkline" data-fluid="true"><svg class="eq-sparkline-svg" role="img" aria-label="Checkout conversion percent, last 30 days: lowest 3.1, highest 6.1, latest 6" width="100%" height="48" viewBox="0 0 96 48" preserveAspectRatio="none"><path class="eq-sparkline-area" d="M0 23.65 L3.31 21.53 L6.62 22.94 L9.93 20.12 L13.24 20.83 L16.55 18.01 L19.86 16.6 L23.17 18.71 L26.48 17.3 L29.79 15.19 L33.1 15.89 L36.41 13.07 L39.72 14.48 L43.03 11.66 L46.34 12.37 L49.66 10.25 L52.97 10.96 L56.28 8.84 L59.59 9.55 L62.9 7.43 L66.21 8.14 L69.52 9.55 L72.83 6.73 L76.14 6.02 L79.45 7.43 L82.76 4.61 L86.07 5.32 L89.38 3.91 L92.69 2.5 L96 3.2 L96 48 L0 48 Z" /><path class="eq-sparkline-line" d="M0 23.65 L3.31 21.53 L6.62 22.94 L9.93 20.12 L13.24 20.83 L16.55 18.01 L19.86 16.6 L23.17 18.71 L26.48 17.3 L29.79 15.19 L33.1 15.89 L36.41 13.07 L39.72 14.48 L43.03 11.66 L46.34 12.37 L49.66 10.25 L52.97 10.96 L56.28 8.84 L59.59 9.55 L62.9 7.43 L66.21 8.14 L69.52 9.55 L72.83 6.73 L76.14 6.02 L79.45 7.43 L82.76 4.61 L86.07 5.32 L89.38 3.91 L92.69 2.5 L96 3.2" /><path class="eq-sparkline-dot" d="M96 3.2h0.01" /></svg><span class="eq-sparkline-data">Values, oldest first: 3.1, 3.4, 3.2, 3.6, 3.5, 3.9, 4.1, 3.8, 4, 4.3, 4.2, 4.6, 4.4, 4.8, 4.7, 5, 4.9, 5.2, 5.1, 5.4, 5.3, 5.1, 5.5, 5.6, 5.4, 5.8, 5.7, 5.9, 6.1, 6</span></span>
</div>Props
| Prop | Type | Default | What it does |
|---|---|---|---|
values | number[] | none | The series, oldest first. Non-finite values are skipped. |
label | string | none | What the line shows. Lowest, highest and latest values are added to it for the accessible name. |
width | number | 96 | Width in pixels. With fluid, the width the line is drawn at before it stretches. |
height | number | 24 | Height in pixels. |
fluid | boolean | false | Fills the width of its container and stretches the line across it; the height, the stroke and the dot keep their size. |
area | boolean | false | A soft fill under the line. |
showLast | boolean | false | A dot on the latest value. |
min | number | none | Fixes the bottom of the scale, so several sparklines compare. |
max | number | none | Fixes the top of the scale. |
locale | string | "en-US" | Formats the numbers in the accessible text. |
...rest | ComponentProps<"span"> | none | Every native span attribute. Set color through style or a class to tone the line. |
Anatomy
| root | .eq-sparkline | An inline span, so it can sit inside a paragraph; a full-width block with data-fluid. |
|---|---|---|
| svg | .eq-sparkline-svg | role="img" with the computed label. |
| line | .eq-sparkline-line | |
| area | .eq-sparkline-area | |
| last point | .eq-sparkline-dot | Grows with the spring when the line is hovered. A round-capped zero-length path when fluid, so stretching cannot squash it. |
| empty rule | .eq-sparkline-empty | A dashed rule drawn when there is no data. |
| data | .eq-sparkline-data | Every value as visually hidden text. |
Notes
- The svg is named with the label plus lowest, highest and latest values, and every value is also in visually hidden text.
- The line uses currentColor, so it follows the text colour around it; give it a signal tone only when the tone carries meaning.
- A flat series sits on the middle line, one value draws a single dot and an empty series draws a dashed rule.
- Renders on the server with no client JavaScript and the same output every time.
Source
Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.
components/equip/sparkline.tsx107 lines
import type { ComponentProps } from "react";
import "./sparkline.css";
export type SparklineProps = ComponentProps<"span"> & {
/** The series, oldest first. */
values: number[];
/** What the line shows, such as "Requests, last 14 days". Lowest, highest and latest are added to it. */
label: string;
/** Drawing width in pixels. With fluid, the width the line is drawn at before it stretches. */
width?: number;
height?: number;
/** Fills the width of its container and stretches the line to fit; height stays fixed. */
fluid?: boolean;
/** A soft fill under the line. */
area?: boolean;
/** A dot on the latest value. */
showLast?: boolean;
/** Fixes the bottom of the scale instead of using the lowest value. */
min?: number;
/** Fixes the top of the scale instead of using the highest value. */
max?: number;
locale?: string;
};
/* Half the dot, so neither the line nor the dot is clipped at the edges. */
const INSET = 2.5;
export function Sparkline({
values,
label,
width = 96,
height = 24,
fluid = false,
area = false,
showLast = false,
min,
max,
locale = "en-US",
className,
...rest
}: SparklineProps) {
const series = values.filter(Number.isFinite);
const number = new Intl.NumberFormat(locale);
const lo = min ?? Math.min(...series);
const hi = max ?? Math.max(...series);
const span = hi - lo;
// Fluid, the line runs edge to edge of the viewBox and the root's padding keeps the inset in real pixels.
const inset = fluid ? 0 : INSET;
const x = (i: number) =>
series.length === 1 ? width / 2 : inset + (i / (series.length - 1)) * (width - inset * 2);
const y = (v: number) => {
if (span <= 0) return height / 2;
const t = Math.min(1, Math.max(0, (v - lo) / span));
return INSET + (1 - t) * (height - INSET * 2);
};
const points = series.map((v, i) => [round(x(i)), round(y(v))] as const);
const line = points.map(([px, py], i) => `${i === 0 ? "M" : "L"}${px} ${py}`).join(" ");
const first = points[0];
const last = points[points.length - 1];
const summary =
series.length === 0
? `${label}: no data`
: series.length === 1
? `${label}: ${number.format(series[0] ?? 0)}`
: `${label}: lowest ${number.format(Math.min(...series))}, highest ${number.format(Math.max(...series))}, latest ${number.format(series[series.length - 1] ?? 0)}`;
return (
<span {...rest} className={["eq-sparkline", className].filter(Boolean).join(" ")} data-fluid={fluid || undefined}>
<svg
className="eq-sparkline-svg"
role="img"
aria-label={summary}
width={fluid ? "100%" : width}
height={height}
viewBox={`0 0 ${width} ${height}`}
{...(fluid ? { preserveAspectRatio: "none" } : {})}
>
{series.length === 0 && (
<line className="eq-sparkline-empty" x1={inset} x2={width - inset} y1={height / 2} y2={height / 2} />
)}
{area && first && last && series.length > 1 && (
<path className="eq-sparkline-area" d={`${line} L${last[0]} ${height} L${first[0]} ${height} Z`} />
)}
{series.length > 1 && <path className="eq-sparkline-line" d={line} />}
{last &&
(showLast || series.length === 1) &&
(fluid ? (
// Stretched, a circle would turn into an ellipse; a round cap on a zero-length line stays round.
<path className="eq-sparkline-dot" d={`M${last[0]} ${last[1]}h0.01`} />
) : (
<circle className="eq-sparkline-dot" cx={last[0]} cy={last[1]} r={2} />
))}
</svg>
{series.length > 1 && (
<span className="eq-sparkline-data">
{`Values, oldest first: ${series.map((v) => number.format(v)).join(", ")}`}
</span>
)}
</span>
);
}
function round(n: number) {
return Math.round(n * 100) / 100;
}components/equip/sparkline.css88 lines
@layer equip {
.eq-sparkline {
position: relative;
display: inline-flex;
vertical-align: middle;
line-height: 0;
}
.eq-sparkline-svg {
display: block;
overflow: visible;
}
/* Fluid: as wide as its container, the line stretched across it at a fixed height. */
.eq-sparkline[data-fluid] {
box-sizing: border-box;
display: flex;
width: 100%;
min-width: 0;
/* Half the dot, as the fixed-width line insets inside its drawing. */
padding-inline: 2.5px;
}
.eq-sparkline[data-fluid] .eq-sparkline-svg {
flex: 1;
min-width: 0;
}
.eq-sparkline-line,
.eq-sparkline-empty {
fill: none;
stroke: currentColor;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
vector-effect: non-scaling-stroke;
}
.eq-sparkline-empty {
stroke: var(--eq-line-strong);
stroke-dasharray: 2 3;
}
.eq-sparkline-area {
fill: color-mix(in srgb, currentColor 14%, transparent);
stroke: none;
}
.eq-sparkline-dot {
fill: currentColor;
transform-box: fill-box;
transform-origin: center;
transition: scale var(--eq-dur-move) var(--eq-spring);
}
/* The fluid dot is a round cap on a zero-length line, drawn in screen pixels so it stays round. */
.eq-sparkline[data-fluid] .eq-sparkline-dot {
fill: none;
stroke: currentColor;
stroke-width: 4;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
transition: stroke-width var(--eq-dur-move) var(--eq-spring);
}
@media (hover: hover) {
.eq-sparkline:hover .eq-sparkline-dot {
scale: 1.6;
}
.eq-sparkline[data-fluid]:hover .eq-sparkline-dot {
scale: none;
stroke-width: 6.4;
}
}
.eq-sparkline-data {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
border: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
}