{"spec_id":"elbow-curve","library":"d3","language":"javascript","code":"// anyplot.ai\n// elbow-curve: Elbow Curve for K-Means Clustering\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 110, right: 80, bottom: 100, left: 140 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// K-means inertia (within-cluster sum of squares) for customer segmentation\n// on annual spend + purchase frequency, k = 1..10. The rate of decrease flattens\n// sharply after k = 4 — the elbow point.\nconst kValues = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];\nconst inertia = [8450, 5120, 3210, 2050, 1780, 1590, 1440, 1330, 1240, 1170];\nconst elbowK = 4;\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales --------------------------------------------------------------------\nconst x = d3.scaleLinear().domain([1, 10]).range([0, iw]);\nconst y = d3.scaleLinear().domain([0, d3.max(inertia)]).nice().range([ih, 0]);\n\n// --- Y-axis gridlines (subtle, no x-axis grid for a single-line chart) --------\ng.append(\"g\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Axes ------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(10).tickFormat(d3.format(\"d\")));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6).tickFormat(d3.format(\",\")));\n\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Elbow guide line -----------------------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", x(elbowK))\n  .attr(\"x2\", x(elbowK))\n  .attr(\"y1\", y(0))\n  .attr(\"y2\", y(inertia[elbowK - 1]))\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-dasharray\", \"4,4\")\n  .attr(\"stroke-width\", 1);\n\n// --- Connecting line (curveMonotoneX: a d3-shape interpolation that keeps the\n// curve monotone between points, so the elbow bend reads as a smooth shape\n// instead of jointed straight segments, without overshooting the data) -------\nconst line = d3\n  .line()\n  .x((d, i) => x(kValues[i]))\n  .y((d) => y(d))\n  .curve(d3.curveMonotoneX);\ng.append(\"path\").datum(inertia).attr(\"fill\", \"none\").attr(\"stroke\", t.palette[0]).attr(\"stroke-width\", 3.5).attr(\"d\", line);\n\n// --- Elbow halo: a non-semantic ink ring (no amber) draws the eye to the\n// optimal point while its size, not its hue, carries the emphasis ------------\ng.append(\"circle\")\n  .attr(\"cx\", x(elbowK))\n  .attr(\"cy\", y(inertia[elbowK - 1]))\n  .attr(\"r\", 17)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 1.5)\n  .attr(\"opacity\", 0.35);\n\n// --- Markers, with the elbow point picked out by size + a bolder page-bg\n// stroke rather than a semantic (warning) color ------------------------------\ng.selectAll(\"circle.point\")\n  .data(inertia)\n  .join(\"circle\")\n  .attr(\"class\", \"point\")\n  .attr(\"cx\", (d, i) => x(kValues[i]))\n  .attr(\"cy\", (d) => y(d))\n  .attr(\"r\", (d, i) => (kValues[i] === elbowK ? 11 : 7))\n  .attr(\"fill\", t.palette[0])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", (d, i) => (kValues[i] === elbowK ? 3 : 2));\n\n// --- Elbow annotation (spec explicitly calls for highlighting the optimal k) —\n// a small-caps label over a bold value gives the callout its own typographic\n// hierarchy instead of a single line at the axis-label weight ----------------\nconst annotation = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(${x(elbowK) + 16},${y(inertia[elbowK - 1]) - 34})`);\nannotation\n  .append(\"text\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"12px\")\n  .style(\"font-weight\", \"500\")\n  .style(\"letter-spacing\", \"0.06em\")\n  .text(\"OPTIMAL CLUSTER COUNT\");\nannotation\n  .append(\"text\")\n  .attr(\"y\", 22)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"20px\")\n  .style(\"font-weight\", \"700\")\n  .text(`k = ${elbowK}`);\n\n// --- Axis labels -------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", margin.left + iw / 2)\n  .attr(\"y\", height - 28)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Number of Clusters (k)\");\n\nsvg\n  .append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -(margin.top + ih / 2))\n  .attr(\"y\", 40)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Inertia (Within-Cluster Sum of Squares)\");\n\n// --- Title -------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"elbow-curve · javascript · d3 · anyplot.ai\");\n"}