{"spec_id":"network-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// network-basic: Basic Network Graph\n// Library: muix 7.29.1 | JavaScript 22.23.1\n// Quality: 91/100 | Created: 2026-07-24\n//# anyplot-orientation: square\n// anyplot.ai\n// network-basic: Basic Network Graph\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-07-24\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst FONT = \"Inter, system-ui, -apple-system, sans-serif\";\n\n// --- Data: a small social network — 20 people across 4 friend circles ------\n// Nodes carry a group (friend circle) for color; edges are friendships.\nconst GROUPS = [\"Family\", \"College\", \"Work\", \"Neighbors\"];\nconst GROUP_COLOR = {\n  Family: t.palette[0],\n  College: t.palette[1],\n  Work: t.palette[2],\n  Neighbors: t.palette[3],\n};\n\nconst NODES = [\n  { label: \"Maya\", group: \"Family\" },\n  { label: \"Noah\", group: \"Family\" },\n  { label: \"Priya\", group: \"Family\" },\n  { label: \"Ethan\", group: \"Family\" },\n  { label: \"Sofia\", group: \"Family\" },\n  { label: \"Liam\", group: \"College\" },\n  { label: \"Aisha\", group: \"College\" },\n  { label: \"Kenji\", group: \"College\" },\n  { label: \"Grace\", group: \"College\" },\n  { label: \"Diego\", group: \"College\" },\n  { label: \"Omar\", group: \"Work\" },\n  { label: \"Isla\", group: \"Work\" },\n  { label: \"Victor\", group: \"Work\" },\n  { label: \"Nadia\", group: \"Work\" },\n  { label: \"Felix\", group: \"Work\" },\n  { label: \"Ruby\", group: \"Neighbors\" },\n  { label: \"Hassan\", group: \"Neighbors\" },\n  { label: \"Chloe\", group: \"Neighbors\" },\n  { label: \"Marcus\", group: \"Neighbors\" },\n  { label: \"Elena\", group: \"Neighbors\" },\n];\n\nconst EDGES = [\n  // Family circle\n  [0, 1], [0, 2], [0, 3], [1, 4], [2, 3], [3, 4], [2, 4],\n  // College circle\n  [5, 6], [5, 7], [6, 8], [7, 8], [7, 9], [8, 9], [5, 9],\n  // Work circle\n  [10, 11], [10, 12], [11, 13], [12, 13], [12, 14], [13, 14], [10, 14],\n  // Neighbors circle\n  [15, 16], [15, 17], [16, 18], [17, 18], [17, 19], [18, 19], [15, 19],\n  // Bridges between circles (small-world shortcuts)\n  [0, 5], [1, 10], [6, 15], [8, 11], [12, 16], [4, 17],\n];\n\nconst DEGREES = NODES.map(() => 0);\nEDGES.forEach(([a, b]) => {\n  DEGREES[a] += 1;\n  DEGREES[b] += 1;\n});\n\n// --- Force-directed layout (Fruchterman-Reingold), computed once, --------\n// deterministic — no random numbers, so the same layout renders every run.\nfunction initialPositions() {\n  const groupCount = GROUPS.length;\n  return NODES.map((node) => {\n    const groupIndex = GROUPS.indexOf(node.group);\n    const groupAngle = (2 * Math.PI * groupIndex) / groupCount;\n    const withinGroup = NODES.filter((n) => n.group === node.group);\n    const localIndex = withinGroup.indexOf(node);\n    const localAngle = (2 * Math.PI * localIndex) / withinGroup.length;\n    return {\n      x: Math.cos(groupAngle) * 0.6 + Math.cos(localAngle) * 0.18,\n      y: Math.sin(groupAngle) * 0.6 + Math.sin(localAngle) * 0.18,\n    };\n  });\n}\n\nfunction forceDirectedLayout() {\n  const positions = initialPositions();\n  const n = NODES.length;\n  const k = 0.32; // ideal spring length\n  const iterations = 300;\n  let temperature = 0.06;\n  const cooling = temperature / iterations;\n\n  for (let iter = 0; iter < iterations; iter += 1) {\n    const disp = positions.map(() => ({ x: 0, y: 0 }));\n\n    for (let i = 0; i < n; i += 1) {\n      for (let j = i + 1; j < n; j += 1) {\n        const dx = positions[i].x - positions[j].x;\n        const dy = positions[i].y - positions[j].y;\n        const dist = Math.sqrt(dx * dx + dy * dy) || 0.001;\n        const force = (k * k) / dist;\n        const fx = (dx / dist) * force;\n        const fy = (dy / dist) * force;\n        disp[i].x += fx;\n        disp[i].y += fy;\n        disp[j].x -= fx;\n        disp[j].y -= fy;\n      }\n    }\n\n    EDGES.forEach(([a, b]) => {\n      const dx = positions[a].x - positions[b].x;\n      const dy = positions[a].y - positions[b].y;\n      const dist = Math.sqrt(dx * dx + dy * dy) || 0.001;\n      const force = (dist * dist) / k;\n      const fx = (dx / dist) * force;\n      const fy = (dy / dist) * force;\n      disp[a].x -= fx;\n      disp[a].y -= fy;\n      disp[b].x += fx;\n      disp[b].y += fy;\n    });\n\n    for (let i = 0; i < n; i += 1) {\n      disp[i].x -= positions[i].x * 0.01;\n      disp[i].y -= positions[i].y * 0.01;\n    }\n\n    for (let i = 0; i < n; i += 1) {\n      const dist = Math.sqrt(disp[i].x ** 2 + disp[i].y ** 2) || 0.001;\n      const move = Math.min(dist, temperature);\n      positions[i].x += (disp[i].x / dist) * move;\n      positions[i].y += (disp[i].y / dist) * move;\n    }\n    temperature -= cooling;\n  }\n\n  return positions;\n}\n\nconst LAYOUT = forceDirectedLayout();\nconst xs = LAYOUT.map((p) => p.x);\nconst ys = LAYOUT.map((p) => p.y);\nconst centerX = (Math.min(...xs) + Math.max(...xs)) / 2;\nconst centerY = (Math.min(...ys) + Math.max(...ys)) / 2;\nconst extent =\n  (Math.max(Math.max(...xs) - Math.min(...xs), Math.max(...ys) - Math.min(...ys)) / 2) * 1.35;\nconst X_DOMAIN = [centerX - extent, centerX + extent];\nconst Y_DOMAIN = [centerY - extent, centerY + extent];\n\n// --- Custom marks — drawn on the MUI X coordinate system --------------------\nfunction NetworkEdges() {\n  const xs2 = useXScale();\n  const ys2 = useYScale();\n  return (\n    <g stroke={t.inkSoft} strokeOpacity={0.4} strokeWidth={1.5}>\n      {EDGES.map(([a, b], i) => (\n        <line\n          key={`edge-${i}`}\n          x1={xs2(LAYOUT[a].x)}\n          y1={ys2(LAYOUT[a].y)}\n          x2={xs2(LAYOUT[b].x)}\n          y2={ys2(LAYOUT[b].y)}\n        />\n      ))}\n    </g>\n  );\n}\n\nfunction NetworkNodes() {\n  const xs2 = useXScale();\n  const ys2 = useYScale();\n  const screenPos = LAYOUT.map((p) => ({ x: xs2(p.x), y: ys2(p.y) }));\n  return (\n    <g fontFamily={FONT}>\n      {NODES.map((node, i) => {\n        const { x: cx, y: cy } = screenPos[i];\n        const r = 6 + DEGREES[i] * 3;\n        // Place the label in the widest angular gap between this node's edges, so\n        // it never sits on top of a line (e.g. the Aisha-Ruby bridge edge).\n        const edgeAngles = [];\n        EDGES.forEach(([a, b]) => {\n          if (a === i) edgeAngles.push(Math.atan2(screenPos[b].y - cy, screenPos[b].x - cx));\n          else if (b === i) edgeAngles.push(Math.atan2(screenPos[a].y - cy, screenPos[a].x - cx));\n        });\n        let labelAngle = Math.PI / 2;\n        if (edgeAngles.length > 0) {\n          edgeAngles.sort((a, b) => a - b);\n          let bestGap = -1;\n          for (let k = 0; k < edgeAngles.length; k += 1) {\n            const start = edgeAngles[k];\n            const end = edgeAngles[(k + 1) % edgeAngles.length] + (k === edgeAngles.length - 1 ? 2 * Math.PI : 0);\n            const gap = end - start;\n            if (gap > bestGap) {\n              bestGap = gap;\n              labelAngle = start + gap / 2;\n            }\n          }\n        }\n        const labelDist = r + 15;\n        const lx = cx + Math.cos(labelAngle) * labelDist;\n        const ly = cy + Math.sin(labelAngle) * labelDist;\n        return (\n          <g key={node.label}>\n            <circle cx={cx} cy={cy} r={r} fill={GROUP_COLOR[node.group]} stroke={t.pageBg} strokeWidth={2} />\n            <text x={lx} y={ly} fontSize={15} fill={t.ink} textAnchor=\"middle\" dominantBaseline=\"middle\">\n              {node.label}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nfunction Legend() {\n  return (\n    <g transform=\"translate(30, 28)\" fontFamily={FONT}>\n      {GROUPS.map((group, i) => (\n        <g key={group} transform={`translate(0, ${i * 30})`}>\n          <circle cx={8} cy={8} r={7} fill={GROUP_COLOR[group]} />\n          <text x={22} y={13} fontSize={15} fill={t.ink}>\n            {group}\n          </text>\n        </g>\n      ))}\n    </g>\n  );\n}\n\nconst TITLE_H = 64;\n\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n  return (\n    <div\n      style={{\n        width: W,\n        height: H,\n        background: t.pageBg,\n        fontFamily: FONT,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <div style={{ height: TITLE_H, display: \"flex\", alignItems: \"center\", justifyContent: \"center\" }}>\n        <span style={{ fontSize: 22, fontWeight: 600, color: t.ink }}>\n          network-basic · javascript · muix · anyplot.ai\n        </span>\n      </div>\n      <ChartContainer\n        width={W}\n        height={H - TITLE_H}\n        skipAnimation\n        series={[]}\n        margin={{ top: 30, right: 30, bottom: 40, left: 30 }}\n        xAxis={[{ min: X_DOMAIN[0], max: X_DOMAIN[1], scaleType: \"linear\" }]}\n        yAxis={[{ min: Y_DOMAIN[0], max: Y_DOMAIN[1], scaleType: \"linear\" }]}\n      >\n        <NetworkEdges />\n        <NetworkNodes />\n        <Legend />\n      </ChartContainer>\n    </div>\n  );\n}\n"}