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.jsonUsage
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
- 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 + interceptis drawn full-strength in the foreground color. - Margin band as a parallelogram. The band of half-width
marginWidthis bounded by two lines parallel to the separator, offset bymarginWidth · √(slope² + 1)along the y-axis — converting the perpendicular half-width into a vertical offset. The band is filled withcb-accent-mutedat low opacity so the points inside it remain readable. - 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.
- 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 acb-warningring around the dot. The header reports the violation count and turnscb-successwhen the count is zero. - 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).
- 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.
- Native slider semantics. All three controls are
LabeledSliderinstances wrapping a native<input type="range">— keyboard navigation (arrows, Home, End), focus ring, and AT semantics come for free. - Spring transitions. Line endpoints, the margin band, and every point follow with
SPRINGS.smooth.prefers-reduced-motion: reducecollapses every spring to an instant swap.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
positiveClass | readonly MarginPoint[] | required | Positive-class points (accent fill). |
negativeClass | readonly MarginPoint[] | required | Negative-class points (muted fg fill). |
slope | number | — | Controlled slope of the separating line. |
defaultSlope | number | 0 | Initial uncontrolled slope. |
onSlopeChange | (slope: number) => void | — | Fires when slope changes. |
intercept | number | — | Controlled intercept of the separating line. |
defaultIntercept | number | 0 | Initial uncontrolled intercept. |
onInterceptChange | (intercept: number) => void | — | Fires when intercept changes. |
marginWidth | number | — | Controlled margin half-width along the line's normal. |
defaultMarginWidth | number | 0.5 | Initial uncontrolled margin half-width. |
onMarginWidthChange | (marginWidth: number) => void | — | Fires when the margin width changes. |
showSupportVectors | boolean | true | Highlight the closest point in each class with a double ring. |
showViolations | boolean | true | Mark points inside the margin or on the wrong side with a warning ring. |
slopeMin | number | -3 | Minimum slider value for slope. |
slopeMax | number | 3 | Maximum slider value for slope. |
interceptMin | number | -3 | Minimum slider value for intercept. |
interceptMax | number | 3 | Maximum slider value for intercept. |
marginWidthMin | number | 0 | Minimum slider value for the margin half-width. |
marginWidthMax | number | 2 | Maximum slider value for the margin half-width. |
transition | Transition | SPRINGS.smooth | Spring for line / margin / point transitions. |
className | string | — | Merged 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 hiddenaria-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">viaLabeledSlider, so keyboard control (arrows, Home, End) and AT semantics are first-class. - Focus ring uses
:focus-visiblewith 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.