opsinjs
ComponentsHealth data display

TrendSparkline

A small line showing how one measurement has moved over time, with a caption that says what changed and leaves the verdict to you.

Preview

Theme
Density
Text
Status
trend-sparkline · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/trend-sparkline

The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.

Usage

import { TrendSparkline } from "@/components/ui/trend-sparkline"
<TrendSparkline
  label="Example measurement"
  unit="steps"
  precision={0}
  locale="en-GB"
  window="the last 6 entries"
  series={series}
  minimumPoints={readingsATrendNeeds}
/>

When to use it

Use it when

  • One measurement over time, where the shape of the change is the point.
  • A compact companion to a reading, showing whether today's number fits a pattern.
  • Anywhere a reader would otherwise ask 'is this new?' about a value shown.

Do not use it when

  • Fewer readings than your product's minimum, so a line would imply an unsupported pattern.Use empty-state instead.
  • The reader compares one value with a reference range, not its own history.Use range-bar instead.
  • The reader needs which day a value belongs to, so axes and hover are required.Use table instead.
  • You show one number and its change since last time, without a line.Use metric-tile instead.
  • The trend crossed a threshold and the reader must do something about it.Use care-card instead.

Clinical meaning

Asserts. That these readings, in this unit, were recorded at these times. Nothing about cause.

Never read as. A prediction, an explanation or a verdict. Upward is not worse and downward is not better, because valence belongs to the measurement and often to the person. Direction only (Trends and change).

Colour axis. Category, on the line alone (Category identity). Status is never drawn: the marked reading is neutral and its verdict is a StatusPill in the caption.

Thresholds. None. changeThreshold is required before any direction word prints, and minimumPoints before any line is drawn (Numbers, units and precision).

Gaps and age. A { value: null } entry breaks the path and is never interpolated. The caption prints the last reading's date and invents no staleness boundary (Uncertainty and staleness).

Anatomy

  1. TrendSparklineThe root: a flex column of period, plot and caption.Controlled by className
  2. TrendSparkline.WindowThe period, drawn only when you supplied a caption.Controlled by window
  3. TrendSparkline.PlotThe SVG. role="img" with a composed name, focusable="false". Absent below minimumPoints.Controlled by series
  4. TrendSparkline.BaselineThe zero anchor: a neutral hairline, so a flat series reads flat.Controlled by series
  5. TrendSparkline.BandThe optional interval, dashed and neutral. Needs a source and both bounds.Controlled by range
  6. TrendSparkline.LineOne path, broken at every gap. Carries data-category, never data-status.Controlled by category
  7. TrendSparkline.GapEdgeA dot each side of a break, in the line's tint.Controlled by series
  8. TrendSparkline.PointThe marked reading: a neutral dot, weight and halo apart. Stamps data-status.Controlled by series
  9. TrendSparkline.CaptionThe visible text twin: sentence, marked reading with pill, coverage, date, range.Controlled by caption

Examples

A band behind the line, and one marked reading

range shades a band only where it carries a source and both bounds. The one-sided range beside it is stated in words, drawn as nothing.

Theme
Density
Text
Status
trend-sparkline-a-band-and-a-marked-reading · base base · style base-lyraOpen under the product theme

Not enough readings

minimumPoints of four, against series of four and three. The short one draws nothing and appends its caption to the refusal.

Theme
Density
Text
Status
trend-sparkline-not-enough-readings · base base · style base-lyraOpen under the product theme

A row of tiles

What category is for: finding one line among several. Greyscale loses no fact.

Theme
Density
Text
Status
trend-sparkline-a-row-of-tiles · base base · style base-lyraOpen under the product theme

States

Data stateWhat TrendSparkline renders
ErrorA non-finite value is counted apart from gaps and called unreadable in the caption.
LoadingNone. The surface fetching the series owns the skeleton.
EmptyBelow minimumPoints it draws no line and prints the refusal, naming count and minimum.
PartialA { value: null } entry is a real break, counted in the caption. Nothing is interpolated.
StaleThe caption prints the last reading's date. No boundary invented, no verdict passed.

