Scatter Plot with LOWESS Regression — MUI X Charts

A scatter plot with a LOWESS (Locally Weighted Scatterplot Smoothing) regression curve overlaid. LOWESS is a non-parametric method that fits smooth curves by performing local weighted regressions at each point, adapting to local data patterns without assuming a specific functional form. This makes it ideal for exploring complex relationships where the underlying pattern is unknown or varies across the data range.

Scatter Plot with LOWESS Regression rendered with MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// scatter-regression-lowess: Scatter Plot with LOWESS Regression
// Library: muix 7.29.1 | JavaScript 22.23.2
// Quality: 88/100 | Created: 2026-09-09
import { ChartContainer } from "@mui/x-charts/ChartContainer";
import { ScatterPlot } from "@mui/x-charts/ScatterChart";
import { LinePlot } from "@mui/x-charts/LineChart";
import { ChartsXAxis } from "@mui/x-charts/ChartsXAxis";
import { ChartsYAxis } from "@mui/x-charts/ChartsYAxis";
import { ChartsGrid } from "@mui/x-charts/ChartsGrid";
import { ChartsLegend } from "@mui/x-charts/ChartsLegend";
import { ChartsTooltip } from "@mui/x-charts/ChartsTooltip";
import { useXScale, useYScale } from "@mui/x-charts/hooks";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

const t = window.ANYPLOT_TOKENS;

// --- Data (in-memory, deterministic) ---------------------------------------
// Tiny fixed-seed LCG — the browser has no seeded RNG.
function makeLcg(seed: number) {
  let state = seed >>> 0;
  return function next() {
    state = (Math.imul(state, 1664525) + 1013904223) >>> 0;
    return state / 4294967296;
  };
}

function randNormal(rng: () => number) {
  const u1 = Math.max(rng(), 1e-9);
  const u2 = rng();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}

function hexToRgba(hex: string, alpha: number) {
  const value = parseInt(hex.slice(1), 16);
  const r = (value >> 16) & 255;
  const g = (value >> 8) & 255;
  const b = value & 255;
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}

const rng = makeLcg(42);
const SAMPLE_SIZE = 170;

// Fuel efficiency peaks at a moderate cruising speed and drops off at both
// low speed (frequent idling/acceleration) and high speed (aerodynamic drag)
// — a non-monotonic pattern LOWESS traces without assuming a parametric form.
const vehicleSpeed: number[] = [];
const fuelEfficiency: number[] = [];
for (let i = 0; i < SAMPLE_SIZE; i += 1) {
  const speed = 20 + rng() * 120;
  const trend = 18.5 - 0.0021 * (speed - 78) ** 2;
  const value = Math.max(3, trend + randNormal(rng) * 1.6);
  vehicleSpeed.push(speed);
  fuelEfficiency.push(value);
}

// --- LOWESS (locally weighted scatterplot smoothing) ------------------------
function tricube(distance: number, bandwidth: number) {
  if (bandwidth <= 0) return distance === 0 ? 1 : 0;
  const u = Math.min(Math.abs(distance) / bandwidth, 1);
  return (1 - u ** 3) ** 3;
}

function lowess(xs: number[], ys: number[], frac: number, gridSize: number) {
  const n = xs.length;
  const windowSize = Math.max(2, Math.round(frac * n));
  const xMin = Math.min(...xs);
  const xMax = Math.max(...xs);
  const grid = Array.from({ length: gridSize }, (_, i) => xMin + ((xMax - xMin) * i) / (gridSize - 1));

  return grid.map((x0) => {
    const distances = xs.map((xi) => Math.abs(xi - x0));
    const bandwidth = [...distances].sort((a, b) => a - b)[windowSize - 1];
    const weights = distances.map((d) => tricube(d, bandwidth));

    // Locally weighted linear regression via weighted normal equations.
    let sw = 0;
    let swx = 0;
    let swy = 0;
    let swxx = 0;
    let swxy = 0;
    for (let i = 0; i < n; i += 1) {
      const w = weights[i];
      sw += w;
      swx += w * xs[i];
      swy += w * ys[i];
      swxx += w * xs[i] * xs[i];
      swxy += w * xs[i] * ys[i];
    }
    const denom = sw * swxx - swx * swx;
    const slope = denom !== 0 ? (sw * swxy - swx * swy) / denom : 0;
    const intercept = sw !== 0 ? (swy - slope * swx) / sw : 0;

    // Local residual spread — the weighted RMS deviation of the raw points
    // from this window's line, reused as a ±1 SD confidence band around the fit.
    let swResidSq = 0;
    for (let i = 0; i < n; i += 1) {
      const resid = ys[i] - (intercept + slope * xs[i]);
      swResidSq += weights[i] * resid * resid;
    }
    const band = sw !== 0 ? Math.sqrt(swResidSq / sw) : 0;

    return { x: x0, y: intercept + slope * x0, band };
  });
}

