Gradient Compass
A contour plot of a 2D scalar field with a draggable probe. At the probe a three-arrow compass visualises the gradient as a composition of its partial derivatives — the horizontal arrow is ∂f/∂x, the vertical arrow is ∂f/∂y, and the diagonal arrow is the resultant ∇f. Dashed construction lines show the parallelogram-of-components, so the eye sees how the two partials compose into the gradient vector.
Generic over the underlying field. Callers supply f and gradient, so the same component drives the bowl x² + y², a saddle x² − y², a Rosenbrock-style ridge, or any other smooth field with a closed-form gradient.
Point at (1, 2). Gradient [2, 4], magnitude 4.5.
Customize
Field
bowl
Interaction
0.25
16 px / unit
Installation
npx shadcn@latest add https://craftbits.dev/r/gradient-compass.jsonUsage
import { useState } from "react";
import { GradientCompass } from "@craft-bits/viz/gradient-compass";
const [point, setPoint] = useState({ x: 1, y: 2 });
<GradientCompass
point={point}
onPointChange={setPoint}
f={(x, y) => x * x + y * y}
gradient={(x, y) => [2 * x, 2 * y]}
domain={{ min: -3, max: 3 }}
contourLevels={[1, 2, 4, 6, 8]}
/>;Swap in a saddle and drop the contour circles when they no longer match the level sets:
<GradientCompass
point={point}
onPointChange={setPoint}
f={(x, y) => x * x - y * y}
gradient={(x, y) => [2 * x, -2 * y]}
contourLevels={[]}
/>Snap to integers for whole-number exploration:
<GradientCompass
point={point}
onPointChange={setPoint}
snapStep={1}
keyboardStep={1}
/>Understanding the component
- Domain → SVG transform. The component renders the rectangle
[domain.min, domain.max]²into the SVGviewBox. Mathxgrows rightward, mathygrows upward — the SVGyaxis is flipped internally so the plot reads like a textbook diagram. - Contour circles. Each level
cincontourLevelsbecomes a circle of radiussqrt(c)around the origin — the level set ofx² + y² = c. With a differentf, those circles stop being true level sets but still encode distance from the origin; non-radial fields will typically passcontourLevels={[]}and overlay their own. - Probe point. Controlled via
point/onPointChange. Drag updates round to multiples ofsnapStep(default0.25); arrow-key updates step bykeyboardStep(Shiftdoubles). Both clamp to the domain. - Compass arrows. The probe spawns three arrows — horizontal
∂f/∂x, vertical∂f/∂y, and the diagonal gradient∇f. Dashed construction lines connect the gradient tip to each partial tip, making the parallelogram-of-components visible. Arrows fade when|∇f|falls below a small threshold so the compass disappears near a stationary point. - Animated
|∇f|readout. A monospaced magnitude readout in the top-right tweens between values onSPRINGS.snap. Reduced-motion users see instant snaps. - Reduced motion. Magnitude tweening collapses to instant under
prefers-reduced-motion: reduce. Drag and keyboard interactions are unaffected.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
point | { x: number; y: number } | required | Current probe location in math coordinates. Controlled. |
onPointChange | (next) => void | required | Called with the next clamped/snapped probe location. |
f | (x, y) => number | (x, y) => x² + y² | Scalar field. Used for the aria-label. |
gradient | (x, y) => [number, number] | (x, y) => [2x, 2y] | Gradient of f. Drives the three arrows and the magnitude readout. |
domain | { min: number; max: number } | { min: -3, max: 3 } | Mathematical range rendered along each axis. |
snapStep | number | 0.25 | Snap step for drag updates. 0 disables snapping. |
keyboardStep | number | 0.25 | Arrow-key step. Shift doubles. |
contourLevels | number[] | [1, 2, 4, 6, 8] | Level values to draw as circles around the origin. |
arrowScale | number | 16 | Pixels per unit of gradient. |
arrowMaxPx | number | 85 | Hard cap on arrow length. |
width | number | 420 | SVG viewBox width. |
height | number | 380 | SVG viewBox height. |
ariaLabel | string | — | Override the SVG aria-label. |
transition | Transition | SPRINGS.snap | Magnitude readout transition. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- The SVG is
role="img"with anaria-labelsummarising the current probe, gradient components, and magnitude. Override viaariaLabel. - The drag/keyboard hit target is
role="slider"witharia-valuetextreporting the current(x, y). Arrow keys move the probe;Shiftdoubles the step. - A polite live region announces the probe location and gradient outside of active drag (drag updates are silent to avoid flooding the buffer).
- The decorative arrows, axes, contour circles, and component labels are
aria-hidden— the same information is encoded in the SVG label and the live region. - Colour is never the only signal — every arrow has both a distinct hue and an attached
∂f/∂x/∂f/∂y/∇flabel, and the focus ring on the probe is rendered with both a stronger stroke width and an accent hue when keyboard focus is present. - Motion respects
prefers-reduced-motion: reduce— the magnitude readout snaps instantly rather than tweening.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/math/GradientCompass.tsx). The source hardwiredf(x, y) = x² + y², baked in a six-branch narration generator keyed off ahasEverDraggedfirst-drag flag, ran an idle "breathing" SVG animation on the probe, and rendered a fixedca-narrationpanel below the plot. The viz extract reduces it to a generic, controlled compass: callers supplyfandgradient, while the lesson-specific narration, breathing animation, and embedded copy live outside the component.