{"spec_id":"count-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// count-basic: Basic Count Plot\n// Library: muix 7.29.1 | JavaScript 22.23.1\n// Quality: 95/100 | Created: 2026-08-11\nimport { BarChart } from \"@mui/x-charts/BarChart\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport Box from \"@mui/material/Box\";\nimport Typography from \"@mui/material/Typography\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: raw support-ticket categories, counted (not pre-aggregated) ------\n// A count plot tallies occurrences from raw observations rather than plotting\n// pre-computed values, so we simulate individual ticket records first and only\n// derive the bar heights by counting them.\nfunction lcg(seed) {\n  let state = seed;\n  return () => {\n    state = (state * 1664525 + 1013904223) % 4294967296;\n    return state / 4294967296;\n  };\n}\n\nconst rand = lcg(42);\nconst TICKET_TYPES = [\n  \"Technical\",\n  \"Billing\",\n  \"Shipping\",\n  \"Account\",\n  \"Feature Request\",\n  \"Other\",\n];\nconst TYPE_WEIGHTS = [0.3, 0.24, 0.18, 0.14, 0.09, 0.05];\nconst TICKET_COUNT = 620;\n\nconst rawTickets = Array.from({ length: TICKET_COUNT }, () => {\n  const r = rand();\n  let cumulative = 0;\n  for (let i = 0; i < TICKET_TYPES.length; i++) {\n    cumulative += TYPE_WEIGHTS[i];\n    if (r < cumulative) return TICKET_TYPES[i];\n  }\n  return TICKET_TYPES[TICKET_TYPES.length - 1];\n});\n\nconst countByType = new Map();\nfor (const ticketType of rawTickets) {\n  countByType.set(ticketType, (countByType.get(ticketType) ?? 0) + 1);\n}\n\n// Sorted by frequency, descending — the default reading order for a count plot.\nconst sortedEntries = [...countByType.entries()].sort((a, b) => b[1] - a[1]);\nconst categories = sortedEntries.map(([category]) => category);\nconst counts = sortedEntries.map(([, count]) => count);\nconst meanCount = counts.reduce((sum, c) => sum + c, 0) / counts.length;\n\n// Leading category vs. the average — surfaces the takeaway instead of leaving\n// the viewer to read it off the bars themselves.\nconst topCategory = categories[0];\nconst pctAboveAvg = Math.round(((counts[0] - meanCount) / meanCount) * 100);\n\n// The y-axis label's offset from the axis line is computed internally by MUI X\n// as `tickFontSize + tickSize + 10` (not from tickLabelStyle.fontSize, the\n// prop that actually sets the rendered glyph size) — so tickFontSize has to be\n// sized to clear the widest tick number, or the label collides with it. Scale\n// it off the real digit count instead of a fixed magic number so the offset\n// stays correct if the data range changes.\nconst yTickDigits = String(Math.max(...counts)).length;\nconst yTickFontSize = 12 + yTickDigits * 8;\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width; // 1600 CSS px (landscape mount)\n  const H = window.ANYPLOT_SIZE.height; // 900 CSS px\n  const CHART_TOP = 108;\n\n  return (\n    <Box sx={{ position: \"relative\", width: W, height: H, bgcolor: t.pageBg }}>\n      {/* Title */}\n      <Box sx={{ position: \"absolute\", top: 24, left: 56, right: 56 }}>\n        <Typography sx={{ color: t.ink, fontSize: 22, fontWeight: 500 }}>\n          count-basic · javascript · muix · anyplot.ai\n        </Typography>\n      </Box>\n\n      {/* Subtitle: surfaces the takeaway directly rather than leaving the\n          viewer to read the sort order + average line themselves. */}\n      <Box sx={{ position: \"absolute\", top: 58, left: 56, right: 56 }}>\n        <Typography sx={{ color: t.inkSoft, fontSize: 15, fontWeight: 400 }}>\n          {topCategory} tickets run {pctAboveAvg}% above the category average\n        </Typography>\n      </Box>\n\n      {/* Bar chart */}\n      <Box\n        sx={{\n          position: \"absolute\",\n          top: CHART_TOP,\n          left: 0,\n          right: 0,\n          bottom: 0,\n        }}\n      >\n        <BarChart\n          width={W}\n          height={H - CHART_TOP}\n          colors={[t.palette[0]]}\n          skipAnimation\n          borderRadius={4}\n          xAxis={[\n            {\n              scaleType: \"band\",\n              data: categories,\n              label: \"Support Ticket Category\",\n              disableTicks: true,\n              disableLine: true,\n              labelStyle: { fontSize: 16, fill: t.ink },\n              tickLabelStyle: { fontSize: 15, fill: t.inkSoft },\n              categoryGapRatio: 0.4,\n            },\n          ]}\n          yAxis={[\n            {\n              label: \"Number of Tickets\",\n              labelStyle: { fontSize: 16, fill: t.ink },\n              tickLabelStyle: { fontSize: 15, fill: t.inkSoft },\n              // tickFontSize only sizes the *reserved layout offset* between the\n              // tick labels and the axis label (tickLabelStyle.fontSize above\n              // wins for the rendered glyph size) — derived from yTickFontSize\n              // above so it stays correct if the data range changes.\n              tickFontSize: yTickFontSize,\n              disableTicks: true,\n              disableLine: true,\n              max: Math.max(...counts) * 1.15,\n            },\n          ]}\n          series={[{ data: counts, label: \"Tickets\" }]}\n          barLabel=\"value\"\n          margin={{ top: 14, right: 40, bottom: 90, left: 96 }}\n          grid={{ horizontal: true }}\n          slotProps={{\n            legend: { hidden: true },\n            // The leading category (dataIndex 0, since bars are sorted\n            // descending) gets a slightly bolder/larger label — a focal-point\n            // emphasis that reinforces the subtitle without changing the\n            // brand-green fill, so data color stays identical across themes.\n            barLabel: (ownerState) => ({\n              style: {\n                fontSize: ownerState.dataIndex === 0 ? 17 : 15,\n                fontWeight: ownerState.dataIndex === 0 ? 700 : 600,\n                fill: t.ink,\n              },\n            }),\n            // A thin ink-colored stroke crisps the bar edges against the page\n            // background — the style guide's sanctioned outline pattern, not\n            // a color/gradient change, so it doesn't affect data color parity.\n            bar: { style: { stroke: t.ink, strokeOpacity: 0.15, strokeWidth: 1 } },\n          }}\n          sx={{\n            \"& .MuiChartsGrid-line\": { stroke: t.grid },\n          }}\n        >\n          {/* Mean reference line: turns \"6 bars\" into \"which categories run\n              above/below the average ticket volume\" — lands in open canvas at\n              the right (short bars), never crossing the in-bar value labels. */}\n          <ChartsReferenceLine\n            y={meanCount}\n            label={`Avg ${Math.round(meanCount)}`}\n            labelAlign=\"end\"\n            lineStyle={{\n              stroke: t.inkSoft,\n              strokeDasharray: \"6 4\",\n              strokeWidth: 1.5,\n            }}\n            labelStyle={{ fill: t.inkSoft, fontSize: 13, fontWeight: 500 }}\n          />\n        </BarChart>\n      </Box>\n    </Box>\n  );\n}\n"}