Logistic Regression Curve Plot in MUI X Charts (JavaScript)

The same plot in 14 other libraries — JavaScript: Chart.js, D3.js, Apache ECharts, Highcharts; Python: Altair, Bokeh, lets-plot, Matplotlib, Plotly, plotnine, Pygal, Seaborn; R: ggplot2; Julia: Makie.jl. Compare all 15 side by side: Logistic Regression Curve Plot in Python, R, Julia and JavaScript.

A logistic regression visualization showing the characteristic S-shaped (sigmoid) probability curve for binary classification. The plot displays data points colored by their binary class, the fitted logistic curve representing predicted probabilities, confidence intervals around the curve, and a dashed decision threshold line at probability 0.5. This visualization is essential for understanding how a logistic model maps continuous input features to class probabilities.

Logistic Regression Curve Plot rendered with MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// logistic-regression: Logistic Regression Curve Plot
// Library: muix 7.29.1 | JavaScript 22.23.2
// Quality: 92/100 | Created: 2026-09-02

import { ChartContainer } from "@mui/x-charts/ChartContainer";
import { LinePlot } from "@mui/x-charts/LineChart";
import { ScatterPlot } from "@mui/x-charts/ScatterChart";
import { ChartsXAxis } from "@mui/x-charts/ChartsXAxis";
import { ChartsYAxis } from "@mui/x-charts/ChartsYAxis";
import { ChartsGrid } from "@mui/x-charts/ChartsGrid";
import { ChartsReferenceLine } from "@mui/x-charts/ChartsReferenceLine";
import { useXScale, useYScale } from "@mui/x-charts/hooks";

const t = window.ANYPLOT_TOKENS;
const { width, height } = window.ANYPLOT_SIZE;

// Reproducible LCG (seed 42) — no Math.random() in the browser harness context
let seed = 42;
function rng() {
  seed = (1664525 * seed + 1013904223) >>> 0;
  return seed / 4294967296;
}

function hexToRgba(hex, alpha) {
  const r = parseInt(hex.slice(1, 3), 16);
  const g = parseInt(hex.slice(3, 5), 16);
  const b = parseInt(hex.slice(5, 7), 16);
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}

// --- Data: marketing conversion vs. customer engagement score --------------
// True generating relationship — a customer with a higher engagement score
// (0-100) is more likely to convert, with a decision midpoint around 55.
function trueProbability(score) {
  return 1 / (1 + Math.exp(-(0.09 * (score - 55))));
}

const N_POINTS = 180;
const engagementScores = Array.from(
  { length: N_POINTS },
  () => Math.round(rng() * 1000) / 10,
);
const converted = engagementScores.map((score) => (rng() < trueProbability(score) ? 1 : 0));

// Jitter around 0 / 1 so overlapping points stay legible — wide enough to
// ease the dense x=40-60 overlap band without touching the -0.08/1.08 axis padding.
const scatterClass0 = [];
const scatterClass1 = [];
engagementScores.forEach((score, i) => {
  const jitter = (rng() - 0.5) * 0.14;
  if (converted[i] === 1) {
    scatterClass1.push({ x: score, y: 1 + jitter, id: `converted-${i}` });
  } else {
    scatterClass0.push({ x: score, y: 0 + jitter, id: `not-converted-${i}` });
  }
});

// --- Fit a logistic regression by batch gradient descent (standardized x) --
function fitLogisticRegression(xs, ys, iterations, learningRate) {
  const n = xs.length;
  const mean = xs.reduce((a, b) => a + b, 0) / n;
  const std = Math.sqrt(xs.reduce((a, b) => a + (b - mean) ** 2, 0) / n);
  const xn = xs.map((v) => (v - mean) / std);

  let b0 = 0;
  let b1 = 0;
  for (let iter = 0; iter < iterations; iter++) {
    let grad0 = 0;
    let grad1 = 0;
    for (let i = 0; i < n; i++) {
      const p = 1 / (1 + Math.exp(-(b0 + b1 * xn[i])));
      const error = p - ys[i];
      grad0 += error;
      grad1 += error * xn[i];
    }
    b0 -= (learningRate * grad0) / n;
    b1 -= (learningRate * grad1) / n;
  }

  // Wald standard errors from the observed Fisher information (X'WX)^-1
  let info00 = 0;
  let info01 = 0;
  let info11 = 0;
  for (let i = 0; i < n; i++) {
    const p = 1 / (1 + Math.exp(-(b0 + b1 * xn[i])));
    const w = p * (1 - p);
    info00 += w;
    info01 += w * xn[i];
    info11 += w * xn[i] * xn[i];
  }
  const det = info00 * info11 - info01 * info01;
  return {
    b0,
    b1,
    mean,
    std,
    varB0: info11 / det,
    varB1: info00 / det,
    covB01: -info01 / det,
  };
}

