{"spec_id":"hive-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// hive-basic: Basic Hive Plot\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-05\n//# anyplot-orientation: square\n// anyplot.ai\n// hive-basic: Basic Hive Plot\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-09-05\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic PRNG (the browser has no seeded Math.random) -------------\nfunction createLcg(seed) {\n  let state = seed >>> 0;\n  return () => {\n    state = (Math.imul(state, 1664525) + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst random = createLcg(42);\n\n// --- Network: a software-module dependency graph, nodes grouped onto 3 hive\n// axes by module type. A hive plot's whole point is a reproducible, fixed\n// layout — position never depends on a force simulation. ---------------------\nconst AXES = [\n  { id: \"core\", label: \"Core\", angle: 90, count: 12 },\n  { id: \"utility\", label: \"Utility\", angle: 210, count: 10 },\n  { id: \"interface\", label: \"Interface\", angle: 330, count: 10 },\n];\n\nconst nodesByAxis = {};\nAXES.forEach((axis) => {\n  nodesByAxis[axis.id] = Array.from({ length: axis.count }, (_, i) => ({\n    id: `${axis.id}-${i}`,\n    axis: axis.id,\n    degree: 0,\n  }));\n});\nconst allNodes = AXES.flatMap((axis) => nodesByAxis[axis.id]);\nconst nodesById = Object.fromEntries(allNodes.map((n) => [n.id, n]));\n\n// Dependencies only cross axes (core<->utility, utility<->interface,\n// interface<->core) — comparing how edges route between groups is the\n// structural point of a hive plot, so same-axis edges are omitted.\nconst AXIS_PAIRS = [\n  [\"core\", \"utility\"],\n  [\"utility\", \"interface\"],\n  [\"interface\", \"core\"],\n];\nconst EDGES_PER_PAIR = 24;\n\nconst edgeKeys = new Set();\nconst edges = [];\nAXIS_PAIRS.forEach(([a, b]) => {\n  let added = 0;\n  let attempts = 0;\n  while (added < EDGES_PER_PAIR && attempts < EDGES_PER_PAIR * 6) {\n    attempts += 1;\n    const source = nodesByAxis[a][Math.floor(random() * nodesByAxis[a].length)];\n    const target = nodesByAxis[b][Math.floor(random() * nodesByAxis[b].length)];\n    const key = `${source.id}|${target.id}`;\n    if (edgeKeys.has(key)) continue;\n    edgeKeys.add(key);\n    edges.push({ source: source.id, target: target.id });\n    source.degree += 1;\n    target.degree += 1;\n    added += 1;\n  }\n});\n\n// --- Radial layout: position along each axis encodes node degree (fewer\n// dependencies near the hub, heavily-depended-on modules pushed to the rim) --\nconst INNER_R = 16;\nconst OUTER_R = 100;\nconst CONTROL_R = 18; // edge curves bow through a small radius near the hub\n\nfunction polarToXY(angleDeg, radius) {\n  const rad = (angleDeg * Math.PI) / 180;\n  return { x: radius * Math.cos(rad), y: radius * Math.sin(rad) };\n}\n\n// Bisecting angle for the short arc between each pair of axes — where edge\n// curves bow toward, so they read as gentle strands instead of straight\n// chords cutting through the hub.\nconst CONTROL_ANGLE = { \"core|utility\": 150, \"utility|interface\": 270, \"interface|core\": 30 };\nfunction controlAngleFor(axisA, axisB) {\n  return CONTROL_ANGLE[`${axisA}|${axisB}`] ?? CONTROL_ANGLE[`${axisB}|${axisA}`];\n}\n\nAXES.forEach((axis) => {\n  const ordered = [...nodesByAxis[axis.id]].sort((a, b) => a.degree - b.degree);\n  ordered.forEach((node, i) => {\n    const r = ordered.length > 1 ? INNER_R + (i / (ordered.length - 1)) * (OUTER_R - INNER_R) : OUTER_R;\n    const { x, y } = polarToXY(axis.angle, r);\n    node.x = x;\n    node.y = y;\n  });\n});\n\n// --- Chart geometry ----------------------------------------------------------\nconst TITLE = \"hive-basic · javascript · muix · anyplot.ai\";\nconst TITLE_HEIGHT = 70;\n\n// The 3-axis star is vertically lopsided (the Core spoke points straight up\n// while Utility/Interface point down-left/down-right) and wider than it is\n// tall. Center the domain on the content's own bounding box, using a\n// *separate* half-span per axis so each hugs its own content tightly — then\n// give the plot area that same width:height ratio (below) so the\n// pixel-per-unit mapping stays equal on both axes (required for undistorted\n// angles) without padding the shorter axis out to match the longer one.\nconst LABEL_R = OUTER_R * 1.16;\nconst TOP_Y = LABEL_R; // Core points at 90°, sin(90°) = 1\nconst BOTTOM_Y = LABEL_R * Math.sin((210 * Math.PI) / 180); // Utility/Interface, negative\nconst SIDE_X = LABEL_R * Math.cos((330 * Math.PI) / 180); // symmetric left/right extent\nconst CONTENT_Y_CENTER = (TOP_Y + BOTTOM_Y) / 2;\nconst PAD = 1.12; // clearance for label text extending past its anchor point\nconst X_HALF_SPAN = SIDE_X * PAD;\nconst Y_HALF_SPAN = ((TOP_Y - BOTTOM_Y) / 2) * PAD;\nconst X_MIN = -X_HALF_SPAN;\nconst X_MAX = X_HALF_SPAN;\nconst Y_MIN = CONTENT_Y_CENTER - Y_HALF_SPAN;\nconst Y_MAX = CONTENT_Y_CENTER + Y_HALF_SPAN;\n\n// Fit the widest possible plot area of ratio X_HALF_SPAN:Y_HALF_SPAN inside\n// the canvas (minus a flat outer-margin floor), then split the leftover\n// canvas space evenly as margin on whichever pair of sides has slack.\nconst MIN_MARGIN = 48;\nconst FRAME_W = window.ANYPLOT_SIZE.width;\nconst FRAME_H = window.ANYPLOT_SIZE.height - TITLE_HEIGHT;\nconst domainRatio = X_HALF_SPAN / Y_HALF_SPAN;\nconst innerW = FRAME_W - 2 * MIN_MARGIN;\nconst innerH = FRAME_H - 2 * MIN_MARGIN;\nconst plotW = innerW / innerH > domainRatio ? innerH * domainRatio : innerW;\nconst plotH = innerW / innerH > domainRatio ? innerH : innerW / domainRatio;\nconst MARGIN = {\n  top: (FRAME_H - plotH) / 2,\n  bottom: (FRAME_H - plotH) / 2,\n  left: (FRAME_W - plotW) / 2,\n  right: (FRAME_W - plotW) / 2,\n};\n\nconst AXIS_INDEX = Object.fromEntries(AXES.map((axis, i) => [axis.id, i]));\n\n// Degree-based halo behind each marker reinforces the radial ordering: nodes\n// pushed to the rim (higher degree) get a visibly larger glow than nodes near\n// the hub, so the encoded property reads at a glance instead of only through\n// position.\nconst degrees = allNodes.map((n) => n.degree);\nconst MIN_DEGREE = Math.min(...degrees);\nconst MAX_DEGREE = Math.max(...degrees);\nconst HALO_MIN_PX = 6;\nconst HALO_MAX_PX = 20;\nfunction haloRadiusFor(node) {\n  if (MAX_DEGREE === MIN_DEGREE) return (HALO_MIN_PX + HALO_MAX_PX) / 2;\n  const frac = (node.degree - MIN_DEGREE) / (MAX_DEGREE - MIN_DEGREE);\n  return HALO_MIN_PX + frac * (HALO_MAX_PX - HALO_MIN_PX);\n}\n\n// --- Custom overlay: axis spokes, category labels, and edge curves. Community\n// `@mui/x-charts/hooks` (useXScale/useYScale) map the hand-computed polar\n// layout into the same pixel space the ScatterPlot markers use, so hubs,\n// spokes and edges stay aligned at any render size. --------------------------\nfunction HiveGeometry() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const toPx = (p) => ({ x: xScale(p.x), y: yScale(p.y) });\n\n  return (\n    <g>\n      {allNodes.map((node) => {\n        const p = toPx(node);\n        return (\n          <circle\n            key={`halo-${node.id}`}\n            cx={p.x}\n            cy={p.y}\n            r={haloRadiusFor(node)}\n            fill={t.palette[AXIS_INDEX[node.axis]]}\n            fillOpacity={0.22}\n          />\n        );\n      })}\n      {edges.map((edge, i) => {\n        const source = nodesById[edge.source];\n        const target = nodesById[edge.target];\n        const control = polarToXY(controlAngleFor(source.axis, target.axis), CONTROL_R);\n        const p1 = toPx(source);\n        const p2 = toPx(target);\n        const c = toPx(control);\n        return (\n          <path\n            key={i}\n            d={`M ${p1.x} ${p1.y} Q ${c.x} ${c.y} ${p2.x} ${p2.y}`}\n            fill=\"none\"\n            stroke={t.inkSoft}\n            strokeOpacity={0.32}\n            strokeWidth={1.3}\n          />\n        );\n      })}\n      {AXES.map((axis) => {\n        const hub = toPx(polarToXY(axis.angle, INNER_R * 0.5));\n        const rim = toPx(polarToXY(axis.angle, OUTER_R * 1.04));\n        const labelPt = toPx(polarToXY(axis.angle, OUTER_R * 1.16));\n        return (\n          <g key={axis.id}>\n            <line x1={hub.x} y1={hub.y} x2={rim.x} y2={rim.y} stroke={t.inkSoft} strokeWidth={2.5} />\n            <text\n              x={labelPt.x}\n              y={labelPt.y}\n              fill={t.ink}\n              fontSize={17}\n              fontWeight={600}\n              textAnchor=\"middle\"\n              dominantBaseline=\"middle\"\n            >\n              {axis.label}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  return (\n    <div\n      style={{\n        width: window.ANYPLOT_SIZE.width,\n        height: window.ANYPLOT_SIZE.height,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <div\n        style={{\n          height: TITLE_HEIGHT,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontSize: 22,\n          fontWeight: 500,\n          color: t.ink,\n        }}\n      >\n        {TITLE}\n      </div>\n      <ChartContainer\n        width={window.ANYPLOT_SIZE.width}\n        height={window.ANYPLOT_SIZE.height - TITLE_HEIGHT}\n        margin={MARGIN}\n        skipAnimation\n        xAxis={[{ scaleType: \"linear\", min: X_MIN, max: X_MAX }]}\n        yAxis={[{ scaleType: \"linear\", min: Y_MIN, max: Y_MAX }]}\n        series={AXES.map((axis) => ({\n          type: \"scatter\",\n          id: axis.id,\n          label: axis.label,\n          color: t.palette[AXIS_INDEX[axis.id]],\n          markerSize: 11,\n          data: nodesByAxis[axis.id].map((n) => ({ x: n.x, y: n.y, id: n.id })),\n        }))}\n      >\n        <HiveGeometry />\n        <ScatterPlot />\n      </ChartContainer>\n    </div>\n  );\n}\n"}