{"spec_id":"network-basic","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// network-basic: Basic Network Graph\n// Library: chartjs 4.4.7 | JavaScript 22.23.1\n// Quality: 92/100 | Created: 2026-07-24\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: a small company collaboration network, grouped by department ---\nconst GROUP_NAMES = [\"Engineering\", \"Design\", \"Product\", \"Marketing\"];\n\nconst nodes = [\n  { id: 0, name: \"Ava\", group: 0 },\n  { id: 1, name: \"Noah\", group: 0 },\n  { id: 2, name: \"Mia\", group: 0 },\n  { id: 3, name: \"Ethan\", group: 0 },\n  { id: 4, name: \"Zoe\", group: 0 },\n  { id: 5, name: \"Liam\", group: 1 },\n  { id: 6, name: \"Grace\", group: 1 },\n  { id: 7, name: \"Kai\", group: 1 },\n  { id: 8, name: \"Nora\", group: 1 },\n  { id: 9, name: \"Owen\", group: 1 },\n  { id: 10, name: \"Maya\", group: 2 },\n  { id: 11, name: \"Leo\", group: 2 },\n  { id: 12, name: \"Ivy\", group: 2 },\n  { id: 13, name: \"Finn\", group: 2 },\n  { id: 14, name: \"Ruby\", group: 2 },\n  { id: 15, name: \"Jack\", group: 3 },\n  { id: 16, name: \"Elena\", group: 3 },\n  { id: 17, name: \"Theo\", group: 3 },\n  { id: 18, name: \"Luna\", group: 3 },\n  { id: 19, name: \"Max\", group: 3 },\n];\n\n// Edge tuples are [source, target, weight] — weight is a 1-5 tie-strength\n// (e.g. weekly collaboration touchpoints). Cross-department bridges carry a\n// deliberately low weight since they represent occasional handoffs, not the\n// tight day-to-day ties within a team.\nconst edges = [\n  [0, 1, 4], [0, 2, 3], [1, 2, 5], [1, 3, 3], [2, 3, 4], [3, 4, 3], [2, 4, 2], [0, 4, 3],\n  [5, 6, 4], [5, 7, 3], [6, 7, 5], [6, 8, 3], [7, 8, 4], [8, 9, 3], [7, 9, 2],\n  [10, 11, 3], [10, 12, 4], [11, 12, 3], [11, 13, 5], [12, 13, 3], [13, 14, 4], [12, 14, 2],\n  [15, 16, 4], [15, 17, 3], [16, 17, 5], [16, 18, 3], [17, 18, 4], [18, 19, 3], [17, 19, 2],\n  [0, 5, 1], [2, 10, 1], [6, 11, 2], [8, 15, 1], [12, 16, 1], [4, 17, 2],\n];\n\n// Degree (connection count) per node — drives marker size\nconst degree = new Array(nodes.length).fill(0);\nedges.forEach(([a, b]) => {\n  degree[a] += 1;\n  degree[b] += 1;\n});\n\n// Radius follows degree so hub nodes read as visually larger — shared by the\n// node datasets below and the hub-label plugin so both stay in sync.\nconst nodeRadius = (id) => 9 + degree[id] * 2;\n\n// Highest-degree node per department — labeled directly on the canvas so the\n// static PNG conveys individual identity, not just department color.\nconst hubs = GROUP_NAMES.map((_, g) => {\n  const members = nodes.filter((node) => node.group === g);\n  return members.reduce((best, node) => (degree[node.id] > degree[best.id] ? node : best));\n});\n\n// --- Force-directed layout (Fruchterman-Reingold), deterministic via a fixed-seed LCG ---\nfunction lcg(seed) {\n  let s = seed;\n  return () => {\n    s = (s * 1664525 + 1013904223) % 4294967296;\n    return s / 4294967296;\n  };\n}\nconst rand = lcg(42);\n\nconst nodeCount = nodes.length;\nconst area = 4; // layout unfolds inside a [-1, 1] x [-1, 1] square\nconst k = Math.sqrt(area / nodeCount);\nconst pos = nodes.map(() => ({ x: rand() * 2 - 1, y: rand() * 2 - 1 }));\n\nlet temperature = 0.15;\nconst iterations = 400;\nfor (let iter = 0; iter < iterations; iter++) {\n  const disp = pos.map(() => ({ x: 0, y: 0 }));\n\n  // Repulsion between every pair of nodes keeps clusters from collapsing\n  for (let i = 0; i < nodeCount; i++) {\n    for (let j = i + 1; j < nodeCount; j++) {\n      let dx = pos[i].x - pos[j].x;\n      let dy = pos[i].y - pos[j].y;\n      const dist = Math.max(Math.sqrt(dx * dx + dy * dy), 0.01);\n      const force = (k * k) / dist;\n      dx = (dx / dist) * force;\n      dy = (dy / dist) * force;\n      disp[i].x += dx;\n      disp[i].y += dy;\n      disp[j].x -= dx;\n      disp[j].y -= dy;\n    }\n  }\n\n  // Attraction along edges pulls connected nodes together\n  edges.forEach(([a, b]) => {\n    let dx = pos[a].x - pos[b].x;\n    let dy = pos[a].y - pos[b].y;\n    const dist = Math.max(Math.sqrt(dx * dx + dy * dy), 0.01);\n    const force = (dist * dist) / k;\n    dx = (dx / dist) * force;\n    dy = (dy / dist) * force;\n    disp[a].x -= dx;\n    disp[a].y -= dy;\n    disp[b].x += dx;\n    disp[b].y += dy;\n  });\n\n  // Apply displacement, capped by the cooling temperature\n  for (let i = 0; i < nodeCount; i++) {\n    const d = Math.max(Math.sqrt(disp[i].x ** 2 + disp[i].y ** 2), 0.0001);\n    pos[i].x += (disp[i].x / d) * Math.min(d, temperature);\n    pos[i].y += (disp[i].y / d) * Math.min(d, temperature);\n  }\n  temperature *= 0.99;\n}\n\n// Symmetric bound (with padding) so the square canvas isn't stretched\nconst bound =\n  Math.max(...pos.map((p) => Math.max(Math.abs(p.x), Math.abs(p.y)))) * 1.08;\nnodes.forEach((node, i) => {\n  node.x = pos[i].x;\n  node.y = pos[i].y;\n});\n\n// --- Mount ---\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Edges: drawn under the node markers via a lightweight inline plugin ---\n// Intra-department ties render heavier and darker (weight-scaled); cross-\n// department bridges render thin and faint so the community structure — not\n// just the connections — is legible at a glance.\nconst edgePlugin = {\n  id: \"networkEdges\",\n  beforeDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    ctx.save();\n    ctx.strokeStyle = t.inkSoft;\n    edges.forEach(([a, b, weight]) => {\n      const bridge = nodes[a].group !== nodes[b].group;\n      ctx.globalAlpha = bridge ? 0.18 : 0.22 + weight * 0.035;\n      ctx.lineWidth = bridge ? 1 : 1.4 + weight * 0.3;\n      ctx.beginPath();\n      ctx.moveTo(scales.x.getPixelForValue(nodes[a].x), scales.y.getPixelForValue(nodes[a].y));\n      ctx.lineTo(scales.x.getPixelForValue(nodes[b].x), scales.y.getPixelForValue(nodes[b].y));\n      ctx.stroke();\n    });\n    ctx.restore();\n  },\n};\n\n// --- Hub labels: name tags for the highest-degree node per department,\n// drawn on top of everything so the static PNG identifies key individuals ---\nconst hubLabelPlugin = {\n  id: \"networkHubLabels\",\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    ctx.save();\n    ctx.font = \"600 15px sans-serif\";\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"bottom\";\n    hubs.forEach((node) => {\n      const x = scales.x.getPixelForValue(node.x);\n      const y = scales.y.getPixelForValue(node.y) - nodeRadius(node.id) - 6;\n      const text = node.name;\n      const padX = 5;\n      const { width } = ctx.measureText(text);\n      ctx.fillStyle = t.pageBg;\n      ctx.globalAlpha = 0.82;\n      ctx.fillRect(x - width / 2 - padX, y - 15, width + padX * 2, 18);\n      ctx.globalAlpha = 1;\n      ctx.fillStyle = t.ink;\n      ctx.fillText(text, x, y);\n    });\n    ctx.restore();\n  },\n};\n\n// --- Nodes: one dataset per department so the legend reads as group color ---\nconst groupNodes = GROUP_NAMES.map((_, g) => nodes.filter((node) => node.group === g));\nconst datasets = groupNodes.map((group, g) => ({\n  label: GROUP_NAMES[g],\n  data: group.map((node) => ({ x: node.x, y: node.y })),\n  backgroundColor: t.palette[g],\n  borderColor: t.pageBg,\n  borderWidth: 2,\n  pointRadius: group.map((node) => nodeRadius(node.id)),\n  pointHoverRadius: group.map((node) => nodeRadius(node.id) + 4),\n  showLine: false,\n}));\n\n// --- Chart ---\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: { datasets },\n  plugins: [edgePlugin, hubLabelPlugin],\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: {\n      padding: { top: 10, right: 30, bottom: 20, left: 30 },\n    },\n    plugins: {\n      title: {\n        display: true,\n        text: \"network-basic · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 24, weight: \"normal\" },\n        padding: { top: 12, bottom: 16 },\n      },\n      legend: {\n        display: true,\n        position: \"bottom\",\n        labels: { color: t.ink, font: { size: 16 }, usePointStyle: true, boxWidth: 10 },\n      },\n      tooltip: {\n        callbacks: {\n          title: (items) => (items.length ? GROUP_NAMES[items[0].datasetIndex] : \"\"),\n          label: (item) => {\n            const node = groupNodes[item.datasetIndex][item.dataIndex];\n            return `${node.name} — ${degree[node.id]} connections`;\n          },\n        },\n      },\n    },\n    scales: {\n      x: { display: false, min: -bound, max: bound },\n      y: { display: false, min: -bound, max: bound },\n    },\n  },\n});\n"}