{"spec_id":"funnel-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// funnel-basic: Basic Funnel Chart\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-05\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { useXScale, useYScale, useDrawingArea } from \"@mui/x-charts\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// E-commerce checkout pipeline — product views through completed orders\nconst stages = [\n  { name: \"Product Views\", value: 50000 },\n  { name: \"Added to Cart\", value: 30000 },\n  { name: \"Checkout Started\", value: 16000 },\n  { name: \"Payment Submitted\", value: 9000 },\n  { name: \"Order Completed\", value: 5500 },\n];\nconst stageNames = stages.map((s) => s.name);\nconst MAX_VAL = stages[0].value;\n\n// @mui/x-charts has no native FunnelChart in the community package — this\n// composes ChartContainer's own band/linear scales (as `useXScale`/`useYScale`\n// expose) with a hand-drawn trapezoid per stage, the same low-level pattern\n// as an MUI X gauge/sparkline built from ChartsSurface primitives.\nfunction contrastText(hex) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  const luminance = 0.299 * r + 0.587 * g + 0.114 * b;\n  return luminance > 128 ? \"#1A1A17\" : \"#FFFDF6\";\n}\n\n// Must be rendered inside ChartContainer to access its scale context\nfunction FunnelPlot() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const drawingArea = useDrawingArea();\n\n  // Wait until band scale is fully resolved\n  if (!xScale || !yScale || typeof yScale.bandwidth !== \"function\") return null;\n\n  const x0 = +xScale(0);\n  const cx = drawingArea.left + drawingArea.width / 2;\n  const bandwidth = yScale.bandwidth();\n\n  return (\n    <g>\n      {stages.map((stage, i) => {\n        const bandTop = yScale(stage.name);\n        if (bandTop == null || !isFinite(+bandTop)) return null;\n\n        const y0 = +bandTop;\n        const y1 = y0 + bandwidth;\n        const topW = +xScale(stage.value) - x0;\n        const nextVal = i < stages.length - 1 ? stages[i + 1].value : stage.value;\n        const bottomW = +xScale(nextVal) - x0;\n        const color = t.palette[i % t.palette.length];\n        const labelColor = contrastText(color);\n        const pct = Math.round((stage.value / MAX_VAL) * 100);\n\n        const points = [\n          [cx - topW / 2, y0],\n          [cx + topW / 2, y0],\n          [cx + bottomW / 2, y1],\n          [cx - bottomW / 2, y1],\n        ]\n          .map((p) => p.join(\",\"))\n          .join(\" \");\n\n        return (\n          <g key={stage.name}>\n            <polygon points={points} fill={color} />\n            <text\n              x={cx}\n              y={y0 + bandwidth / 2 - 12}\n              fill={labelColor}\n              fontSize={18}\n              fontWeight={600}\n              textAnchor=\"middle\"\n              fontFamily=\"sans-serif\"\n            >\n              {stage.value.toLocaleString()}\n            </text>\n            <text\n              x={cx}\n              y={y0 + bandwidth / 2 + 14}\n              fill={labelColor}\n              fontSize={14}\n              textAnchor=\"middle\"\n              fontFamily=\"sans-serif\"\n            >\n              {pct}% of total\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nconst TITLE = \"funnel-basic · javascript · muix · anyplot.ai\";\nconst TITLE_HEIGHT = 70;\nconst MARGIN = { top: 20, right: 60, bottom: 30, left: 220 };\n\nexport default function Chart() {\n  const chartWidth = window.ANYPLOT_SIZE.width;\n  const chartHeight = window.ANYPLOT_SIZE.height - TITLE_HEIGHT;\n\n  return (\n    <Box\n      sx={{\n        width: chartWidth,\n        height: window.ANYPLOT_SIZE.height,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <Box\n        sx={{\n          height: TITLE_HEIGHT,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          flexShrink: 0,\n        }}\n      >\n        <Typography sx={{ color: t.ink, fontSize: 22, fontWeight: 500 }}>\n          {TITLE}\n        </Typography>\n      </Box>\n\n      <ChartContainer\n        skipAnimation\n        width={chartWidth}\n        height={chartHeight}\n        margin={MARGIN}\n        series={[\n          {\n            type: \"bar\",\n            data: stages.map((s) => s.value),\n            layout: \"horizontal\",\n          },\n        ]}\n        xAxis={[{ scaleType: \"linear\", min: 0, max: MAX_VAL }]}\n        yAxis={[\n          {\n            scaleType: \"band\",\n            data: stageNames,\n            categoryGapRatio: 0.35,\n            disableLine: true,\n            disableTicks: true,\n            tickLabelStyle: { fontSize: 16, fill: t.inkSoft, fontWeight: 500 },\n          },\n        ]}\n      >\n        <FunnelPlot />\n        <ChartsYAxis />\n      </ChartContainer>\n    </Box>\n  );\n}\n"}