{"spec_id":"donut-nested","library":"muix","language":"javascript","code":"// anyplot.ai\n// donut-nested: Nested Donut Chart\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-08-18\n//# anyplot-orientation: square\n// anyplot.ai\n// donut-nested: Nested Donut Chart\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-08-18\n\nimport { PieChart } from \"@mui/x-charts/PieChart\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Company revenue, two hierarchy levels: business unit (inner ring) ->\n// customer segment (outer ring). Both rings share the same $1,740M total, so\n// the d3 pie layout keeps every segment's angular span nested exactly inside\n// its parent unit's wedge.\nconst BUSINESS_UNITS = [\n  { id: \"software\", label: \"Software\", value: 710 },\n  { id: \"hardware\", label: \"Hardware\", value: 440 },\n  { id: \"services\", label: \"Services\", value: 350 },\n  { id: \"consulting\", label: \"Consulting\", value: 240 },\n];\n\nconst SEGMENTS = [\n  { id: \"enterprise-saas\", label: \"Enterprise SaaS\", value: 380, unit: \"software\" },\n  { id: \"smb-subscriptions\", label: \"SMB Subscriptions\", value: 210, unit: \"software\" },\n  { id: \"developer-tools\", label: \"Developer Tools\", value: 120, unit: \"software\" },\n  { id: \"enterprise-devices\", label: \"Enterprise Devices\", value: 260, unit: \"hardware\" },\n  { id: \"consumer-devices\", label: \"Consumer Devices\", value: 180, unit: \"hardware\" },\n  { id: \"managed-support\", label: \"Managed Support\", value: 150, unit: \"services\" },\n  { id: \"professional-services\", label: \"Professional Services\", value: 130, unit: \"services\" },\n  { id: \"training\", label: \"Training\", value: 70, unit: \"services\" },\n  { id: \"strategy-advisory\", label: \"Strategy Advisory\", value: 140, unit: \"consulting\" },\n  { id: \"implementation\", label: \"Implementation\", value: 100, unit: \"consulting\" },\n];\n\n// Business unit -> Imprint hue (units are abstract categories, so canonical\n// palette order applies; first series is always brand green).\nconst UNIT_COLOR = {\n  software: t.palette[0],\n  hardware: t.palette[1],\n  services: t.palette[2],\n  consulting: t.palette[3],\n};\n\n// Same hue per unit across both rings; children step towards white by a\n// fixed ratio so every color stays an identical solid hex in both themes\n// (unlike alpha blending, which would shift visibly against the different\n// page backgrounds) while still reading as clearly nested within its unit.\nconst hexToRgb = (hex) => [\n  parseInt(hex.slice(1, 3), 16),\n  parseInt(hex.slice(3, 5), 16),\n  parseInt(hex.slice(5, 7), 16),\n];\nconst lighten = (hex, amount) => {\n  const [r, g, b] = hexToRgb(hex);\n  const mix = (channel) => Math.round(channel + (255 - channel) * amount);\n  return `rgb(${mix(r)}, ${mix(g)}, ${mix(b)})`;\n};\nconst CHILD_TINTS = [0.15, 0.38, 0.6, 0.8, 0.92];\n\nconst formatRevenue = (item) => `$${item.value}M`;\n\nconst unitData = BUSINESS_UNITS.map((unit) => ({\n  id: unit.id,\n  value: unit.value,\n  // Excluded from the legend (return undefined for 'legend') since every\n  // unit is large enough to carry its own arc label; the legend is reserved\n  // for the smaller outer-ring segments per the spec's labeling guidance.\n  label: (location) => (location === \"legend\" ? undefined : unit.label),\n  color: UNIT_COLOR[unit.id],\n}));\n\nconst unitChildIndex = {};\nconst segmentData = SEGMENTS.map((segment) => {\n  const index = unitChildIndex[segment.unit] ?? 0;\n  unitChildIndex[segment.unit] = index + 1;\n  return {\n    id: segment.id,\n    value: segment.value,\n    label: segment.label,\n    color: lighten(UNIT_COLOR[segment.unit], CHILD_TINTS[index]),\n  };\n});\n\nconst TITLE = \"donut-nested · javascript · muix · anyplot.ai\";\nconst MARGIN = { top: 130, bottom: 50, left: 90, right: 260 };\nconst TOTAL_REVENUE = BUSINESS_UNITS.reduce((sum, unit) => sum + unit.value, 0);\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const pieCx = (MARGIN.left + width - MARGIN.right) / 2;\n  const pieCy = (MARGIN.top + height - MARGIN.bottom) / 2;\n\n  return (\n    <div\n      style={{\n        position: \"relative\",\n        width,\n        height,\n        fontFamily: \"'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif\",\n      }}\n    >\n      {/* Title */}\n      <div\n        style={{\n          position: \"absolute\",\n          top: 20,\n          left: 0,\n          right: 0,\n          textAlign: \"center\",\n          fontSize: 22,\n          fontWeight: 600,\n          color: t.ink,\n          letterSpacing: \"0.01em\",\n          lineHeight: 1.3,\n        }}\n      >\n        {TITLE}\n      </div>\n\n      {/* Nested donut — inner ring: business units, outer ring: customer\n          segments. Both series total 1,740 so their arcs stay angularly\n          aligned, and a radius gap between the rings gives visual separation. */}\n      <PieChart\n        width={width}\n        height={height}\n        skipAnimation\n        margin={MARGIN}\n        series={[\n          {\n            id: \"business-units\",\n            data: unitData,\n            innerRadius: 85,\n            outerRadius: 200,\n            paddingAngle: 1.5,\n            cornerRadius: 3,\n            arcLabel: \"label\",\n            arcLabelMinAngle: 0,\n            valueFormatter: formatRevenue,\n          },\n          {\n            id: \"customer-segments\",\n            data: segmentData,\n            innerRadius: 225,\n            outerRadius: 420,\n            paddingAngle: 1.5,\n            cornerRadius: 3,\n            arcLabel: \"label\",\n            arcLabelMinAngle: 18,\n            valueFormatter: formatRevenue,\n          },\n        ]}\n        slotProps={{\n          legend: {\n            direction: \"column\",\n            position: { vertical: \"middle\", horizontal: \"right\" },\n            itemMarkWidth: 14,\n            itemMarkHeight: 14,\n            markGap: 8,\n            itemGap: 12,\n            labelStyle: { fontSize: 15, fill: t.inkSoft },\n          },\n        }}\n        sx={{\n          \"& .MuiPieArc-root\": { stroke: t.pageBg, strokeWidth: 2 },\n          \"& .MuiPieArcLabel-root\": {\n            fill: t.ink,\n            paintOrder: \"stroke\",\n            stroke: t.pageBg,\n            strokeWidth: 4,\n            strokeLinejoin: \"round\",\n          },\n          \"& .MuiPieArcLabel-series-business-units\": { fontSize: 18, fontWeight: 700 },\n          \"& .MuiPieArcLabel-series-customer-segments\": { fontSize: 15, fontWeight: 600 },\n        }}\n      />\n\n      {/* Center overlay — total revenue across all business units */}\n      <div\n        style={{\n          position: \"absolute\",\n          top: pieCy,\n          left: pieCx,\n          transform: \"translate(-50%, -50%)\",\n          textAlign: \"center\",\n          pointerEvents: \"none\",\n          lineHeight: 1.2,\n        }}\n      >\n        <div style={{ fontSize: 34, fontWeight: 700, color: t.ink }}>\n          {`$${(TOTAL_REVENUE / 1000).toFixed(2)}B`}\n        </div>\n        <div\n          style={{\n            fontSize: 11,\n            color: t.inkSoft,\n            marginTop: 6,\n            letterSpacing: \"0.08em\",\n            textTransform: \"uppercase\",\n          }}\n        >\n          Total Revenue\n        </div>\n      </div>\n    </div>\n  );\n}\n"}