Loss Landscape

A 2D loss surface drawn as a grid heatmap, with an optional optimizer trajectory overlaid on top. Pick one of the four built-in surfaces (convex bowl, saddle, double-well, Rosenbrock) or pass a custom function — the component samples a gridSize x gridSize heatmap, draws axes through the origin when visible, and springs a highlighted dot to whichever trajectory step you scrub to.

Loss landscape: bowl.Loss landscape bowl. Step 13 of 13. Position (-0.01, 0.01). Loss 0.000.
Customize
Landscape
bowl
Trajectory
12
Resolution
32

Installation

npx shadcn@latest add https://craftbits.dev/r/loss-landscape.json

Usage

import { LossLandscape } from "@craft-bits/core";
 
<LossLandscape landscape="bowl" />

Drop in a trajectory and scrub through it:

<LossLandscape
  landscape="rosenbrock"
  trajectory={[
    { x: -1.5, y: 1.5 },
    { x: -1.2, y: 1.2 },
    { x: -0.6, y: 0.4 },
    { x:  0.2, y: 0.0 },
    { x:  1.0, y: 1.0 },
  ]}
  currentStep={2}
/>

Pass a custom function and a different colour ramp:

<LossLandscape
  landscape={{ fn: (x, y) => Math.sin(x) * Math.cos(y) + 0.1 * (x * x + y * y) }}
  xRange={[-3, 3]}
  yRange={[-3, 3]}
  colorScheme="viridis"
/>

Understanding the component

  1. Heatmap, not contours. Each of gridSize x gridSize cells samples the surface at its centre, then maps the value to a fill color. The default accent scheme tints --cb-accent by intensity; viridis and plasma use three-stop perceptual ramps in oklch so the gradient stays legible under light and dark themes.
  2. Four built-in surfaces. bowl is the convex baseline. saddle has zero gradient at the origin — SGD stalls there. double-well has two minima at (plus or minus 1, 0) — gradient descent is trapped by whichever basin it starts in. rosenbrock is the classic narrow curved valley.
  3. Custom surfaces via { fn }. Any pure (x, y) -> number plugs in directly; the component normalises the visible range automatically across the sampled grid.
  4. Trajectory is fully optional. When omitted, the heatmap stands alone. When present, the trajectory renders as a polyline + dots, and the dot at currentStep is enlarged and springs into position with SPRINGS.smooth from @craft-bits/core/motion.
  5. Cell-centre sampling. Each grid cell samples the surface once at its centre — clean enough for 32x32 without flickering edges. Bump gridSize to 48 for smoother gradients (and a slightly heavier DOM).
  6. Reduced-motion fallback. With prefers-reduced-motion: reduce, the highlighted dot snaps to position with no spring; no per-step motion is shown.

Props

PropTypeDefaultDescription
landscape'bowl' | 'saddle' | 'double-well' | 'rosenbrock' | { fn }'bowl'Loss surface to draw.
xRangereadonly [number, number][-2, 2]Visible math-space x-range.
yRangereadonly [number, number][-2, 2]Visible math-space y-range.
gridSizenumber32Heatmap resolution in cells per side.
trajectoryreadonly LossLandscapePoint[]Optional optimizer trajectory to overlay.
currentStepnumberControlled highlighted step. Pair with onCurrentStepChange.
defaultCurrentStepnumber0Uncontrolled initial step.
onCurrentStepChange(step: number) => voidFires when the highlighted step changes.
colorScheme'accent' | 'viridis' | 'plasma''accent'Heatmap color ramp.
sizenumber360SVG side length in pixels.
transitionTransitionSPRINGS.smoothSpring for the current-dot.
classNamestringMerged onto the root via cn().

Accessibility

  • The figure is role="figure" with a labelled title that names the active surface.
  • An aria-live="polite" summary announces the surface, the current step index, the math-space position, and the loss value whenever they change.
  • Color is never the only signal — when a trajectory is present, the summary spells out the position and loss textually, so the figure remains readable without color.
  • prefers-reduced-motion: reduce snaps the highlighted dot with no spring.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/math/LossLandscape.tsx). Stripped the lesson-specific Observe/Predict modes, prediction-quiz scoreboard, click-to-place-start interaction, transport bar, undo/redo plumbing, and the built-in three-optimizer simulation (now handled by OptimizerRacetrack). Generalised to a pure 2D-surface primitive: four built-in landscapes, custom { fn } support, three color ramps, and an optional trajectory overlay driven by currentStep.