Dimension Changer

A 3-component input vector enters a 2×3 matrix "lens." Two values emerge on the other side and the third input component is projected away — it contributed to both dot products but landed in neither output slot.

The walkthrough runs as a three-phase narrative — idle, animating, revealed — advanced by a single action button or by a controlling parent via the phase / onPhaseChange API. The visual is the mental model behind every dimensionality-reduction technique: even if your input has more components than your matrix has rows, the dot-product machinery still runs across every input — the rows just stop earlier.

2 by 3 matrix lens. Input vector [1, 2, 4]. Press the action button to see dimensionality reduction.

A 3D vector is about to enter a 2×3 matrix lens. The matrix has 2 rows — it can only produce 2 outputs. One dimension will not make it through.

Customize
State
idle
Layout

Installation

npx shadcn@latest add https://craftbits.dev/r/dimension-changer.json

Usage

import { DimensionChanger } from "@craft-bits/viz/dimension-changer";
 
<DimensionChanger />

Drive the walkthrough from a parent:

const [phase, setPhase] = useState<DimensionChangerPhase>("idle");
 
<DimensionChanger phase={phase} onPhaseChange={setPhase} />

Render a custom action button instead of the built-in one:

<DimensionChanger
  hideAction
  renderAction={({ label, disabled, onClick }) => (
    <MyButton onClick={onClick} disabled={disabled}>{label}</MyButton>
  )}
/>

Swap in your own matrix and vector — any 2×3 / 3-vector pair renders correctly:

<DimensionChanger
  matrix={[
    [1, 0, 0],
    [0, 1, 0],
  ]}
  vector={[5, -3, 9]}
/>

Hide the narration when embedding inside an article that already explains the projection:

<DimensionChanger hideNarration />

Understanding the component

  1. Layout in three zones. The SVG splits horizontally into an input column (three stacked slots, one per vector component), a matrix lens framed by serif brackets, and an output column with two slots.
  2. Row-2 fades into the lens. When the visitor clicks "Send through", the third input slot translates right and fades — a physical metaphor for the component that will not survive the projection.
  3. Flow lines draw, cells highlight column-by-column. A dashed accent line draws from the input column into the lens, then row 0's three cells light up left-to-right at 200ms intervals, followed by row 1's three cells. The staggered highlights trace the two dot products literally.
  4. Outputs fan out and count up. Two fan lines draw to the output column, then each output text counter animates from 0 to its result. The third input slot stays gone; an italic "projected away" annotation pins underneath where it used to be.
  5. Breakdown below the SVG. Once revealed, two monospace lines show each dot product term-by-term. The factor that came from x[2] is coloured so the visitor can see exactly which contribution to each row came from the projected-away component.
  6. Reduced motion. Under prefers-reduced-motion: reduce, the entire timeline snaps to its revealed end-state without animating — the visual conclusion is preserved.

Props

PropTypeDefaultDescription
matrix[[n,n,n],[n,n,n]][[1,0,2],[3,1,-1]]The 2×3 lens. Row count fixed at 2; column count fixed at 3.
vector[n,n,n][1,2,4]The 3-component input vector.
phase"idle" | "animating" | "revealed"Controlled phase. Pair with onPhaseChange.
defaultPhase"idle" | "animating" | "revealed""idle"Initial phase for the uncontrolled API.
onPhaseChange(next) => voidFires after the component commits to the next phase.
transitionTransitionSPRINGS.snapOverride the spring for the row-2 input fade.
actionLabelsPartial<Record<phase, string>>Override the action-button label per phase.
narrationCopyPartial<Record<phase, string>>Override the narration copy per phase.
hideNarrationbooleanfalseHide the narration paragraph.
hideBreakdownbooleanfalseHide the per-row dot-product breakdown.
hideActionbooleanfalseHide the built-in action button.
renderAction({ phase, label, disabled, onClick }) => ReactNodeRender slot for a custom action button.
classNamestringMerged onto the root via cn().

Accessibility

  • The root carries aria-labelledby pointing to a hidden <span> summary of the current phase, so screen readers hear the narrative state without parsing the SVG.
  • The SVG itself is role="img" with the same aria-label, so consumers who slot the SVG into another tree still surface a readable name.
  • An aria-live="assertive" region under the SVG announces the revealed result ("Dimensionality reduction complete. Output vector: …") so non-sighted users hear the conclusion.
  • The action button is a real <button type="button"> with :focus-visible outline, disabled state during animation, and standard keyboard activation.
  • Status is never the only signal — colour signals (accent for row 0, warning for row 1) are reinforced by row position and the projected away annotation.
  • Motion respects prefers-reduced-motion: reduce — the entire timeline collapses to an instant attribute set with no springs running.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/math/DimensionChanger.tsx). The library extract drops the project-specific SvgLabel and ChallengeBtn chrome in favour of plain <text> and a cn()-styled <button>, swaps the per-tone --color-accent-* / --color-warn-* / --color-ink-* palette for the canonical --cb-accent / --cb-warning / --cb-fg token vars, replaces the inline SPRINGS.snappy reference with SPRINGS.snap, and adds a controlled phase / onPhaseChange API plus renderAction / narrationCopy / actionLabels slots so consumers can drive the walkthrough from a parent stepper, a keyboard, or a custom button.