Margin Visualizer

Two clouds of 2D class points sit on either side of a user-controlled line y = slope x + intercept. A semi-transparent band of half-width marginWidth, measured along the line's normal, hovers around the line — the buffer zone where a max-margin classifier doesn't want any points. Points inside the band (or on the wrong side of the line for their class) light up as violations. The closest representatives of each class — the support vectors of a hard-margin classifier — get a double-ring decoration.

Margin visualizer. 8 positive points, 8 negative points. Line y = 0.60 x + 0.40. Margin half-width 0.60. 16 margin violations.
y = 0.60 x + 0.40margin ±0.60 · 16 violations
0.60
0.40
0.60
Customize
Line
0.60
0.40
Margin
0.60

Installation

npx shadcn@latest add https://craftbits.dev/r/margin-visualizer.json

Usage

import { MarginVisualizer } from "@craft-bits/core";
 
<MarginVisualizer
  positiveClass={[
    { x: -1, y: 0.5 },
    { x: 0.2, y: 1.2 },
    { x: 1.4, y: 1.9 },
  ]}
  negativeClass={[
    { x: -1.2, y: -1.0 },
    { x: 0.0, y: -0.6 },
    { x: 1.0, y: 0.0 },
  ]}
/>

Drive slope, intercept, and margin width from outside:

const [slope, setSlope] = useState(0.5);
const [intercept, setIntercept] = useState(0.2);
const [marginWidth, setMarginWidth] = useState(0.6);
 
<MarginVisualizer
  positiveClass={positives}
  negativeClass={negatives}
  slope={slope}
  onSlopeChange={setSlope}
  intercept={intercept}
  onInterceptChange={setIntercept}
  marginWidth={marginWidth}
  onMarginWidthChange={setMarginWidth}
/>

Hide the support-vector rings for a barer picture:

<MarginVisualizer
  positiveClass={positives}
  negativeClass={negatives}
  showSupportVectors={false}
/>

Understanding the component

  1. Two classes, one line. Positive-class points are rendered as accent-filled dots, negative-class points as muted-fg dots. The separating line y = slope x + intercept is drawn full-strength in the foreground color.
  2. Margin band as a parallelogram. The band of half-width marginWidth is bounded by two lines parallel to the separator, offset by marginWidth · √(slope² + 1) along the y-axis — converting the perpendicular half-width into a vertical offset. The band is filled with cb-accent-muted at low opacity so the points inside it remain readable.
  3. Signed perpendicular distance. For each point the component computes the signed normal distance to the line. Positive class wants positive distance; negative class wants negative distance.
  4. Violations. A point is a violation when it sits inside the band (its |distance| is below marginWidth) or on the wrong side of the line for its class. Violation points get a cb-warning ring around the dot. The header reports the violation count and turns cb-success when the count is zero.
  5. Support vectors. Among the points of each class, the ones with the smallest |distance| are highlighted with a faint outer ring — the support vectors of a hard-margin classifier at that line position. The ring is hidden when the margin width is zero (no margin to support).
  6. Auto-fit viewport. The visible math domain comes from both clouds' extents plus a 12–18 percent margin, extended to keep both line endpoints inside the chart whatever the intercept.
  7. Native slider semantics. All three controls are LabeledSlider instances wrapping a native <input type="range"> — keyboard navigation (arrows, Home, End), focus ring, and AT semantics come for free.
  8. Spring transitions. Line endpoints, the margin band, and every point follow with SPRINGS.smooth. prefers-reduced-motion: reduce collapses every spring to an instant swap.

Props

PropTypeDefaultDescription
positiveClassreadonly MarginPoint[]requiredPositive-class points (accent fill).
negativeClassreadonly MarginPoint[]requiredNegative-class points (muted fg fill).
slopenumberControlled slope of the separating line.
defaultSlopenumber0Initial uncontrolled slope.
onSlopeChange(slope: number) => voidFires when slope changes.
interceptnumberControlled intercept of the separating line.
defaultInterceptnumber0Initial uncontrolled intercept.
onInterceptChange(intercept: number) => voidFires when intercept changes.
marginWidthnumberControlled margin half-width along the line's normal.
defaultMarginWidthnumber0.5Initial uncontrolled margin half-width.
onMarginWidthChange(marginWidth: number) => voidFires when the margin width changes.
showSupportVectorsbooleantrueHighlight the closest point in each class with a double ring.
showViolationsbooleantrueMark points inside the margin or on the wrong side with a warning ring.
slopeMinnumber-3Minimum slider value for slope.
slopeMaxnumber3Maximum slider value for slope.
interceptMinnumber-3Minimum slider value for intercept.
interceptMaxnumber3Maximum slider value for intercept.
marginWidthMinnumber0Minimum slider value for the margin half-width.
marginWidthMaxnumber2Maximum slider value for the margin half-width.
transitionTransitionSPRINGS.smoothSpring for line / margin / point transitions.
classNamestringMerged onto the root via cn().

The MarginPoint shape:

interface MarginPoint {
  x: number; // x coordinate in math space
  y: number; // y coordinate in math space
}

Accessibility

  • The root is role="figure" with a visually hidden aria-live="polite" summary covering point counts, current line formula, margin width, and the live violation count — screen readers hear the geometry change whenever the sliders move.
  • All three sliders are native <input type="range"> via LabeledSlider, so keyboard control (arrows, Home, End) and AT semantics are first-class.
  • Focus ring uses :focus-visible with the accent color so keyboard users always see where they are.
  • Motion respects prefers-reduced-motion: reduce — every spring collapses to an instant swap.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/nn/MarginVisualizer.tsx). The source paired a 1D number-line of points with a hinge-loss readout, multi-phase narration, breathing-pulse hints, and an animated total-loss counter. The library extract is the pure 2D margin primitive — two clouds in, a draggable line plus margin band plus support-vector + violation highlights out.