{"spec_id":"funnel-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// funnel-basic: Basic Funnel Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// E-commerce checkout funnel: visitors thinning out at each subsequent step.\nconst stages = [\n  \"Website Visitors\",\n  \"Product Page Views\",\n  \"Added to Cart\",\n  \"Checkout Started\",\n  \"Purchase Completed\",\n];\nconst values = [12000, 6400, 3100, 1450, 820];\n\n// Floating (centered) horizontal bars: each spans [-value/2, value/2], so\n// stacking them by decreasing value produces the tapering funnel silhouette.\nconst segments = values.map((v) => [-v / 2, v / 2]);\nconst maxHalfWidth = values[0] / 2;\nconst colors = stages.map((_, i) => t.palette[i % t.palette.length]);\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Funnel shape + label plugin (native Chart.js plugin API) ---------------\n// The underlying \"bar\" dataset only supplies layout (category bands, x scale\n// pixel mapping) and is drawn fully transparent. This plugin paints the real\n// funnel silhouette: each stage is a trapezoid whose top width is its own\n// value and whose bottom width is the *next* stage's value, so consecutive\n// segments share an identical edge and the whole shape tapers continuously\n// instead of stacking as separated rectangles.\nconst funnelShape = {\n  id: \"funnelShape\",\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    const meta = chart.getDatasetMeta(0);\n    const centerX = scales.x.getPixelForValue(0);\n    const halfWidthPx = values.map(\n      (v) => scales.x.getPixelForValue(v / 2) - centerX,\n    );\n\n    ctx.save();\n    meta.data.forEach((bar, i) => {\n      const { y, height } = bar.getProps([\"y\", \"height\"], true);\n      const top = y - height / 2;\n      const bottom = y + height / 2;\n      const topHalf = halfWidthPx[i];\n      const bottomHalf =\n        i < values.length - 1 ? halfWidthPx[i + 1] : halfWidthPx[i];\n\n      ctx.beginPath();\n      ctx.moveTo(centerX - topHalf, top);\n      ctx.lineTo(centerX + topHalf, top);\n      ctx.lineTo(centerX + bottomHalf, bottom);\n      ctx.lineTo(centerX - bottomHalf, bottom);\n      ctx.closePath();\n      ctx.fillStyle = colors[i];\n      ctx.fill();\n\n      if (i > 0) {\n        ctx.strokeStyle = t.pageBg;\n        ctx.lineWidth = 2;\n        ctx.beginPath();\n        ctx.moveTo(centerX - topHalf, top);\n        ctx.lineTo(centerX + topHalf, top);\n        ctx.stroke();\n      }\n    });\n\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    ctx.font = \"700 20px sans-serif\";\n    ctx.fillStyle = \"#FFFFFF\";\n    ctx.shadowColor = \"rgba(26, 26, 23, 0.5)\";\n    ctx.shadowBlur = 6;\n    ctx.shadowOffsetY = 1;\n    meta.data.forEach((bar, i) => {\n      const { x, y } = bar.getCenterPoint();\n      const share = Math.round((values[i] / values[0]) * 100);\n      const label = `${values[i].toLocaleString()} (${share}%)`;\n      ctx.fillText(label, x, y);\n    });\n    ctx.restore();\n  },\n};\n\n// --- Chart -------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"bar\",\n  data: {\n    labels: stages,\n    datasets: [\n      {\n        data: segments,\n        backgroundColor: \"transparent\",\n        borderWidth: 0,\n      },\n    ],\n  },\n  options: {\n    indexAxis: \"y\",\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    barPercentage: 1,\n    categoryPercentage: 1,\n    plugins: {\n      title: {\n        display: true,\n        text: \"funnel-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n        padding: { bottom: 24 },\n      },\n      legend: { display: false },\n    },\n    scales: {\n      x: {\n        display: false,\n        min: -maxHalfWidth * 1.05,\n        max: maxHalfWidth * 1.05,\n        grid: { display: false },\n        border: { display: false },\n      },\n      y: {\n        ticks: { color: t.inkSoft, font: { size: 16 } },\n        grid: { display: false },\n        border: { display: false },\n      },\n    },\n  },\n  plugins: [funnelShape],\n});\n"}