{"spec_id":"lollipop-grouped","library":"d3","language":"javascript","code":"// anyplot.ai\n// lollipop-grouped: Grouped Lollipop Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 150, right: 90, bottom: 80, left: 210 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Model benchmark: three metrics per algorithm, sorted ascending by mean score\n// so scaleBand's range([ih,0]) puts the strongest model at the top.\nconst metrics = [\"Accuracy\", \"Precision\", \"Recall\"];\n// Gradient Boosting trades precision for recall (a realistic classifier\n// trade-off), breaking the otherwise strict Accuracy > Precision > Recall\n// ordering shared by the other four models.\nconst models = [\n  { name: \"SVM (RBF)\", Accuracy: 85.1, Precision: 84.0, Recall: 82.3 },\n  { name: \"Logistic Regression\", Accuracy: 87.9, Precision: 86.4, Recall: 85.1 },\n  { name: \"Random Forest\", Accuracy: 92.3, Precision: 91.5, Recall: 90.2 },\n  { name: \"Gradient Boosting\", Accuracy: 94.7, Precision: 92.6, Recall: 93.8 },\n  { name: \"Neural Network\", Accuracy: 96.2, Precision: 95.4, Recall: 94.8 },\n];\nconst cells = models.flatMap((m) =>\n  metrics.map((metric) => ({ model: m.name, metric, value: m[metric] })),\n);\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 y0 = d3\n  .scaleBand()\n  .domain(models.map((m) => m.name))\n  .range([ih, 0])\n  .paddingInner(0.4)\n  .paddingOuter(0.25);\nconst y1 = d3.scaleBand().domain(metrics).range([0, y0.bandwidth()]).padding(0.25);\nconst x = d3.scaleLinear().domain([0, 100]).range([0, iw]);\nconst color = d3.scaleOrdinal().domain(metrics).range(t.palette);\n\n// --- Top-performer highlight (data-storytelling focal point) ------------------\n// Neural Network sorts last (highest accuracy) and lands at the top row.\nconst topModel = models[models.length - 1];\nconst topBandTop = y0(topModel.name);\nconst topBandHeight = y0.bandwidth();\nconst bandPad = 14;\ng.append(\"rect\")\n  .attr(\"class\", \"top-highlight\")\n  .attr(\"x\", -margin.left + 24)\n  .attr(\"y\", topBandTop - bandPad)\n  .attr(\"width\", iw + margin.left + margin.right - 48)\n  .attr(\"height\", topBandHeight + bandPad * 2)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"fill-opacity\", 0.08);\ng.append(\"rect\")\n  .attr(\"class\", \"top-highlight-accent\")\n  .attr(\"x\", -margin.left + 24)\n  .attr(\"y\", topBandTop - bandPad)\n  .attr(\"width\", 4)\n  .attr(\"height\", topBandHeight + bandPad * 2)\n  .attr(\"fill\", t.palette[0]);\n\n// --- Gridlines ------------------------------------------------------------------\ng.selectAll(\".grid\")\n  .data(x.ticks(5))\n  .join(\"line\")\n  .attr(\"class\", \"grid\")\n  .attr(\"x1\", (d) => x(d))\n  .attr(\"x2\", (d) => x(d))\n  .attr(\"y1\", 0)\n  .attr(\"y2\", ih)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Axes -----------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(5).tickFormat((d) => `${d}%`));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y0).tickSize(0).tickPadding(14));\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\nyAxis\n  .selectAll(\".tick\")\n  .filter((d) => d === topModel.name)\n  .select(\"text\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-weight\", \"700\");\n\n// --- X-axis label -----------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Score (%)\");\n\n// --- Grouped lollipops --------------------------------------------------------\nconst cellY = (d) => y0(d.model) + y1(d.metric) + y1.bandwidth() / 2;\n\ng.selectAll(\".stem\")\n  .data(cells)\n  .join(\"line\")\n  .attr(\"class\", \"stem\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", (d) => x(d.value))\n  .attr(\"y1\", cellY)\n  .attr(\"y2\", cellY)\n  .attr(\"stroke\", (d) => color(d.metric))\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"stroke-opacity\", 0.7);\n\ng.selectAll(\".dot\")\n  .data(cells)\n  .join(\"circle\")\n  .attr(\"class\", \"dot\")\n  .attr(\"cx\", (d) => x(d.value))\n  .attr(\"cy\", cellY)\n  .attr(\"r\", 8)\n  .attr(\"fill\", (d) => color(d.metric))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\ng.selectAll(\".val\")\n  .data(cells)\n  .join(\"text\")\n  .attr(\"class\", \"val\")\n  .attr(\"x\", (d) => x(d.value) + 14)\n  .attr(\"y\", cellY)\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .style(\"font-weight\", \"500\")\n  .text((d) => d.value.toFixed(1));\n\n// --- Top-performer callout ---------------------------------------------------\ng.append(\"text\")\n  .attr(\"class\", \"top-callout\")\n  .attr(\"x\", 0)\n  .attr(\"y\", Math.max(topBandTop - bandPad - 8, 14))\n  .attr(\"fill\", t.palette[0])\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"700\")\n  .text(`★ Top performer — highest accuracy (${topModel.Accuracy.toFixed(1)}%)`);\n\n// --- Legend ---------------------------------------------------------------------\nconst legend = svg.append(\"g\").attr(\"transform\", \"translate(0, 96)\");\nlet xOffset = 0;\nfor (const metric of metrics) {\n  const item = legend.append(\"g\").attr(\"transform\", `translate(${xOffset},0)`);\n  item.append(\"circle\").attr(\"r\", 9).attr(\"cy\", -5).attr(\"fill\", color(metric));\n  const label = item\n    .append(\"text\")\n    .attr(\"x\", 20)\n    .attr(\"y\", 0)\n    .attr(\"dominant-baseline\", \"middle\")\n    .attr(\"fill\", t.ink)\n    .style(\"font-size\", \"15px\")\n    .text(metric);\n  xOffset += 20 + label.node().getBBox().width + 40;\n}\nconst legendWidth = xOffset - 40;\nlegend.attr(\"transform\", `translate(${(width - legendWidth) / 2}, 96)`);\n\n// --- Title ------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"lollipop-grouped · javascript · d3 · anyplot.ai\");\n"}