Algebra Geometry Bridge

A two-panel layout for visualising the correspondence between an algebraic identity and its geometric interpretation. The left panel renders the identity plus one slider per declared parameter; the right panel renders a caller-supplied SVG construction; between them sits an animated equivalence glyph that pulses whenever the shared derived value changes.

Generic over the underlying identity: works for vector dot product, polar ↔ cartesian, slope-intercept lines, quadratic factoring, complex multiplication, or any algebraic identity with a geometric reading.

Algebra

a · b = a₁·b₁ + a₂·b₂
a₁2.0
a₂1.0
b₁3.0
b₂4.0

Algebra ≡

10.00

Geometry

Geometry ≡

10.00

Customize
Identity
dot
Geometry
240 px

Installation

npx shadcn@latest add https://craftbits.dev/r/algebra-geometry-bridge.json

Usage

import { AlgebraGeometryBridge } from "@craft-bits/viz/algebra-geometry-bridge";
 
<AlgebraGeometryBridge
  equation="a · b = a₁·b₁ + a₂·b₂"
  params={params}
  onParamsChange={setParams}
  paramDefs={[
    { key: "ax", label: "a₁", min: -5, max: 5 },
    { key: "ay", label: "a₂", min: -5, max: 5 },
    { key: "bx", label: "b₁", min: -5, max: 5 },
    { key: "by", label: "b₂", min: -5, max: 5 },
  ]}
  result={(p) => (p.ax * p.bx + p.ay * p.by).toFixed(2)}
  geometry={({ params, size }) => (
    /* SVG content drawing the two vectors from the origin */
    <g>...</g>
  )}
/>

Drop the result prop entirely for a bridge with no shared scalar — just sliders ↔ a geometric drawing:

<AlgebraGeometryBridge
  equation="y = m·x + b"
  params={params}
  onParamsChange={setParams}
  paramDefs={[
    { key: "m", label: "m", min: -3, max: 3 },
    { key: "b", label: "b", min: -3, max: 3 },
  ]}
  geometry={({ params, size }) => (/* draw the line */)}
/>

Override the panel labels when "Algebra" / "Geometry" are too generic:

<AlgebraGeometryBridge
  algebraLabel="Cartesian"
  geometryLabel="Polar"
  equation="x = r·cos θ, y = r·sin θ"
  /* ... */
/>

Understanding the component

  1. Two parallel panels. A tinted accent rectangle on the left for the algebraic identity, a neutral rectangle on the right for the geometric drawing. Each panel is a labelled <section> so screen readers announce a clear boundary.
  2. Controlled parameter map. params is a Record<string, number> flat map. onParamsChange is invoked with the next full map whenever a slider moves or the geometry render-prop calls setParam from a pointer handler. The component never owns state.
  3. One slider per paramDefs entry. Each definition declares the param key, the visible label, and the min / max / step. Optional format re-renders the numeric readout (e.g. degrees, percent).
  4. Geometry render prop. geometry receives { params, setParam, size } and returns SVG content placed inside <svg viewBox="0 0 size size">. Drag handles can call setParam directly to drive the algebra side from a pointer interaction.
  5. Shared result. When result is set, both panels render Algebra ≡ / Geometry ≡ readouts underneath, both showing the same scalar — the visual claim is that the two computations agree.
  6. Equivalence bridge. The glyph between the panels remounts on every change of result(params) and runs a one-shot scale + opacity pulse on SPRINGS.snap. Under prefers-reduced-motion: reduce the pulse collapses to a static render.

Props

PropTypeDefaultDescription
equationReactNoderequiredAlgebraic identity rendered above the slider stack.
geometry(args) => ReactNoderequiredRender prop returning SVG content. Receives params, setParam, size.
paramsRecord<string, number>requiredControlled parameter map.
onParamsChange(next) => voidrequiredCalled with the full next map whenever any param changes.
paramDefsParamDef[]requiredSlider definitions for the algebra side.
result(params) => ReactNodeOptional shared derived value, rendered under both panels.
algebraLabelReactNode"Algebra"Label above the algebra panel.
geometryLabelReactNode"Geometry"Label above the geometry panel.
geometrySizenumber240Side length (px) of the geometry SVG viewBox.
transitionTransitionSPRINGS.snapEquivalence-pulse transition.
classNamestringMerged onto the root via cn().

Accessibility

  • Both panels are <section> elements with aria-labelledby linked to their visible title, so screen readers announce a clear panel boundary.
  • Each slider gets an aria-label derived from the param def's label (falling back to the key).
  • The geometry <svg> is role="img" with a generic "Geometric construction" label — callers wanting a richer description can wrap with their own labelled element or place a <title> inside the render prop.
  • The equivalence bridge is aria-hidden — the equivalence is already encoded by the matching Algebra ≡ / Geometry ≡ readouts.
  • Colour is never the only signal — the algebra panel uses both an accent tint and an accent label; the geometry panel uses a neutral tint and a muted label.
  • Motion respects prefers-reduced-motion: reduce — the equivalence pulse collapses to a static render. Slider interactions are unaffected.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/math/AlgebraGeometryBridge.tsx). The source was a fully hand-rolled dot-product lesson: it baked in two specific vectors, a hard-coded a·b = |a||b|cos θ identity, three modes (explore / predict / challenge) wired to the lesson chrome, in-component scoring, and a custom drag-the-vector-tips SVG plane. The viz extract reduces all of that to a generic two-panel layout — algebra (sliders) ↔ geometry (caller-supplied SVG) — so the same component drives any algebraic identity with a geometric interpretation. Modes, scoring, and lesson-specific chrome live outside the component.