Logistic Regression Curve Plot in Apache ECharts (JavaScript)

The same plot in 14 other libraries — JavaScript: Chart.js, D3.js, Highcharts, MUI X Charts; 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 Apache ECharts

Renders

JavaScript source (Apache ECharts)

// anyplot.ai
// logistic-regression: Logistic Regression Curve Plot
// Library: echarts 6.1.0 | JavaScript 22.23.2
// Quality: 89/100 | Created: 2026-09-02

const t = window.ANYPLOT_TOKENS;

// --- Data (in-memory, deterministic) ----------------------------------------
// A tiny fixed-seed LCG stands in for a seeded RNG (the browser has none).
function makeLcg(seed) {
  let state = seed >>> 0;
  return function () {
    state = (state * 1664525 + 1013904223) >>> 0;
    return state / 4294967296;
  };
}
const rand = makeLcg(42);

const N = 180;
const X_DOMAIN_MAX = 30;
const TRUE_BETA0 = -3.5;
const TRUE_BETA1 = 0.35; // ad exposures per week -> conversion probability

const sigmoid = (z) => 1 / (1 + Math.exp(-z));

const adExposures = Array.from({ length: N }, () => rand() * X_DOMAIN_MAX);
const converted = adExposures.map((x) => (rand() < sigmoid(TRUE_BETA0 + TRUE_BETA1 * x) ? 1 : 0));

// Jitter the binary outcome around 0/1 so overlapping points stay visible.
const notConvertedPoints = [];
const convertedPoints = [];
adExposures.forEach((x, i) => {
  const jitter = (rand() - 0.5) * 0.08;
  const point = [x, converted[i] + jitter];
  if (converted[i] === 1) convertedPoints.push(point);
  else notConvertedPoints.push(point);
});

// --- Fit a logistic regression via full-batch gradient descent -------------
const xMean = adExposures.reduce((a, b) => a + b, 0) / N;
const xStd = Math.sqrt(adExposures.reduce((a, x) => a + (x - xMean) ** 2, 0) / N);
const xNorm = adExposures.map((x) => (x - xMean) / xStd);

let w0 = 0;
let w1 = 0;
const LEARNING_RATE = 0.5;
for (let iter = 0; iter < 4000; iter++) {
  let grad0 = 0;
  let grad1 = 0;
  for (let i = 0; i < N; i++) {
    const p = sigmoid(w0 + w1 * xNorm[i]);
    const err = p - converted[i];
    grad0 += err;
    grad1 += err * xNorm[i];
  }
  w0 -= (LEARNING_RATE * grad0) / N;
  w1 -= (LEARNING_RATE * grad1) / N;
}

// Fisher information (Hessian of the log-likelihood) at the fitted weights,
// inverted analytically to get the asymptotic covariance of (w0, w1).
let h00 = 0;
let h01 = 0;
let h11 = 0;
for (let i = 0; i < N; i++) {
  const p = sigmoid(w0 + w1 * xNorm[i]);
  const wgt = p * (1 - p);
  h00 += wgt;
  h01 += wgt * xNorm[i];
  h11 += wgt * xNorm[i] * xNorm[i];
}
const det = h00 * h11 - h01 * h01;
const cov00 = h11 / det;
const cov01 = -h01 / det;
const cov11 = h00 / det;

// Real-scale coefficients (undo the x-normalization) for the annotation.
const realBeta1 = w1 / xStd;
const realBeta0 = w0 - (w1 * xMean) / xStd;
const accuracy =
  adExposures.reduce((correct, x, i) => {
    const predicted = sigmoid(realBeta0 + realBeta1 * x) > 0.5 ? 1 : 0;
    return correct + (predicted === converted[i] ? 1 : 0);
  }, 0) / N;

// --- Fitted curve + 95% confidence band (logit-scale, mapped back to prob) --
const CURVE_POINTS = 100;
const xMin = Math.min(...adExposures);
const xMax = Math.max(...adExposures);
const curveFit = [];
const curveLower = [];
const curveBandHeight = [];
for (let i = 0; i < CURVE_POINTS; i++) {
  const x = xMin + ((xMax - xMin) * i) / (CURVE_POINTS - 1);
  const xn = (x - xMean) / xStd;
  const eta = w0 + w1 * xn;
  const varEta = cov00 + 2 * xn * cov01 + xn * xn * cov11;
  const se = Math.sqrt(Math.max(varEta, 0));
  const pLo = sigmoid(eta - 1.96 * se);
  const pHi = sigmoid(eta + 1.96 * se);
  curveFit.push([x, sigmoid(eta)]);
  curveLower.push([x, pLo]);
  curveBandHeight.push([x, pHi - pLo]);
}

