100% Stacked Bar Chart — MUI X Charts

A 100% stacked bar chart displays multiple data series as proportional segments within each bar, where every bar is normalized to sum to 100%. This visualization emphasizes composition and relative proportions rather than absolute values, making it ideal for comparing how different components contribute to a whole across categories. It reveals patterns in distribution and share that might be obscured when absolute values vary significantly between categories.

100% Stacked Bar Chart rendered with MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// bar-stacked-percent: 100% Stacked Bar Chart
// Library: muix 7.29.1 | JavaScript 22.23.2
// Quality: 94/100 | Created: 2026-08-18
import { BarChart } from "@mui/x-charts/BarChart";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

const t = window.ANYPLOT_TOKENS;

// Fixed (non-theme-flipping) text colors for in-bar percentage labels — the
// segment fills stay the same hex in both themes, so label contrast is chosen
// per series color rather than following t.ink/t.inkSoft.
const LABEL_ON_LIGHT_FILL = "#1A1A17"; // dark ink — for the green/lavender/ochre segments
const LABEL_ON_DARK_FILL = "#F0EFE8"; // light ink — for the darker blue segment

// --- Data (in-memory, deterministic) ----------------------------------------
// Electricity generation mix (TWh/year, illustrative) for six grids whose
// total output spans two orders of magnitude — China dwarfs Norway in raw
// terawatt-hours, which is exactly why a percent-stacked view (composition)
// tells a clearer story here than a raw stacked view (dominated by scale).
const rawByCountry = {
  China: { renewables: 3100, nuclear: 400, gas: 300, coal: 5200 },
  USA: { renewables: 900, nuclear: 800, gas: 1700, coal: 700 },
  Germany: { renewables: 270, nuclear: 0, gas: 80, coal: 150 },
  France: { renewables: 120, nuclear: 320, gas: 30, coal: 5 },
  Brazil: { renewables: 550, nuclear: 15, gas: 60, coal: 15 },
  Norway: { renewables: 150, nuclear: 0, gas: 0.5, coal: 0.1 },
};

// Sorted descending by renewables share so the bars themselves form a visual
// gradient (all-renewable Norway -> coal-heavy China) that reinforces the
// subtitle's contrast, rather than an arbitrary country ordering.
const renewablesShare = (country) => {
  const raw = rawByCountry[country];
  const total = raw.renewables + raw.nuclear + raw.gas + raw.coal;
  return raw.renewables / total;
};
const countries = Object.keys(rawByCountry).sort((a, b) => renewablesShare(b) - renewablesShare(a));

// Renewables listed first so it lands on Imprint position 1 (brand green) —
// a natural semantic fit (growth/nature). Order stays fixed across every bar.
const components = [
  { id: "renewables", label: "Renewables", labelFill: LABEL_ON_LIGHT_FILL },
  { id: "nuclear", label: "Nuclear", labelFill: LABEL_ON_LIGHT_FILL },
  { id: "gas", label: "Natural Gas", labelFill: LABEL_ON_DARK_FILL },
  { id: "coal", label: "Coal", labelFill: LABEL_ON_LIGHT_FILL },
];
const labelFillById = Object.fromEntries(components.map((c) => [c.id, c.labelFill]));

const totalsByCountry = countries.map((country) =>
  components.reduce((sum, c) => sum + rawByCountry[country][c.id], 0),
);
const series = components.map((c) => ({
  id: c.id,
  label: c.label,
  stack: "total",
  data: countries.map((country, i) => (rawByCountry[country][c.id] / totalsByCountry[i]) * 100),
}));

export default function Chart() {
  const W = window.ANYPLOT_SIZE.width; // 1600 CSS px (landscape mount)
  const H = window.ANYPLOT_SIZE.height; // 900 CSS px
  const CHART_TOP = 84;

  return (
    <Box sx={{ position: "relative", width: W, height: H, bgcolor: t.pageBg }}>
      {/* Title + subtitle */}
      <Box sx={{ position: "absolute", top: 24, left: 56, right: 56 }}>
        <Typography sx={{ color: t.ink, fontSize: 22, fontWeight: 500 }}>
          bar-stacked-percent · javascript · muix · anyplot.ai
        </Typography>
        <Typography sx={{ color: t.inkSoft, fontSize: 14, mt: 0.5 }}>
          Norway's grid runs almost entirely on renewables, while China still leans on coal
        </Typography>
      </Box>

      {/* Bar chart */}
      <Box
        sx={{
          position: "absolute",
          top: CHART_TOP,
          left: 0,
          right: 0,
          bottom: 0,
        }}
      >
        <BarChart
          width={W}
          height={H - CHART_TOP}
          colors={t.palette}
          skipAnimation
          xAxis={[
            {
              scaleType: "band",
              data: countries,
              label: "Country",
              disableTicks: true,
              labelStyle: { fontSize: 15, fill: t.ink },
              tickLabelStyle: { fontSize: 14, fill: t.inkSoft },
              categoryGapRatio: 0.35,
            },
          ]}
          yAxis={[
            {
              label: "Share (%)",
              labelStyle: { fontSize: 15, fill: t.ink },
              // tickFontSize (unlike tickLabelStyle.fontSize) drives the axis
              // label's offset from the tick labels — pushed wide to clear
              // the "100%" tick text without inflating its visual size.
              tickFontSize: 70,
              tickLabelStyle: { fontSize: 14, fill: t.inkSoft },
              disableTicks: true,
              min: 0,
              max: 100,
              valueFormatter: (v) => `${v}%`,
            },
          ]}
          series={series.map((s) => ({
            ...s,
            valueFormatter: (v) => `${v.toFixed(1)}%`,
          }))}
          barLabel={(item) => (item.value != null && item.value >= 6 ? `${item.value.toFixed(0)}%` : null)}
          margin={{ top: 14, right: 90, bottom: 90, left: 140 }}
          grid={{ horizontal: true }}
          slotProps={{
            legend: {
              position: { vertical: "middle", horizontal: "right" },
              direction: "column",
              labelStyle: { fontSize: 14, fill: t.inkSoft },
            },
            barLabel: (ownerState) => ({
              style: {
                fontSize: 13,
                fontWeight: 600,
                fill: labelFillById[ownerState.seriesId] ?? LABEL_ON_LIGHT_FILL,
              },
            }),
          }}
          sx={{
            // Lighter, grid-weight axis line instead of the default full-ink
            // stroke — a more refined chrome treatment than the library default.
            "& .MuiChartsAxis-line": { stroke: t.grid },
            "& .MuiChartsGrid-line": { stroke: t.grid },
          }}
        />
      </Box>
    </Box>
  );
}

Part of 100% Stacked Bar Chart on anyplot.ai.

Other implementations