{"spec_id":"parliament-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// parliament-basic: Parliament Seat Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Generic fictional legislature, ordered left-to-right along the political\n// spectrum so seat blocks read naturally once laid out below.\nconst parties = [\n  { name: \"Green Alliance\", seats: 38 },\n  { name: \"Social Democrats\", seats: 52 },\n  { name: \"Liberal Union\", seats: 24 },\n  { name: \"Centrist Coalition\", seats: 18 },\n  { name: \"Conservative Bloc\", seats: 46 },\n  { name: \"Independents\", seats: 22 },\n];\nconst totalSeats = parties.reduce((sum, p) => sum + p.seats, 0);\n\n// --- Semicircular seat layout ------------------------------------------------\n// Seats sit on concentric arcs; each arc's seat capacity scales with its\n// radius (longer arc = more room), so dot spacing stays roughly even across\n// rows. Seats are then globally ordered by angle (left = pi, right = 0) so\n// each party forms a contiguous wedge from the outer edge to the center.\nconst numRows = 6;\nconst rMin = 0.38;\nconst rMax = 1.0;\nconst rowRadii = Array.from({ length: numRows }, (_, i) => rMin + (i * (rMax - rMin)) / (numRows - 1));\nconst capacitySum = rowRadii.reduce((sum, r) => sum + r, 0);\n\n// Largest-remainder apportionment: floor each row's exact share, then hand the\n// leftover seats to the rows with the biggest fractional remainder. This keeps\n// per-seat arc spacing far more even across rows than dumping all rounding\n// slack into the last row (which visibly starved the innermost arc).\nconst exactRowSeats = rowRadii.map((radius) => (totalSeats * radius) / capacitySum);\nconst seatsPerRow = exactRowSeats.map(Math.floor);\nlet remainder = totalSeats - seatsPerRow.reduce((sum, n) => sum + n, 0);\nconst byRemainder = exactRowSeats\n  .map((exact, i) => ({ i, frac: exact - Math.floor(exact) }))\n  .sort((a, b) => b.frac - a.frac);\nfor (let k = 0; k < remainder; k++) seatsPerRow[byRemainder[k].i] += 1;\n\nconst seatPositions = [];\nrowRadii.forEach((radius, i) => {\n  const rowSeats = seatsPerRow[i];\n  for (let j = 0; j < rowSeats; j++) {\n    const angle = Math.PI - ((j + 0.5) / rowSeats) * Math.PI;\n    seatPositions.push({ x: radius * Math.cos(angle), y: radius * Math.sin(angle), angle });\n  }\n});\nseatPositions.sort((a, b) => b.angle - a.angle);\n\n// --- Data storytelling: majority threshold + plurality emphasis -------------\n// The 200-seat chamber needs 101 seats for a majority; that seat falls inside\n// whichever party's wedge crosses the 101st position once seats are ordered\n// left-to-right (angle descending), which is the same order used below to\n// slice seats into party datasets.\nconst majoritySeatCount = Math.floor(totalSeats / 2) + 1;\nconst majorityAngle = seatPositions[majoritySeatCount - 1].angle;\nconst pluralityParty = parties.reduce((max, p) => (p.seats > max.seats ? p : max), parties[0]);\n\nlet cursor = 0;\nconst datasets = parties.map((party, i) => {\n  const points = seatPositions.slice(cursor, cursor + party.seats).map(({ x, y }) => ({ x, y }));\n  cursor += party.seats;\n  const isPlurality = party.name === pluralityParty.name;\n  return {\n    label: `${party.name} (${party.seats})${isPlurality ? \" — largest\" : \"\"}`,\n    data: points,\n    backgroundColor: t.palette[i % t.palette.length],\n    pointBorderColor: t.pageBg,\n    pointBorderWidth: isPlurality ? 2.5 : 1.5,\n    pointRadius: isPlurality ? 10 : 9,\n    pointHoverRadius: isPlurality ? 10 : 9,\n  };\n});\n\n// A small custom plugin draws a dashed radial line at the majority-threshold\n// angle plus its seat count, giving the chart a focal point beyond the raw\n// seat scatter (Chart.js has no built-in \"reference line\" for scatter data).\nconst majorityLinePlugin = {\n  id: \"majorityLine\",\n  afterDatasetsDraw(chart) {\n    const { ctx, scales, chartArea } = chart;\n    const rInner = rMin - 0.03;\n    const rOuter = rMax + 0.06;\n    const x1 = scales.x.getPixelForValue(rInner * Math.cos(majorityAngle));\n    const y1 = scales.y.getPixelForValue(rInner * Math.sin(majorityAngle));\n    const x2 = scales.x.getPixelForValue(rOuter * Math.cos(majorityAngle));\n    // Clamp the line tip to the chart area so it (and its label) never pokes\n    // above into the title's reserved space, whatever angle the threshold falls at.\n    const y2 = Math.max(scales.y.getPixelForValue(rOuter * Math.sin(majorityAngle)), chartArea.top);\n\n    ctx.save();\n    ctx.strokeStyle = t.inkSoft;\n    ctx.lineWidth = 2;\n    ctx.setLineDash([6, 5]);\n    ctx.beginPath();\n    ctx.moveTo(x1, y1);\n    ctx.lineTo(x2, y2);\n    ctx.stroke();\n\n    ctx.setLineDash([]);\n    ctx.fillStyle = t.ink;\n    ctx.font = \"600 13px sans-serif\";\n    ctx.textAlign = majorityAngle > Math.PI / 2 + 0.05 ? \"right\" : majorityAngle < Math.PI / 2 - 0.05 ? \"left\" : \"center\";\n    ctx.textBaseline = \"top\";\n    ctx.fillText(`Majority: ${majoritySeatCount}`, x2, y2 + 6);\n    ctx.restore();\n  },\n};\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: { datasets },\n  plugins: [majorityLinePlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 10, bottom: 10, left: 20, right: 20 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"parliament-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 34 },\n      },\n      legend: {\n        position: \"bottom\",\n        labels: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          usePointStyle: true,\n          pointStyle: \"circle\",\n          padding: 16,\n        },\n      },\n      tooltip: {\n        callbacks: {\n          title: () => \"\",\n          label: (ctx) => ctx.dataset.label,\n        },\n      },\n    },\n    scales: {\n      x: { display: false, min: -1.08, max: 1.08 },\n      y: { display: false, min: -0.02, max: 1.02 },\n    },\n  },\n});\n"}