{"spec_id":"hive-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// hive-basic: Basic Hive Plot\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 88/100 | Created: 2026-09-05\n//# anyplot-orientation: square\n// anyplot.ai\n// hive-basic: Basic Hive Plot\n// Library: Highcharts 12.6.0 | Node 22\n// License: Highcharts — commercial license, free for non-commercial use (highcharts.com/license)\n// Quality: pending | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG) — Math.random() is not reproducible ----------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction pick(arr) {\n  return arr[Math.floor(rand() * arr.length)];\n}\n\n// --- Data: software module dependency network -------------------------------\n// Nodes are assigned to one of 3 radial axes by module type. Position along\n// each axis encodes degree (how many dependencies touch the module) so the\n// layout is fully reproducible — identical input always renders identically.\nconst AXES = [\"Core\", \"Utility\", \"Interface\"];\nconst NODES_PER_AXIS = 10;\n\nconst nodes = [];\nAXES.forEach((axisName, axisIdx) => {\n  for (let i = 0; i < NODES_PER_AXIS; i++) {\n    nodes.push({ id: nodes.length, axis: axisIdx, name: `${axisName[0]}${i + 1}`, degree: 0 });\n  }\n});\n\n// Dependency edges follow a typical layered architecture: Interface calls\n// Utility, Utility calls Core, and a few Interface modules call Core directly.\n// Only cross-axis edges are drawn — hive plots encode structure *between*\n// axes, so same-axis pairs are omitted for clarity.\nconst edges = [];\nfunction connect(fromAxis, toAxis, count) {\n  const fromNodes = nodes.filter((n) => n.axis === fromAxis);\n  const toNodes = nodes.filter((n) => n.axis === toAxis);\n  for (let i = 0; i < count; i++) {\n    const source = pick(fromNodes);\n    const target = pick(toNodes);\n    edges.push({ source: source.id, target: target.id, pairCount: count });\n    source.degree += 1;\n    target.degree += 1;\n  }\n}\nconnect(2, 1, 24); // Interface -> Utility\nconnect(1, 0, 20); // Utility -> Core\nconnect(2, 0, 9); // Interface -> Core (direct)\nconst busiestPairCount = Math.max(...edges.map((e) => e.pairCount));\n\nconst maxDegree = Math.max(...nodes.map((n) => n.degree), 1);\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    spacing: [10, 10, 10, 10],\n    events: {\n      load: function () {\n        const chart = this;\n        const renderer = chart.renderer;\n        const cx = chart.plotLeft + chart.plotWidth / 2;\n        const plotCy = chart.plotTop + chart.plotHeight / 2;\n        const half = Math.min(chart.plotWidth, chart.plotHeight) / 2;\n        const axisLen = half * 0.72; // leave room for axis-name labels\n        const labelLen = half * 0.9;\n        const rMin = axisLen * 0.15;\n        const rMax = axisLen * 0.98;\n\n        // angleDeg=0 points straight up; the three axes are 120° apart —\n        // the classic hive-plot fan (up / lower-right / lower-left).\n        function axisAngleRad(axisIdx) {\n          return ((axisIdx * 120 - 90) * Math.PI) / 180;\n        }\n\n        // The fan is not vertically symmetric: one axis reaches a full\n        // radius above center while the other two only reach half that\n        // radius below it (their angles are 30deg/150deg off horizontal).\n        // Centering on the plot's raw geometric center therefore leaves a\n        // large empty band under the shape. Instead, recenter cy so the\n        // vertical bounding box of the axis tips is centered in the plot\n        // area — this generalizes to any axis-count/angle choice.\n        const sines = AXES.map((_, axisIdx) => Math.sin(axisAngleRad(axisIdx)));\n        const verticalBalance = (Math.min(...sines) + Math.max(...sines)) / 2;\n        const cy = plotCy - labelLen * verticalBalance;\n\n        function pointAt(axisIdx, radius) {\n          const rad = axisAngleRad(axisIdx);\n          return { x: cx + radius * Math.cos(rad), y: cy + radius * Math.sin(rad) };\n        }\n\n        // Axis spokes + labels (label color mirrors its axis's node color,\n        // doubling as the legend — a separate legend box would be redundant).\n        AXES.forEach((name, axisIdx) => {\n          const tip = pointAt(axisIdx, axisLen);\n          renderer\n            .path([\"M\", cx, cy, \"L\", tip.x, tip.y])\n            .attr({ stroke: t.inkSoft, \"stroke-width\": 2, opacity: 0.5 })\n            .add();\n\n          const labelPos = pointAt(axisIdx, labelLen);\n          const dy = Math.sin(axisAngleRad(axisIdx)) < -0.3 ? -6 : 16;\n          renderer\n            .text(name, labelPos.x, labelPos.y + dy)\n            .attr({ align: \"center\", zIndex: 5 })\n            .css({ color: t.palette[axisIdx], fontSize: \"16px\", fontWeight: \"600\" })\n            .add();\n        });\n\n        // Node pixel positions — farther from center means more dependencies.\n        // Nodes are ranked (not placed at a raw degree value) within their own\n        // axis so ties never collapse onto the same point; the property still\n        // reads left-to-right along the axis, low degree near the hub.\n        const position = {};\n        AXES.forEach((_, axisIdx) => {\n          const onAxis = nodes.filter((n) => n.axis === axisIdx).sort((a, b) => a.degree - b.degree);\n          onAxis.forEach((n, rank) => {\n            const radius = onAxis.length > 1 ? rMin + (rank / (onAxis.length - 1)) * (rMax - rMin) : rMin;\n            position[n.id] = pointAt(axisIdx, radius);\n          });\n        });\n\n        // Edges as gentle bezier curves bowed toward the center, which keeps\n        // dense connections readable instead of a straight-line hairball.\n        // The busiest axis-pair (most edges) gets a thinner, more transparent\n        // stroke so its near-parallel curves don't fuse into a solid band.\n        edges.forEach((e) => {\n          const p1 = position[e.source];\n          const p2 = position[e.target];\n          const midX = (p1.x + p2.x) / 2;\n          const midY = (p1.y + p2.y) / 2;\n          const ctrlX = midX + (cx - midX) * 0.35;\n          const ctrlY = midY + (cy - midY) * 0.35;\n          const isBusiest = e.pairCount === busiestPairCount;\n          renderer\n            .path([\"M\", p1.x, p1.y, \"Q\", ctrlX, ctrlY, p2.x, p2.y])\n            .attr({\n              stroke: t.inkSoft,\n              \"stroke-width\": isBusiest ? 0.9 : 1.2,\n              fill: \"none\",\n              opacity: isBusiest ? 0.2 : 0.3,\n            })\n            .add();\n        });\n\n        // Nodes on top of the edges.\n        nodes.forEach((n) => {\n          const p = position[n.id];\n          const radius = 6 + 5 * (n.degree / maxDegree);\n          renderer\n            .circle(p.x, p.y, radius)\n            .attr({ fill: t.palette[n.axis], stroke: t.pageBg, \"stroke-width\": 1.5 })\n            .add();\n        });\n      },\n    },\n  },\n  title: {\n    text: \"hive-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  credits: { enabled: false },\n  xAxis: { visible: false },\n  yAxis: { visible: false },\n  legend: { enabled: false },\n  tooltip: { enabled: false },\n  series: [],\n});\n"}