{"spec_id":"parliament-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// parliament-basic: Parliament Seat Chart\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-02\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ---------------------------------------\nconst parties = [\n  { name: \"Coastal Alliance\", seats: 58 },\n  { name: \"Progress Union\", seats: 46 },\n  { name: \"Heritage Party\", seats: 38 },\n  { name: \"Civic Forum\", seats: 30 },\n  { name: \"Green Horizon\", seats: 18 },\n  { name: \"Unity Bloc\", seats: 10 },\n];\nconst totalSeats = parties.reduce((sum, party) => sum + party.seats, 0);\nconst majoritySeats = Math.floor(totalSeats / 2) + 1;\n\n// --- Hemicycle seat layout ---------------------------------------------------\n// Concentric arcs (rows) grow outward from an inner radius; each row's seat\n// capacity scales with its arc length. Capacities are rescaled (largest\n// remainder method) to sum to exactly `totalSeats`, then every seat position\n// is sorted by angle left-to-right so parties form the classic contiguous\n// wedges of a parliament diagram.\nconst INNER_RADIUS = 140;\nconst ROW_GAP = 34;\nconst SEAT_SPACING = 34;\n\nconst rawCapacities = [];\nlet rowRadius = INNER_RADIUS;\nwhile (rawCapacities.reduce((a, b) => a + b, 0) < totalSeats) {\n  rawCapacities.push(Math.max(1, Math.floor((Math.PI * rowRadius) / SEAT_SPACING)));\n  rowRadius += ROW_GAP;\n}\nconst capacitySum = rawCapacities.reduce((a, b) => a + b, 0);\nconst scaledCapacities = rawCapacities.map((c) => (c / capacitySum) * totalSeats);\nconst rowSeatCounts = scaledCapacities.map(Math.floor);\nconst remainder = totalSeats - rowSeatCounts.reduce((a, b) => a + b, 0);\nconst byFraction = scaledCapacities\n  .map((value, i) => ({ i, frac: value - Math.floor(value) }))\n  .sort((a, b) => b.frac - a.frac);\nfor (let k = 0; k < remainder; k++) rowSeatCounts[byFraction[k].i] += 1;\n\nconst seatPoints = [];\nlet maxRadius = INNER_RADIUS;\nrowSeatCounts.forEach((count, row) => {\n  const radius = INNER_RADIUS + row * ROW_GAP;\n  maxRadius = Math.max(maxRadius, radius);\n  for (let j = 0; j < count; j++) {\n    const angle = Math.PI - (Math.PI * (j + 0.5)) / count;\n    seatPoints.push({ angle, x: radius * Math.cos(angle), y: radius * Math.sin(angle) });\n  }\n});\nseatPoints.sort((a, b) => b.angle - a.angle); // left (180°) -> right (0°)\n\n// Majority-threshold angle: the boundary between the last seat outside a\n// majority coalition (built left-to-right) and the first seat that would\n// tip it over 50%+1, used to draw a subtle radial guide.\nconst majorityAngle =\n  (seatPoints[majoritySeats - 2].angle + seatPoints[majoritySeats - 1].angle) / 2;\n\nlet cursor = 0;\nconst series = parties.map((party, i) => {\n  const data = seatPoints.slice(cursor, cursor + party.seats).map((p) => ({ x: p.x, y: p.y }));\n  cursor += party.seats;\n  return { name: `${party.name} (${party.seats})`, color: t.palette[i], data };\n});\n\n// --- Chart -------------------------------------------------------------------\nconst pad = 40;\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      render: function () {\n        if (this.majorityGuideDrawn) return;\n        this.majorityGuideDrawn = true;\n        const xAxis = this.xAxis[0];\n        const yAxis = this.yAxis[0];\n        const rInner = INNER_RADIUS - 30;\n        const rOuter = maxRadius + 10;\n        const rLabel = maxRadius + 26;\n        const x1 = xAxis.toPixels(rInner * Math.cos(majorityAngle));\n        const y1 = yAxis.toPixels(rInner * Math.sin(majorityAngle));\n        const x2 = xAxis.toPixels(rOuter * Math.cos(majorityAngle));\n        const y2 = yAxis.toPixels(rOuter * Math.sin(majorityAngle));\n        this.renderer\n          .path([\"M\", x1, y1, \"L\", x2, y2])\n          .attr({\n            \"stroke-dasharray\": \"4,4\",\n            stroke: t.inkSoft,\n            \"stroke-width\": 1.5,\n            opacity: 0.55,\n            zIndex: 5,\n          })\n          .add();\n        const lx = xAxis.toPixels(rLabel * Math.cos(majorityAngle));\n        const ly = yAxis.toPixels(rLabel * Math.sin(majorityAngle));\n        this.renderer\n          .text(`Majority (${majoritySeats})`, lx, ly)\n          .attr({ align: \"center\", zIndex: 5 })\n          .css({ color: t.inkSoft, fontSize: \"12px\" })\n          .add();\n      },\n    },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"parliament-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    min: -(maxRadius + pad),\n    max: maxRadius + pad,\n    visible: false,\n  },\n  yAxis: {\n    min: -pad,\n    max: maxRadius + pad,\n    title: { text: null },\n    visible: false,\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n    symbolRadius: 6,\n  },\n  tooltip: {\n    headerFormat: \"\",\n    pointFormatter: function () {\n      return `<span style=\"color:${this.series.color}\">●</span> ${this.series.name}`;\n    },\n  },\n  plotOptions: {\n    series: {\n      animation: false,\n      marker: { radius: 17, lineWidth: 0 },\n      states: { hover: { enabled: false } },\n    },\n  },\n  series,\n});\n"}