{"spec_id":"hive-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// hive-basic: Basic Hive Plot\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-05\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (LCG) ------------------------------------------------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\n// --- Data: software module dependency network -------------------------------\n// Three axes group modules by type; a node's position along its axis encodes\n// its degree, so the same network always renders identically — the reason a\n// hive plot is chosen over a force-directed \"hairball\" layout.\nconst AXES = [\n  { key: \"core\", label: \"Core\", angle: 90, color: t.palette[0] },\n  { key: \"utility\", label: \"Utility\", angle: 210, color: t.palette[1] },\n  { key: \"interface\", label: \"Interface\", angle: 330, color: t.palette[2] },\n];\nconst NODES_PER_AXIS = 12;\nconst R_MIN = 3;\nconst R_MAX = 11.5;\nconst AXIS_END = 12.5;\nconst LABEL_R = 13.8;\nconst EDGE_PROBABILITY = 0.11;\n\nconst nodes = [];\nAXES.forEach((axis) => {\n  for (let i = 0; i < NODES_PER_AXIS; i++) {\n    nodes.push({ id: `${axis.key}-${i + 1}`, axis: axis.key, degree: 0 });\n  }\n});\n\n// Hive plots draw only cross-axis dependencies; same-axis edges would need\n// arcs bowed off-axis and are skipped for clarity (per the spec's guidance).\nconst edges = [];\nfor (let i = 0; i < nodes.length; i++) {\n  for (let j = i + 1; j < nodes.length; j++) {\n    if (nodes[i].axis === nodes[j].axis) continue;\n    if (rand() < EDGE_PROBABILITY) {\n      edges.push([nodes[i], nodes[j]]);\n      nodes[i].degree += 1;\n      nodes[j].degree += 1;\n    }\n  }\n}\n\nconst maxDegree = Math.max(...nodes.map((n) => n.degree));\nconst minDegree = Math.min(...nodes.map((n) => n.degree));\nconst degreeSpan = maxDegree - minDegree || 1;\n\n// Position by degree-rank (not raw degree) within each axis: several nodes\n// often share the same degree, and raw-value placement would stack them on\n// the exact same pixel. Ranking preserves the ordering (higher degree = further\n// out) while guaranteeing every node its own spot along the axis.\nAXES.forEach((axis) => {\n  const axisNodes = nodes.filter((n) => n.axis === axis.key);\n  axisNodes.sort((a, b) => a.degree - b.degree);\n  const rad = (axis.angle * Math.PI) / 180;\n  axisNodes.forEach((node, rank) => {\n    const radius = R_MIN + (rank / (axisNodes.length - 1)) * (R_MAX - R_MIN);\n    node.x = radius * Math.cos(rad);\n    node.y = radius * Math.sin(rad);\n    node.axisColor = axis.color;\n  });\n});\n\nconst axisGeometry = AXES.map((axis) => {\n  const rad = (axis.angle * Math.PI) / 180;\n  return {\n    ...axis,\n    endX: AXIS_END * Math.cos(rad),\n    endY: AXIS_END * Math.sin(rad),\n    labelX: LABEL_R * Math.cos(rad),\n    labelY: LABEL_R * Math.sin(rad),\n  };\n});\n\nfunction bisectorControlPoint(angleA, angleB) {\n  const diff = ((angleB - angleA + 540) % 360) - 180;\n  const mid = angleA + diff / 2;\n  const rad = (mid * Math.PI) / 180;\n  const r = 2.6;\n  return { x: r * Math.cos(rad), y: r * Math.sin(rad) };\n}\n\nconst edgeGeometry = edges.map(([a, b]) => ({\n  a,\n  b,\n  control: bisectorControlPoint(\n    AXES.find((ax) => ax.key === a.axis).angle,\n    AXES.find((ax) => ax.key === b.axis).angle,\n  ),\n}));\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Custom plugin: draws hive axes + curved edges behind the node markers --\nconst hiveGeometryPlugin = {\n  id: \"hiveGeometry\",\n  beforeDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    const px = (v) => scales.x.getPixelForValue(v);\n    const py = (v) => scales.y.getPixelForValue(v);\n\n    ctx.save();\n    ctx.globalAlpha = 0.3;\n    ctx.lineWidth = 1.25;\n    edgeGeometry.forEach(({ a, b, control }) => {\n      // Gradient along the edge rather than a.axisColor alone, so a cross-axis\n      // edge is colored symmetrically by both endpoints instead of favoring\n      // whichever node happened to sort first.\n      const gradient = ctx.createLinearGradient(px(a.x), py(a.y), px(b.x), py(b.y));\n      gradient.addColorStop(0, a.axisColor);\n      gradient.addColorStop(1, b.axisColor);\n      ctx.beginPath();\n      ctx.moveTo(px(a.x), py(a.y));\n      ctx.quadraticCurveTo(px(control.x), py(control.y), px(b.x), py(b.y));\n      ctx.strokeStyle = gradient;\n      ctx.stroke();\n    });\n    ctx.globalAlpha = 1;\n\n    ctx.strokeStyle = t.grid;\n    ctx.lineWidth = 2;\n    axisGeometry.forEach((axis) => {\n      ctx.beginPath();\n      ctx.moveTo(px(0), py(0));\n      ctx.lineTo(px(axis.endX), py(axis.endY));\n      ctx.stroke();\n    });\n\n    ctx.fillStyle = t.ink;\n    ctx.font = \"600 20px sans-serif\";\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n    axisGeometry.forEach((axis) => {\n      ctx.fillText(axis.label, px(axis.labelX), py(axis.labelY));\n    });\n    ctx.restore();\n  },\n};\n\n// --- Chart -------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: AXES.map((axis) => ({\n      label: `${axis.label} modules`,\n      data: nodes\n        .filter((n) => n.axis === axis.key)\n        .map((n) => ({ x: n.x, y: n.y, id: n.id, degree: n.degree })),\n      backgroundColor: axis.color,\n      borderColor: t.pageBg,\n      borderWidth: 1.5,\n      pointRadius: (pointCtx) => 4 + ((pointCtx.raw.degree - minDegree) / degreeSpan) * 8,\n      pointHoverRadius: (pointCtx) => 6 + ((pointCtx.raw.degree - minDegree) / degreeSpan) * 8,\n    })),\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: 24 },\n    scales: {\n      x: { type: \"linear\", min: -16, max: 16, display: false },\n      // Hive geometry spans y ≈ -6.9..13.8 (axis angles 90/210/330), not\n      // symmetric around 0 — an asymmetric domain centers the triangle\n      // instead of leaving a blank third of the canvas below it.\n      y: { type: \"linear\", min: -9, max: 15, display: false },\n    },\n    plugins: {\n      title: {\n        display: true,\n        text: \"hive-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 20 },\n      },\n      legend: {\n        position: \"bottom\",\n        labels: { color: t.ink, font: { size: 16 }, usePointStyle: true },\n      },\n      tooltip: {\n        callbacks: {\n          label: (tooltipCtx) => `${tooltipCtx.raw.id} · degree ${tooltipCtx.raw.degree}`,\n        },\n      },\n    },\n  },\n  plugins: [hiveGeometryPlugin],\n});\n"}