Gradient Step On Curve

One-dimensional gradient descent on the loss parabola L(w) = (w − wOpt)². A dot sits on the curve at the current weight. The learner tunes a learning-rate slider and taps Step to apply one update — w_new = w − lr × dL/dw — and watches the dot spring along the curve toward (or past) the minimum.

A live tangent line shows the local slope at the probe, a faint horizontal arrow previews where the next step will land, and the walker auto-classifies its behaviour as start / stepping / converged / overshooting / diverging from the loss trajectory — the dot, tangent, trail, and narration all recolour to match.

Small learning rates settle in many tiny steps. Medium learning rates converge cleanly. Large learning rates bounce past the minimum. Very large learning rates diverge — the loss grows step over step and the dot turns red.

Gradient descent on the parabola L of w equals w minus 3 all squared.
Step 0. Weight: 0, Loss: 9.0, Learning rate: 0.10. Descending.

Loss is 9.0 at w = 0. The slope says go right. Choose a learning rate and click Step.

Customize
Parabola
0.0
3.0
Walker
0.10
0.05
100 steps

Installation

npx shadcn@latest add https://craftbits.dev/r/gradient-step-on-curve.json

Usage

import { GradientStepOnCurve } from "@craft-bits/viz/gradient-step-on-curve";
 
<GradientStepOnCurve />

Pre-load a learning rate that will overshoot:

<GradientStepOnCurve defaultLearningRate={0.4} />

Re-key the parabola — descend toward w = 5:

<GradientStepOnCurve startWeight={1} optimalWeight={5} />

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

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

Understanding the component

  1. The plot. A wide SVG renders the w ∈ [−1, 7], L ∈ [0, 16] window with a faint grid and a dashed target marker at (wOpt, 0). The parabola is sampled once on first render — its shape never changes during a step.
  2. The dot. The probe is a glowing filled circle that sits on the curve. Step animation interpolates w from start to end and imperatively writes cx / cy on the dot, glow, and tangent line via motion's animate() — no React re-render per frame.
  3. Tangent line. A short dashed segment passes through the probe with slope 2(w − wOpt). It updates live during the step so the slope visibly flattens near the minimum and flips sign on overshoot.
  4. Step preview. A horizontal dashed arrow at the bottom of the plot shows where w will land if the learner taps Step at the current learning rate. The arrow lengthens with lr and shrinks as the gradient magnitude decreases.
  5. Phase machine. The walker classifies its own behaviour from the loss trajectory: start (only one record), stepping (loss decreasing), converged (loss below threshold), overshooting (the probe crossed wOpt), diverging (loss grew significantly). The phase drives every accent colour and the narration copy.
  6. Trail. Every visited weight is drawn as a small ghost dot connected by a dashed polyline. The trail recolours along with the phase.
  7. In-SVG learning-rate slider. The slider lives at the bottom of the SVG with ticks at the canonical 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 idle, converged, and divergence pulses all disable.

Props

PropTypeDefaultDescription
startWeightnumber0Initial weight. Clamped to the visible domain.
optimalWeightnumber3The minimum of L(w) = (w − optimalWeight)².
defaultLearningRatenumber0.1Initial slider value. Clamped to the slider range.
convergeThresholdnumber0.05Loss 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.
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 weight, 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 weight, 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, and divergence pulses all disable.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/math/GradientStepOnCurve.tsx). The source was a tightly bundled lesson component — it consumed SvgLabel and ChallengeBtn from the lesson chrome, hardcoded the parabola at (w − 3)², depended on per-track lesson palette tokens, and inlined its own ad-hoc spring names. The viz extract drops the lesson chrome, generalises the parabola via startWeight and optimalWeight props, 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.