Descent Walker

A click-to-place gradient-descent visualizer on the contour plot of f(x, y) = x² + y². The learner drops a starting point anywhere on the surface, watches a negative-gradient arrow show the descent direction, tunes a learning-rate slider, and taps Step to walk one iteration downhill. A trail traces every visited position, and the walker auto-classifies its behaviour as placing / stepping / converged / overshooting / diverging from the loss trajectory — the dot, arrow, trail, and narration all recolour to match.

The recurrence is the canonical first lesson in optimization. Compute the gradient, take a step in the opposite direction, repeat. Small learning rates settle slowly. Medium learning rates converge cleanly. Large learning rates oscillate past the minimum. Very large learning rates diverge — the path spirals outward and the dot turns red.

Gradient descent walker on f of x, y equals x squared plus y squared.
Click on the contour plot to place your starting point.

Click anywhere on the surface to place your starting point. You are the optimizer -- you will walk downhill from there.

Customize
Walker
0.10
0.08
100 steps

Installation

npx shadcn@latest add https://craftbits.dev/r/descent-walker.json

Usage

import { DescentWalker } from "@craft-bits/viz/descent-walker";
 
<DescentWalker />

Set a different starting learning rate:

<DescentWalker defaultLearningRate={0.3} />

Subscribe to step events to drive an external loss-vs-step chart:

<DescentWalker
  onStep={(history) => {
    /* read history.map((h) => h.loss) */
  }}
/>

Understanding the component

  1. The plot. A square SVG renders a symmetric window around the origin with six concentric contour circles and a dashed crosshair at the origin marking the global minimum. The domain is symmetric so the bowl reads as a true paraboloid.
  2. Click to place. The learner clicks anywhere on the plot in the placing phase. The click is projected from screen to math coordinates via getScreenCTM, snapped to a quarter-unit grid, and clamped to the visible domain. Clicks too close to the origin are rejected — the point would already be converged.
  3. Negative-gradient arrow. At every static frame, an arrow from the dot points in the direction of the negative gradient. The arrow's pixel length scales linearly with the gradient magnitude and is capped so steep starts don't overwhelm the plot.
  4. Step. The action button computes the next position from the gradient and the current learning rate, then springs the dot to it via motion's animate() driving raw SVG attributes — no re-render per frame. After landing, the new point is pushed onto the history and the phase reclassifies.
  5. Phase machine. The walker classifies its own behaviour from the loss trajectory: placing (no point yet), stepping (loss decreasing), converged (loss below threshold), overshooting (sign flip on either axis), diverging (loss grew significantly). The phase drives every accent colour and the narration copy.
  6. Trail. Every visited position is drawn as a small dot, colour-coded green if the loss decreased from the previous step and red if it grew. A dashed polyline connects them so the eye traces the path. Trail dots fade in opacity from oldest to most recent.
  7. In-SVG learning-rate slider. The slider lives at the bottom of the SVG with ticks at the canonical learning-rate sweet spots. Pointer drag and keyboard both move it; Space or Enter while focused triggers Step so the entire interaction is keyboard-reachable.
  8. Reduced motion. Under prefers-reduced-motion: reduce, the step animation collapses to an instant attribute set, and the placing, converged, overshooting, and diverging pulses all disable.

Props

PropTypeDefaultDescription
defaultLearningRatenumber0.1Initial slider value. Clamped to the slider range.
convergeThresholdnumber0.08Loss below this enters the converged phase.
maxStepsnumber100Hard cap on iterations before Step disables.
transitionTransitionSPRINGS.smoothOverride the step animation transition.
onStep(history) => voidFires after each step, post-animation, with full history.
onPlace(point) => voidFires when a fresh starting point is placed.
onReset() => voidFires when the user clicks Reset.
classNamestringMerged onto the root via cn().

Accessibility

  • The plot SVG is role="img" with an aria-label summarising the current point, loss, learning rate, and step count.
  • The in-SVG learning-rate slider is role="slider" with min/max/now/text set, full keyboard support (arrow keys for plus/minus a step, Shift for five-step jumps, Home and End for bounds), and a visible focus ring.
  • Space or Enter on the focused slider triggers Step — the entire walker is operable without a mouse.
  • A live region below the buttons announces position, loss, learning rate, and phase after each step. It mutes while the slider is being dragged to avoid noisy updates.
  • The narration paragraph also has aria-live="polite" and reads as plain prose; it is the canonical explanation for each phase.
  • Colour is never the only signal — the phase is also encoded in the narration prose and the live-region status text.
  • Motion respects prefers-reduced-motion: reduce — the dot snaps instantly, and the idle, converged, overshoot, and divergence pulses all disable.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/math/DescentWalker.tsx). The source was a tightly bundled lesson component — it consumed SvgLabel and ChallengeBtn from the lesson chrome, depended on per-track lesson palette tokens, and inlined its own ad-hoc spring names. The viz extract drops the lesson chrome, remaps the colour palette to var(--cb-*) semantic tokens so consumer themes repaint freely, and re-keys the step animation to the canonical SPRINGS.smooth so all motion comes from the same place as every other craft-bits component.