GrStatistic

  • core
  • data
  • API · 21 props

Highlights a key metric with a caption, formatting and a trend line.

import { GrStatistic } from '@feugene/granularity/components/GrStatistic'

Overview

When to take it

  • one metric is the main one — revenue, the number of orders, conversion: a large figure is read first;
  • a trend is needed beside it — trend with an arrow and the label “+12 % against last week”;
  • the number is long — digit grouping and precision by locale, with no manual formatting;
  • the value changes before your eyes — animate brings the figure to the new value rather than replacing it;
  • the tile is clickable — href/clickable lead into a section with the details.

When to take something else

NeedTake
There are several metrics and they are comparedGrChartBar
The course over time mattersGrChartLine / GrSparkline
A share of a whole is shownGrProgressCircle
The value is a status rather than a numberGrBadge
The quantity stands inside a line of text rather than as a tileGrDelta

The component is presentational: it does not load the data and does not compute the delta — it only formats and presents. The trend arrives ready, because “compared to what” is known by the application rather than by the tile.

Variants every tone and view, rendered by the real component

tone

  • primary
  • neutral
  • success
  • warning
  • danger
  • info
  • slate
  • azure

Sizes

  • xs
  • sm
  • md
  • lg

States

  • normal
  • loading

The grid is live: hover and press it. Hover, focus and active states are not drawn here — they belong to the browser, not to a prop.

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

Examples 4 each with a live demo and its code

  • A number with a name

    The value is formatted by the locale, the sign and the unit live in prefix and suffix, and the title is part of the component — a number without a name is not a statistic.

    Revenue
    €92,350
    Active customers
    18,342
    Conversion
    3.1%
  • Growth next to the value

    trend is a direction — up, down, flat — and trend-text is the phrase under the number; the change itself comes from the metric, the component does not compute it. A rise is good news by default. For churn a fall is: trend-polarity="negative-good" paints the trend line green and leaves the value neutral.

    Recurring revenue
    €48,920
    Increase+8.2% this month
    Trial conversion
    18.6%
    Decrease−1.2 pp this month
    Monthly churn
    2.1%
    Decrease−0.4 pp this month
    Seats per account
    4.2
    No changeNo change
  • Waiting and a click through

    loading holds the place of the number instead of showing a zero, and href turns the whole tile into a link to the page where the number can be taken apart.

    Support desk

  • A dashboard of several figures

    Figures are read together rather than one at a time: four tiles for the period and the daily revenue under them. Switch the period — animate counts from the previous value, so the change is visible instead of a new number appearing out of nowhere.

    Store overview

    Revenue
    €21,480
    Increase+12.4% vs previous
    Orders
    486
    Increase+6.1% vs previous
    Conversion
    3.4%
    Decrease−0.3 pp vs previous
    Average basket
    €44.20
    No changeNo change

    Daily revenue

    Thousands of euros

API generated from the package · cannot drift

Props

PropTypedefaultDescription
tone
"primary" | "neutral" | "success" | "warning" | "danger" | ""primary" | "neutral" | "success" | "warning" | "danger" | "info" | "slate" | "azure" | undefined
undefinedThe tone of the value; overridden pointwise by `--gr-statistic-value-color`.
titlestring | undefinedundefinedThe label above the value.
iconstring | Component | undefinedundefinedAn icon to the left of the block: a Vue component or an icon class (`'i-lucide-users'` — the application sets up the rule for the class: in the engine factory or through a provider, see the “Icons” section of `docs/installation.md`).
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe type size of the value, and with it of the title, the additions and the dynamics line. Unset — from `GrConfigProvider`, otherwise `md`.
loadingboolean | undefinedfalseThe loading state: a placeholder instead of the value.
animateboolean | undefinedfalseRun through the numbers on appearing and on a change of the value. A non-numeric value is put in at once, and under `prefers-reduced-motion: reduce` so is any other.
asstring | Component | undefinedundefinedA root tag of your own: a link component (`RouterLink`, Inertia’s `Link`) or a semantic tag of the page (`section`, `article`). Stronger than `href`. A non-interactive tag is only a replacement for `div`: no highlight, no focus ring.
hrefstring | undefinedundefinedA metric as a link: a move to the details.
clickableboolean | undefinedfalseA metric as a button: the whole tile is interactive.
localestring | undefinedundefinedThe BCP-47 locale of the formatting. Unset — it is taken from the i18n adapter; with no adapter either, the manual separators work.
precisionnumber | undefinedundefinedThe number of digits after the decimal point.
prefixstring | undefinedundefinedAn addition before the value (a currency, a sign).
suffixstring | undefinedundefinedAn addition after the value (a unit of measurement, `%`).
polarityGrDeltaPolarity | undefinedundefinedWhat counts as good for a **signed value** — a balance, a gain, a difference: the tone is derived from the sign of the value itself. For revenue growth is a success, for the cost price and churn it is the other way round. An explicit `tone` is stronger: the derived tone is a default rather than a dictate. An absolute figure (churn of 2.4 %, response time) carries no sign — for it the polarity is given to the dynamics, `trendPolarity`, and the value stays neutral.

