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
Requests, last 14 days
4,712Values, 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
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.

api-gatewayValues, oldest first: 41, 44, 39, 47, 52, 49, 55, 58
billing-workerValues, oldest first: 62, 68, 74, 79, 83, 88, 91, 94
search-indexerValues, oldest first: 22, 20, 25, 23, 21, 24, 22, 23
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.

New regionFirst daySteadyValues, oldest first: 12, 12, 12, 12, 12, 12
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.

Checkout conversion, last 30 daysValues, 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
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

PropTypeDefaultWhat it does
valuesnumber[]noneThe series, oldest first. Non-finite values are skipped.
labelstringnoneWhat the line shows. Lowest, highest and latest values are added to it for the accessible name.
widthnumber96Width in pixels. With fluid, the width the line is drawn at before it stretches.
heightnumber24Height in pixels.
fluidbooleanfalseFills the width of its container and stretches the line across it; the height, the stroke and the dot keep their size.
areabooleanfalseA soft fill under the line.
showLastbooleanfalseA dot on the latest value.
minnumbernoneFixes the bottom of the scale, so several sparklines compare.
maxnumbernoneFixes the top of the scale.
localestring"en-US"Formats the numbers in the accessible text.
...restComponentProps<"span">noneEvery native span attribute. Set color through style or a class to tone the line.

Anatomy

root.eq-sparklineAn inline span, so it can sit inside a paragraph; a full-width block with data-fluid.
svg.eq-sparkline-svgrole="img" with the computed label.
line.eq-sparkline-line
area.eq-sparkline-area
last point.eq-sparkline-dotGrows 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-emptyA dashed rule drawn when there is no data.
data.eq-sparkline-dataEvery 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;
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index