// x-value where the fitted curve crosses p = 0.5 (the decision threshold).
const xThreshold = -realBeta0 / realBeta1;

// --- Title (mandated format, fontsize scaled to the descriptive prefix) ----
const titleText = "Marketing Conversion · logistic-regression · javascript · echarts · anyplot.ai";
const titleRatio = titleText.length > 67 ? 67 / titleText.length : 1.0;
const titleFontSize = Math.max(14, Math.round(22 * titleRatio));
const subtext = `Fitted: p = σ(${realBeta0.toFixed(2)} + ${realBeta1.toFixed(2)}·x) · Accuracy: ${Math.round(accuracy * 100)}%`;

// --- Init ---------------------------------------------------------------
const chart = echarts.init(document.getElementById("container"));

// --- Option ---------------------------------------------------------------
chart.setOption({
  animation: false,
  backgroundColor: "transparent",
  color: t.palette,
  title: {
    text: titleText,
    subtext,
    left: "center",
    top: 20,
    textStyle: { color: t.ink, fontSize: titleFontSize, fontWeight: 500 },
    subtextStyle: { color: t.inkSoft, fontSize: 16 },
  },
  legend: {
    data: ["Not Converted", "Converted", "Fitted Probability"],
    top: 96,
    left: "center",
    textStyle: { color: t.ink, fontSize: 16 },
    itemWidth: 22,
    itemHeight: 14,
  },
  grid: { left: 110, right: 90, top: 190, bottom: 110 },
  xAxis: {
    type: "value",
    name: "Ad Exposures per Week",
    nameLocation: "middle",
    nameGap: 45,
    nameTextStyle: { color: t.ink, fontSize: 18 },
    min: 0,
    max: X_DOMAIN_MAX,
    axisLabel: { color: t.inkSoft, fontSize: 14 },
    axisLine: { lineStyle: { color: t.inkSoft } },
    splitLine: { lineStyle: { color: t.grid } },
  },
  yAxis: {
    type: "value",
    name: "Probability",
    nameLocation: "middle",
    nameGap: 60,
    nameTextStyle: { color: t.ink, fontSize: 18 },
    min: -0.1,
    max: 1.1,
    axisLabel: { color: t.inkSoft, fontSize: 14 },
    axisLine: { lineStyle: { color: t.inkSoft } },
    splitLine: { lineStyle: { color: t.grid } },
  },
  series: [
    {
      name: "ci-lower",
      type: "line",
      data: curveLower,
      stack: "confidence",
      symbol: "none",
      lineStyle: { opacity: 0 },
      areaStyle: { opacity: 0 },
      silent: true,
      tooltip: { show: false },
      z: 1,
    },
    {
      name: "ci-band",
      type: "line",
      data: curveBandHeight,
      stack: "confidence",
      symbol: "none",
      lineStyle: { opacity: 0 },
      areaStyle: { color: t.palette[2], opacity: 0.24 },
      silent: true,
      tooltip: { show: false },
      z: 1,
    },
    {
      name: "Not Converted",
      type: "scatter",
      data: notConvertedPoints,
      symbolSize: 10,
      itemStyle: { color: t.palette[0], opacity: 0.6, borderColor: t.pageBg, borderWidth: 1 },
      z: 3,
    },
    {
      name: "Converted",
      type: "scatter",
      data: convertedPoints,
      symbolSize: 10,
      itemStyle: { color: t.palette[1], opacity: 0.6, borderColor: t.pageBg, borderWidth: 1 },
      z: 3,
    },
    {
      name: "Fitted Probability",
      type: "line",
      data: curveFit,
      symbol: "none",
      lineStyle: { color: t.palette[2], width: 3 },
      z: 2,
      markLine: {
        silent: true,
        symbol: "none",
        lineStyle: { type: "dashed", color: t.inkSoft, width: 2 },
        label: { formatter: "p = 0.5", color: t.inkSoft, fontSize: 14, position: "insideEndTop" },
        data: [{ yAxis: 0.5 }],
      },
      markPoint: {
        silent: true,
        symbol: "circle",
        symbolSize: 14,
        itemStyle: { color: t.palette[2], borderColor: t.pageBg, borderWidth: 2 },
        label: {
          formatter: `x ≈ ${xThreshold.toFixed(1)}`,
          color: t.ink,
          fontSize: 13,
          position: "top",
          distance: 10,
        },
        data: [{ coord: [xThreshold, 0.5] }],
      },
    },
  ],
});

Retrieve this implementation

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

Part of Logistic Regression Curve Plot on anyplot.ai.

Other implementations