ComponentAdded in v15.1

Bullet Chart

One measure against its target, over qualitative bands: Stephen Few's bullet graph, a compact replacement for a gauge.

Installation

Import the class, and @use the entry point in your global styles: without it the component renders unstyled.

import { Component } from '@angular/core';
import { WrBulletChart } from 'ngwr/charts/bullet-chart';

@Component({ imports: [WrBulletChart] })
export class MyComponent {}

Basic

The value is the bar, the target the marker across it. ranges are the upper limits of the bands, drawn as greys that fade from zero outwards.

<wr-bullet-chart
  label="Revenue"
  subLabel="USD, thousands"
  [value]="270"
  [target]="250"
  [ranges]="[150, 225, 300]"
/>

A column of measures

Give the label column one width with --wr-bullet-chart-label-width, and stacked bullets line up.

@for (kpi of kpis; track kpi.label) {
  <wr-bullet-chart
    style="--wr-bullet-chart-label-width: 7rem"
    [label]="kpi.label"
    [subLabel]="kpi.subLabel"
    [value]="kpi.value"
    [target]="kpi.target"
    [ranges]="kpi.ranges"
  />
}

Scale

With no max the scale ends at the largest of the value, the target and the ranges. A fixed max draws anything past it to the end. A negative value starts the scale below zero, and a narrow chart prints fewer ticks.

<!-- Ends at 300, the largest of the three -->
<wr-bullet-chart label="Default" [value]="270" [target]="250" [ranges]="[150, 225, 300]" />

<!-- A fixed end: 270 is drawn to it -->
<wr-bullet-chart label="max 250" [value]="270" [target]="200" [ranges]="[150, 225, 300]" [max]="250" />

<!-- Below zero: the bar runs from -20 up to zero -->
<wr-bullet-chart label="Margin" subLabel="%" [value]="-20" [target]="10" [ranges]="[0, 20, 40]" />

Number format

Numbers follow LOCALE_ID. valueFormat replaces it for the ticks, the tooltip and the accessible name.

<wr-bullet-chart label="Revenue" [value]="270" [target]="250" [ranges]="[150, 225, 300]" [valueFormat]="format" />
const usd = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', notation: 'compact' });

// The figures are thousands: 270 reads "$270K".
format = (value: number): string => usd.format(value * 1000);

Missing values

A value that is not a number draws no bar, and the accessible name says there is none rather than reading zero. Without a target there is no marker.

<!-- No value yet: no bar, and the name says so -->
<wr-bullet-chart label="Revenue" [value]="null" [target]="250" [ranges]="[150, 225, 300]" />

<!-- No target: no marker -->
<wr-bullet-chart label="Revenue" [value]="270" [ranges]="[150, 225, 300]" />

Accessibility

PartBehaviour
Drawing One role="img", named by a sentence: "Revenue: 270 of target 250". ariaLabel replaces it.
Labels Hidden from screen readers, since the name already carries the label. The sub-label is the drawing's description.
BandsNot announced: they have no names. Say what they mean in ariaLabel where it matters.
ColourThe bands differ in lightness, and the bar and the marker in shape, so none of it relies on hue.
KeyboardNo tab stop. The tooltip is pointer only and says what the name says.

API

NameDescriptionTypeDefault
valuerequiredThe measure. A value that is not a finite number — null while it loads — draws no bar, and the chart is announced as having no value rather than as zero.number | null—
targetThe comparative measure, drawn as a marker across the line. null, or anything that is not a finite number, draws none and leaves it out of the accessible name.number | nullnull
rangesThe upper limit of each qualitative band, in any order: [150, 225, 300] draws a band from the scale's start to 150, one from 150 to 225 and one from 225 to 300. They are sorted, a repeated limit counts once, and one that is not a finite number is dropped. The band nearest zero is the strongest grey and each one after it fainter, between --wr-bullet-chart-range-start and --wr-bullet-chart-range-end. Few recommends no more than five. The first band is everything below the first limit, so it has room only where the scale reaches below that limit. A negative first limit lower than both the value and the target is where the scale starts, which leaves that band no room: it is not drawn, and the drawing opens with the second band and its paler grey. Where the scale runs on past the last limit, a rule marks where the last band ends.readonly number[][]
maxThe end of the scale. 0 takes the largest of value, target and ranges, so the last band reaches the end; a value above 0 fixes it, and anything past it is drawn to the end — the accessible name and the tooltip still give its own figure.number0
labelWhat is measured, shown at inline start beside the drawing, ellipsised past --wr-bullet-chart-label-max-width. It heads the tooltip and opens the accessible name; without it the name opens with bulletChart.label ("Bullet chart").string | nullnull
subLabelA line under the label — the unit, the period. The drawing is described by it (aria-describedby), so a screen reader hears it after the name.string | nullnull
ariaLabelAccessible name of the drawing, in place of the sentence the chart writes itself — "Revenue: 270 of target 250". Give one where the bands mean something a reader needs, since they have no names of their own.string | nullnull
valueFormatWrites every number the chart shows: the scale's ticks, the tooltip and the accessible name. null uses Intl.NumberFormat in LOCALE_ID, with compact ticks (1.2K) unless the full numbers are shorter.((value: number) => string) | nullnull
showScaleShow the scale's ticks under the drawing.booleantrue
tooltipShow a tooltip with the label, the value and the target while the pointer is over the drawing. Pointer only: the accessible name says the same.booleantrue