const model = fitLogisticRegression(engagementScores, converted, 600, 0.5);

// Inflection point (p = 0.5) and in-sample accuracy at that threshold, for
// the model-summary annotation drawn near the curve's midpoint.
const midpointX = model.mean + (-model.b0 / model.b1) * model.std;
const accuracy =
  engagementScores.reduce((correct, score, i) => {
    const xn = (score - model.mean) / model.std;
    const p = 1 / (1 + Math.exp(-(model.b0 + model.b1 * xn)));
    const predictedClass = p >= 0.5 ? 1 : 0;
    return correct + (predictedClass === converted[i] ? 1 : 0);
  }, 0) / N_POINTS;

// 95% Wald confidence interval on the predicted probability, via the delta
// method on the linear predictor (standard logistic-regression CI approach).
const Z95 = 1.96;
function predictWithCI(x) {
  const xn = (x - model.mean) / model.std;
  const eta = model.b0 + model.b1 * xn;
  const se = Math.sqrt(model.varB0 + 2 * xn * model.covB01 + xn * xn * model.varB1);
  const sigmoid = (v) => 1 / (1 + Math.exp(-v));
  return {
    probability: sigmoid(eta),
    lower: sigmoid(eta - Z95 * se),
    upper: sigmoid(eta + Z95 * se),
  };
}

const CURVE_POINTS = 200;
const curveXs = Array.from({ length: CURVE_POINTS }, (_, i) => (i / (CURVE_POINTS - 1)) * 100);
const curvePredictions = curveXs.map(predictWithCI);
const curveY = curvePredictions.map((p) => p.probability);
const ciLower = curvePredictions.map((p) => p.lower);
const ciUpper = curvePredictions.map((p) => p.upper);

