Accumulator Dial

A compact semicircular gauge that visualises an accumulating quantity against a target ceiling. The arc fills and the needle sweeps from left (0) to right (target). When previousValue is supplied and differs from value, the dial fires a one-shot delta beat (a subtle scale pop on the needle) so the eye lands on the dial whose accumulator just changed.

Generic over the underlying quantity: works for an Adagrad cache, optimizer step counter, gradient magnitude, budget consumption, capacity meter, or any "how full is this?" reading.

Accumulator: 0 of 80.00
Customize
Shape
160 px
State
80
0
Playback
700 ms

Installation

npx shadcn@latest add https://craftbits.dev/r/accumulator-dial.json

Usage

import { AccumulatorDial } from "@craft-bits/viz/accumulator-dial";
 
<AccumulatorDial value={42} target={80} />

Drive a per-render delta beat from a parent that already tracks history:

<AccumulatorDial value={current} target={limit} previousValue={prior} />

Stack a short label above the dial:

<AccumulatorDial value={0.42} target={1} label="budget" size={160} />

Understanding the component

  1. Self-contained SVG. The dial renders as a viewBox-sized <svg> element with no positional dependencies. The caller decides where it sits in the layout.
  2. Fraction-driven geometry. The arc fill and needle endpoint are derived from the fraction value over target. Negative values clamp to zero; values above the target clamp to the full sweep.
  3. Track + fill. A muted full-sweep track sits beneath the accent-coloured arc fill, so the dial reads as "how much of the ceiling is consumed" even at small fractions.
  4. Delta beat. When previousValue differs from value (or the internally tracked last-rendered value differs from the new value), the needle group fires a one-shot scale pop, capped at 1.04 to honour the subtle-deformation-scale ceiling.
  5. Value swap. The center digit rides AnimatePresence — the new readout fades in from below as the old one slides out, so rapid ticks feel responsive without flicker.
  6. Endpoint labels. 0 sits under the left endpoint, the target value sits under the right. Both render in cb-fg-subtle so they read as ambient axis labels rather than data.
  7. Reduced motion. Under prefers-reduced-motion: reduce, the arc transition, value swap, and delta beat all collapse to instant. The digits still update; only the motion drops.

Props

PropTypeDefaultDescription
valuenumberrequiredCurrent accumulator reading.
targetnumberrequiredCeiling for the gauge — rightmost needle position.
previousValuenumberPrior reading. When set and different from value, fires a delta beat.
labelReactNodeOptional short label rendered above the dial.
sizenumber140Diameter in pixels. Floored at 80.
ariaLabelstringOverride the announced string. Defaults to the canonical "Accumulator: value of target" form.
transitionTransitionSPRINGS.smoothArc / value-swap / delta-beat transition. Reduced-motion users snap regardless.
classNamestringMerged onto the root via cn().

Accessibility

  • The outer <svg> is role="img" with a <title> derived from ariaLabel or the canonical "Accumulator: value of target" string. Screen readers hear both the current value and the ceiling without parsing the SVG geometry.
  • The dial exposes data-cb-viz="accumulator-dial" on the root so consumer apps can hook custom styles or assistive tooling.
  • Value is never the only signal — the center digit is always visible, the arc fills with a colour-independent length, and the endpoint labels remain rendered.
  • Motion respects prefers-reduced-motion: reduce — the arc transition, value swap, and delta beat all collapse to instant. The text content still updates.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/math/AccumulatorDial.tsx). The source was a lesson-specific Adagrad explainer that hardwired the gradient stream, the step counter, the phase-based narration palette, and the lesson chrome. The craft-bits version strips the simulation and the lesson chrome, accepts a value / target / previousValue triple so the same dial covers any accumulating quantity, and reduces the API to the minimum surface needed to plug it into an arbitrary parent.