Basic Funnel Chart — MUI X Charts

A funnel chart visualizes sequential stages of a process where values progressively decrease from one stage to the next. Each stage is represented as a trapezoidal segment that narrows from top to bottom, making it easy to identify drop-offs between stages. This chart is ideal for tracking conversion rates, sales pipelines, and multi-step processes where understanding stage-to-stage transitions is critical.

Basic Funnel Chart rendered with MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// funnel-basic: Basic Funnel Chart
// Library: muix 7.29.1 | JavaScript 22.23.2
// Quality: 93/100 | Created: 2026-09-05

import { ChartContainer } from "@mui/x-charts/ChartContainer";
import { ChartsYAxis } from "@mui/x-charts/ChartsYAxis";
import { useXScale, useYScale, useDrawingArea } from "@mui/x-charts";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

const t = window.ANYPLOT_TOKENS;

// E-commerce checkout pipeline — product views through completed orders
const stages = [
  { name: "Product Views", value: 50000 },
  { name: "Added to Cart", value: 30000 },
  { name: "Checkout Started", value: 16000 },
  { name: "Payment Submitted", value: 9000 },
  { name: "Order Completed", value: 5500 },
];
const stageNames = stages.map((s) => s.name);
const MAX_VAL = stages[0].value;

// @mui/x-charts has no native FunnelChart in the community package — this
// composes ChartContainer's own band/linear scales (as `useXScale`/`useYScale`
// expose) with a hand-drawn trapezoid per stage, the same low-level pattern
// as an MUI X gauge/sparkline built from ChartsSurface primitives.
function contrastText(hex) {
  const r = parseInt(hex.slice(1, 3), 16);
  const g = parseInt(hex.slice(3, 5), 16);
  const b = parseInt(hex.slice(5, 7), 16);
  const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
  return luminance > 128 ? "#1A1A17" : "#FFFDF6";
}

// Must be rendered inside ChartContainer to access its scale context
function FunnelPlot() {
  const xScale = useXScale();
  const yScale = useYScale();
  const drawingArea = useDrawingArea();

  // Wait until band scale is fully resolved
  if (!xScale || !yScale || typeof yScale.bandwidth !== "function") return null;

  const x0 = +xScale(0);
  const cx = drawingArea.left + drawingArea.width / 2;
  const bandwidth = yScale.bandwidth();

  return (
    <g>
      {stages.map((stage, i) => {
        const bandTop = yScale(stage.name);
        if (bandTop == null || !isFinite(+bandTop)) return null;

        const y0 = +bandTop;
        const y1 = y0 + bandwidth;
        const topW = +xScale(stage.value) - x0;
        const nextVal = i < stages.length - 1 ? stages[i + 1].value : stage.value;
        const bottomW = +xScale(nextVal) - x0;
        const color = t.palette[i % t.palette.length];
        const labelColor = contrastText(color);
        const pct = Math.round((stage.value / MAX_VAL) * 100);

        const points = [
          [cx - topW / 2, y0],
          [cx + topW / 2, y0],
          [cx + bottomW / 2, y1],
          [cx - bottomW / 2, y1],
        ]
          .map((p) => p.join(","))
          .join(" ");

        return (
          <g key={stage.name}>
            <polygon points={points} fill={color} />
            <text
              x={cx}
              y={y0 + bandwidth / 2 - 12}
              fill={labelColor}
              fontSize={18}
              fontWeight={600}
              textAnchor="middle"
              fontFamily="sans-serif"
            >
              {stage.value.toLocaleString()}
            </text>
            <text
              x={cx}
              y={y0 + bandwidth / 2 + 14}
              fill={labelColor}
              fontSize={14}
              textAnchor="middle"
              fontFamily="sans-serif"
            >
              {pct}% of total
            </text>
          </g>
        );
      })}
    </g>
  );
}

const TITLE = "funnel-basic · javascript · muix · anyplot.ai";
const TITLE_HEIGHT = 70;
const MARGIN = { top: 20, right: 60, bottom: 30, left: 220 };

export default function Chart() {
  const chartWidth = window.ANYPLOT_SIZE.width;
  const chartHeight = window.ANYPLOT_SIZE.height - TITLE_HEIGHT;

  return (
    <Box
      sx={{
        width: chartWidth,
        height: window.ANYPLOT_SIZE.height,
        display: "flex",
        flexDirection: "column",
      }}
    >
      <Box
        sx={{
          height: TITLE_HEIGHT,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          flexShrink: 0,
        }}
      >
        <Typography sx={{ color: t.ink, fontSize: 22, fontWeight: 500 }}>
          {TITLE}
        </Typography>
      </Box>

      <ChartContainer
        skipAnimation
        width={chartWidth}
        height={chartHeight}
        margin={MARGIN}
        series={[
          {
            type: "bar",
            data: stages.map((s) => s.value),
            layout: "horizontal",
          },
        ]}
        xAxis={[{ scaleType: "linear", min: 0, max: MAX_VAL }]}
        yAxis={[
          {
            scaleType: "band",
            data: stageNames,
            categoryGapRatio: 0.35,
            disableLine: true,
            disableTicks: true,
            tickLabelStyle: { fontSize: 16, fill: t.inkSoft, fontWeight: 500 },
          },
        ]}
      >
        <FunnelPlot />
        <ChartsYAxis />
      </ChartContainer>
    </Box>
  );
}

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/funnel-basic/muix/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "funnel-basic",
  "language": "javascript",
  "library": "muix",
  "page": "https://anyplot.ai/funnel-basic/javascript/muix",
  "hub": "https://anyplot.ai/funnel-basic",
  "code_json": "https://api.anyplot.ai/specs/funnel-basic/muix/code",
  "spec_json": "https://api.anyplot.ai/specs/funnel-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/funnel-basic/javascript/muix/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/funnel-basic/javascript/muix/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/funnel-basic/javascript/muix/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/funnel-basic/javascript/muix/plot-dark.html",
  "quality_score": 93.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Basic Funnel Chart on anyplot.ai.

Other implementations