Skip to content
OpenPerf
Esc
↑↓navigate↵open⌘Jpreview
On this page

Meter

Small bars inside rows and panels. No axes; the number beside each bar carries its value.

import { Meter } from "@openperf/deft";

import { Preview } from "../../src/Preview";

export default function MeterBasic() {
  return (
    <Preview>
      <Meter.Root value={0.48} />
    </Preview>
  );
}
  • Use a meter for one value per row, inside a table, list or panel. For a value over time, use a Sparkline or a Chart.
  • Meter.Root prints value, a fraction from 0 to 1, as a percentage and fills the bar to value / max; max defaults to 1.
  • For row-relative bars, give every row the same max, usually the largest row’s value, so the largest row fills its bar and the others stay comparable. Set showValue={false} when the percent would read as the bar’s length.
  • Use the same scale for bars that people compare.
  • Print percentages and values as text; a bar is never the only place a number lives.

Range

import { Meter } from "@openperf/deft";

import { Preview } from "../../src/Preview";

export default function MeterRange() {
  return (
    <Preview>
      <Meter.Range domain={[10, 1000]} p50={20} p95={120} p99={480} />
      <Meter.Range domain={[10, 1000]} failing={true} p50={120} p95={480} p99={1000} />
    </Preview>
  );
}

Meter.Range draws p50 to p95 as the main segment, p95 to p99 as the tail and a tick at p95. The scale is logarithmic, so domain starts above zero; values outside it are clamped to the edges. Share domain across rows. Set failing only when a row exceeds its limit; red is reserved for failing rows.

Distribution

import { Meter } from "@openperf/deft";

import { Preview } from "../../src/Preview";

export default function MeterDistribution() {
  return (
    <Preview stack={true}>
      <Meter.Distribution fair={20} good={70} poor={10} />
    </Preview>
  );
}

Use Meter.Distribution for the good, fair and poor parts of a web vital. Each segment’s width is its share of the three values, so percentages and fractions both work; a part of 0 is left out. Keep the percentages as text next to the bar; colour and segment width cannot carry the values alone. Use size="sm", a thinner bar, inside table rows.

Part to whole

import { Meter } from "@openperf/deft";

import { Preview } from "../../src/Preview";

export default function MeterParts() {
  return (
    <Preview stack={true}>
      <Meter.Parts
        parts={Array.from({ length: 4 }, (_, index) => ({
          id: `part-${index + 1}`,
          label: `Part ${index + 1}`,
          value: 48 / 2 ** index,
        }))}
      />
    </Preview>
  );
}

Meter.Parts draws the parts in the order given, up to seven segments: with more than seven, the first six stay and the rest are grouped as Other. Pass them largest first. Segment n takes colour n of partsRampClassNames, so a table next to the bar can show the same swatch per row. The legend prints each value as a number; pass formatValue to show a percentage instead, and set legend={false} to draw the bar alone. Hover a segment or a legend item to highlight its counterpart.

Compare

import { Meter } from "@openperf/deft";

import { Preview } from "../../src/Preview";

export default function MeterCompare() {
  return (
    <Preview stack={true}>
      <Meter.Compare
        current={{ label: "Current", text: "120", value: 120 }}
        previous={{ label: "Previous", text: "48", value: 48 }}
      />
    </Preview>
  );
}

Meter.Compare puts the current period above the previous one, each as a row of its label, a bar and its value as text. Both bars share one scale, the larger of the two values. The value column keeps a minimum width, so the bars of meters stacked in a column start and end together. The current bar takes tone, story, warning or danger, to match the metric’s health; the previous one stays in the grey of a data bar.

Scale

import { Meter } from "@openperf/deft";

import { Preview } from "../../src/Preview";

export default function MeterScale() {
  return (
    <Preview stack={true}>
      <Meter.Scale good={48} poor={120} value={72} />
    </Preview>
  );
}

Meter.Scale shows where a value sits against two limits, for a metric where lower is better, such as a web vital. The track has three equal segments: up to good, from good to poor, and past poor. The marker moves through each segment in proportion to value and stops at the end of the track, at one and a half times poor, so scales with different limits line up. Leave value out when there is no data. Print the value as text next to the scale.

API

PropType
valuenumber

On Root, a fraction from 0 to 1. On Scale, optional.

Typenumber
max?number

On Root.

Typenumber
Default1
showValue?boolean

On Root.

Typeboolean
Defaulttrue
p50number

On Range. Also `p95` and `p99`.

Typenumber
domain[number, number]

On Range. Both above 0.

Type[number, number]
failing?boolean

On Range.

Typeboolean
Defaultfalse
goodnumber

On Distribution. Also `fair` and `poor`.

Typenumber
size?"sm" | "md"

On Distribution.

Type"sm" | "md"
Default"md"
parts{ id: string; label: ReactNode; value: number }[]

On Parts.

Type{ id: string; label: ReactNode; value: number }[]
formatValue?(value: number, total: number) => ReactNode

On Parts.

Type(value: number, total: number) => ReactNode
legend?boolean

On Parts.

Typeboolean
Defaulttrue
current{ label: ReactNode; text: ReactNode; value: number }

On Compare. Also `previous`.

Type{ label: ReactNode; text: ReactNode; value: number }
tone?"story" | "warning" | "danger"

On Compare.

Type"story" | "warning" | "danger"
Default"story"
poornumber

On Scale. Also `good`, in the units of `value`.

Typenumber

Parts: Root, Compare, Distribution, Parts, Range, Scale. partsRampClassNames holds the segment colours of Parts, in order, for swatches that match them. Other props go to the native element.

Was this page helpful?