GrDelta

  • core
  • data
  • API · 10 props

Shows a signed value with its sign, tone and arrow inline in a sentence.

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

Overview

When to take it

  • a signed number stands inside a sentence — in the description of an operation, in a table cell, in the caption under a chart;
  • the sign decides the colour — growth, decline and “unchanged” are seen before the quantity itself is read;
  • growth can be bad — polarity="negative-good" for cost, response time and churn;
  • the quantity may be missing — “no data” and “zero” are drawn differently.

When to take something else

NeedTake
A large metric as a tile, with a label and a trendGrStatistic
A number as a status or a labelGrBadge
A share of a whole as a bar or a ringGrProgressBar / GrProgressCircle
A series of values over timeGrSparkline
A “property → value” pairGrDescriptionList

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 change inside a sentence

    At the default size the delta has no font size of its own: inside a heading it is large, inside a sentence it is the size of the sentence, and the arrow grows with the number. Zero is «no change» and stays neutral.

    Revenue in September +8.4%

    Recurring revenue reached €48,920, +8.2% week over week. Trial conversion slipped by -1.2 pp, and seats per account stayed at 4.2 — 0%, which is neither good nor bad news.

  • No data is not zero

    null prints empty-text with no tone, arrow or affixes — «€—» would read as zero euros. The sign comes from Intl, so locale moves the separators and the sign still lands before the currency.

    • Germany+12.6%
    • Poland0.0%
    • Sweden-3.4%
    • Portugalnew market
    • en-GB+€1,284.50
    • de-DE+1.284,50 €
    • fr-FR+1 284,50 €
  • When growth is bad news

    polarity="negative-good" flips the tone for cost, latency and churn, but never the sign: a cost that fell is still −€0.42, and it is green. none drops the tone where the sign is not a verdict.

    MetricThis monthChangepolarity
    Revenue€92,350+6.3%positive-good · more is better
    Cost per order€3.18-€0.42negative-good · less is better
    Latency, p95412 ms+38 msnegative-good · less is better
    Monthly churn2.1 %+0.4 ppnegative-good · less is better
    Cash balance shift€18,400-€1,240none · the sign is not a verdict
  • Steps for a row of controls, hooks for the rest

    Next to controls size="sm" matches them instead of the text around. --gr-delta-suffix-color and --gr-delta-arrow-size mute a unit or enlarge the arrow, and the public deltaTone gives a badge the same verdict as the delta.

    Orders +4.6% Refunds +0.3 pp
    Latency -12msFaster than last week

API generated from the package · cannot drift

Props

PropTypedefaultDescription
size"xs" | "sm" | "md" | "lg" | undefinedundefinedThe type size by the controls scale. `md` by default — the type size of the surrounding line; `xs`, `sm`, `lg` are needed when the quantity stands in a row with controls.
emptyTextstring | undefinedundefinedWhat to print a missing quantity with.
localestring | undefinedundefinedThe locale of the formatting. Unset — the locale of the i18n adapter.
precisionnumber | undefinedundefinedDigits after the decimal point. Unset — as the locale gives it.
prefixstring | undefinedundefinedAn affix before the number: a currency, a unit.
suffixstring | undefinedundefinedAn affix after the number: `%`, `ms`.
polarityGrDeltaPolarity | undefinedundefinedWhat counts as good. For revenue growth is a success, for cost and response time it is the opposite; without this the component would lie in half of the cases.
showSignboolean | undefinedundefinedPut a `+` on positive values. The minus is set by `Intl` itself.
showArrowboolean | undefinedundefinedAn arrow of direction. Decorative: the direction is already in the sign.
valuerequirednumber | null—The quantity. `null` means there is none; that is not a zero.

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 { GrDeltaPolarity, GrDeltaTone, GrDeltaProps } from '@feugene/granularity/components/GrDelta'

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

grDeltaConfigconst
const grDeltaConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrDelta">;

Filedist/types/components/shared/deltaTone.d.ts

GrDeltaPolaritytype
type GrDeltaPolarity = 'positive-good'
GrDeltaTonetype
type GrDeltaTone = 'success' | 'danger' | 'neutral';
deltaTonefunction
function deltaTone(value: number, polarity?: GrDeltaPolarity): GrDeltaTone;
deltaDirectionfunction
function deltaDirection(value: number): 'up' | 'down' | 'flat';

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

GrDeltaPropsinterface
interface GrDeltaProps {
    value: number | null;
    precision?: number;
    prefix?: string;
    suffix?: string;
    polarity?: GrDeltaPolarity;
    showSign?: boolean;
    showArrow?: boolean;
    emptyText?: string;
    locale?: string;
    size?: GrComponentSize;
}

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

GrDeltaConfigurablePropsinterface
interface GrDeltaConfigurableProps {
    size: GrComponentSize;
    polarity: GrDeltaPolarity;
    showSign: boolean;
    showArrow: boolean;
    emptyText: string;
}

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

