Logistic Regression Curve Plot in D3.js (JavaScript)

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

Renders

JavaScript source (D3.js)

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

const t = window.ANYPLOT_TOKENS;
const { width, height } = window.ANYPLOT_SIZE;
const margin = { top: 140, right: 60, bottom: 100, left: 100 };
const iw = width - margin.left - margin.right;
const ih = height - margin.top - margin.bottom;

// --- Data: synthetic credit-risk scenario (deterministic LCG) --------------
let seed = 42;
function rand() {
  seed = (seed * 1664525 + 1013904223) % 4294967296;
  return seed / 4294967296;
}

const n = 180;
const beta0True = -4.2;
const beta1True = 0.085;

const points = [];
for (let i = 0; i < n; i++) {
  const utilization = rand() * 100;
  const logit = beta0True + beta1True * utilization;
  const pTrue = 1 / (1 + Math.exp(-logit));
  const defaulted = rand() < pTrue ? 1 : 0;
  points.push({ utilization, defaulted });
}

// --- Fit logistic regression via Newton-Raphson (IRLS) ----------------------
let b0 = 0;
let b1 = 0;
for (let iter = 0; iter < 20; iter++) {
  let g0 = 0;
  let g1 = 0;
  let h00 = 0;
  let h01 = 0;
  let h11 = 0;
  for (const d of points) {
    const eta = b0 + b1 * d.utilization;
    const p = 1 / (1 + Math.exp(-eta));
    const w = p * (1 - p);
    const err = d.defaulted - p;
    g0 += err;
    g1 += err * d.utilization;
    h00 += w;
    h01 += w * d.utilization;
    h11 += w * d.utilization * d.utilization;
  }
  const det = h00 * h11 - h01 * h01;
  b0 += (h11 * g0 - h01 * g1) / det;
  b1 += (h00 * g1 - h01 * g0) / det;
}

// Covariance matrix = inverse Fisher information at the MLE — feeds the 95% CI band.
let h00f = 0;
let h01f = 0;
let h11f = 0;
for (const d of points) {
  const eta = b0 + b1 * d.utilization;
  const p = 1 / (1 + Math.exp(-eta));
  const w = p * (1 - p);
  h00f += w;
  h01f += w * d.utilization;
  h11f += w * d.utilization * d.utilization;
}
const detF = h00f * h11f - h01f * h01f;
const cov00 = h11f / detF;
const cov01 = -h01f / detF;
const cov11 = h00f / detF;

const accuracy =
  points.filter((d) => {
    const predicted = 1 / (1 + Math.exp(-(b0 + b1 * d.utilization))) >= 0.5 ? 1 : 0;
    return predicted === d.defaulted;
  }).length / n;

// --- Fitted curve + 95% confidence band over a grid --------------------------
const gridN = 100;
const curve = [];
for (let i = 0; i <= gridN; i++) {
  const xi = (i / gridN) * 100;
  const eta = b0 + b1 * xi;
  const seEta = Math.sqrt(cov00 + 2 * xi * cov01 + xi * xi * cov11);
  curve.push({
    x: xi,
    p: 1 / (1 + Math.exp(-eta)),
    lo: 1 / (1 + Math.exp(-(eta - 1.96 * seEta))),
    hi: 1 / (1 + Math.exp(-(eta + 1.96 * seEta))),
  });
}

// Jitter for point display only — class assignment itself stays binary.
const jittered = points.map((d) => ({
  ...d,
  yJitter: d.defaulted + (rand() - 0.5) * 0.08,
}));

// --- Scales -------------------------------------------------------------------
const x = d3.scaleLinear().domain([0, 100]).range([0, iw]);
const y = d3.scaleLinear().domain([-0.08, 1.08]).range([ih, 0]);

// --- SVG mount ------------------------------------------------------------
const svg = d3.select("#container").append("svg").attr("width", width).attr("height", height);
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);

// --- Gridlines (y-axis only) -------------------------------------------------
g.append("g")
  .call(d3.axisLeft(y).tickValues([0, 0.25, 0.5, 0.75, 1]).tickSize(-iw).tickFormat(""))
  .call((gr) => gr.select(".domain").remove())
  .call((gr) => gr.selectAll("line").attr("stroke", t.grid));

