{"spec_id":"parliament-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// parliament-basic: Parliament Seat Chart\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-02\n\nconst t = window.ANYPLOT_TOKENS;\nconst size = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Faculty Senate composition by academic division — a semicircular \"seat\n// chart\" reads identically whether the body is a legislature or, as here, a\n// university senate, so we pick the non-political application from the spec.\nconst divisions = [\n  { name: \"Sciences\", seats: 34 },\n  { name: \"Engineering\", seats: 26 },\n  { name: \"Business\", seats: 24 },\n  { name: \"Social Sciences\", seats: 30 },\n  { name: \"Humanities\", seats: 22 },\n  { name: \"Health Sciences\", seats: 44 },\n];\nconst totalSeats = divisions.reduce((sum, d) => sum + d.seats, 0);\n\n// --- Seat layout: concentric arcs, equal-density row algorithm --------------\nconst outerRadius = 100;\nconst innerRadius = 40;\nconst numRows = Math.min(12, Math.max(4, Math.round(Math.sqrt(totalSeats) / 2)));\nconst rowSpacing = (outerRadius - innerRadius) / (numRows - 1);\n\n// Row weight is proportional to arc length (∝ radius) at fixed 180° span, so\n// seats-per-row scales with radius and dot spacing stays roughly constant.\nconst rowRadii = Array.from({ length: numRows }, (_, i) => innerRadius + i * rowSpacing);\nconst weightSum = rowRadii.reduce((sum, r) => sum + r, 0);\nconst seatsPerRow = rowRadii.map((r) => Math.round((totalSeats * r) / weightSum));\nseatsPerRow[seatsPerRow.length - 1] += totalSeats - seatsPerRow.reduce((sum, n) => sum + n, 0);\n\nconst seatPositions = [];\nrowRadii.forEach((radius, rowIndex) => {\n  const count = seatsPerRow[rowIndex];\n  for (let j = 0; j < count; j += 1) {\n    const angleDeg = count === 1 ? 90 : 180 - (j * 180) / (count - 1);\n    const angleRad = (angleDeg * Math.PI) / 180;\n    seatPositions.push({\n      x: radius * Math.cos(angleRad),\n      y: radius * Math.sin(angleRad),\n      angle: angleDeg,\n    });\n  }\n});\n// Left-to-right fill: sort by angle (180°=left … 0°=right), then hand out\n// contiguous blocks to each division in list order.\nseatPositions.sort((a, b) => b.angle - a.angle);\n\nlet cursor = 0;\nconst seriesData = divisions.map((division, i) => {\n  const block = seatPositions.slice(cursor, cursor + division.seats);\n  cursor += division.seats;\n  return {\n    name: division.name,\n    type: \"scatter\",\n    data: block.map((p) => [p.x, p.y]),\n    itemStyle: { color: t.palette[i], borderColor: t.pageBg, borderWidth: 1 },\n  };\n});\n\n// --- Coordinate system: preserve 1:1 aspect so seats stay circular ----------\nconst dotDiameter = rowSpacing * 0.8;\nconst pad = dotDiameter / 2;\nconst xRange = 2 * (outerRadius + pad);\nconst yRange = outerRadius + 2 * pad;\n\n// --- Majority threshold marker (spec's optional 50%+1 highlight) -----------\n// A dashed radial markLine through the seat that tips the chamber into a\n// majority, using ECharts' coord-pair markLine (not a plain scatter point).\nconst majoritySeats = Math.floor(totalSeats / 2) + 1;\nconst thresholdSeat = seatPositions[majoritySeats - 1];\nconst thresholdAngleRad = (thresholdSeat.angle * Math.PI) / 180;\nconst thresholdRadius = outerRadius + pad;\nconst thresholdEnd = {\n  x: thresholdRadius * Math.cos(thresholdAngleRad),\n  y: thresholdRadius * Math.sin(thresholdAngleRad),\n};\nconst majorityMarkerSeries = {\n  type: \"scatter\",\n  data: [],\n  silent: true,\n  tooltip: { show: false },\n  markLine: {\n    silent: true,\n    symbol: \"none\",\n    lineStyle: { color: t.inkSoft, type: \"dashed\", width: 2 },\n    label: {\n      show: true,\n      formatter: () => `Majority · ${majoritySeats}`,\n      color: t.inkSoft,\n      fontSize: 13,\n    },\n    data: [[{ coord: [0, 0] }, { coord: [thresholdEnd.x, thresholdEnd.y] }]],\n  },\n};\n\nconst topReserved = 70;\nconst bottomReserved = 110;\nconst sideReserved = 80;\nconst availableW = size.width - 2 * sideReserved;\nconst availableH = size.height - topReserved - bottomReserved;\nconst scale = Math.min(availableW / xRange, availableH / yRange);\nconst gridWidth = scale * xRange;\nconst gridHeight = scale * yRange;\nconst gridLeft = sideReserved + (availableW - gridWidth) / 2;\nconst gridTop = topReserved + (availableH - gridHeight) / 2;\n\n// --- Title (length-scaled per anyplot title-fontsize rule) ------------------\nconst titleText = \"Faculty Senate Composition · parliament-basic · javascript · echarts · anyplot.ai\";\nconst titleFontSize = Math.round(22 * Math.min(1, 67 / titleText.length));\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: titleText,\n    left: \"center\",\n    top: 24,\n    textStyle: { color: t.ink, fontSize: titleFontSize, fontWeight: 500 },\n  },\n  legend: {\n    bottom: 24,\n    left: \"center\",\n    itemWidth: 16,\n    itemHeight: 16,\n    textStyle: { color: t.inkSoft, fontSize: 15 },\n    itemGap: 20,\n    formatter: (name) => {\n      const division = divisions.find((d) => d.name === name);\n      return division ? `${name} (${division.seats})` : name;\n    },\n  },\n  tooltip: {\n    trigger: \"item\",\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    textStyle: { color: t.ink },\n    formatter: (params) => {\n      const division = divisions.find((d) => d.name === params.seriesName);\n      if (!division) return \"\";\n      const pct = ((division.seats / totalSeats) * 100).toFixed(1);\n      return `<strong>${division.name}</strong><br/>${division.seats} seats (${pct}%)`;\n    },\n  },\n  grid: { left: gridLeft, top: gridTop, width: gridWidth, height: gridHeight },\n  xAxis: {\n    type: \"value\",\n    min: -(outerRadius + pad),\n    max: outerRadius + pad,\n    show: false,\n  },\n  yAxis: {\n    type: \"value\",\n    min: -pad,\n    max: outerRadius + pad,\n    show: false,\n  },\n  series: [\n    ...seriesData.map((s) => ({\n      ...s,\n      symbolSize: dotDiameter * scale,\n    })),\n    majorityMarkerSeries,\n  ],\n});\n"}