{"spec_id":"pie-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// pie-basic: Basic Pie Chart\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-08-20\n//# anyplot-orientation: square\n// anyplot.ai\n// pie-basic: Basic Pie 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-20\n\nimport { PieChart } from \"@mui/x-charts/PieChart\";\nimport { DefaultChartsLegend } from \"@mui/x-charts/ChartsLegend\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Website traffic sources for a mid-size e-commerce site (share of visits, sums to 100)\nconst CATEGORIES = [\n  { id: \"organic-search\", label: \"Organic Search\", value: 38 },\n  { id: \"direct\", label: \"Direct\", value: 22 },\n  { id: \"social-media\", label: \"Social Media\", value: 16 },\n  { id: \"referral\", label: \"Referral\", value: 12 },\n  { id: \"paid-search\", label: \"Paid Search\", value: 8 },\n  { id: \"email\", label: \"Email\", value: 4 },\n];\n\n// Largest slice gets a slight pop-out for emphasis\nconst EXPLODED_ID = \"organic-search\";\nconst EXPLODE_OFFSET = 30;\n\nconst TITLE = \"pie-basic · javascript · muix · anyplot.ai\";\nconst TITLE_FONT_SIZE =\n  TITLE.length > 67 ? Math.max(15, Math.round(22 * (67 / TITLE.length))) : 22;\nconst MARGIN = { top: 120, right: 60, bottom: 110, left: 60 };\n\n// Per-slice arc-label fill chosen for WCAG contrast against each Imprint color\n// (matches CATEGORIES order: green, lavender, blue, ochre, red, cyan).\nconst LABEL_FILLS = [\n  \"#1A1A17\",\n  \"#1A1A17\",\n  \"#FFFFFF\",\n  \"#1A1A17\",\n  \"#FFFFFF\",\n  \"#1A1A17\",\n];\n\n// The exploded slice lives in a second, appended series (see below), which\n// would otherwise push it to the end of the auto-generated legend. Reorder\n// the legend items to match the pie's clockwise slice order instead, so the\n// exploded, most-important category (\"Organic Search\") leads the legend.\nconst LEGEND_ORDER = CATEGORIES.map((c) => c.id);\nfunction OrderedLegend(props) {\n  const ordered = [...props.seriesToDisplay].sort(\n    (a, b) => LEGEND_ORDER.indexOf(a.itemId) - LEGEND_ORDER.indexOf(b.itemId),\n  );\n  return <DefaultChartsLegend {...props} seriesToDisplay={ordered} />;\n}\n\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n  const drawingWidth = width - MARGIN.left - MARGIN.right;\n  const drawingHeight = height - MARGIN.top - MARGIN.bottom;\n  const availableRadius = Math.min(drawingWidth, drawingHeight) / 2;\n  const outerRadius = Math.round(availableRadius * 0.78);\n  const centerX = drawingWidth / 2;\n  const centerY = drawingHeight / 2;\n\n  // Cumulative angle boundaries (deg) in data order, mirroring MUI X's internal\n  // d3 pie generator (startAngle 0 = 12 o'clock, clockwise, no padding).\n  const total = CATEGORIES.reduce((sum, c) => sum + c.value, 0);\n  let cumulative = 0;\n  const boundaries = CATEGORIES.map((c) => {\n    const start = (cumulative / total) * 360;\n    cumulative += c.value;\n    const end = (cumulative / total) * 360;\n    return { id: c.id, start, end };\n  });\n  const explodedBounds = boundaries.find((b) => b.id === EXPLODED_ID);\n  const midAngle =\n    ((explodedBounds.start + explodedBounds.end) / 2) * (Math.PI / 180);\n  const offsetX = Math.sin(midAngle) * EXPLODE_OFFSET;\n  const offsetY = -Math.cos(midAngle) * EXPLODE_OFFSET;\n\n  // The main series carries all six slices so proportions stay correct, but the\n  // exploded slice is painted transparent and dropped from the legend — a second,\n  // single-item series re-draws it with its real color, offset outward, on top.\n  const mainData = CATEGORIES.map((c) =>\n    c.id === EXPLODED_ID\n      ? { id: c.id, value: c.value, color: \"transparent\" }\n      : { id: c.id, value: c.value, label: c.label },\n  );\n  const explodedCategory = CATEGORIES.find((c) => c.id === EXPLODED_ID);\n  const explodedData = [\n    {\n      id: explodedCategory.id,\n      value: explodedCategory.value,\n      label: explodedCategory.label,\n      color: t.palette[0],\n    },\n  ];\n\n  // The main series suppresses the exploded slice's own (invisible) label so\n  // only the offset overlay series shows its \"38%\" text.\n  const mainArcLabel = (item) =>\n    item.id === EXPLODED_ID ? \"\" : `${item.value}%`;\n  const explodedArcLabel = (item) => `${item.value}%`;\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: TITLE_FONT_SIZE,\n          fontWeight: 600,\n          color: t.ink,\n          letterSpacing: \"0.01em\",\n          lineHeight: 1.3,\n        }}\n      >\n        {TITLE}\n      </div>\n\n      {/* Pie chart — Imprint palette, animation off */}\n      <PieChart\n        width={width}\n        height={height}\n        colors={t.palette}\n        skipAnimation\n        margin={MARGIN}\n        slots={{ legend: OrderedLegend }}\n        series={[\n          {\n            id: \"main\",\n            data: mainData,\n            paddingAngle: 0,\n            cornerRadius: 2,\n            outerRadius,\n            arcLabel: mainArcLabel,\n            arcLabelMinAngle: 6,\n          },\n          {\n            id: \"exploded\",\n            data: explodedData,\n            paddingAngle: 0,\n            cornerRadius: 2,\n            outerRadius,\n            startAngle: explodedBounds.start,\n            endAngle: explodedBounds.end,\n            cx: centerX + offsetX,\n            cy: centerY + offsetY,\n            arcLabel: explodedArcLabel,\n            arcLabelMinAngle: 6,\n          },\n        ]}\n        slotProps={{\n          legend: {\n            direction: \"row\",\n            position: { vertical: \"bottom\", horizontal: \"middle\" },\n            padding: 0,\n            itemMarkWidth: 14,\n            itemMarkHeight: 14,\n            markGap: 6,\n            itemGap: 20,\n            labelStyle: { fontSize: 13, fill: t.ink },\n          },\n        }}\n        sx={{\n          \"& .MuiPieArc-root\": { stroke: t.pageBg, strokeWidth: 3 },\n          \"& .MuiPieArcLabel-root\": { fontSize: 15, fontWeight: \"bold\" },\n          \"& .MuiPieArcLabel-root:nth-child(1)\": { fill: LABEL_FILLS[0] },\n          \"& .MuiPieArcLabel-root:nth-child(2)\": { fill: LABEL_FILLS[1] },\n          \"& .MuiPieArcLabel-root:nth-child(3)\": { fill: LABEL_FILLS[2] },\n          \"& .MuiPieArcLabel-root:nth-child(4)\": { fill: LABEL_FILLS[3] },\n          \"& .MuiPieArcLabel-root:nth-child(5)\": { fill: LABEL_FILLS[4] },\n          \"& .MuiPieArcLabel-root:nth-child(6)\": { fill: LABEL_FILLS[5] },\n        }}\n      />\n    </div>\n  );\n}\n"}