Line Fit Viz

A 2D scatter chart with a user-controlled line y = slope x + intercept. Drag the slope and intercept sliders to fit the line through the points; dashed vertical drops show the residual at each point, and a faint dashed reference marks the analytical least-squares optimum so you can compare. R² is reported next to the formula.

Line fit. 12 points. y = 0.30 x + 0.20. R² 0.546.
y = 0.30 x + 0.20 0.546
0.30
0.20
Customize
Line
0.30
0.20
Display

Installation

npx shadcn@latest add https://craftbits.dev/r/line-fit-viz.json

Usage

import { LineFitViz } from "@craft-bits/core";
 
<LineFitViz
  points={[
    { x: -2, y: -0.6 },
    { x: -1, y: 0.1 },
    { x: 0, y: 0.6 },
    { x: 1, y: 1.0 },
    { x: 2, y: 1.7 },
  ]}
/>

Drive slope and intercept from outside:

const [slope, setSlope] = useState(0.5);
const [intercept, setIntercept] = useState(0.2);
 
<LineFitViz
  points={cloud}
  slope={slope}
  onSlopeChange={setSlope}
  intercept={intercept}
  onInterceptChange={setIntercept}
/>

Hide the residuals and the optimal reference for a bare line-fit picker:

<LineFitViz
  points={cloud}
  showResiduals={false}
  showOptimal={false}
/>

Understanding the component

  1. Auto-fit viewport. The visible math domain is derived from the cloud's extents plus a 10–15 percent margin, then extended to keep both line endpoints (user's and, when shown, the optimum) inside the chart. No range prop — the picture adapts to any reasonable point cloud.
  2. Analytical least-squares fit. With centered data, the closed-form slope is the covariance of x and y divided by the variance of x; the intercept is mean(y) − slope · mean(x). No gradient descent, no iteration — one pass over the points yields the optimum. Degenerate clouds (variance of x near zero) fall back to a horizontal line at the data's mean y.
  3. R² against the user's line. The coefficient of determination is 1 − SSres / SStot, where SSres is the sum of squared residuals to the user's line and SStot is the total variance around mean y. When the data has zero variance the readout is dashed instead of NaN.
  4. Residual drops. Each residual is a vertical segment from the point to the user's line, drawn in var(--cb-warning) with a dashed stroke. They make the cost being minimised visible — the line that flattens the bars is the best fit.
  5. Faint optimal reference. When showOptimal is on, the analytical least-squares line is rendered in var(--cb-fg-muted) as a long-dash reference. The user's line is full-strength accent on top, so visual comparison is immediate.
  6. Native slider semantics. Both controls are LabeledSlider instances wrapping a native <input type="range"> — keyboard navigation (arrows, Home, End), focus ring, and AT semantics come for free.
  7. Spring transitions. Line endpoints, residuals, and points all follow with SPRINGS.smooth. prefers-reduced-motion: reduce collapses every spring to an instant swap.

Props

PropTypeDefaultDescription
pointsreadonly LineFitPoint[]requiredScatter points the line is fit against.
slopenumberControlled slope of the user's line.
defaultSlopenumber0Initial uncontrolled slope.
onSlopeChange(slope: number) => voidFires when slope changes.
interceptnumberControlled intercept of the user's line.
defaultInterceptnumber0Initial uncontrolled intercept.
onInterceptChange(intercept: number) => voidFires when intercept changes.
showResidualsbooleantrueRender vertical drops from each point to the user's line.
showOptimalbooleantrueRender the analytical least-squares line as a faint dashed reference.
showRSquaredbooleantrueRender the R² readout next to the formula.
slopeMinnumber-2Minimum slider value for slope.
slopeMaxnumber2Maximum slider value for slope.
interceptMinnumber-2Minimum slider value for intercept.
interceptMaxnumber2Maximum slider value for intercept.
transitionTransitionSPRINGS.smoothSpring for line / residual / point transitions.
classNamestringMerged onto the root <div> via cn().

The LineFitPoint shape:

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

Accessibility

  • The root <div> is role="figure" with a visually hidden summary of point count, current formula, and R² — screen readers hear the fit whenever the data or sliders change.
  • Both sliders are native <input type="range"> through LabeledSlider, so keyboard control (arrows, Home, End) and AT semantics come for free.
  • 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/viz/LineFitViz.tsx). The source paired the visualisation with Explore / Predict modes, a loss sparkline, gradient-direction quiz rounds, narration heuristics, history-undo state, and a Widget chrome. The library extract is the pure regression primitive — points in, draggable line plus residuals plus optional least-squares reference plus R² out.