Design System

Data visualization & metrics

One charting foundation, one metric treatment and one visualization palette. Charts reuse the V2 typography, spacing, radius, elevation and semantic colour tokens, the existing Card, Select, Date and Menu components, and the shared loading, empty and error states.

What is this?

Metrics communicate one number, trends communicate how it moved, and charts communicate a relationship across many values. Everything is built on the shared ChartContainer frame and the semantic chart series ramp.

When to use it

  • Metric — a single summary value that matters on its own.
  • Chart — a trend, comparison or part-to-whole relationship.
  • Table — exact values, or many columns users compare record by record.

How to use it

  • Wrap dashboard visualizations in DashboardChartCard and render the chart with appearance="plain".
  • Give every chart a title and a summary sentence, and keep the data table alternative on.
  • Compose filters from the existing Select, DateRangePicker, Checkbox and Button components.

What to avoid

  • Per-chart colours, fonts or spacing outside the visualization tokens.
  • Communicating positive / negative movement with colour alone.
  • Decorative animation, or an area chart where the filled volume means nothing.

Metric

One labelled number with optional trend, supporting line and lifecycle states.

Metric

Present a single important value using the shared display typography, so no dashboard invents its own number size.

import { Metric, MetricGroup, MetricCard } from "@/design-system";

When to use

  • A headline number in a page header, summary strip or card.
  • A value whose movement matters — pair it with a trend.

When not to use

  • Several values that should be compared — use a chart or a table.
  • Long text or status — use Badge or the data display components.

Summary strip

Total residents1,248Increase: 12%vs. previous 30 days
Active services86Increase: 4%Across 12 sites
Open requests24Decrease: 8%Oldest open 3 days
Average response4.2hNo change: No changeTarget 6h

Lifecycle states

Total residents1,248Live
Total residentsLoading Total residents
Total residents

Inside a card

Open requests24Decrease: 8%Last 30 days
8% fewer
Open requests trended down over the last six months.
Resident growth1,248Increase: 12%Last 6 months

Accessibility

  • The label is real text, never only an icon.
  • Trend direction is announced as words, not implied by the arrow colour.
  • Loading metrics expose aria-busy and a hidden 'Loading' message.
  • Errors are announced through the shared InlineFeedback component.

Trend indicator

How a value moved — direction, magnitude and period, never colour alone.

TrendIndicator

State the change in a value with a directional arrow, the magnitude and an optional period label.

import { TrendIndicator } from "@/design-system";

When to use

  • Beside a metric, in a table cell or on a chart card header.
  • When the direction of change carries meaning for the reader.

When not to use

  • As a status pill — use Badge.
  • When the comparison period is unknown or arbitrary.

Directions

Increase of 12%this monthDecrease of 8%this monthNo change, No change

Inverted sentiment and subtle appearance

Decrease of 8%open requestsIncrease of 6%overdueIncrease of 12%

Accessibility

  • A visually hidden phrase states 'Increase of' / 'Decrease of' / 'No change'.
  • The arrow is decorative and hidden from assistive tech.
  • Sentiment only affects colour, so an inverted metric still reads correctly.

Line chart

Trends over a continuous scale, usually time.

LineChart

Show how one or more values move over time, with axis labels, tooltip, legend and the accessible data table.

import { LineChart } from "@/design-system";

When to use

  • Activity, usage, requests or performance over time.
  • Comparing two or three series over the same period.

When not to use

  • Comparing unrelated categories — use a bar chart.
  • A single value — use a metric.

Single series

Residents over time

Last 6 months

Residents grew steadily from 1,148 in September to 1,248 in February.

View data as table
MonthResidents
Sep1,148
Oct1,176
Nov1,194
Dec1,201
Jan1,226
Feb1,248

Multiple series

Monthly activity

Requests raised against requests resolved

  • Requests raised
  • Requests resolved

Requests raised rose from 182 to 236 while resolutions tracked slightly below them each month.

View data as table
MonthRequests raisedRequests resolved
Sep182168
Oct204191
Nov176170
Dec148139
Jan221198
Feb236214

Accessibility

  • Each chart carries a title and a summary sentence read in place of the graphic.
  • Multi-series lines also differ by dash pattern, not colour alone.
  • The same values are available in the 'View data as table' panel.

Bar chart

Comparison across discrete categories.

BarChart

Compare values across categories, grouped for series comparison or stacked for composition.

import { BarChart } from "@/design-system";

When to use

  • Requests by service, occupancy by site, tasks by status.
  • Long category names — switch to horizontal bars.

When not to use

  • Time series with many points — use a line chart.
  • Exact values users must read precisely — use a table.

Vertical bars

Requests by service

Last 30 days