Content guidelines

Captions state direction where a changeThreshold allows one, then magnitude, window, the marked reading, coverage, date and range. Avoid improving, worsening, better and on track, because each is valence nobody signed. Pass locale, or date order is the runtime's.

Do

"Down, from 20 steps to 16 steps, over the last 14 days." Checkable against the picture.

Don’t

"Trending in the right direction." Valence with no author, magnitude or window.

Accessibility

Audited against WCAG 2.2 AA in a source pass and a rendered pass. The audit is author-run, not an independent review, and clinical review is still pending.

What the audit found. Nothing to change. The rendered pass was clean, with no serious axe, target-size or focus breach at 320px, and the source pass confirmed every check: the two colour axes stay apart, and status is carried by the caption StatusPill, never by colour alone. The committed check:a11y gate reruns the literal, bound, colour and axis rules.

Residual and deferred.

  • No per-point data table twin. The text alternative is the role="img" name plus the visible caption. A full table is an enhancement, not an AA requirement.
  • className merges onto the whole subtree, so an sr-only variant can hide the caption, the only text carrier. A documented API risk; keep it visible.
  • Under forced colours the line drops its category tint to CanvasText. Category is identity only, so nothing else is lost, and this is intentional.
KeyActionNotes
TabSkips the whole componentNothing is focusable. focusable="false" covers engines that made SVG focusable.
Shift+TabSkips the whole componentNever enters the focus order, in either direction.
Any other keyNothingNo hover, no tooltip, no point-by-point navigation. Days belong in a table.
PairThemeAPCA LcWCAG 2.2Floor
activity ink on activity surfacelight88.59.61:1Pass
activity ink on activity surfacedark-83.112.58:1Pass
activity line on activity surfacelight67.64.42:1Pass
activity line on activity surfacedark-69.09.93:1Pass
activity accent on activity surfacelight44.12.34:1Below floor
activity accent on activity surfacedark-53.37.26:1Pass
activity accent on the pagelight46.12.41:1Below floor
activity accent on the pagedark-54.68.22:1Pass
activity ink on the pagelight90.59.90:1Pass
activity ink on the pagedark-84.414.24:1Pass
activity line on the bandlight66.14.32:1Pass
activity line on the banddark-67.48.60:1Pass
heart ink on heart surfacelight89.110.51:1Pass
heart ink on heart surfacedark-80.512.55:1Pass
heart line on heart surfacelight70.45.05:1Pass
heart line on heart surfacedark-64.59.46:1Pass
heart accent on heart surfacelight60.33.64:1Pass
heart accent on heart surfacedark-48.26.68:1Pass
heart accent on the pagelight63.23.81:1Pass
heart accent on the pagedark-49.57.27:1Pass
heart ink on the pagelight92.010.97:1Pass
heart ink on the pagedark-81.813.65:1Pass
heart line on the bandlight69.95.01:1Pass
heart line on the banddark-62.87.88:1Pass
labs ink on labs surfacelight88.89.78:1Pass
labs ink on labs surfacedark-83.112.69:1Pass
labs line on labs surfacelight67.94.51:1Pass
labs line on labs surfacedark-68.49.90:1Pass
labs accent on labs surfacelight59.33.49:1Pass
labs accent on labs surfacedark-52.77.21:1Pass
labs accent on the pagelight61.53.60:1Pass
labs accent on the pagedark-53.98.07:1Pass
labs ink on the pagelight90.910.09:1Pass
labs ink on the pagedark-84.314.22:1Pass
labs line on the bandlight66.54.42:1Pass
labs line on the banddark-66.78.49:1Pass
mind ink on mind surfacelight89.910.36:1Pass
mind ink on mind surfacedark-81.312.56:1Pass
mind line on mind surfacelight70.74.94:1Pass
mind line on mind surfacedark-65.49.54:1Pass
mind accent on mind surfacelight60.03.55:1Pass
mind accent on mind surfacedark-49.16.77:1Pass
mind accent on the pagelight62.83.70:1Pass
mind accent on the pagedark-50.07.42:1Pass
mind ink on the pagelight92.710.80:1Pass
mind ink on the pagedark-82.213.77:1Pass
mind line on the bandlight70.04.89:1Pass
mind line on the banddark-63.48.00:1Pass
nutrition ink on nutrition surfacelight89.310.06:1Pass
nutrition ink on nutrition surfacedark-81.912.55:1Pass
nutrition line on nutrition surfacelight69.74.78:1Pass
nutrition line on nutrition surfacedark-66.79.66:1Pass
nutrition accent on nutrition surfacelight39.22.11:1Below floor
nutrition accent on nutrition surfacedark-50.26.88:1Pass
nutrition accent on the pagelight41.82.19:1Below floor
nutrition accent on the pagedark-51.37.63:1Pass
nutrition ink on the pagelight91.910.44:1Pass
nutrition ink on the pagedark-82.913.91:1Pass
nutrition line on the bandlight68.84.71:1Pass
nutrition line on the banddark-64.88.19:1Pass
sleep ink on sleep surfacelight89.610.19:1Pass
sleep ink on sleep surfacedark-81.912.60:1Pass
sleep line on sleep surfacelight70.04.80:1Pass
sleep line on sleep surfacedark-66.49.65:1Pass
sleep accent on sleep surfacelight64.64.05:1Pass
sleep accent on sleep surfacedark-50.46.94:1Pass
sleep accent on the pagelight67.34.22:1Pass
sleep accent on the pagedark-51.47.67:1Pass
sleep ink on the pagelight92.310.60:1Pass
sleep ink on the pagedark-82.913.92:1Pass
sleep line on the bandlight69.14.75:1Pass
sleep line on the banddark-64.58.16:1Pass
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor
activity ink on the card materiallight93.610.34:1Pass
activity ink on the card materialdark-83.512.91:1Pass
heart ink on the card materiallight95.111.47:1Pass
heart ink on the card materialdark-80.912.38:1Pass
labs ink on the card materiallight94.010.54:1Pass
labs ink on the card materialdark-83.512.90:1Pass
mind ink on the card materiallight95.711.28:1Pass
mind ink on the card materialdark-81.412.49:1Pass
nutrition ink on the card materiallight95.010.91:1Pass
nutrition ink on the card materialdark-82.012.61:1Pass
sleep ink on the card materiallight95.411.08:1Pass
sleep ink on the card materialdark-82.112.62:1Pass

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

Data attributes

AttributeConditionValue
data-slotOn the root and every rendered parttrend-sparkline, trend-sparkline-window, trend-sparkline-plot, trend-sparkline-baseline, trend-sparkline-band, trend-sparkline-line, trend-sparkline-gap-edge, trend-sparkline-point, trend-sparkline-caption
data-categoryOn the line, for a known categorysleep | heart | activity | nutrition | mind | labs
data-statusOn the marked reading alonesteady | watch | attention | urgent
data-opsinjs-valueOn the marked reading and every caption Value. De-duplicate before scrapingThe unrounded number behind that reading

API reference

Prop

Type

Generated from TrendSparklineProps in registry/bases/base/trend-sparkline.tsx.

precision, series and minimumPoints are required with no default, because none is a number opsinjs could supply. TrendPoint and ReferenceRange come from @/lib/opsinjs, so no second copy can drift.

  • RangeBar shows where one value sits in a reference range, not how a series moved.
  • MetricTile can embed a sparkline. A sparkline is not a tile.
  • ScoreDial is a state at one moment, not a shape over time.
  • EmptyState owns the too-short refusal where the surface has no chart slot.

On this page