Slots

SlotTypeDescription
defaultanyThe value instead of the `value` prop.
iconanyAn icon before the label.
titleanyThe label of the metric instead of the `title` prop.
prefixanyAn addition before the value: a currency sign, an arrow.
suffixanyAn addition after the value: a unit of measurement, a per cent.
trendanyThe dynamics under the value instead of the built-in `GrDelta`.

Events

EventSignatureDescription
click(event: "click", event: MouseEvent): void—

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrStatisticFormatOptions, GrStatisticProps, GrStatisticEmits } from '@feugene/granularity/components/GrStatistic'

Filedist/types/components/GrStatistic/config.d.ts

grStatisticConfigconst
const grStatisticConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrStatistic">;

Filedist/types/components/GrStatistic/formatStatisticValue.d.ts

GrStatisticFormatOptionstype
type GrStatisticFormatOptions = {
    precision?: number;
    groupSeparator?: string;
    decimalSeparator?: string;
    locale?: string;
};
formatStatisticValuefunction
function formatStatisticValue(value: number | string, options?: GrStatisticFormatOptions): string;

Filedist/types/components/GrStatistic/GrStatistic.vue.d.ts

GrStatisticPropsinterface
interface GrStatisticProps {
    value: number | string;
    title?: string;
    precision?: number;
    groupSeparator?: string;
    decimalSeparator?: string;
    locale?: string;
    prefix?: string;
    suffix?: string;
    icon?: string | Component;
    size?: GrStatisticSize;
    tone?: GrStatisticTone;
    polarity?: GrDeltaPolarity;
    trendPolarity?: GrDeltaPolarity;
    trend?: GrStatisticTrend;
    trendText?: string;
    loading?: boolean;
    animate?: boolean;
    animateDuration?: number;
    as?: string | Component;
    href?: string;
    clickable?: boolean;
}
GrStatisticEmitsinterface
interface GrStatisticEmits {
    (e: 'click', event: MouseEvent): void;
}

Filedist/types/components/GrStatistic/grStatisticStyles.d.ts

GrStatisticSizetype
type GrStatisticSize = GrComponentSize;
GrStatisticTonetype
type GrStatisticTone = GrTone;
GrStatisticTrendtype
type GrStatisticTrend = 'up' | 'down' | 'flat';

Filedist/types/components/GrStatistic/safelist.d.ts

grStatisticSafelistconst
const grStatisticSafelist: string[];

Filedist/types/components/GrStatistic/defaults.d.ts

GrStatisticConfigurablePropsinterface
interface GrStatisticConfigurableProps {
    size: GrStatisticSize;
    polarity: GrDeltaPolarity;
    trendPolarity: GrDeltaPolarity;
}

Usage from the library prose

Formatting by locale
vue
<GrStatistic title="Revenue" :value="1234567.5" :precision="2" prefix="₽" />

The separators come from the locale through Intl.NumberFormat, and the locale from the i18n adapter of the package. Nothing has to be configured: ru will give 1 234 567,50, and de-DE 1.234.567,50.

The resolution order: groupSeparator/decimalSeparator → the locale prop → the locale of the adapter → the built-in default (a narrow space and a dot). An explicit separator replaces only its own part; the rest of the rules of the locale — grouping by three or by the Indian scheme, the minus sign — the component keeps.

A non-numeric value is printed as it is: “2 h 15 min” and ”—” need no workaround props. The formatting lives in a pure formatStatisticValue function of its own and is available from the package.

A mandatory value that did not arrive is drawn as a dash — the same sign of emptiness with which GrDelta shows a missing quantity — and is explained with a warning in dev mode. A tile with the word undefined looks like data rather than like an error, so that variant is excluded.

The trend
vue
<GrStatistic :value="482" trend="up" trend-text="+12.5 % against last week" />

trend sets the colour and the icon of the line, and trendText the text itself. The direction icons are built in; the icon prop to the left of the block accepts a Vue component or an icon class from your build (see “Icons”). The direction is additionally announced with a hidden label (“Growth”, “Decline”, “No change”): the icon is decorative, and “+12.5 %” without context does not tell growth from decline — the colour is not visible to a screen reader either. The label stays with a #trend slot of your own as well.

The affixes are drawn by GrValue
vue
<GrStatistic title="Average order" :value="14.99" :precision="2" prefix="$" suffix="per order" />

The tile does not draw the record of the quantity — the prefix, the number, the suffix — itself: that is done by the GrValue primitive, shared with GrDelta. The defaults come from there as well: on the left the affix is set like a number ($14.99), on the right it is dimmed and smaller (42 %).

The tile decides only the tone and the type size, and puts them on the container of the quantity — the affixes inherit both. That is why a negative sum turns red as a whole, together with the currency sign, rather than the number apart from it.

