Eigenvector Transform Viz
A square SVG plane that animates a single unit vector and the image of that vector under a 2x2 matrix A. As the angle sweeps the circle, the input arrow (dim) and the output arrow (accent) point in different directions. When the input direction aligns with a real eigenvector line, both arrows become collinear — only the length of the output arrow changes, scaled by the eigenvalue. Eigenvectors are the directions the transform leaves invariant.
Unit vector at angle 0.00 degrees. Image (2.00, 1.00).
theta0.00 degA v(2.00, 1.00)free direction
Customize
Row 1 (a, b)
2
1
Row 2 (c, d)
1
2
Display
0.6
Installation
npx shadcn@latest add https://craftbits.dev/r/eigenvector-transform-viz.jsonUsage
import { EigenvectorTransformViz } from "@craft-bits/core";
<EigenvectorTransformViz
defaultMatrix={[
[2, 1],
[1, 2],
]}
playing
/>Drive both matrix and angle as controlled values:
const [matrix, setMatrix] = useState<EigenvectorTransformMatrix2D>([
[2, 1],
[1, 2],
]);
const [theta, setTheta] = useState(0);
<EigenvectorTransformViz
matrix={matrix}
onMatrixChange={setMatrix}
theta={theta}
onThetaChange={setTheta}
/>A diagonal stretch — axis-aligned eigenvectors at (1, 0) and (0, 1):
<EigenvectorTransformViz
defaultMatrix={[[3, 0], [0, 1]]}
playing
playSpeed={0.8}
/>Hide the eigenvector lines to focus on the bare input / image arrows:
<EigenvectorTransformViz
defaultMatrix={[[2, 1], [1, 2]]}
showEigenvectors={false}
playing
/>Understanding the component
- Single sweeping vector. Unlike a full unit-circle cloud, this primitive isolates one vector
v(theta) = (cos theta, sin theta)and its imageA v. With one input direction in motion the geometry of alignment is unambiguous. - Closed-form 2x2 eigen-decomposition. Eigenvalues are the roots of the characteristic polynomial
lambda squared minus trace times lambda plus det = 0. The discriminanttrace squared minus 4 detdecides the branch — non-negative gives two real eigenvalues, negative gives a conjugate pair. - Unit eigenvectors from the null space. For each real eigenvalue, the unit eigenvector is solved from
A minus lambda I times v equals 0and normalized. A numerically safe branch picks whichever row ofA minus lambda Ihas larger magnitude — that avoids dividing by a near-zero pivot at scalar multiples of the identity. - Alignment is a thresholded dot product. The component computes the absolute dot product between the input direction and each eigenvector's unit vector. When that value exceeds
cos(0.06 rad) ≈ 0.998the input is considered "aligned" — the input arrow recolours to accent and the readout calls out the eigenvalue. The output arrow stays collinear; the only visible change is its length (scaled by lambda). - Complex case fallback. When the matrix has complex eigenvalues, the eigenvector lines are omitted entirely (no real invariant direction exists) and the readout calls out
complex eigenvalues. The input and image arrows continue to animate so rotation matrices still visualize sensibly. - Auto-fit viewport. The viewport is sized so the largest of the eigenvalue magnitudes and the matrix's row norms comfortably fits, with a 30 percent margin. Override with
rangewhen you need a fixed window. - RAF autoplay. When
playingis true the component drivesthetaforward atplaySpeedradians per second viarequestAnimationFrame. Each frame walksthetamodulo2 piand firesonThetaChange. The loop cleans up on unmount and onplayingflipping back to false. - Spring transitions. The two arrows animate to their new endpoints with
SPRINGS.smooth.prefers-reduced-motion: reducecollapses every spring toduration: 0and pauses the autoplay entirely.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
matrix | EigenvectorTransformMatrix2D | — | Controlled 2x2 matrix. Pair with onMatrixChange. |
defaultMatrix | EigenvectorTransformMatrix2D | [[2, 1], [1, 2]] | Uncontrolled initial matrix. |
onMatrixChange | (next: EigenvectorTransformMatrix2D) => void | — | Fires when the matrix changes. |
theta | number | — | Controlled vector angle in radians. Pair with onThetaChange. |
defaultTheta | number | 0 | Uncontrolled initial vector angle in radians. |
onThetaChange | (next: number) => void | — | Fires whenever the vector angle changes. |
showEigenvectors | boolean | true | Render the dashed eigenvector lines through the origin. |
playing | boolean | false | When true, sweep theta at playSpeed radians per second. |
playSpeed | number | 0.6 | Angular sweep speed in radians per second. |
range | readonly [number, number] | auto | Visible math-space domain on both axes. |
size | number | 320 | SVG side length in pixels (the plane is square). |
transition | Transition | SPRINGS.smooth | Spring for input / image arrow transitions. |
className | string | — | Merged onto the root <div> via cn(). |
The EigenvectorTransformMatrix2D shape is a row-major tuple — [[a, b], [c, d]] represents
| a b |
| c d |
Accessibility
- The SVG is
role="img"with anaria-labelledbyheading also rendered as a visually hiddenaria-live="polite"summary — assistive tech announces the current angle, image coordinates, and alignment status as they change. - The visualization is read-only inside the plot; consumers wire any external slider, play/pause control, or matrix entry that drives
thetaandmatrix. - The readout uses tabular numerals so columns stay aligned.
- Animation respects
prefers-reduced-motion: reduce— every spring collapses to an instant swap and autoplay is paused.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/viz/EigenvectorTransformViz.tsx). The source shipped a 12-vector cloud with explore/predict/challenge modes, multi-preset toggles, narration heuristics, and a click-to-classify game. The library extract is the pure single-vector primitive — one input vector, its image, the eigenvector lines, the alignment readout, and an optional RAF sweep.