grDeltaSafelistconst
const grDeltaSafelist: string[];

Usage from the library prose

Zero is neutral, null is not zero

Two states that are not expressed by two colours, and both have already led to bugs in applications:

  • zero — “unchanged”. Neither a success nor an error: the tone is neutral with any polarity. A condition of the form value < 0 ? danger : success paints a zero movement green, like an income;
  • null — there is no quantity at all. It is printed as a dash (emptyText), with no tone, no arrow and no affixes: $— reads as “zero dollars”.

The choice of tone lives in a pure function of its own and is covered by tests without mounting — it is exactly that switch that used to spread through an application in copies.

The function is public: deltaTone(value, polarity) and deltaDirection(value) are given away by the package. They are needed where the markup of a delta does not fit but the rule is the same — the tone of GrStatistic and GrBadge in the tiles of a report. Rewritten in an application, it diverges from this one at the very first edit — which is what happened with the zero painted green like an income.

Polarity inverts the tone but not the sign
vue
<GrDelta :value="-15" suffix="%" polarity="negative-good" />

Cost fell by 15 % — that is a success, and the quantity is green. But it is still −15 %: the sign belongs to the number, not to the judgement. The arrow follows the sign as well — downwards, because the quantity decreased.

polarity="none" removes the tone entirely: for a balance or an offset the sign says nothing about quality.

The sign is set by Intl, not by string concatenation

showSign switches on signDisplay: 'exceptZero' in the formatting rather than appending a '+' to a ready string. The difference is not stylistic: concatenation has already broken GrStatistic — the string '+1,234' stops being a number, and the digit groups are lost at the next step.

The locale comes from the i18n adapter unless it is set with a prop.

The sign stands before the currency

Together with prefix the sign moves into a node of its own before the affix: +$0,0280, not $+0,0280. The sign belongs to the quantity as a whole rather than to the number after the currency symbol, and the second order is accepted in no typographic tradition.

The invariant above is not weakened in the process: the sign is still set by Intl, the component merely takes it out of formatToParts — the string is not concatenated and the digit groups are not reassembled. Without prefix there is nowhere to take it out to, and the markup stays as it was.

There is deliberately no way to configure the position of the sign. A separate subtlety is RTL: there the sign is preceded by an invisible direction mark, which is what turns it around relative to the digits; it moves together with the sign, because it is the sign it governs.

The affixes themselves are drawn by GrValue — the primitive shared with GrStatistic. Their styling and the --gr-value-* tokens come from there too: a currency on the left is set like a number, a unit on the right is dimmed. A currency on the right (+1 000 ₽) is obtained by changing the default of the suffix, and the recipe is on the page of the primitive.

Tuning points of its own

The delta has four hooks of its own (--gr-delta-*, the full list is in ../tokens.md), and they did not appear for the sake of symmetry.

The delta does not dim the affix: “−15 %” is set in the type size and colour of the line as a whole, otherwise the percent reads as a caption beside the number. That is an override of the default of GrValue, and it is applied with an inline style — and inline beats any CSS of the consumer. Had the component set the colour as a literal, there would be nothing left to bring the dimming back with; the override therefore refers to --gr-delta-suffix-color and --gr-delta-suffix-size, leaving the decision to whoever set them:

vue
<GrDelta :value="-15" suffix="%" style="--gr-delta-suffix-color: var(--gr-muted-fg)" />

The arrow is tuned by --gr-delta-arrow-size and --gr-delta-arrow-gap. The size is a fraction of the type size of the quantity rather than a step of the scale: at md the delta has no type size of its own at all, and a pixel value would have left the arrow tiny inside a heading. The spacing is logical, so in RTL it stands on the proper side.

The type size comes from the line

The default step (md) does not set font-size at all: the quantity is set in the type size of the line it stands in. Inside a heading it is large, in a caption under a chart it is small, in a table cell it is exactly like the rest of the text of the cell — and all of that without a single prop.

vue
<h2 class="text-[length:var(--gr-text-3xl)]">
  Revenue for March <GrDelta :value="8.4" :precision="1" suffix="%" show-arrow />
</h2>

The edges of the ladder stay explicit and take the control scale — xs 12 px, sm 13 px, lg 16 px. They are needed in the other case: when the quantity stands not inside a sentence but in a row with controls, and has to match them rather than the surrounding text.

The arrow is set in em and grows together with the number — it has no step of its own. When overriding the type size from the outside, override the pair: the component does not touch the line height, and a half-measure leaves the line with someone else’s leading.

The arrow is decorative

showArrow draws the direction but marks the icon aria-hidden: the direction is already announced by the sign, and there is no point duplicating it for a screen reader. There is no arrow by default — in a line of text it more often adds noise than helps.

Limits

The component does not compute the delta: it accepts a ready one. “Compared to what” — the period, the base value, the filters — is known by the application, and dragging that into a presentational component means dragging its data model in there too. Percentages, comparisons of periods and a sparkline are outside as well.