CSS variables

Set these on the component's selector or a wrapper, not on :root: the defaults are declared on ngwr/charts/bullet-chart's own element.

VariableDefaultDeclared on
--wr-bullet-chart-bar-thicknesscalc(100% / 3).wr-bullet-chart
--wr-bullet-chart-colorvar(--wr-color-on-surface).wr-bullet-chart
--wr-bullet-chart-font-familyvar(--wr-font-family-base).wr-bullet-chart
--wr-bullet-chart-label-colorvar(--wr-color-on-surface).wr-bullet-chart
--wr-bullet-chart-label-font-sizevar(--wr-text-sm).wr-bullet-chart
--wr-bullet-chart-label-font-weightvar(--wr-font-weight-semibold).wr-bullet-chart
--wr-bullet-chart-label-gapvar(--wr-space-md).wr-bullet-chart
--wr-bullet-chart-label-max-width12rem.wr-bullet-chart
--wr-bullet-chart-label-widthfit-content(var(--wr-bullet-chart-label-max-width)).wr-bullet-chart
--wr-bullet-chart-range-edge-colorvar(--wr-color-on-surface-muted).wr-bullet-chart +1 variant override
--wr-bullet-chart-range-endvar(--wr-color-fill-strong).wr-bullet-chart
--wr-bullet-chart-range-startcolor-mix(in srgb, var(--wr-color-on-surface-muted) 50%, var(--wr-color-fill-strong)).wr-bullet-chart
--wr-bullet-chart-scale-gapvar(--wr-space-xs).wr-bullet-chart
--wr-bullet-chart-sub-label-colorvar(--wr-color-on-surface-muted).wr-bullet-chart
--wr-bullet-chart-sub-label-font-sizevar(--wr-text-xs).wr-bullet-chart
--wr-bullet-chart-sub-label-line-heightvar(--wr-text-xs-leading).wr-bullet-chart
--wr-bullet-chart-target-colorvar(--wr-color-on-surface).wr-bullet-chart +1 variant override
--wr-bullet-chart-target-length75%.wr-bullet-chart
--wr-bullet-chart-target-ringvar(--wr-color-surface).wr-bullet-chart +1 variant override
--wr-bullet-chart-target-ring-width1px.wr-bullet-chart
--wr-bullet-chart-target-thickness3px.wr-bullet-chart
--wr-bullet-chart-tick-colorvar(--wr-color-on-surface-muted).wr-bullet-chart
--wr-bullet-chart-tick-font-sizevar(--wr-text-xs).wr-bullet-chart
--wr-bullet-chart-tick-lengthvar(--wr-space-xs).wr-bullet-chart
--wr-bullet-chart-tick-line-heightvar(--wr-text-xs-leading).wr-bullet-chart
--wr-bullet-chart-track-colorvar(--wr-color-fill).wr-bullet-chart +1 variant override
--wr-bullet-chart-track-height1.5rem.wr-bullet-chart
--wr-bullet-chart-track-min-width6rem.wr-bullet-chart
--wr-bullet-chart-track-radiusvar(--wr-border-radius-sm).wr-bullet-chart
--wr-bullet-chart-value-colorvar(--wr-color-on-surface).wr-bullet-chart +1 variant override

Dependencies

Built onPopover

See also