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.
Installation
npx shadcn@latest add https://craftbits.dev/r/amortized-analysis.jsonUsage
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
- One bar per op. Each entry in
opsrenders as a vertical bar sized tocost / maxCost. The y-axis is anchored at zero so the spike heights read truthfully against the cheap-op baseline. - Kind drives colour.
cheappaints in neutral foreground,spikepaints in accent,neutralpaints as undifferentiated background scaffolding. Only the fill / stroke / cost label change — the layout is identical across kinds. - 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.
- Click-to-scrub. Tap any bar to set
stepto that bar's 1-based index. Lit ops draw in their kind colour with their cost label; un-lit ops fade to background. Pair withstep+onStepChangefor controlled mode,defaultStepfor uncontrolled. - 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.
- maxCost as a stability prop. When omitted,
maxCostis derived from the largest cost inops. Pass it explicitly to keep the y-axis stable when you mutateopsor scrub past a spike — otherwise the bar heights re-scale. - Reduced motion. Bar enter animation, value transitions, and the amortized-line slide all collapse to instant under
prefers-reduced-motion: reduce.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
ops | AmortizedOp[] | required | One entry per operation in the sequence. |
step | number | — | Controlled current step. Pair with onStepChange. |
defaultStep | number | ops.length | Uncontrolled initial step. |
onStepChange | (step: number) => void | — | Fires when a bar is clicked, with the 1-based step. |
maxCost | number | derived | Maximum cost the y-axis accommodates. Pin it for a stable axis. |
interactive | boolean | true | When false, bars are not clickable or focusable. |
showAmortizedLine | boolean | true | When false, the running-average line is hidden. |
barWidth | number | 18 | Bar width in pixels. |
barGap | number | 4 | Gap between bars in pixels. |
plotHeight | number | 140 | Plot area height in pixels. |
transition | Transition | SPRINGS.smooth | Override bar / line transitions. Reduced-motion users snap regardless. |
className | string | — | Merged onto the <svg> root via cn(). |
Accessibility
- The outer
<svg>isrole="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"withtabIndex={0}, an explicitaria-labelnaming 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-interactiveanddata-stepon the root, anddata-stateplusdata-kindon 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.