{"spec_id":"parliament-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// parliament-basic: Parliament Seat Chart\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-02\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: fictional national assembly, ordered left-to-right along the\n// political spectrum (in-memory, deterministic) -----------------------------\nconst PARTIES = [\n  { name: \"Green Alliance\", seats: 24 },\n  { name: \"Progress Party\", seats: 38 },\n  { name: \"Unity Coalition\", seats: 52 },\n  { name: \"Civic Alliance\", seats: 46 },\n  { name: \"Heritage Party\", seats: 28 },\n  { name: \"Reform Movement\", seats: 12 },\n];\nconst TOTAL_SEATS = PARTIES.reduce((sum, party) => sum + party.seats, 0); // 200\nconst MAJORITY_SEATS = Math.floor(TOTAL_SEATS / 2) + 1;\n\n// --- Seat geometry: concentric semicircular arcs. Each row's seat count is\n// proportional to its arc length (radius), which keeps seat spacing roughly\n// constant across rows. Seats are then handed to parties in left-to-right\n// angular order, so each party forms a contiguous wedge across the arcs. ----\nconst OUTER_R = 628;\nconst INNER_R = 252;\nconst ROW_COUNT = Math.min(9, Math.max(3, Math.round(Math.sqrt(TOTAL_SEATS / 6))));\n\nconst rowRadii = Array.from({ length: ROW_COUNT }, (_, i) =>\n  ROW_COUNT === 1 ? OUTER_R : INNER_R + (i * (OUTER_R - INNER_R)) / (ROW_COUNT - 1),\n);\nconst radiusSum = rowRadii.reduce((sum, r) => sum + r, 0);\nconst rowCapacities = rowRadii.map((r) => Math.max(1, Math.round((TOTAL_SEATS * r) / radiusSum)));\nconst capacityDrift = TOTAL_SEATS - rowCapacities.reduce((sum, c) => sum + c, 0);\nrowCapacities[ROW_COUNT - 1] += capacityDrift;\n\nconst slots = [];\nrowRadii.forEach((r, rowIndex) => {\n  const capacity = rowCapacities[rowIndex];\n  for (let j = 0; j < capacity; j += 1) {\n    const theta = ((j + 0.5) / capacity) * Math.PI;\n    slots.push({ r, theta, x: r * Math.cos(theta), y: r * Math.sin(theta) });\n  }\n});\nslots.sort((a, b) => a.x - b.x); // left-to-right, matches PARTIES order\n\nconst seats = [];\nlet cursor = 0;\nPARTIES.forEach((party, partyIndex) => {\n  for (let i = 0; i < party.seats; i += 1) {\n    seats.push({ ...slots[cursor], party: partyIndex });\n    cursor += 1;\n  }\n});\n\nconst rowRadialGap = ROW_COUNT > 1 ? (OUTER_R - INNER_R) / (ROW_COUNT - 1) : OUTER_R - INNER_R;\nconst minAngularSpacing = Math.min(...rowRadii.map((r, i) => (r * Math.PI) / rowCapacities[i]));\nconst SEAT_R = Math.min(20, Math.max(5, 0.42 * Math.min(rowRadialGap, minAngularSpacing)));\n\n// Majority threshold: the angle bisecting the seat where the assembly tips 50%+1.\nconst majorityLower = slots[MAJORITY_SEATS - 1];\nconst majorityUpper = slots[MAJORITY_SEATS] ?? majorityLower;\nconst MAJORITY_THETA = (majorityLower.theta + majorityUpper.theta) / 2;\n\n// --- Layout: domain sized so the x/y scale matches 1:1 (keeps seats circular\n// and the arc a true semicircle instead of an ellipse). ---------------------\nconst TOP_PAD = 90;\nconst BOTTOM_PAD = 80;\nconst X_PAD = 164;\nconst domain = {\n  xMin: -(OUTER_R + X_PAD),\n  xMax: OUTER_R + X_PAD,\n  yMin: -BOTTOM_PAD,\n  yMax: OUTER_R + TOP_PAD,\n};\n\n// --- Custom SVG layers, positioned via the chart's own scales --------------\nfunction Seats() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  return (\n    <g data-drawing-container>\n      {seats.map((seat, i) => (\n        <circle\n          key={`seat-${i}`}\n          cx={xScale(seat.x)}\n          cy={yScale(seat.y)}\n          r={SEAT_R}\n          fill={t.palette[seat.party]}\n          stroke={t.pageBg}\n          strokeWidth={1}\n        />\n      ))}\n    </g>\n  );\n}\n\n// Subtle background wedge behind the seats spanning from the majority line to\n// the leftmost edge — visually distinguishes the contiguous run of parties\n// (Green Alliance -> ... ) whose combined seats cross the 101-seat threshold.\nfunction MajorityHighlight() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const highlightR = OUTER_R + 16;\n  const steps = 48;\n  const arcPoints = Array.from({ length: steps + 1 }, (_, i) => {\n    const theta = MAJORITY_THETA + ((Math.PI - MAJORITY_THETA) * i) / steps;\n    return `${xScale(highlightR * Math.cos(theta))},${yScale(highlightR * Math.sin(theta))}`;\n  });\n  const pathD = `M ${xScale(0)} ${yScale(0)} L ${arcPoints.join(\" L \")} Z`;\n  return (\n    <g data-drawing-container>\n      <path d={pathD} fill={t.ink} fillOpacity={0.06} stroke=\"none\" />\n    </g>\n  );\n}\n\nfunction MajorityLine() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const innerX = 30 * Math.cos(MAJORITY_THETA);\n  const innerY = 30 * Math.sin(MAJORITY_THETA);\n  const outerX = (OUTER_R + 44) * Math.cos(MAJORITY_THETA);\n  const outerY = (OUTER_R + 44) * Math.sin(MAJORITY_THETA);\n  const labelX = (OUTER_R + 80) * Math.cos(MAJORITY_THETA);\n  const labelY = (OUTER_R + 80) * Math.sin(MAJORITY_THETA);\n  return (\n    <g data-drawing-container>\n      <line\n        x1={xScale(innerX)}\n        y1={yScale(innerY)}\n        x2={xScale(outerX)}\n        y2={yScale(outerY)}\n        stroke={t.ink}\n        strokeOpacity={0.45}\n        strokeWidth={1.5}\n        strokeDasharray=\"7,6\"\n      />\n      <text x={xScale(labelX)} y={yScale(labelY)} textAnchor=\"middle\" fontSize={14} fill={t.inkSoft}>\n        {`Majority · ${MAJORITY_SEATS}`}\n      </text>\n      <text\n        x={xScale(0)}\n        y={yScale(-BOTTOM_PAD * 0.5)}\n        textAnchor=\"middle\"\n        fontSize={16}\n        fontWeight={500}\n        fill={t.ink}\n      >\n        {`${TOTAL_SEATS} seats`}\n      </text>\n    </g>\n  );\n}\n\n// --- Title + legend chrome ---------------------------------------------------\nconst TITLE = \"parliament-basic · javascript · muix · anyplot.ai\";\nconst TITLE_FONT_DEFAULT = 30;\nconst titleFontSize =\n  TITLE.length > 67 ? Math.round(TITLE_FONT_DEFAULT * (67 / TITLE.length)) : TITLE_FONT_DEFAULT;\nconst TITLE_H = 42;\nconst LEGEND_H = 36;\n\nfunction Legend() {\n  return (\n    <div style={{ height: LEGEND_H, display: \"flex\", alignItems: \"center\", gap: \"18px\", flexWrap: \"wrap\" }}>\n      {PARTIES.map((party, i) => (\n        <div key={party.name} style={{ display: \"flex\", alignItems: \"center\", gap: \"7px\" }}>\n          <span\n            style={{\n              width: \"12px\",\n              height: \"12px\",\n              borderRadius: \"50%\",\n              backgroundColor: t.palette[i],\n              display: \"inline-block\",\n            }}\n          />\n          <span style={{ fontSize: \"14px\", color: t.inkSoft }}>\n            {party.name} ({party.seats})\n          </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 chartHeight = height - TITLE_H - LEGEND_H;\n\n  return (\n    <div style={{ width, height, display: \"flex\", flexDirection: \"column\" }}>\n      <div\n        style={{\n          height: `${TITLE_H}px`,\n          lineHeight: `${TITLE_H}px`,\n          fontSize: `${titleFontSize}px`,\n          fontWeight: 500,\n          color: t.ink,\n        }}\n      >\n        {TITLE}\n      </div>\n      <Legend />\n      <ChartContainer\n        width={width}\n        height={chartHeight}\n        series={[]}\n        margin={{ top: 8, bottom: 8, left: 8, right: 8 }}\n        xAxis={[{ id: \"x\", scaleType: \"linear\", min: domain.xMin, max: domain.xMax }]}\n        yAxis={[{ id: \"y\", scaleType: \"linear\", min: domain.yMin, max: domain.yMax }]}\n        skipAnimation\n      >\n        <MajorityHighlight />\n        <Seats />\n        <MajorityLine />\n      </ChartContainer>\n    </div>\n  );\n}\n"}