// 95% confidence band, drawn as a filled path from the live axis scales —
// the two Imprint stops the band would need don't apply here (this is an
// uncertainty band around a single fit, not sequential/diverging data), so
// it reuses the neutral ink token at low opacity, matching the fitted line.
function ConfidenceBand() {
  const xScale = useXScale();
  const yScale = useYScale();
  if (!xScale || !yScale) return null;

  const upper = curveXs.map((x, i) => [xScale(x), yScale(ciUpper[i])]);
  const lower = curveXs.map((x, i) => [xScale(x), yScale(ciLower[i])]);
  const points = [...upper, ...lower.slice().reverse()];
  const d =
    points.map((p, i) => `${i === 0 ? "M" : "L"}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(" ") +
    " Z";

  return <path d={d} fill={t.ink} fillOpacity={0.14} stroke="none" />;
}

// Small model-summary callout anchored on the curve's inflection point
// (p = 0.5). Drawn in the empty mid-band between the two jittered scatter
// clusters, so it never collides with data points, the legend, or the
// dashed threshold-line label (which sits at the axis' left edge).
function InflectionAnnotation() {
  const xScale = useXScale();
  const yScale = useYScale();
  if (!xScale || !yScale) return null;

  const markerX = xScale(midpointX);
  const markerY = yScale(0.5);
  const labelY = yScale(0.8);

  return (
    <g>
      <circle cx={markerX} cy={markerY} r={5} fill={t.pageBg} stroke={t.ink} strokeWidth={2} />
      <text x={markerX} y={labelY} fontSize={13} fill={t.inkSoft} textAnchor="middle">
        <tspan x={markerX} dy={0}>{`Midpoint ≈ ${midpointX.toFixed(1)}`}</tspan>
        <tspan x={markerX} dy={16}>{`Accuracy at p=0.5: ${(accuracy * 100).toFixed(0)}%`}</tspan>
      </text>
    </g>
  );
}

const TITLE = "Customer Conversion · logistic-regression · javascript · muix · anyplot.ai";
const TITLE_FONT_DEFAULT = 22;
const titleFontSize =
  TITLE.length > 67 ? Math.round(TITLE_FONT_DEFAULT * (67 / TITLE.length)) : TITLE_FONT_DEFAULT;
const TITLE_HEIGHT = 60;

// Legend built by hand so each swatch matches its series' real mark shape —
// circular dots for the two scatter series, a short stroke for the fitted
// line — instead of ChartsLegend's uniform bar swatches.
const LEGEND_ITEMS = [
  { type: "circle", color: t.palette[4], label: "Not converted (y = 0)" },
  { type: "circle", color: t.palette[0], label: "Converted (y = 1)" },
  { type: "line", color: t.ink, label: "Fitted probability" },
];

export default function Chart() {
  return (
    <div style={{ width, height, backgroundColor: t.pageBg }}>
      <div
        style={{
          height: TITLE_HEIGHT,
          position: "relative",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          fontSize: titleFontSize,
          fontWeight: 600,
          color: t.ink,
        }}
      >
        {TITLE}
        <div
          style={{
            position: "absolute",
            top: "50%",
            right: 70,
            transform: "translateY(-50%)",
            display: "flex",
            flexDirection: "column",
            gap: 5,
            alignItems: "flex-start",
          }}
        >
          {LEGEND_ITEMS.map((item) => (
            <div key={item.label} style={{ display: "flex", alignItems: "center", gap: 8 }}>
              {item.type === "circle" ? (
                <span
                  style={{
                    width: 9,
                    height: 9,
                    borderRadius: "50%",
                    backgroundColor: item.color,
                    flexShrink: 0,
                  }}
                />
              ) : (
                <span style={{ width: 18, height: 3, backgroundColor: item.color, flexShrink: 0 }} />
              )}
              <span style={{ fontSize: 13, color: t.ink }}>{item.label}</span>
            </div>
          ))}
        </div>
      </div>
      <ChartContainer
        width={width}
        height={height - TITLE_HEIGHT}
        margin={{ top: 24, right: 64, bottom: 84, left: 92 }}
        sx={{ "& .MuiLineElement-series-fitted-curve": { strokeWidth: 3 } }}
        series={[
          {
            type: "line",
            id: "fitted-curve",
            data: curveY,
            label: "Fitted probability",
            color: t.ink,
            showMark: false,
            curve: "monotoneX",
            xAxisId: "engagement",
          },
          {
            type: "scatter",
            id: "class-0",
            data: scatterClass0,
            label: "Not converted (y = 0)",
            color: hexToRgba(t.palette[4], 0.6),
            markerSize: 8,
            xAxisId: "engagement",
          },
          {
            type: "scatter",
            id: "class-1",
            data: scatterClass1,
            label: "Converted (y = 1)",
            color: hexToRgba(t.palette[0], 0.6),
            markerSize: 8,
            xAxisId: "engagement",
          },
        ]}
        xAxis={[
          {
            id: "engagement",
            scaleType: "linear",
            data: curveXs,
            min: 0,
            max: 100,
            label: "Customer Engagement Score",
            tickInterval: [0, 20, 40, 60, 80, 100],
            valueFormatter: (v) => `${v}`,
            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },
            labelStyle: { fontSize: 16, fill: t.ink },
          },
        ]}
        yAxis={[
          {
            id: "probability",
            min: -0.08,
            max: 1.08,
            label: "Probability",
            tickInterval: [0, 0.2, 0.4, 0.6, 0.8, 1],
            valueFormatter: (v) => v.toFixed(1),
            tickLabelStyle: { fontSize: 14, fill: t.inkSoft },
            labelStyle: { fontSize: 16, fill: t.ink },
          },
        ]}
      >
        <ChartsGrid horizontal />
        <ConfidenceBand />
        <ScatterPlot skipAnimation />
        <LinePlot skipAnimation />
        <InflectionAnnotation />
        <ChartsXAxis
          axisId="engagement"
          tickLabelStyle={{ fontSize: 14, fill: t.inkSoft }}
          labelStyle={{ fontSize: 16, fill: t.ink }}
        />
        <ChartsYAxis
          axisId="probability"
          tickLabelStyle={{ fontSize: 14, fill: t.inkSoft }}
          labelStyle={{ fontSize: 16, fill: t.ink }}
        />
        <ChartsReferenceLine
          y={0.5}
          axisId="probability"
          label="Decision threshold (p = 0.5)"
          labelAlign="start"
          labelStyle={{ fill: t.inkSoft, fontSize: 13 }}
          lineStyle={{ stroke: t.inkSoft, strokeDasharray: "8 5", strokeWidth: 1.5 }}
        />
      </ChartContainer>
    </div>
  );
}

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/logistic-regression/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": "logistic-regression",
  "language": "javascript",
  "library": "muix",
  "page": "https://anyplot.ai/logistic-regression/javascript/muix",
  "hub": "https://anyplot.ai/logistic-regression",
  "code_json": "https://api.anyplot.ai/specs/logistic-regression/muix/code",
  "spec_json": "https://api.anyplot.ai/specs/logistic-regression",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/logistic-regression/javascript/muix/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/logistic-regression/javascript/muix/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/logistic-regression/javascript/muix/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/logistic-regression/javascript/muix/plot-dark.html",
  "quality_score": 92.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Logistic Regression Curve Plot on anyplot.ai.

Other implementations