What an affix is — a currency, a unit of measurement or a note — is decided by nobody: the styling of both is configured with the --gr-value-* tokens. That is also how a currency on the right (1 284 500 ₽) is obtained, for which the default of the suffix does not fit — the recipe is on the page of the primitive.

The tone by sign and the trend line are different signals
vue
<GrStatistic title="Margin" :value="-1240" prefix="₽" polarity="positive-good" />

polarity is for a signed value: a balance, an increase, a difference from the plan. It colours the value by the sign of the quantity itself: positive-good for margin, negative-good for overspending, none when the sign says nothing about quality. Zero is neutral with any polarity, and a non-numeric value (“2 h 15 min”, ”—”) has no sign and gets no tone.

trend sets the line under the value: the direction with an icon and a hidden label, and the colour with the polarity rather than the direction. Growing churn is bad news, falling overdue debt is good news; the “up” arrow is green only where growth really is good. The tone is computed by the same deltaTone: negative-good colours up as danger, down as success, none and flat get no tone.

vue
<GrStatistic title="Churn" :value="2.4" suffix="%" trend="up" trend-text="+0.6 pp" trend-polarity="negative-good" />

An absolute metric — a churn of 2.4 %, a response time, the number of overdue invoices — carries no sign: there is nothing to colour the number itself for. Such a metric gets its polarity on the trend, trendPolarity, and the value stays neutral while the line ”↑ +0.6 pp” turns red. Without trendPolarity the trend takes polarity; with neither set, growth is good, as with revenue.

An explicit tone is stronger than polarity: an inferred tone is a default rather than a dictate. The same choice of tone is available as a separate deltaTone function — see GrDelta, where it lives.

The counter animation
vue
<GrStatistic title="Revenue" :value="revenue" animate :animate-duration="900" />

animate runs through the numbers when the tile appears (from zero) and on every change of the value — from the previous number rather than from zero: a run from zero on every update of a dashboard would read as a reset of the data.

The duration is set by animateDuration in milliseconds (600 by default). It is deliberately not set with a token: the --gr-duration-* scale ends at 300 ms and describes a change of state — a colour, a border, the appearance of a layer. A run through numbers is a different genre, and its number lives where the tween does — in JS.

Only numbers are run through: “2 h 15 min” and ”—” are set at once. The width of the line does not jerk — the value is set in tabular-nums.

The component reads prefers-reduced-motion itself. The global clamp in base.css holds CSS animations and transitions but not a JS tween — see ../motion.md. Under reduce the value is set instantly, with not a single intermediate frame.

A screen reader hears the final value. While the run is going on, the visible number is marked aria-hidden, and a visually hidden node with the result lives beside it: “1,284,500” on the screen and “743,210” in the ears is not noise but wrong data.

The step into the details
vue
<GrStatistic title="Orders" :value="1284" href="/orders" />

<GrStatistic title="Orders" :value="1284" clickable @click="drill" />

<GrStatistic title="Orders" :value="1284" :as="RouterLink" :to="{ name: 'orders' }" />

The same device as in GrCard and GrListItem: href gives a link, clickable a button, as a tag or a router component of your own (stronger than href). An interactive tile gets a cursor, a highlight and a focus ring; it has no fill of its own — a metric usually lies in a card already, and a second surface would argue with it.

There is no separate ariaLabel: the accessible name is assembled from the content, and the label of the metric is its name.

Loading

loading replaces the value with a skeleton of the same height, so that the block does not jump; the area is marked role="status" and contains a hidden loading text.

Styling
PointWhat it sets
size (xs…lg)the ladders of the type sizes of the label, the value, the affixes and the trend; read from GrConfigProvider
tonethe colour of the value; the tones come from the -text tokens — a saturated tone as text does not pass by contrast
polarityinfers tone from the sign of a signed value itself; read from GrConfigProvider
trendPolaritywhat counts as good for the trend; when not set — polarity, otherwise growth is good; read from GrConfigProvider
--gr-statistic-value-colorthe colour of the value pointwise, stronger than tone
--gr-statistic-title-colorthe colour of the label

The #icon, #title, #prefix, #suffix and #trend slots and the default slot (instead of the formatted value) replace the corresponding parts with markup.

The markup: the label and the value are a pair

The label and the value are output as <dl> → <dt> → <dd>: that is a “term — value” pair rather than two neighbouring blocks a screen reader simply reads in a row. The <dl> appears only together with the label — a definition list without a <dt> would be the same incoherence, only with a claim to semantics.

The trend line stays outside the <dl>: only dt/dd groups are allowed inside a definition list. Its place in the DOM is unchanged, and the layout does not change. The margins of the <dl> and the <dd> are zeroed with a class — the preflight of the package resets them for body alone.

All of the data attributes (data-gr-statistic-title, data-gr-statistic-value, …) are kept: the consumer’s styles written against them keep working.