Cleaning received the most requests at 96, followed by maintenance at 84, security at 60 and grounds at 38.

View data as table
ServiceRequests
Cleaning96
Maintenance84
Security60
Grounds38

Horizontal bars

Requests by service

Horizontal bars keep long category names readable

Cleaning 96, maintenance 84, security 60, grounds 38 requests.

View data as table
ServiceRequests
Cleaning96
Maintenance84
Security60
Grounds38

Bars with a trend line

Visitor trend

Hours on site as bars, visitor count as an overlay line

  • Visitors
  • Hours

Hours peak in November at 358 and August at 333; visitors peak in October at 26 and August at 24.

View data as table
MonthVisitorsHours
Oct26312
Nov15358
Dec448
Jan14190
Feb546
Mar24
Apr324
May17309
Jun771
Jul15240
Aug24333
Sep224

Accessibility

  • Category labels stay horizontal and readable rather than shrinking.
  • Values are duplicated in the accessible data table.
  • Bar corner rounding stays minimal so lengths remain comparable.

Area chart

Only where the filled volume means something.

AreaChart

Show cumulative volume or a total over time, with a restrained low-opacity fill.

import { AreaChart } from "@/design-system";

When to use

  • Totals or cumulative volume over time.
  • Composition of a total across two or three stacked series.

When not to use

  • Purely decorative emphasis on a line chart.
  • More than three overlapping series — readability collapses.

Stacked volume

Request volume

Raised and resolved, stacked

  • Requests raised
  • Requests resolved

Total request volume peaked in February at 450 across raised and resolved.

View data as table
MonthRequests raisedRequests resolved
Sep182168
Oct204191
Nov176170
Dec148139
Jan221198
Feb236214

Accessibility

  • The fill is a gradient of the same series token, so contrast between series is preserved.
  • Stacked totals are stated in the summary sentence and the data table.

Donut chart

Part-to-whole for a small number of categories.

DonutChart

Show how a total splits across a handful of categories, with each slice named and valued in the legend.

import { DonutChart } from "@/design-system";

When to use

  • Two to five categories that add up to a meaningful whole.
  • A total worth showing in the centre of the ring.

When not to use

  • More than about six categories — use a bar chart.
  • Comparing precise category sizes against each other.

Service distribution

Service distribution

Share of requests by service

  • Cleaning · 40%
  • Maintenance · 35%
  • Security · 25%

Cleaning accounts for 40%, maintenance 35% and security 25% of all requests.

View data as table
ServiceShare
Cleaning40%
Maintenance35%
Security25%

Accessibility

  • Every slice is labelled with its value in the legend and the table.
  • Slice separation uses the surface token, so rings read in both themes.

Progress

Completion as a data point, built on the existing Progress bar.

ProgressMetric

Show how far something has progressed, with the value in text beside the label.

import { ProgressMetric, Progress } from "@/design-system";

When to use

  • Profile or onboarding completion.
  • Service or task completion against a known total.

When not to use

  • Loading feedback where real progress is unknown — use a loading state.
  • Comparing many values — use a bar chart.

Determinate

Profile completion75%
3 of 4 sections complete
Service completion18 / 30
February schedule

Indeterminate

Importing residentsIn progress
Only when real progress is genuinely unknown

Accessibility

  • The percentage is text, so the bar is never the only representation.
  • The bar is a Radix progress role with an accessible name from the label.
  • Indeterminate bars respect reduced motion.

Legend & chart tooltip

Series identity and on-hover detail — reusing the shared overlay treatment.

ChartLegend

Identify each plotted series with a colour and, for line charts, its dash pattern; optionally toggle series on and off.

import { ChartLegend, ChartTooltipContent } from "@/design-system";

When to use

  • More than one series is plotted.
  • Users need to isolate a series.

When not to use

  • A single series that the chart title already names.

Static and interactive legends

  • Requests raised
  • Requests resolved

Tooltip content

Category, series name and value — kept to three concise lines.

The tooltip shows the same values as the table alternative.
MonthRequests raisedRequests resolved
Feb236214

Accessibility

  • Interactive entries are buttons with aria-pressed, reachable by keyboard.
  • Inactive series are struck through as well as dimmed.
  • Chart tooltips use the shared overlay surface, not a second overlay system.

Dashboard chart card

Title, supporting line, filters, chart, legend and actions — composed from the existing Card.

DashboardChartCard

Place a chart on a dashboard with its heading, filters and actions in one consistent container.

import { DashboardChartCard } from "@/design-system";

When to use

  • Any chart on a dashboard or summary page.
  • Charts that need a filter, a legend or a 'View details' action.

When not to use

  • A chart already inside another card or drawer — render it plain instead.

