{"spec_id":"funnel-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// funnel-basic: Basic Funnel Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// Data — software engineering hiring pipeline, applications through hire\nconst stages = [\n  { name: \"Applications\", value: 1200 },\n  { name: \"Resume Screened\", value: 480 },\n  { name: \"Phone Interview\", value: 210 },\n  { name: \"Onsite Interview\", value: 95 },\n  { name: \"Offer Extended\", value: 52 },\n  { name: \"Hired\", value: 34 },\n];\n\n// Layout — funnel on the left, stage labels on the right, centered as one block\nconst margin = { top: 130, right: 60, bottom: 50, left: 60 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\nconst bandGap = 34;\nconst bandHeight = (ih - bandGap * (stages.length - 1)) / stages.length;\n\nconst funnelMaxWidth = iw * 0.46;\nconst gapToLabel = 70;\nconst labelBlockWidth = 260;\nconst contentWidth = funnelMaxWidth + gapToLabel + labelBlockWidth;\nconst startX = margin.left + (iw - contentWidth) / 2;\nconst cx = startX + funnelMaxWidth / 2;\nconst labelX = startX + funnelMaxWidth + gapToLabel;\n\nconst widthScale = d3.scaleLinear().domain([0, stages[0].value]).range([0, funnelMaxWidth]);\n\nconst bands = stages.map((d, i) => {\n  const topW = widthScale(d.value);\n  const bottomW = i < stages.length - 1 ? widthScale(stages[i + 1].value) : widthScale(d.value);\n  const y0 = margin.top + i * (bandHeight + bandGap);\n  const y1 = y0 + bandHeight;\n  return {\n    ...d,\n    topW,\n    bottomW,\n    y0,\n    y1,\n    color: t.palette[i % t.palette.length],\n    pctOfFirst: Math.round((d.value / stages[0].value) * 100),\n  };\n});\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// --- Funnel segments -----------------------------------------------------\nsvg\n  .selectAll(\"path.stage\")\n  .data(bands)\n  .join(\"path\")\n  .attr(\"class\", \"stage\")\n  .attr(\n    \"d\",\n    (d) =>\n      `M${cx - d.topW / 2},${d.y0} L${cx + d.topW / 2},${d.y0} ` +\n      `L${cx + d.bottomW / 2},${d.y1} L${cx - d.bottomW / 2},${d.y1} Z`\n  )\n  .attr(\"fill\", (d) => d.color);\n\n// --- Drop-off labels between stages ---------------------------------------\nsvg\n  .selectAll(\"text.dropoff\")\n  .data(bands.slice(0, -1))\n  .join(\"text\")\n  .attr(\"class\", \"dropoff\")\n  .attr(\"x\", cx)\n  .attr(\"y\", (d) => d.y1 + bandGap / 2 + 5)\n  .attr(\"text-anchor\", \"middle\")\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"500\")\n  .attr(\"fill\", t.inkSoft)\n  .text((d, i) => `↓ ${Math.round((1 - bands[i + 1].value / d.value) * 100)}%`);\n\n// --- Leader lines + stage labels -------------------------------------------\nconst labelGroup = svg.selectAll(\"g.label\").data(bands).join(\"g\").attr(\"class\", \"label\");\n\nlabelGroup.each(function (d) {\n  const g = d3.select(this);\n  const midY = (d.y0 + d.y1) / 2;\n  const midW = (d.topW + d.bottomW) / 2;\n  const edgeX = cx + midW / 2;\n\n  g.append(\"line\")\n    .attr(\"x1\", edgeX)\n    .attr(\"y1\", midY)\n    .attr(\"x2\", labelX - 12)\n    .attr(\"y2\", midY)\n    .attr(\"stroke\", t.grid)\n    .attr(\"stroke-width\", 1.5);\n\n  g.append(\"text\")\n    .attr(\"x\", labelX)\n    .attr(\"y\", midY - 8)\n    .style(\"font-size\", \"18px\")\n    .style(\"font-weight\", \"600\")\n    .attr(\"fill\", t.ink)\n    .text(d.name);\n\n  g.append(\"text\")\n    .attr(\"x\", labelX)\n    .attr(\"y\", midY + 16)\n    .style(\"font-size\", \"15px\")\n    .attr(\"fill\", t.inkSoft)\n    .text(`${d.value.toLocaleString()} · ${d.pctOfFirst}%`);\n});\n\n// --- Title -------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 56)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"24px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"funnel-basic · javascript · d3 · anyplot.ai\");\n"}