Scatter Projector

A square SVG plane that visualizes projecting a 2D scatter cloud onto a chosen direction. The user rotates the projection axis with a slider; the component drops a perpendicular from every point onto the axis, lands a projected dot at each foot, and updates a 1D histogram of projected values beneath the plot. Used to teach scalar projection, variance along an axis, and the intuition behind PCA and random projections.

Projecting 20 points onto direction 45 degrees. Projected variance 4.34.
Customize
Angle
45°
Data
elongated
Overlays

Installation

npx shadcn@latest add https://craftbits.dev/r/scatter-projector.json

Usage

import { ScatterProjector } from "@craft-bits/core";
 
<ScatterProjector
  points={[
    { x: 1.2, y: 0.5 },
    { x: 2.4, y: 1.1 },
    { x: -1.0, y: -0.4 },
  ]}
  defaultTheta={Math.PI / 4}
/>

Control the angle from a parent so an external animation can sweep it:

const [theta, setTheta] = useState(0);
 
<ScatterProjector
  points={cloud}
  theta={theta}
  onThetaChange={setTheta}
/>

Hide the perpendicular drops and the histogram for a minimal embed:

<ScatterProjector
  points={cloud}
  showProjectionLines={false}
  showHistogram={false}
/>

Understanding the component

  1. One direction, one dot product. Every projection is the scalar product of the point with the unit vector u = (cos θ, sin θ). For a point p the foot on the axis is t · u, with signed length t = p.x · cos θ + p.y · sin θ.
  2. Rotate the line, watch the spread. Aligning the axis with the cloud's long direction keeps projected dots far apart (high projected variance); rotating 90 degrees collapses them toward the origin. The 1D histogram makes the change quantitative — the wider the bars, the more variance survives.
  3. Native slider as the primary controller. A single range input sweeps theta across [0, 2π] in one-degree steps. Browsers ship full a11y for free — Tab to focus, arrow keys to nudge, screen readers announce the current degree value.
  4. Auto-fit viewport. The SVG plane sizes itself to the maximum absolute coordinate in points with a 15 percent margin — no range prop. The projection axis extends to the viewport diagonal so it always covers the visible plane regardless of theta.
  5. Two colors. Original scatter points draw in cb-fg-muted; the axis and projected dots draw in cb-accent; the perpendicular drops draw in cb-fg-muted at 55 percent opacity with a dashed pattern.
  6. Histogram bins over a stable range. Bin edges span the viewport's diagonal radius regardless of theta, so widths don't change as the axis rotates — only the heights animate.
  7. Two spring tiers. Projection axis, foot dots, and perpendicular drops follow with SPRINGS.smooth. Histogram bars use SPRINGS.snap so changes in counts feel crisp. prefers-reduced-motion: reduce collapses both to duration: 0.
  8. Controlled and uncontrolled. Pass theta plus onThetaChange for controlled, or defaultTheta for uncontrolled. The default angle is 0 — the projection axis aligned with the positive x-axis.

Props

PropTypeDefaultDescription
pointsreadonly ScatterPoint[]required2D points to project.
thetanumberControlled projection angle in radians. Pair with onThetaChange.
defaultThetanumber0Uncontrolled initial angle in radians.
onThetaChange(theta: number) => voidFires on every slider change.
showProjectionLinesbooleantrueDrop a dashed perpendicular from each point onto the axis.
showHistogrambooleantrueRender a 1D histogram of projected scalars beneath the plot.
histogramBinsnumber12Number of histogram bins.
sizenumber320SVG side length in pixels (the plane is square).
transitionTransitionSPRINGS.smoothSpring for projected-dot and projection-line transitions.
classNamestringMerged onto the root via cn().

The ScatterPoint shape:

FieldTypeDescription
xnumberx coordinate in math space.
ynumbery coordinate in math space.

Accessibility

  • The scatter SVG is role="figure" with an aria-labelledby heading also rendered as a visually hidden aria-live="polite" summary — screen readers hear the projected variance and current degree whenever theta changes.
  • The histogram SVG is role="img" with an aria-label describing the projected-value distribution.
  • The slider is a native range input carrying aria-valuemin / aria-valuemax / aria-valuenow / aria-valuetext. Keyboard parity is automatic.
  • Motion respects prefers-reduced-motion: reduce — every spring collapses to an instant swap.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/math/ScatterProjector.tsx). The source paired the visualization with an Observe / PC1 / PC2 mode strip, narration heuristics, baked-in spread-bar overlays, and seeded Box-Muller cloud generation driven by eigenvalue and rotation props. The library extract is the pure visualization primitive — the caller supplies the points and the angle. Mode strips, narration, and seeded cloud generators belong in the consuming lesson, not the primitive.