// --- Confidence band ----------------------------------------------------------
const band = d3
  .area()
  .x((d) => x(d.x))
  .y0((d) => y(d.lo))
  .y1((d) => y(d.hi))
  .curve(d3.curveMonotoneX);
g.append("path").datum(curve).attr("d", band).attr("fill", t.palette[2]).attr("opacity", 0.18);

// --- Decision threshold line ---------------------------------------------------
g.append("line")
  .attr("x1", 0)
  .attr("x2", iw)
  .attr("y1", y(0.5))
  .attr("y2", y(0.5))
  .attr("stroke", t.ink)
  .attr("stroke-width", 1.5)
  .attr("stroke-dasharray", "6,5")
  .attr("opacity", 0.55);

g.append("text")
  .attr("x", iw)
  .attr("y", y(0.5) - 12)
  .attr("text-anchor", "end")
  .attr("fill", t.inkSoft)
  .style("font-size", "14px")
  .text("decision threshold (p = 0.5)");

// --- Fitted logistic curve ------------------------------------------------------
const line = d3
  .line()
  .x((d) => x(d.x))
  .y((d) => y(d.p))
  .curve(d3.curveMonotoneX);
g.append("path").datum(curve).attr("d", line).attr("fill", "none").attr("stroke", t.palette[2]).attr("stroke-width", 3);

// --- Data points (jittered, colored by class) -----------------------------------
g.selectAll("circle")
  .data(jittered)
  .join("circle")
  .attr("cx", (d) => x(d.utilization))
  .attr("cy", (d) => y(d.yJitter))
  .attr("r", 7)
  .attr("fill", (d) => (d.defaulted ? t.palette[1] : t.palette[0]))
  .attr("fill-opacity", 0.6)
  .attr("stroke", t.pageBg)
  .attr("stroke-width", 0.8);

// --- Axes -----------------------------------------------------------------------
const xAxis = g
  .append("g")
  .attr("transform", `translate(0,${ih})`)
  .call(d3.axisBottom(x).ticks(8).tickFormat((d) => `${d}%`));
const yAxis = g.append("g").call(d3.axisLeft(y).tickValues([0, 0.25, 0.5, 0.75, 1]));
for (const ax of [xAxis, yAxis]) {
  ax.selectAll("text").attr("fill", t.inkSoft).style("font-size", "16px");
  ax.selectAll("line").attr("stroke", t.grid);
  ax.select(".domain").attr("stroke", t.inkSoft);
}

// --- Axis labels ------------------------------------------------------------
g.append("text")
  .attr("x", iw / 2)
  .attr("y", ih + 70)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "18px")
  .text("Credit Utilization Rate (%)");

g.append("text")
  .attr("transform", "rotate(-90)")
  .attr("x", -ih / 2)
  .attr("y", -66)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "18px")
  .text("Probability of Default");

// --- Header row: legend + model stats (kept clear of the plot area) -------------
const legendData = [
  { label: "No default (y = 0)", color: t.palette[0] },
  { label: "Default (y = 1)", color: t.palette[1] },
];
const legendG = svg.append("g").attr("transform", `translate(${margin.left}, 92)`);
legendData.forEach((item, i) => {
  const row = legendG.append("g").attr("transform", `translate(${i * 230}, 0)`);
  row.append("circle").attr("r", 7).attr("cy", -5).attr("fill", item.color).attr("fill-opacity", 0.8);
  row.append("text").attr("x", 16).attr("fill", t.inkSoft).style("font-size", "16px").text(item.label);
});

svg
  .append("text")
  .attr("x", width - margin.right)
  .attr("y", 92)
  .attr("text-anchor", "end")
  .attr("fill", t.inkSoft)
  .style("font-size", "16px")
  .text(`β₀ = ${b0.toFixed(2)} · β₁ = ${b1.toFixed(3)} · accuracy = ${(accuracy * 100).toFixed(0)}%`);

// --- Title -----------------------------------------------------------------
svg
  .append("text")
  .attr("x", width / 2)
  .attr("y", 54)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "22px")
  .style("font-weight", "600")
  .text("logistic-regression · javascript · d3 · anyplot.ai");

Retrieve this implementation

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