Filtered chart card

Monthly activity

Requests raised against requests resolved

  • Requests raised
  • Requests resolved

Requests raised rose from 182 to 236 while resolutions tracked slightly below them each month.

View data as table
MonthRequests raisedRequests resolved
Sep182168
Oct204191
Nov176170
Dec148139
Jan221198
Feb236214

Accessibility

  • The card heading names the chart; the chart still carries its own summary sentence.
  • Filters and actions reuse Select, DateRangePicker, Button, IconButton and menus, so keyboard behaviour is unchanged.

Chart states

One lifecycle, reusing the existing skeleton, empty and error components.

Chart states

Cover default, loading, no-data and error consistently through ChartContainer rather than per-chart feedback.

import { LineChart, NoResultsState } from "@/design-system";

When to use

  • Any chart backed by an asynchronous request.
  • Charts behind filters that can exclude every value.

When not to use

  • Bespoke chart-only spinners, banners or error cards.

Switch state

Monthly activity

Last 6 months

  • Requests raised
  • Requests resolved

Requests raised rose from 182 to 236 across the period.

View data as table
MonthRequests raisedRequests resolved
Sep182168
Oct204191
Nov176170
Dec148139
Jan221198
Feb236214

Partial data

Monthly activity

December is incomplete — two sites had not reported

  • Requests raised
  • Requests resolved

Requests dipped to 148 in December, where data is incomplete, before rising to 236 in February.

View data as table
MonthRequests raisedRequests resolved
Sep182168
Oct204191
Nov176170
Dec148139
Jan221198
Feb236214

No results, not no data

Filters excluded every value — use the No results state, not the empty chart.

No results found

No requests match “Grounds · Elm Lodge” in the selected range. Clear a filter to see data again.

Accessibility

  • Loading exposes aria-busy with a hidden loading message; skeleton bars are decorative.
  • No-data and error states are real headings and text, not just an illustration.
  • Partial data is stated in the description so a gap is not read as zero.

Centrim Life dashboard

Metrics, trend, line, donut, bar and progress on one page — all from the components above.

Total residents1,248Increase: 12%This month
Active services86Increase: 4%Across 12 sites
Open requests24Decrease: 8%Oldest open 3 days
Average response4.2hNo change: No changeTarget 6h

Monthly activity

Requests raised against requests resolved

Increase of 12%vs. last period
  • Requests raised
  • Requests resolved

Requests raised rose from 182 in September to 236 in February, with resolutions tracking just below.

View data as table
MonthRequests raisedRequests resolved
Sep182168
Oct204191
Nov176170
Dec148139
Jan221198
Feb236214

Service distribution

Share of requests by service

  • Cleaning · 40%
  • Maintenance · 35%
  • Security · 25%

Cleaning 40%, maintenance 35% and security 25% of all requests.

View data as table
ServiceShare
Cleaning40%
Maintenance35%
Security25%

Requests by service

Last 30 days

Cleaning 96, maintenance 84, security 60 and grounds 38 requests.

View data as table
ServiceRequests
Cleaning96
Maintenance84
Security60
Grounds38

Completion

Across resident and service records

Profile completion75%
312 residents missing a contact
Service completion18 / 30
February schedule

Choosing a representation

Chart, table or metric — pick by the question the reader is asking.

Chart vs table vs metric
RepresentationAnswersUse whenAvoid when
MetricHow much, right now?One number carries the messageThe reader needs the breakdown
ChartHow is this moving or splitting?Trend, comparison or part-to-wholeExact values matter
TableWhat exactly is each value?Detailed records, many columnsThe shape of the data is the point
Metric + chartHow much, and how did it get there?A headline number with its historySpace is tight on mobile

Why one visualization foundation

Every chart renders inside ChartContainer, so titles, legends, lifecycle states and the data table alternative behave the same everywhere.

Series colours come from the chart token ramp, so light and dark themes and adjacent-series contrast are handled centrally rather than per chart.

Dash patterns, point shapes and legends keep series distinguishable without colour perception, and animation is short and disabled entirely under reduced-motion.

Do

  • Give every chart a title and a summary sentence.
  • Keep the 'View data as table' alternative available.
  • Compose filters and actions from the existing Select, DateRangePicker, Button, IconButton and menu components.
  • Reduce axis label density on mobile rather than shrinking type.
  • Use the No results state when filters exclude everything, and the empty chart state when there is genuinely no data.

Don't

  • Do not introduce per-chart colours, fonts, spacing or another chart library.
  • Do not signal positive or negative movement with colour alone.
  • Do not use an area chart when the filled volume is meaningless.
  • Do not use a donut chart beyond about six categories.
  • Do not add decorative animation or chart-specific feedback components.