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.json

Usage

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

  1. Single sweeping vector. Unlike a full unit-circle cloud, this primitive isolates one vector v(theta) = (cos theta, sin theta) and its image A v. With one input direction in motion the geometry of alignment is unambiguous.
  2. Closed-form 2x2 eigen-decomposition. Eigenvalues are the roots of the characteristic polynomial lambda squared minus trace times lambda plus det = 0. The discriminant trace squared minus 4 det decides the branch — non-negative gives two real eigenvalues, negative gives a conjugate pair.
  3. Unit eigenvectors from the null space. For each real eigenvalue, the unit eigenvector is solved from A minus lambda I times v equals 0 and normalized. A numerically safe branch picks whichever row of A minus lambda I has larger magnitude — that avoids dividing by a near-zero pivot at scalar multiples of the identity.
  4. 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.998 the 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).
  5. 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.
  6. 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 range when you need a fixed window.
  7. RAF autoplay. When playing is true the component drives theta forward at playSpeed radians per second via requestAnimationFrame. Each frame walks theta modulo 2 pi and fires onThetaChange. The loop cleans up on unmount and on playing flipping back to false.
  8. Spring transitions. The two arrows animate to their new endpoints with SPRINGS.smooth. prefers-reduced-motion: reduce collapses every spring to duration: 0 and pauses the autoplay entirely.

Props

PropTypeDefaultDescription
matrixEigenvectorTransformMatrix2DControlled 2x2 matrix. Pair with onMatrixChange.
defaultMatrixEigenvectorTransformMatrix2D[[2, 1], [1, 2]]Uncontrolled initial matrix.
onMatrixChange(next: EigenvectorTransformMatrix2D) => voidFires when the matrix changes.
thetanumberControlled vector angle in radians. Pair with onThetaChange.
defaultThetanumber0Uncontrolled initial vector angle in radians.
onThetaChange(next: number) => voidFires whenever the vector angle changes.
showEigenvectorsbooleantrueRender the dashed eigenvector lines through the origin.
playingbooleanfalseWhen true, sweep theta at playSpeed radians per second.
playSpeednumber0.6Angular sweep speed in radians per second.
rangereadonly [number, number]autoVisible math-space domain on both axes.
sizenumber320SVG side length in pixels (the plane is square).
transitionTransitionSPRINGS.smoothSpring for input / image arrow transitions.
classNamestringMerged 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 an aria-labelledby heading also rendered as a visually hidden aria-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 theta and matrix.
  • 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.