Amortized Analysis

A self-contained <svg> timeline of per-operation cost. Pass an ops array of { cost, kind } entries; the component lays out one bar per op coloured by kind (cheap, spike, neutral) and overlays the running average across the lit prefix as a dashed reference line. The canonical use case is dynamic array doubling — most pushes cost 1, the occasional grow costs O(n), and the running average stays bounded around 2.

Generic enough to cover any "rare spike, common cheap" analysis: stack with multipop, splay-tree, union-find with path compression, the accounting / banker / potential method. The component does not bake in any specific algorithm — it plots cost-per-op and the running mean, and lets the caller pick the colours and the scrub step.

Amortized cost timeline of 16 operations (cheap, spike, cheap, spike, cheap, cheap, cheap, spike, cheap, cheap, cheap, cheap, cheap, cheap, cheap, spike). Running average at step 16: 1.94.112grow133grow1516175grow191101111121131141159grow
Customize
Layout
18
140
Behaviour

Installation

npx shadcn@latest add https://craftbits.dev/r/amortized-analysis.json

Usage

import { AmortizedAnalysis } from "@craft-bits/core";
 
<AmortizedAnalysis
  ops={[
    { cost: 1, kind: "cheap" },
    { cost: 2, kind: "spike", label: "grow" },
    { cost: 1, kind: "cheap" },
    { cost: 1, kind: "cheap" },
    { cost: 5, kind: "spike", label: "grow" },
  ]}
/>

Controlled scrub — parent owns the step, click a bar to advance:

const [step, setStep] = useState(ops.length);
 
<AmortizedAnalysis
  ops={ops}
  step={step}
  onStepChange={setStep}
/>

Read-only static chart (no click handlers, no focusable bars):

<AmortizedAnalysis ops={ops} interactive={false} />

Hide the running-average line until later in a lesson:

<AmortizedAnalysis ops={ops} showAmortizedLine={false} />

Understanding the component

  1. One bar per op. Each entry in ops renders as a vertical bar sized to cost / maxCost. The y-axis is anchored at zero so the spike heights read truthfully against the cheap-op baseline.
  2. Kind drives colour. cheap paints in neutral foreground, spike paints in accent, neutral paints as undifferentiated background scaffolding. Only the fill / stroke / cost label change — the layout is identical across kinds.
  3. Running average reference line. The dashed accent line tracks the sum of costs across the lit prefix divided by step. As the cursor walks past each spike, the line jumps up, then trends back down as cheap ops dilute the average.
  4. Click-to-scrub. Tap any bar to set step to that bar's 1-based index. Lit ops draw in their kind colour with their cost label; un-lit ops fade to background. Pair with step + onStepChange for controlled mode, defaultStep for uncontrolled.
  5. Hit target. Each interactive bar carries an invisible 44px-wide hit rectangle behind it so narrow bars still satisfy WCAG 2.5.8 — an 8px bar on a 4px gap is still safely tappable on mobile.
  6. maxCost as a stability prop. When omitted, maxCost is derived from the largest cost in ops. Pass it explicitly to keep the y-axis stable when you mutate ops or scrub past a spike — otherwise the bar heights re-scale.
  7. Reduced motion. Bar enter animation, value transitions, and the amortized-line slide all collapse to instant under prefers-reduced-motion: reduce.

Props

PropTypeDefaultDescription
opsAmortizedOp[]requiredOne entry per operation in the sequence.
stepnumberControlled current step. Pair with onStepChange.
defaultStepnumberops.lengthUncontrolled initial step.
onStepChange(step: number) => voidFires when a bar is clicked, with the 1-based step.
maxCostnumberderivedMaximum cost the y-axis accommodates. Pin it for a stable axis.
interactivebooleantrueWhen false, bars are not clickable or focusable.
showAmortizedLinebooleantrueWhen false, the running-average line is hidden.
barWidthnumber18Bar width in pixels.
barGapnumber4Gap between bars in pixels.
plotHeightnumber140Plot area height in pixels.
transitionTransitionSPRINGS.smoothOverride bar / line transitions. Reduced-motion users snap regardless.
classNamestringMerged onto the <svg> root via cn().

Accessibility

  • The outer <svg> is role="img" with a <title> summarising the op count, kinds, and current running average. Screen readers hear the chart without parsing the SVG geometry.
  • Every interactive bar is role="button" with tabIndex={0}, an explicit aria-label naming the op index, optional label, current cost, and scrub target, and Space / Enter keyboard activation that mirrors the click-to-scrub behaviour.
  • Each bar carries an invisible 44px-wide hit rectangle so narrow bars still satisfy WCAG 2.5.8 AAA on touch screens.
  • The component exposes data-interactive and data-step on the root, and data-state plus data-kind on every bar group so consumer apps can hook custom styles or assistive tooling.
  • Colour is never the only signal — the cost label renders on every lit bar regardless of kind, so colourblind users see the magnitude even when the spike / cheap hue is hard to discriminate. The amortized-line numeric label backs up the line position.
  • Motion respects prefers-reduced-motion: reduce.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/construction/AmortizedAnalysis.tsx). The source was a 2000-line lesson component bundling a six-phase reducer (misconception → token budget → invariant discovery → code bridge → what-if → done), audio cues, prediction gates, formula construction UI, and a per-phase scoring rollup. The library extract keeps only the visualisation primitive — an ops sequence, controlled / uncontrolled step scrub, running-average overlay — and lets the caller compose any reducer-driven scoring on top.