{"spec_id":"network-force-directed","library":"d3","language":"javascript","code":"// anyplot.ai\n// network-force-directed: Force-Directed Graph\n// Library: d3 7.9.0 | JavaScript 22.23.0\n// Quality: 87/100 | Created: 2026-07-01\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 72, right: 185, bottom: 60, left: 60 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: Tech Ecosystem Dependency Network (35 nodes, 51 edges) ---\nconst GROUPS = [\"Languages\", \"Frameworks\", \"Databases\", \"DevOps\", \"ML / Data\"];\n\nconst rawNodes = [\n  { id: \"Python\",        group: 0 }, { id: \"JavaScript\",    group: 0 },\n  { id: \"TypeScript\",    group: 0 }, { id: \"Java\",          group: 0 },\n  { id: \"Go\",            group: 0 }, { id: \"Scala\",         group: 0 },\n  { id: \"Ruby\",          group: 0 },\n  { id: \"React\",         group: 1 }, { id: \"Angular\",       group: 1 },\n  { id: \"Vue\",           group: 1 }, { id: \"Django\",        group: 1 },\n  { id: \"FastAPI\",       group: 1 }, { id: \"Spring\",        group: 1 },\n  { id: \"Express\",       group: 1 }, { id: \"Rails\",         group: 1 },\n  { id: \"Next.js\",       group: 1 }, { id: \"Svelte\",        group: 1 },\n  { id: \"PostgreSQL\",    group: 2 }, { id: \"MySQL\",         group: 2 },\n  { id: \"MongoDB\",       group: 2 }, { id: \"Redis\",         group: 2 },\n  { id: \"SQLite\",        group: 2 }, { id: \"Elasticsearch\", group: 2 },\n  { id: \"Docker\",        group: 3 }, { id: \"Kubernetes\",    group: 3 },\n  { id: \"AWS\",           group: 3 }, { id: \"GitHub\",        group: 3 },\n  { id: \"Terraform\",     group: 3 }, { id: \"Nginx\",         group: 3 },\n  { id: \"TensorFlow\",    group: 4 }, { id: \"PyTorch\",       group: 4 },\n  { id: \"NumPy\",         group: 4 }, { id: \"Pandas\",        group: 4 },\n  { id: \"Spark\",         group: 4 }, { id: \"Keras\",         group: 4 },\n];\n\nconst rawLinks = [\n  { source: \"Python\",     target: \"Django\"         },\n  { source: \"Python\",     target: \"FastAPI\"        },\n  { source: \"Python\",     target: \"TensorFlow\"     },\n  { source: \"Python\",     target: \"PyTorch\"        },\n  { source: \"Python\",     target: \"NumPy\"          },\n  { source: \"Python\",     target: \"Pandas\"         },\n  { source: \"Python\",     target: \"Keras\"          },\n  { source: \"JavaScript\", target: \"React\"          },\n  { source: \"JavaScript\", target: \"Angular\"        },\n  { source: \"JavaScript\", target: \"Vue\"            },\n  { source: \"JavaScript\", target: \"Express\"        },\n  { source: \"JavaScript\", target: \"Svelte\"         },\n  { source: \"JavaScript\", target: \"TypeScript\"     },\n  { source: \"TypeScript\", target: \"React\"          },\n  { source: \"TypeScript\", target: \"Angular\"        },\n  { source: \"TypeScript\", target: \"Next.js\"        },\n  { source: \"React\",      target: \"Next.js\"        },\n  { source: \"Java\",       target: \"Spring\"         },\n  { source: \"Java\",       target: \"Elasticsearch\"  },\n  { source: \"Scala\",      target: \"Spark\"          },\n  { source: \"Scala\",      target: \"Java\"           },\n  { source: \"Go\",         target: \"Docker\"         },\n  { source: \"Go\",         target: \"Kubernetes\"     },\n  { source: \"Ruby\",       target: \"Rails\"          },\n  { source: \"Django\",     target: \"PostgreSQL\"     },\n  { source: \"Django\",     target: \"MySQL\"          },\n  { source: \"Django\",     target: \"SQLite\"         },\n  { source: \"Django\",     target: \"Redis\"          },\n  { source: \"FastAPI\",    target: \"PostgreSQL\"     },\n  { source: \"FastAPI\",    target: \"MongoDB\"        },\n  { source: \"FastAPI\",    target: \"Redis\"          },\n  { source: \"Spring\",     target: \"PostgreSQL\"     },\n  { source: \"Spring\",     target: \"MySQL\"          },\n  { source: \"Express\",    target: \"MongoDB\"        },\n  { source: \"Express\",    target: \"PostgreSQL\"     },\n  { source: \"Rails\",      target: \"PostgreSQL\"     },\n  { source: \"Rails\",      target: \"MySQL\"          },\n  { source: \"Rails\",      target: \"SQLite\"         },\n  { source: \"Docker\",     target: \"Kubernetes\"     },\n  { source: \"Kubernetes\", target: \"AWS\"            },\n  { source: \"Terraform\",  target: \"AWS\"            },\n  { source: \"Nginx\",      target: \"Docker\"         },\n  { source: \"Nginx\",      target: \"Kubernetes\"     },\n  { source: \"GitHub\",     target: \"Docker\"         },\n  { source: \"GitHub\",     target: \"Kubernetes\"     },\n  { source: \"TensorFlow\", target: \"Keras\"          },\n  { source: \"TensorFlow\", target: \"NumPy\"          },\n  { source: \"PyTorch\",    target: \"NumPy\"          },\n  { source: \"Pandas\",     target: \"NumPy\"          },\n  { source: \"Pandas\",     target: \"PostgreSQL\"     },\n  { source: \"Spark\",      target: \"Elasticsearch\"  },\n];\n\n// --- Degree computation (before simulation mutates links) ---\nconst degMap = new Map(rawNodes.map(n => [n.id, 0]));\nrawLinks.forEach(({ source, target }) => {\n  degMap.set(source, degMap.get(source) + 1);\n  degMap.set(target, degMap.get(target) + 1);\n});\nconst nodes = rawNodes.map(n => ({ ...n, degree: degMap.get(n.id) }));\nconst links = rawLinks.map(l => ({ ...l }));\n\nconst maxDeg = d3.max(nodes, d => d.degree);\nconst nodeRadius = d => 6 + (d.degree / maxDeg) * 13;\n\n// --- Force simulation (run synchronously for static render) ---\nconst sim = d3.forceSimulation(nodes)\n  .force(\"link\", d3.forceLink(links).id(d => d.id).distance(80).strength(0.7))\n  .force(\"charge\", d3.forceManyBody().strength(-280))\n  .force(\"center\", d3.forceCenter(iw / 2, ih / 2))\n  .force(\"x\", d3.forceX(iw / 2).strength(0.10))\n  .force(\"y\", d3.forceY(ih / 2).strength(0.10))\n  .force(\"collide\", d3.forceCollide().radius(d => nodeRadius(d) + 5).iterations(2))\n  .stop();\n\nfor (let i = 0; i < 300; i++) sim.tick();\n\nnodes.forEach(n => {\n  const r = nodeRadius(n);\n  n.x = Math.max(r + 2, Math.min(iw - r - 2, n.x));\n  n.y = Math.max(r + 2, Math.min(ih - r - 2, n.y));\n});\n\n// --- SVG mount ---\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width).attr(\"height\", height);\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Edges ---\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(links)\n  .join(\"line\")\n  .attr(\"x1\", d => d.source.x).attr(\"y1\", d => d.source.y)\n  .attr(\"x2\", d => d.target.x).attr(\"y2\", d => d.target.y)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1)\n  .attr(\"stroke-opacity\", 0.35);\n\n// --- Nodes (hub nodes get thicker stroke for visual emphasis) ---\ng.append(\"g\")\n  .selectAll(\"circle\")\n  .data(nodes)\n  .join(\"circle\")\n  .attr(\"cx\", d => d.x)\n  .attr(\"cy\", d => d.y)\n  .attr(\"r\", d => nodeRadius(d))\n  .attr(\"fill\", d => t.palette[d.group])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", d => d.degree >= 4 ? 2.5 : 1.5)\n  .attr(\"fill-opacity\", 0.9);\n\n// --- Labels for hub nodes (degree >= 4) ---\ng.append(\"g\")\n  .selectAll(\"text\")\n  .data(nodes.filter(d => d.degree >= 4))\n  .join(\"text\")\n  .attr(\"x\", d => d.x)\n  .attr(\"y\", d => d.y - nodeRadius(d) - 5)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"600\")\n  .text(d => d.id);\n\n// --- Legend ---\nconst lgX = margin.left + iw + 24;\nconst lgY = margin.top + 20;\nconst lgG = svg.append(\"g\").attr(\"transform\", `translate(${lgX},${lgY})`);\n\nlgG.append(\"text\")\n  .attr(\"x\", 0).attr(\"y\", 0)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Community\");\n\nGROUPS.forEach((name, i) => {\n  const row = lgG.append(\"g\").attr(\"transform\", `translate(0,${24 + i * 28})`);\n  row.append(\"circle\")\n    .attr(\"cx\", 8).attr(\"cy\", 0).attr(\"r\", 7)\n    .attr(\"fill\", t.palette[i]).attr(\"fill-opacity\", 0.9)\n    .attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 1.5);\n  row.append(\"text\")\n    .attr(\"x\", 22).attr(\"y\", 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(name);\n});\n\nlgG.append(\"text\")\n  .attr(\"x\", 0)\n  .attr(\"y\", 24 + GROUPS.length * 28 + 24)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Node size ∝ degree\");\n\n// --- Title ---\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"network-force-directed · javascript · d3 · anyplot.ai\");\n"}