const smoothed = lowess(vehicleSpeed, fuelEfficiency, 0.4, 120);
const smoothedX = smoothed.map((point) => point.x);
const smoothedY = smoothed.map((point) => point.y);
const smoothedUpper = smoothed.map((point) => point.y + point.band);
const smoothedLower = smoothed.map((point) => point.y - point.band);

const scatterData = vehicleSpeed.map((speed, i) => ({
  x: speed,
  y: fuelEfficiency[i],
  id: i,
}));

// A shaded ±1 SD band behind the fit line, drawn from the chart's own scales
// (community `useXScale`/`useYScale` hooks) rather than as a legend series —
// it should read as context for the fit, not compete with it for attention.
function ConfidenceBand({ x, upper, lower, fill }: { x: number[]; upper: number[]; lower: number[]; fill: string }) {
  const xScale = useXScale("speed");
  const yScale = useYScale();
  const topEdge = x.map((xi, i) => `${i === 0 ? "M" : "L"}${xScale(xi)},${yScale(upper[i])}`);
  const bottomEdge = [...x]
    .map((xi, i) => ({ xi, y: lower[i] }))
    .reverse()
    .map((point) => `L${xScale(point.xi)},${yScale(point.y)}`);
  return <path d={`${topEdge.join(" ")} ${bottomEdge.join(" ")} Z`} fill={fill} stroke="none" />;
}

// --- Chart (default-exported component — the harness mounts it) -------------
export default function Chart() {
  const W = window.ANYPLOT_SIZE.width;
  const H = window.ANYPLOT_SIZE.height;
  const CHART_TOP = 60;

  const title = "scatter-regression-lowess · javascript · muix · anyplot.ai";
  const titleSize = title.length > 67 ? Math.round((22 * 67) / title.length) : 22;

  // "muted" semantic anchor (adaptive, outside the categorical pool) — used at
  // low alpha for the confidence-band fill so it sits behind the data.
  const mutedHex = t.theme === "dark" ? "#A8A79F" : "#6B6A63";
  const bandFill = hexToRgba(mutedHex, 0.18);

  return (
    <Box sx={{ position: "relative", width: W, height: H, bgcolor: t.pageBg }}>
      <Box sx={{ position: "absolute", top: 20, left: 56, right: 56 }}>
        <Typography sx={{ color: t.ink, fontSize: titleSize, fontWeight: 500 }}>{title}</Typography>
      </Box>
      <Box sx={{ position: "absolute", top: CHART_TOP, left: 0, right: 0, bottom: 0 }}>
        <ChartContainer
          width={W}
          height={H - CHART_TOP}
          skipAnimation
          margin={{ top: 30, right: 40, bottom: 70, left: 90 }}
          series={[
            {
              type: "scatter",
              data: scatterData,
              color: hexToRgba(t.palette[0], 0.6),
              markerSize: 8,
              label: "Vehicles (observed)",
            },
            {
              type: "line",
              data: smoothedY,
              xAxisId: "speed",
              color: t.palette[1],
              curve: "natural",
              showMark: false,
              label: "LOWESS fit",
            },
          ]}
          xAxis={[
            {
              id: "speed",
              data: smoothedX,
              scaleType: "linear",
              label: "Vehicle Speed (km/h)",
              labelStyle: { fontSize: 16 },
              tickLabelStyle: { fontSize: 14 },
              valueFormatter: (value: number) => value.toFixed(0),
            },
          ]}
          yAxis={[
            {
              label: "Fuel Efficiency (km/L)",
              labelStyle: { fontSize: 16 },
              tickLabelStyle: { fontSize: 14 },
            },
          ]}
          sx={{
            "& .MuiLineElement-root": { strokeWidth: 3.5 },
          }}
        >
          <ChartsGrid horizontal vertical />
          <ConfidenceBand x={smoothedX} upper={smoothedUpper} lower={smoothedLower} fill={bandFill} />
          <ScatterPlot />
          <LinePlot />
          <ChartsXAxis />
          <ChartsYAxis />
          <ChartsLegend direction="row" position={{ horizontal: "right", vertical: "top" }} slotProps={{ legend: { labelStyle: { fontSize: 14 } } }} />
          <ChartsTooltip trigger="item" />
        </ChartContainer>
      </Box>
    </Box>
  );
}

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/scatter-regression-lowess/muix/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "scatter-regression-lowess",
  "language": "javascript",
  "library": "muix",
  "page": "https://anyplot.ai/scatter-regression-lowess/javascript/muix",
  "hub": "https://anyplot.ai/scatter-regression-lowess",
  "code_json": "https://api.anyplot.ai/specs/scatter-regression-lowess/muix/code",
  "spec_json": "https://api.anyplot.ai/specs/scatter-regression-lowess",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/scatter-regression-lowess/javascript/muix/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/scatter-regression-lowess/javascript/muix/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/scatter-regression-lowess/javascript/muix/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/scatter-regression-lowess/javascript/muix/plot-dark.html",
  "quality_score": 88.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Scatter Plot with LOWESS Regression on anyplot.ai.

Other implementations