{"spec_id":"network-bipartite","library":"d3","language":"javascript","code":"// anyplot.ai\n// network-bipartite: Bipartite Network Graph\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Gene-disease association network: which genetic markers are linked to which\n// conditions, and how strong the evidence for each link is.\nconst genes = [\n  \"BRCA1\", \"BRCA2\", \"TP53\", \"EGFR\", \"KRAS\", \"MYC\", \"PTEN\",\n  \"APC\", \"VHL\", \"RB1\", \"ATM\", \"CDKN2A\", \"MLH1\", \"APOE\",\n];\nconst diseases = [\n  \"Breast Cancer\", \"Ovarian Cancer\", \"Lung Cancer\", \"Colorectal Cancer\",\n  \"Pancreatic Cancer\", \"Renal Cell Carcinoma\", \"Retinoblastoma\",\n  \"Melanoma\", \"Lynch Syndrome\", \"Alzheimer's Disease\",\n];\nconst links = [\n  { source: \"BRCA1\", target: \"Breast Cancer\", weight: 0.95 },\n  { source: \"BRCA1\", target: \"Ovarian Cancer\", weight: 0.85 },\n  { source: \"BRCA2\", target: \"Breast Cancer\", weight: 0.9 },\n  { source: \"BRCA2\", target: \"Ovarian Cancer\", weight: 0.75 },\n  { source: \"BRCA2\", target: \"Pancreatic Cancer\", weight: 0.35 },\n  { source: \"TP53\", target: \"Breast Cancer\", weight: 0.6 },\n  { source: \"TP53\", target: \"Lung Cancer\", weight: 0.7 },\n  { source: \"TP53\", target: \"Colorectal Cancer\", weight: 0.55 },\n  { source: \"TP53\", target: \"Pancreatic Cancer\", weight: 0.4 },\n  { source: \"TP53\", target: \"Melanoma\", weight: 0.4 },\n  { source: \"EGFR\", target: \"Lung Cancer\", weight: 0.9 },\n  { source: \"EGFR\", target: \"Colorectal Cancer\", weight: 0.35 },\n  { source: \"KRAS\", target: \"Lung Cancer\", weight: 0.65 },\n  { source: \"KRAS\", target: \"Colorectal Cancer\", weight: 0.85 },\n  { source: \"KRAS\", target: \"Pancreatic Cancer\", weight: 0.6 },\n  { source: \"MYC\", target: \"Breast Cancer\", weight: 0.5 },\n  { source: \"MYC\", target: \"Lung Cancer\", weight: 0.45 },\n  { source: \"MYC\", target: \"Colorectal Cancer\", weight: 0.4 },\n  { source: \"PTEN\", target: \"Breast Cancer\", weight: 0.55 },\n  { source: \"PTEN\", target: \"Melanoma\", weight: 0.5 },\n  { source: \"PTEN\", target: \"Renal Cell Carcinoma\", weight: 0.3 },\n  { source: \"APC\", target: \"Colorectal Cancer\", weight: 0.95 },\n  { source: \"VHL\", target: \"Renal Cell Carcinoma\", weight: 0.9 },\n  { source: \"RB1\", target: \"Retinoblastoma\", weight: 0.95 },\n  { source: \"RB1\", target: \"Lung Cancer\", weight: 0.3 },\n  { source: \"ATM\", target: \"Breast Cancer\", weight: 0.45 },\n  { source: \"CDKN2A\", target: \"Melanoma\", weight: 0.85 },\n  { source: \"CDKN2A\", target: \"Lung Cancer\", weight: 0.3 },\n  { source: \"MLH1\", target: \"Lynch Syndrome\", weight: 0.95 },\n  { source: \"MLH1\", target: \"Colorectal Cancer\", weight: 0.7 },\n  { source: \"APOE\", target: \"Alzheimer's Disease\", weight: 0.9 },\n];\n\n// Degree = number of edges touching a node, drives node radius.\nconst degree = new Map([...genes, ...diseases].map((name) => [name, 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}\n\n// --- Reduce edge crossings: barycenter reordering within each column --------\n// Alternately sort each column by the mean position of its neighbors in the\n// opposite column, converging toward fewer crossing edges.\nfunction barycenterOrder(names, neighbors, oppositeIndex) {\n  return [...names].sort((a, b) => {\n    const na = neighbors.get(a);\n    const nb = neighbors.get(b);\n    const ba = na.length ? d3.mean(na, (n) => oppositeIndex.get(n)) : Infinity;\n    const bb = nb.length ? d3.mean(nb, (n) => oppositeIndex.get(n)) : Infinity;\n    return ba - bb;\n  });\n}\nconst geneNeighbors = new Map(genes.map((g) => [g, links.filter((l) => l.source === g).map((l) => l.target)]));\nconst diseaseNeighbors = new Map(diseases.map((d) => [d, links.filter((l) => l.target === d).map((l) => l.source)]));\n\nlet orderedGenes = genes;\nlet orderedDiseases = diseases;\nfor (let i = 0; i < 4; i++) {\n  const diseaseIndex = new Map(orderedDiseases.map((name, idx) => [name, idx]));\n  orderedGenes = barycenterOrder(orderedGenes, geneNeighbors, diseaseIndex);\n  const geneIndex = new Map(orderedGenes.map((name, idx) => [name, idx]));\n  orderedDiseases = barycenterOrder(orderedDiseases, diseaseNeighbors, geneIndex);\n}\n\n// --- Layout ------------------------------------------------------------------\nconst margin = { top: 135, right: 230, bottom: 140, left: 130 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\nconst leftX = 0;\nconst rightX = iw;\n\nfunction columnPositions(names) {\n  const step = ih / (names.length + 1);\n  return new Map(names.map((name, i) => [name, (i + 1) * step]));\n}\nconst genesY = columnPositions(orderedGenes);\nconst diseasesY = columnPositions(orderedDiseases);\n\nconst maxDegree = d3.max([...degree.values()]);\nconst radius = d3.scaleSqrt().domain([1, maxDegree]).range([9, 26]);\nconst weightExtent = d3.extent(links, (d) => d.weight);\nconst edgeWidth = d3.scaleLinear().domain(weightExtent).range([1.25, 6]);\nconst edgeOpacity = d3.scaleLinear().domain(weightExtent).range([0.22, 0.8]);\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: d3-shape horizontal links between the two columns ---------------\nconst linkGenerator = d3.linkHorizontal()\n  .source((d) => [leftX, genesY.get(d.source)])\n  .target((d) => [rightX, diseasesY.get(d.target)]);\n\ng.append(\"g\")\n  .selectAll(\"path\")\n  .data(links)\n  .join(\"path\")\n  .attr(\"d\", linkGenerator)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", (d) => edgeWidth(d.weight))\n  .attr(\"stroke-opacity\", (d) => edgeOpacity(d.weight));\n\n// --- Nodes: genes (left column) --------------------------------------------\ng.append(\"g\")\n  .selectAll(\"circle\")\n  .data(orderedGenes)\n  .join(\"circle\")\n  .attr(\"cx\", leftX)\n  .attr(\"cy\", (d) => genesY.get(d))\n  .attr(\"r\", (d) => radius(degree.get(d)))\n  .attr(\"fill\", t.palette[0])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\ng.append(\"g\")\n  .selectAll(\"text\")\n  .data(orderedGenes)\n  .join(\"text\")\n  .attr(\"x\", (d) => leftX - radius(degree.get(d)) - 12)\n  .attr(\"y\", (d) => genesY.get(d))\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"text-anchor\", \"end\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text((d) => d);\n\n// --- Nodes: diseases (right column) -----------------------------------------\ng.append(\"g\")\n  .selectAll(\"circle\")\n  .data(orderedDiseases)\n  .join(\"circle\")\n  .attr(\"cx\", rightX)\n  .attr(\"cy\", (d) => diseasesY.get(d))\n  .attr(\"r\", (d) => radius(degree.get(d)))\n  .attr(\"fill\", t.palette[1])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\ng.append(\"g\")\n  .selectAll(\"text\")\n  .data(orderedDiseases)\n  .join(\"text\")\n  .attr(\"x\", (d) => rightX + radius(degree.get(d)) + 12)\n  .attr(\"y\", (d) => diseasesY.get(d))\n  .attr(\"dy\", \"0.35em\")\n  .attr(\"text-anchor\", \"start\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text((d) => d);\n\n// --- Column headers double as the set-membership legend ---------------------\ng.append(\"text\")\n  .attr(\"x\", leftX)\n  .attr(\"y\", -30)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.palette[0])\n  .style(\"font-size\", \"18px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Genes\");\n\ng.append(\"text\")\n  .attr(\"x\", rightX)\n  .attr(\"y\", -30)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.palette[1])\n  .style(\"font-size\", \"18px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Diseases\");\n\n// --- Legend: degree -> radius and weight -> width/opacity keys --------------\nconst legend = g.append(\"g\").attr(\"transform\", `translate(0,${ih + 55})`);\n\nlegend.append(\"text\")\n  .attr(\"x\", 0)\n  .attr(\"y\", -16)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Node size = degree\");\n\nlet sx = 0;\nfor (const d of [1, maxDegree]) {\n  const r = radius(d);\n  legend.append(\"circle\")\n    .attr(\"cx\", sx + r)\n    .attr(\"cy\", 10)\n    .attr(\"r\", r)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", 1.5);\n  legend.append(\"text\")\n    .attr(\"x\", sx + 2 * r + 10)\n    .attr(\"y\", 10)\n    .attr(\"dy\", \"0.35em\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"12px\")\n    .text(`degree ${d}`);\n  sx += 2 * r + 10 + 85;\n}\n\nconst weightX = sx + 55;\nlegend.append(\"text\")\n  .attr(\"x\", weightX)\n  .attr(\"y\", -16)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Edge width/opacity = strength\");\n\nlet wx = weightX;\nfor (const w of weightExtent) {\n  legend.append(\"line\")\n    .attr(\"x1\", wx)\n    .attr(\"x2\", wx + 40)\n    .attr(\"y1\", 10)\n    .attr(\"y2\", 10)\n    .attr(\"stroke\", t.inkSoft)\n    .attr(\"stroke-width\", edgeWidth(w))\n    .attr(\"stroke-opacity\", edgeOpacity(w));\n  legend.append(\"text\")\n    .attr(\"x\", wx + 50)\n    .attr(\"y\", 10)\n    .attr(\"dy\", \"0.35em\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"12px\")\n    .text(w.toFixed(2));\n  wx += 110;\n}\n\n// --- Title + subtitle --------------------------------------------------------\nsvg.append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 52)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"network-bipartite · javascript · d3 · anyplot.ai\");\n\nsvg.append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 84)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(\"Node size ∝ degree · edge width & opacity ∝ association strength\");\n"}