Elbow Curve for K-Means Clustering — D3.js

An elbow curve visualizes the relationship between the number of clusters (k) and within-cluster sum of squares (inertia/distortion) in K-means clustering. The plot helps identify the optimal number of clusters by finding the "elbow point" where adding more clusters yields diminishing returns in reducing inertia. This is a fundamental diagnostic tool for unsupervised learning parameter selection.

Elbow Curve for K-Means Clustering rendered with D3.js

Renders

JavaScript source (D3.js)

// anyplot.ai
// elbow-curve: Elbow Curve for K-Means Clustering
// Library: d3 7.9.0 | JavaScript 22.23.2
// Quality: 92/100 | Created: 2026-09-05

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

// --- Data (in-memory, deterministic) ----------------------------------------
// K-means inertia (within-cluster sum of squares) for customer segmentation
// on annual spend + purchase frequency, k = 1..10. The rate of decrease flattens
// sharply after k = 4 — the elbow point.
const kValues = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const inertia = [8450, 5120, 3210, 2050, 1780, 1590, 1440, 1330, 1240, 1170];
const elbowK = 4;

// --- 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})`);

// --- Scales --------------------------------------------------------------------
const x = d3.scaleLinear().domain([1, 10]).range([0, iw]);
const y = d3.scaleLinear().domain([0, d3.max(inertia)]).nice().range([ih, 0]);

// --- Y-axis gridlines (subtle, no x-axis grid for a single-line chart) --------
g.append("g")
  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(""))
  .call((sel) => sel.select(".domain").remove())
  .selectAll("line")
  .attr("stroke", t.grid);

// --- Axes ------------------------------------------------------------------
const xAxis = g
  .append("g")
  .attr("transform", `translate(0,${ih})`)
  .call(d3.axisBottom(x).ticks(10).tickFormat(d3.format("d")));
const yAxis = g.append("g").call(d3.axisLeft(y).ticks(6).tickFormat(d3.format(",")));

for (const ax of [xAxis, yAxis]) {
  ax.selectAll("text").attr("fill", t.inkSoft).style("font-size", "14px");
  ax.selectAll("line").attr("stroke", t.inkSoft);
  ax.select(".domain").attr("stroke", t.inkSoft);
}

// --- Elbow guide line -----------------------------------------------------
g.append("line")
  .attr("x1", x(elbowK))
  .attr("x2", x(elbowK))
  .attr("y1", y(0))
  .attr("y2", y(inertia[elbowK - 1]))
  .attr("stroke", t.inkSoft)
  .attr("stroke-dasharray", "4,4")
  .attr("stroke-width", 1);

// --- Connecting line (curveMonotoneX: a d3-shape interpolation that keeps the
// curve monotone between points, so the elbow bend reads as a smooth shape
// instead of jointed straight segments, without overshooting the data) -------
const line = d3
  .line()
  .x((d, i) => x(kValues[i]))
  .y((d) => y(d))
  .curve(d3.curveMonotoneX);
g.append("path").datum(inertia).attr("fill", "none").attr("stroke", t.palette[0]).attr("stroke-width", 3.5).attr("d", line);

// --- Elbow halo: a non-semantic ink ring (no amber) draws the eye to the
// optimal point while its size, not its hue, carries the emphasis ------------
g.append("circle")
  .attr("cx", x(elbowK))
  .attr("cy", y(inertia[elbowK - 1]))
  .attr("r", 17)
  .attr("fill", "none")
  .attr("stroke", t.ink)
  .attr("stroke-width", 1.5)
  .attr("opacity", 0.35);

// --- Markers, with the elbow point picked out by size + a bolder page-bg
// stroke rather than a semantic (warning) color ------------------------------
g.selectAll("circle.point")
  .data(inertia)
  .join("circle")
  .attr("class", "point")
  .attr("cx", (d, i) => x(kValues[i]))
  .attr("cy", (d) => y(d))
  .attr("r", (d, i) => (kValues[i] === elbowK ? 11 : 7))
  .attr("fill", t.palette[0])
  .attr("stroke", t.pageBg)
  .attr("stroke-width", (d, i) => (kValues[i] === elbowK ? 3 : 2));

// --- Elbow annotation (spec explicitly calls for highlighting the optimal k) —
// a small-caps label over a bold value gives the callout its own typographic
// hierarchy instead of a single line at the axis-label weight ----------------
const annotation = g
  .append("g")
  .attr("transform", `translate(${x(elbowK) + 16},${y(inertia[elbowK - 1]) - 34})`);
annotation
  .append("text")
  .attr("fill", t.inkSoft)
  .style("font-size", "12px")
  .style("font-weight", "500")
  .style("letter-spacing", "0.06em")
  .text("OPTIMAL CLUSTER COUNT");
annotation
  .append("text")
  .attr("y", 22)
  .attr("fill", t.ink)
  .style("font-size", "20px")
  .style("font-weight", "700")
  .text(`k = ${elbowK}`);

// --- Axis labels -------------------------------------------------------------
svg
  .append("text")
  .attr("x", margin.left + iw / 2)
  .attr("y", height - 28)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "16px")
  .text("Number of Clusters (k)");

svg
  .append("text")
  .attr("transform", "rotate(-90)")
  .attr("x", -(margin.top + ih / 2))
  .attr("y", 40)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "16px")
  .text("Inertia (Within-Cluster Sum of Squares)");

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

Retrieve this implementation

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

Part of Elbow Curve for K-Means Clustering on anyplot.ai.

Other implementations