{"spec_id":"network-weighted","library":"d3","language":"javascript","code":"// anyplot.ai\n// network-weighted: Weighted Network Graph with Edge Thickness\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-02\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Layout bands (title / graph / legend) ----------------------------------\nconst titleY = 50;\nconst chartTop = 100;\nconst chartBottom = height - 150;\nconst chartLeft = 75;\nconst chartRight = width - 75;\nconst iw = chartRight - chartLeft;\nconst ih = chartBottom - chartTop;\nconst legendRegionY = chartBottom + 45;\nconst legendEdgeY = chartBottom + 95;\n\n// --- Data: research-collaboration network (in-memory, deterministic) -------\n// Nodes = universities, group = home continent. Edges = co-authored papers\n// (2023-2025), weight = paper count, mapped to edge thickness + opacity.\nconst nodes = [\n  { id: \"MIT\", group: \"na\" },\n  { id: \"Stanford\", group: \"na\" },\n  { id: \"Caltech\", group: \"na\" },\n  { id: \"Berkeley\", group: \"na\" },\n  { id: \"Toronto\", group: \"na\" },\n  { id: \"Oxford\", group: \"eu\" },\n  { id: \"Cambridge\", group: \"eu\" },\n  { id: \"ETH Zurich\", group: \"eu\" },\n  { id: \"TU Munich\", group: \"eu\" },\n  { id: \"EPFL\", group: \"eu\" },\n  { id: \"Max Planck\", group: \"eu\" },\n  { id: \"Imperial\", group: \"eu\" },\n  { id: \"Tokyo\", group: \"asia\" },\n  { id: \"NUS\", group: \"asia\" },\n  { id: \"Tsinghua\", group: \"asia\" },\n];\n\nconst links = [\n  { source: \"MIT\", target: \"Stanford\", weight: 28 },\n  { source: \"MIT\", target: \"Oxford\", weight: 15 },\n  { source: \"MIT\", target: \"ETH Zurich\", weight: 9 },\n  { source: \"MIT\", target: \"Caltech\", weight: 22 },\n  { source: \"MIT\", target: \"Tsinghua\", weight: 12 },\n  { source: \"Stanford\", target: \"Berkeley\", weight: 31 },\n  { source: \"Stanford\", target: \"Cambridge\", weight: 11 },\n  { source: \"Stanford\", target: \"Tokyo\", weight: 14 },\n  { source: \"Oxford\", target: \"Cambridge\", weight: 35 },\n  { source: \"Oxford\", target: \"ETH Zurich\", weight: 19 },\n  { source: \"Oxford\", target: \"Imperial\", weight: 24 },\n  { source: \"ETH Zurich\", target: \"Max Planck\", weight: 27 },\n  { source: \"ETH Zurich\", target: \"EPFL\", weight: 33 },\n  { source: \"Cambridge\", target: \"Imperial\", weight: 18 },\n  { source: \"Cambridge\", target: \"Max Planck\", weight: 8 },\n  { source: \"Caltech\", target: \"Berkeley\", weight: 16 },\n  { source: \"Caltech\", target: \"Toronto\", weight: 7 },\n  { source: \"Berkeley\", target: \"Toronto\", weight: 13 },\n  { source: \"Tokyo\", target: \"NUS\", weight: 21 },\n  { source: \"Tokyo\", target: \"Tsinghua\", weight: 17 },\n  { source: \"NUS\", target: \"Tsinghua\", weight: 25 },\n  { source: \"TU Munich\", target: \"Max Planck\", weight: 29 },\n  { source: \"TU Munich\", target: \"EPFL\", weight: 14 },\n  { source: \"EPFL\", target: \"Imperial\", weight: 10 },\n  { source: \"Toronto\", target: \"MIT\", weight: 6 },\n];\n\n// Weighted degree (sum of incident edge weights) drives node size.\nconst weightedDegree = new Map(nodes.map((d) => [d.id, 0]));\nlinks.forEach((l) => {\n  weightedDegree.set(l.source, weightedDegree.get(l.source) + l.weight);\n  weightedDegree.set(l.target, weightedDegree.get(l.target) + l.weight);\n});\nnodes.forEach((d) => {\n  d.weightedDegree = weightedDegree.get(d.id);\n});\n\n// --- Scales -------------------------------------------------------------\nconst [minWeight, maxWeight] = d3.extent(links, (d) => d.weight);\nconst radius = d3\n  .scaleSqrt()\n  .domain(d3.extent(nodes, (d) => d.weightedDegree))\n  .range([16, 42]);\nconst edgeWidth = d3.scaleLinear().domain([minWeight, maxWeight]).range([1.5, 9]);\nconst edgeOpacity = d3.scaleLinear().domain([minWeight, maxWeight]).range([0.3, 0.85]);\n// Wider floor (170 vs. the prior 140) keeps the heaviest-weight pairs — which\n// cluster in the dense Europe sub-network — from being pulled in tight enough\n// for their edges to cross neighboring nodes and labels.\nconst linkDistance = d3.scaleLinear().domain([minWeight, maxWeight]).range([380, 170]);\nconst groupColor = d3\n  .scaleOrdinal()\n  .domain([\"na\", \"eu\", \"asia\"])\n  .range([t.palette[0], t.palette[1], t.palette[2]]);\n\n// Two hub nodes (by weighted degree) get a bolder stroke and render above the\n// rest of the nodes/edges to sharpen the visual focal point.\nconst hubIds = new Set(\n  [...nodes].sort((a, b) => b.weightedDegree - a.weightedDegree).slice(0, 2).map((d) => d.id),\n);\n\n// --- Force layout, advanced synchronously (no animation in the static PNG) --\nconst simulation = d3\n  .forceSimulation(nodes)\n  .force(\n    \"link\",\n    d3\n      .forceLink(links)\n      .id((d) => d.id)\n      .distance((d) => linkDistance(d.weight))\n      .strength(0.55),\n  )\n  .force(\"charge\", d3.forceManyBody().strength(-1500))\n  .force(\"center\", d3.forceCenter(chartLeft + iw / 2, chartTop + ih / 2))\n  .force(\n    \"collide\",\n    d3.forceCollide().radius((d) => radius(d.weightedDegree) + 22),\n  )\n  .stop();\n\nfor (let i = 0; i < 600; i += 1) simulation.tick();\n\n// Keep every node (and its label) inside the graph band.\nnodes.forEach((d) => {\n  const r = radius(d.weightedDegree);\n  d.x = Math.max(chartLeft + r, Math.min(chartRight - r, d.x));\n  d.y = Math.max(chartTop + r, Math.min(chartBottom - r, d.y));\n});\n\n// --- SVG mount ----------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\n// --- Edges ----------------------------------------------------------------\nsvg\n  .append(\"g\")\n  .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-width\", (d) => edgeWidth(d.weight))\n  .attr(\"stroke-opacity\", (d) => edgeOpacity(d.weight))\n  .attr(\"stroke-linecap\", \"round\");\n\n// --- Nodes + labels ---------------------------------------------------------\n// Draw ascending by weighted degree so the two hub nodes render last (on top\n// of every crossing edge and neighboring node) for a sharper focal point.\nconst drawOrder = [...nodes].sort((a, b) => a.weightedDegree - b.weightedDegree);\nconst nodeGroups = svg\n  .append(\"g\")\n  .selectAll(\"g\")\n  .data(drawOrder)\n  .join(\"g\")\n  .attr(\"transform\", (d) => `translate(${d.x},${d.y})`);\n\nnodeGroups\n  .append(\"circle\")\n  .attr(\"r\", (d) => radius(d.weightedDegree))\n  .attr(\"fill\", (d) => groupColor(d.group))\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", (d) => (hubIds.has(d.id) ? 4 : 2.5));\n\nnodeGroups\n  .append(\"text\")\n  .attr(\"y\", (d) => radius(d.weightedDegree) + 18)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", 500)\n  .text((d) => d.id);\n\n// --- Legend: region colors ---------------------------------------------\nconst regionLegend = [\n  { label: \"North America\", color: t.palette[0] },\n  { label: \"Europe\", color: t.palette[1] },\n  { label: \"Asia\", color: t.palette[2] },\n];\nconst regionSpacing = 230;\nconst regionLegendG = svg.append(\"g\");\nregionLegend.forEach((d, i) => {\n  const g = regionLegendG\n    .append(\"g\")\n    .attr(\"transform\", `translate(${chartLeft + i * regionSpacing},${legendRegionY})`);\n  g.append(\"circle\").attr(\"r\", 9).attr(\"fill\", d.color);\n  g.append(\"text\")\n    .attr(\"x\", 20)\n    .attr(\"y\", 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"15px\")\n    .text(d.label);\n});\n\n// --- Legend: edge-weight scale --------------------------------------------\nconst edgeLegendG = svg.append(\"g\").attr(\"transform\", `translate(${chartLeft},${legendEdgeY})`);\nedgeLegendG\n  .append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", 50)\n  .attr(\"y1\", 0)\n  .attr(\"y2\", 0)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", edgeWidth(minWeight))\n  .attr(\"stroke-linecap\", \"round\");\nedgeLegendG\n  .append(\"text\")\n  .attr(\"x\", 60)\n  .attr(\"y\", 5)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(`${minWeight} papers`);\nedgeLegendG\n  .append(\"line\")\n  .attr(\"x1\", 180)\n  .attr(\"x2\", 230)\n  .attr(\"y1\", 0)\n  .attr(\"y2\", 0)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", edgeWidth(maxWeight))\n  .attr(\"stroke-linecap\", \"round\");\nedgeLegendG\n  .append(\"text\")\n  .attr(\"x\", 240)\n  .attr(\"y\", 5)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(`${maxWeight} papers`);\nedgeLegendG\n  .append(\"text\")\n  .attr(\"x\", 340)\n  .attr(\"y\", 5)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"Co-authored papers (edge weight)\");\n\n// --- Title ------------------------------------------------------------------\nconst titleText = \"network-weighted · javascript · d3 · anyplot.ai\";\nconst titleDefaultSize = 22;\nconst titleFloor = 15;\nconst titleRatio = titleText.length > 67 ? 67 / titleText.length : 1;\nconst titleFontSize = Math.max(titleFloor, Math.round(titleDefaultSize * titleRatio));\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", titleY)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-weight\", 600)\n  .style(\"font-size\", `${titleFontSize}px`)\n  .text(titleText);\n"}