{"spec_id":"network-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// network-basic: Basic Network Graph\n// Library: d3 7.9.0 | JavaScript 22.23.1\n// Quality: 93/100 | Created: 2026-07-24\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 130, right: 70, bottom: 60, left: 70 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: a small friendship network across four social circles ----------\nconst circles = [\"College Friends\", \"Coworkers\", \"Neighbors\", \"Running Club\"];\n\nconst nodes = [\n  { id: \"Ava\", group: 0 }, { id: \"Liam\", group: 0 }, { id: \"Maya\", group: 0 },\n  { id: \"Noah\", group: 0 }, { id: \"Zoe\", group: 0 }, { id: \"Kai\", group: 0 },\n  { id: \"Mia\", group: 1 }, { id: \"Leo\", group: 1 }, { id: \"Ivy\", group: 1 },\n  { id: \"Finn\", group: 1 }, { id: \"Luna\", group: 1 }, { id: \"Owen\", group: 1 },\n  { id: \"Emma\", group: 2 }, { id: \"Jack\", group: 2 }, { id: \"Nora\", group: 2 },\n  { id: \"Theo\", group: 2 }, { id: \"Lily\", group: 2 },\n  { id: \"Ella\", group: 3 }, { id: \"Sam\", group: 3 }, { id: \"Ruby\", group: 3 },\n  { id: \"Max\", group: 3 }, { id: \"Iris\", group: 3 },\n];\n\nconst links = [\n  // College Friends\n  { source: \"Ava\", target: \"Liam\" }, { source: \"Ava\", target: \"Maya\" },\n  { source: \"Liam\", target: \"Noah\" }, { source: \"Maya\", target: \"Noah\" },\n  { source: \"Maya\", target: \"Zoe\" }, { source: \"Noah\", target: \"Kai\" },\n  { source: \"Zoe\", target: \"Kai\" }, { source: \"Ava\", target: \"Zoe\" },\n  // Coworkers\n  { source: \"Mia\", target: \"Leo\" }, { source: \"Mia\", target: \"Ivy\" },\n  { source: \"Leo\", target: \"Finn\" }, { source: \"Ivy\", target: \"Finn\" },\n  { source: \"Ivy\", target: \"Luna\" }, { source: \"Finn\", target: \"Owen\" },\n  { source: \"Luna\", target: \"Owen\" }, { source: \"Mia\", target: \"Luna\" },\n  // Neighbors\n  { source: \"Emma\", target: \"Jack\" }, { source: \"Emma\", target: \"Nora\" },\n  { source: \"Jack\", target: \"Theo\" }, { source: \"Nora\", target: \"Theo\" },\n  { source: \"Nora\", target: \"Lily\" }, { source: \"Theo\", target: \"Emma\" },\n  // Running Club\n  { source: \"Ella\", target: \"Sam\" }, { source: \"Ella\", target: \"Ruby\" },\n  { source: \"Sam\", target: \"Max\" }, { source: \"Ruby\", target: \"Max\" },\n  { source: \"Ruby\", target: \"Iris\" }, { source: \"Max\", target: \"Ella\" },\n  // Bridges between circles\n  { source: \"Kai\", target: \"Mia\" }, { source: \"Owen\", target: \"Emma\" },\n  { source: \"Lily\", target: \"Ella\" }, { source: \"Iris\", target: \"Ava\" },\n];\n\n// --- Degree -> node radius (encodes number of connections) -----------------\nconst degree = new Map(nodes.map((d) => [d.id, 0]));\nfor (const l of links) {\n  degree.set(l.source, degree.get(l.source) + 1);\n  degree.set(l.target, degree.get(l.target) + 1);\n}\nconst radiusScale = d3\n  .scaleLinear()\n  .domain([d3.min(degree.values()), d3.max(degree.values())])\n  .range([16, 32]);\nfor (const d of nodes) d.radius = radiusScale(degree.get(d.id));\n\n// --- Layout: force simulation, ticked to convergence (no animation) --------\n// The four social circles are bridged in a ring (0-1, 1-2, 2-3, 3-0). Anchoring\n// each group near its own canvas corner (with a strong pull) and stretching the\n// bridge links (longer distance, weaker strength than intra-group links) lets\n// that ring stretch out to the full square instead of collapsing into a tight\n// diagonal band through the center.\nconst groupAnchor = [\n  { x: iw * 0.12, y: ih * 0.14 },\n  { x: iw * 0.88, y: ih * 0.14 },\n  { x: iw * 0.88, y: ih * 0.86 },\n  { x: iw * 0.12, y: ih * 0.86 },\n];\nconst groupById = new Map(nodes.map((d) => [d.id, d.group]));\nconst sameGroup = (d) => {\n  const s = typeof d.source === \"object\" ? d.source.group : groupById.get(d.source);\n  const target = typeof d.target === \"object\" ? d.target.group : groupById.get(d.target);\n  return s === target;\n};\nconst simulation = d3\n  .forceSimulation(nodes)\n  .force(\n    \"link\",\n    d3\n      .forceLink(links)\n      .id((d) => d.id)\n      .distance((d) => (sameGroup(d) ? 90 : 280))\n      .strength((d) => (sameGroup(d) ? 0.6 : 0.22)),\n  )\n  .force(\"charge\", d3.forceManyBody().strength(-280))\n  .force(\"x\", d3.forceX((d) => groupAnchor[d.group].x).strength(0.25))\n  .force(\"y\", d3.forceY((d) => groupAnchor[d.group].y).strength(0.25))\n  .force(\"collide\", d3.forceCollide((d) => d.radius + 12))\n  .stop();\nfor (let i = 0; i < 400; i++) simulation.tick();\n\nconst labelPad = 46; // room for the node label drawn beneath each circle\nfor (const d of nodes) {\n  d.x = Math.max(d.radius, Math.min(iw - d.radius, d.x));\n  d.y = Math.max(d.radius, Math.min(ih - d.radius - labelPad, d.y));\n}\n\n// --- Color: one hue per social circle (abstract groups -> canonical order) -\nconst color = d3.scaleOrdinal().domain(d3.range(circles.length)).range(t.palette);\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Edges (drawn first, beneath nodes) --------------------------------------\n// forceLink().id() resolves link.source/target from ids into node object\n// references once the simulation runs, so they carry .x/.y directly here.\ng.selectAll(\"line\")\n  .data(links)\n  .join(\"line\")\n  .attr(\"x1\", (d) => d.source.x)\n  .attr(\"y1\", (d) => d.source.y)\n  .attr(\"x2\", (d) => d.target.x)\n  .attr(\"y2\", (d) => d.target.y)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-opacity\", 0.35)\n  .attr(\"stroke-width\", 1.75);\n\n// --- Nodes --------------------------------------------------------------------\ng.selectAll(\"circle\")\n  .data(nodes)\n  .join(\"circle\")\n  .attr(\"cx\", (d) => d.x)\n  .attr(\"cy\", (d) => d.y)\n  .attr(\"r\", (d) => d.radius)\n  .attr(\"fill\", (d) => color(d.group))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2.5);\n\n// --- Labels ---------------------------------------------------------------\ng.selectAll(\"text.node-label\")\n  .data(nodes)\n  .join(\"text\")\n  .attr(\"class\", \"node-label\")\n  .attr(\"x\", (d) => d.x)\n  .attr(\"y\", (d) => d.y + d.radius + 16)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text((d) => d.id);\n\n// --- Legend (social circles) -----------------------------------------------\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(${margin.left}, 78)`);\nconst legendItem = legend\n  .selectAll(\"g\")\n  .data(circles)\n  .join(\"g\")\n  .attr(\"transform\", (d, i) => `translate(${i * (iw / circles.length)}, 0)`);\nlegendItem\n  .append(\"circle\")\n  .attr(\"r\", 9)\n  .attr(\"cy\", -5)\n  .attr(\"fill\", (d, i) => color(i));\nlegendItem\n  .append(\"text\")\n  .attr(\"x\", 18)\n  .attr(\"y\", 0)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text((d) => d);\n\n// --- Title --------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"network-basic · javascript · d3 · anyplot.ai\");\n"}