Basic Count Plot — MUI X Charts

A count plot displays the frequency of observations in each category of a categorical variable using vertical bars. Unlike a basic bar chart that requires pre-computed values, a count plot automatically counts occurrences from raw data. This makes it ideal for quick exploratory analysis of categorical distributions without manual aggregation.

Basic Count Plot rendered with MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// count-basic: Basic Count Plot
// Library: muix 7.29.1 | JavaScript 22.23.1
// Quality: 95/100 | Created: 2026-08-11
import { BarChart } from "@mui/x-charts/BarChart";
import { ChartsReferenceLine } from "@mui/x-charts/ChartsReferenceLine";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

const t = window.ANYPLOT_TOKENS;

// --- Data: raw support-ticket categories, counted (not pre-aggregated) ------
// A count plot tallies occurrences from raw observations rather than plotting
// pre-computed values, so we simulate individual ticket records first and only
// derive the bar heights by counting them.
function lcg(seed) {
  let state = seed;
  return () => {
    state = (state * 1664525 + 1013904223) % 4294967296;
    return state / 4294967296;
  };
}

const rand = lcg(42);
const TICKET_TYPES = [
  "Technical",
  "Billing",
  "Shipping",
  "Account",
  "Feature Request",
  "Other",
];
const TYPE_WEIGHTS = [0.3, 0.24, 0.18, 0.14, 0.09, 0.05];
const TICKET_COUNT = 620;

const rawTickets = Array.from({ length: TICKET_COUNT }, () => {
  const r = rand();
  let cumulative = 0;
  for (let i = 0; i < TICKET_TYPES.length; i++) {
    cumulative += TYPE_WEIGHTS[i];
    if (r < cumulative) return TICKET_TYPES[i];
  }
  return TICKET_TYPES[TICKET_TYPES.length - 1];
});

const countByType = new Map();
for (const ticketType of rawTickets) {
  countByType.set(ticketType, (countByType.get(ticketType) ?? 0) + 1);
}

// Sorted by frequency, descending — the default reading order for a count plot.
const sortedEntries = [...countByType.entries()].sort((a, b) => b[1] - a[1]);
const categories = sortedEntries.map(([category]) => category);
const counts = sortedEntries.map(([, count]) => count);
const meanCount = counts.reduce((sum, c) => sum + c, 0) / counts.length;

// Leading category vs. the average — surfaces the takeaway instead of leaving
// the viewer to read it off the bars themselves.
const topCategory = categories[0];
const pctAboveAvg = Math.round(((counts[0] - meanCount) / meanCount) * 100);

// The y-axis label's offset from the axis line is computed internally by MUI X
// as `tickFontSize + tickSize + 10` (not from tickLabelStyle.fontSize, the
// prop that actually sets the rendered glyph size) — so tickFontSize has to be
// sized to clear the widest tick number, or the label collides with it. Scale
// it off the real digit count instead of a fixed magic number so the offset
// stays correct if the data range changes.
const yTickDigits = String(Math.max(...counts)).length;
const yTickFontSize = 12 + yTickDigits * 8;

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 = 108;

  return (
    <Box sx={{ position: "relative", width: W, height: H, bgcolor: t.pageBg }}>
      {/* Title */}
      <Box sx={{ position: "absolute", top: 24, left: 56, right: 56 }}>
        <Typography sx={{ color: t.ink, fontSize: 22, fontWeight: 500 }}>
          count-basic · javascript · muix · anyplot.ai
        </Typography>
      </Box>

      {/* Subtitle: surfaces the takeaway directly rather than leaving the
          viewer to read the sort order + average line themselves. */}
      <Box sx={{ position: "absolute", top: 58, left: 56, right: 56 }}>
        <Typography sx={{ color: t.inkSoft, fontSize: 15, fontWeight: 400 }}>
          {topCategory} tickets run {pctAboveAvg}% above the category average
        </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[0]]}
          skipAnimation
          borderRadius={4}
          xAxis={[
            {
              scaleType: "band",
              data: categories,
              label: "Support Ticket Category",
              disableTicks: true,
              disableLine: true,
              labelStyle: { fontSize: 16, fill: t.ink },
              tickLabelStyle: { fontSize: 15, fill: t.inkSoft },
              categoryGapRatio: 0.4,
            },
          ]}
          yAxis={[
            {
              label: "Number of Tickets",
              labelStyle: { fontSize: 16, fill: t.ink },
              tickLabelStyle: { fontSize: 15, fill: t.inkSoft },
              // tickFontSize only sizes the *reserved layout offset* between the
              // tick labels and the axis label (tickLabelStyle.fontSize above
              // wins for the rendered glyph size) — derived from yTickFontSize
              // above so it stays correct if the data range changes.
              tickFontSize: yTickFontSize,
              disableTicks: true,
              disableLine: true,
              max: Math.max(...counts) * 1.15,
            },
          ]}
          series={[{ data: counts, label: "Tickets" }]}
          barLabel="value"
          margin={{ top: 14, right: 40, bottom: 90, left: 96 }}
          grid={{ horizontal: true }}
          slotProps={{
            legend: { hidden: true },
            // The leading category (dataIndex 0, since bars are sorted
            // descending) gets a slightly bolder/larger label — a focal-point
            // emphasis that reinforces the subtitle without changing the
            // brand-green fill, so data color stays identical across themes.
            barLabel: (ownerState) => ({
              style: {
                fontSize: ownerState.dataIndex === 0 ? 17 : 15,
                fontWeight: ownerState.dataIndex === 0 ? 700 : 600,
                fill: t.ink,
              },
            }),
            // A thin ink-colored stroke crisps the bar edges against the page
            // background — the style guide's sanctioned outline pattern, not
            // a color/gradient change, so it doesn't affect data color parity.
            bar: { style: { stroke: t.ink, strokeOpacity: 0.15, strokeWidth: 1 } },
          }}
          sx={{
            "& .MuiChartsGrid-line": { stroke: t.grid },
          }}
        >
          {/* Mean reference line: turns "6 bars" into "which categories run
              above/below the average ticket volume" — lands in open canvas at
              the right (short bars), never crossing the in-bar value labels. */}
          <ChartsReferenceLine
            y={meanCount}
            label={`Avg ${Math.round(meanCount)}`}
            labelAlign="end"
            lineStyle={{
              stroke: t.inkSoft,
              strokeDasharray: "6 4",
              strokeWidth: 1.5,
            }}
            labelStyle={{ fill: t.inkSoft, fontSize: 13, fontWeight: 500 }}
          />
        </BarChart>
      </Box>
    </Box>
  );
}

Part of Basic Count Plot on anyplot.ai.

Other implementations