{"spec_id":"lollipop-grouped","library":"muix","language":"javascript","code":"// anyplot.ai\n// lollipop-grouped: Grouped Lollipop Chart\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-09-02\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { useXScale, useYScale, useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ---------------------------------------\n// Quarterly revenue ($M) by product line, across regions — sorted by total\n// revenue descending to reveal the regional ranking at a glance. Asia-Pacific\n// (a hardware-manufacturing hub) leads with Hardware rather than Software,\n// so the cross-group comparison isn't a flat repeat of the same ranking.\nconst regions = [\"North America\", \"Asia-Pacific\", \"Europe\", \"Latin America\"];\nconst productLines = [\"Hardware\", \"Software\", \"Services\"];\nconst revenueByRegion = [\n  [48, 71, 39],\n  [52, 45, 33],\n  [33, 58, 30],\n  [21, 24, 17],\n];\nconst focalRegion = \"North America\";\nconst seriesColors = [t.palette[0], t.palette[1], t.palette[2]];\nconst maxRevenue = Math.max(...revenueByRegion.flat());\n\n// --- Focal-region highlight: a subtle band behind the top-revenue region ---\nfunction FocalHighlight() {\n  const xScale = useXScale();\n  const { top, height } = useDrawingArea();\n  const x0 = xScale(focalRegion) ?? 0;\n  return <rect x={x0} y={top} width={xScale.bandwidth()} height={height} fill={t.palette[0]} opacity={0.07} rx={10} />;\n}\n\n// --- Custom marks: thin stems + circular heads, grouped per category -------\n// MUI X community has no built-in lollipop series, so the stems/markers are\n// drawn as plain SVG using the ChartContainer's own band/linear scales — this\n// guarantees the marks line up exactly with the shared axes.\nfunction Lollipops() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const bandwidth = xScale.bandwidth();\n  const groupPadding = bandwidth * 0.18;\n  const slotWidth = (bandwidth - 2 * groupPadding) / productLines.length;\n  const baselineY = yScale(0);\n\n  return (\n    <>\n      {regions.map((region, ri) =>\n        productLines.map((_, si) => {\n          const value = revenueByRegion[ri][si];\n          const cx = (xScale(region) ?? 0) + groupPadding + slotWidth * (si + 0.5);\n          const cy = yScale(value);\n          return (\n            <g key={`${region}-${si}`}>\n              <line\n                x1={cx}\n                y1={baselineY}\n                x2={cx}\n                y2={cy}\n                stroke={seriesColors[si]}\n                strokeWidth={4}\n                strokeLinecap=\"round\"\n              />\n              <circle cx={cx} cy={cy} r={12} fill={seriesColors[si]} stroke={t.pageBg} strokeWidth={2.5} />\n              <text x={cx} y={cy - 19} textAnchor=\"middle\" fontSize={12} fill={t.ink}>\n                {`$${value}M`}\n              </text>\n            </g>\n          );\n        }),\n      )}\n    </>\n  );\n}\n\n// --- Y-axis title (manual — ChartsYAxis's built-in `label` offset formula\n// assumes narrow tick text and overlaps wide \"$NNM\" tick labels here) --------\nfunction YAxisTitle({ text }) {\n  const { top, height } = useDrawingArea();\n  const cx = 22;\n  const cy = top + height / 2;\n  return (\n    <text x={cx} y={cy} transform={`rotate(-90, ${cx}, ${cy})`} textAnchor=\"middle\" fontSize={16} fill={t.inkSoft}>\n      {text}\n    </text>\n  );\n}\n\n// --- Legend (manual — custom marks aren't registered as chart series) ------\nfunction Legend() {\n  return (\n    <div style={{ display: \"flex\", justifyContent: \"center\", gap: 28, height: 36, alignItems: \"center\" }}>\n      {productLines.map((name, i) => (\n        <div key={name} style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n          <span\n            style={{\n              width: 14,\n              height: 14,\n              borderRadius: \"50%\",\n              background: seriesColors[i],\n              display: \"inline-block\",\n            }}\n          />\n          <span style={{ fontSize: 15, color: t.inkSoft }}>{name}</span>\n        </div>\n      ))}\n    </div>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const titleHeight = 56;\n  const legendHeight = 36;\n  const chartHeight = height - titleHeight - legendHeight;\n\n  return (\n    <div style={{ width, height, display: \"flex\", flexDirection: \"column\" }}>\n      <div\n        style={{\n          height: titleHeight,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontSize: 22,\n          fontWeight: 500,\n          color: t.ink,\n        }}\n      >\n        lollipop-grouped · javascript · muix · anyplot.ai\n      </div>\n      <Legend />\n      <ChartContainer\n        width={width}\n        height={chartHeight}\n        series={[]}\n        skipAnimation\n        margin={{ top: 20, bottom: 44, left: 92, right: 24 }}\n        xAxis={[{ id: \"region-axis\", scaleType: \"band\", data: regions, categoryGapRatio: 0.3 }]}\n        yAxis={[\n          {\n            id: \"revenue-axis\",\n            scaleType: \"linear\",\n            min: 0,\n            max: maxRevenue * 1.25,\n            valueFormatter: (v) => `$${v}M`,\n          },\n        ]}\n      >\n        <FocalHighlight />\n        <ChartsGrid horizontal />\n        <ChartsXAxis axisId=\"region-axis\" tickLabelStyle={{ fontSize: 15 }} />\n        <ChartsYAxis axisId=\"revenue-axis\" tickLabelStyle={{ fontSize: 14 }} />\n        <YAxisTitle text=\"Revenue ($M)\" />\n        <Lollipops />\n      </ChartContainer>\n    </div>\n  );\n}\n"}