{"spec_id":"network-bipartite","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// network-bipartite: Bipartite Network Graph\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Author-paper affiliation network: which researchers contributed to which papers.\nconst authors = [\n  \"Chen\", \"Diaz\", \"Kumar\", \"Novak\", \"Osei\", \"Petrov\",\n  \"Silva\", \"Tanaka\", \"Ahmed\", \"Brooks\", \"Costa\", \"Duran\",\n];\nconst papers = Array.from({ length: 16 }, (_, i) => `Paper ${i + 1}`);\n\n// Fixed-seed LCG — the browser has no seeded RNG, so pseudo-randomness must be\n// hand-rolled for reproducible output.\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\n// Each paper draws 1-3 co-authors; weight is that author's credited\n// contribution share (0.3-1.0), encoded later as edge width/opacity.\nconst edges = [];\npapers.forEach((_, paperIdx) => {\n  const numAuthors = 1 + Math.floor(rand() * 3);\n  const chosen = new Set();\n  while (chosen.size < numAuthors) {\n    chosen.add(Math.floor(rand() * authors.length));\n  }\n  chosen.forEach((authorIdx) => {\n    edges.push({ authorIdx, paperIdx, weight: 0.3 + rand() * 0.7 });\n  });\n});\n\nconst authorDegree = authors.map((_, i) => edges.filter((e) => e.authorIdx === i).length);\nconst paperDegree = papers.map((_, j) => edges.filter((e) => e.paperIdx === j).length);\n\n// Reorder each column by degree (descending) so hub nodes cluster near the\n// top on both sides — a simple crossing-minimization heuristic that turns\n// the raw insertion-order \"hairball\" into a readable hub/cluster layout.\nconst authorOrder = authors.map((_, i) => i).sort((a, b) => authorDegree[b] - authorDegree[a] || a - b);\nconst paperOrder = papers.map((_, j) => j).sort((a, b) => paperDegree[b] - paperDegree[a] || a - b);\nconst authorPos = new Array(authors.length);\nauthorOrder.forEach((origIdx, pos) => {\n  authorPos[origIdx] = pos;\n});\nconst paperPos = new Array(papers.length);\npaperOrder.forEach((origIdx, pos) => {\n  paperPos[origIdx] = pos;\n});\n\nconst authorY = (i) => 1 - (authorPos[i] + 0.5) / authors.length;\nconst paperY = (j) => 1 - (paperPos[j] + 0.5) / papers.length;\nconst radiusFor = (degree) => 9 + Math.min(degree, 8) * 2.3;\n\nfunction withAlpha(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Native Chart.js plugin: draws the bipartite edges behind the node\n// datasets, then the source/target labels on top — no external package.\nconst bipartiteLayout = {\n  id: \"bipartiteLayout\",\n  beforeDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    ctx.save();\n    edges.forEach(({ authorIdx, paperIdx, weight }) => {\n      ctx.beginPath();\n      ctx.moveTo(scales.x.getPixelForValue(0), scales.y.getPixelForValue(authorY(authorIdx)));\n      ctx.lineTo(scales.x.getPixelForValue(1), scales.y.getPixelForValue(paperY(paperIdx)));\n      ctx.lineWidth = 1 + weight * 2.5;\n      ctx.strokeStyle = withAlpha(t.inkSoft, 0.15 + weight * 0.45);\n      ctx.stroke();\n    });\n    ctx.restore();\n  },\n  afterDatasetsDraw(chart) {\n    const { ctx, scales } = chart;\n    ctx.save();\n    ctx.font = \"17px sans-serif\";\n    ctx.fillStyle = t.inkSoft;\n    ctx.textBaseline = \"middle\";\n    ctx.textAlign = \"right\";\n    authors.forEach((name, i) => {\n      const gap = radiusFor(authorDegree[i]) + 8;\n      ctx.fillText(name, scales.x.getPixelForValue(0) - gap, scales.y.getPixelForValue(authorY(i)));\n    });\n    ctx.textAlign = \"left\";\n    papers.forEach((name, j) => {\n      const gap = radiusFor(paperDegree[j]) + 8;\n      ctx.fillText(name, scales.x.getPixelForValue(1) + gap, scales.y.getPixelForValue(paperY(j)));\n    });\n    ctx.restore();\n  },\n};\n\n// --- Chart -------------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"scatter\",\n  data: {\n    datasets: [\n      {\n        label: \"Authors\",\n        data: authors.map((name, i) => ({ x: 0, y: authorY(i), name, degree: authorDegree[i] })),\n        backgroundColor: t.palette[0],\n        borderColor: t.pageBg,\n        borderWidth: 2,\n        pointRadius: (ctx) => radiusFor(ctx.raw.degree),\n        pointHoverRadius: (ctx) => radiusFor(ctx.raw.degree) + 3,\n      },\n      {\n        label: \"Papers\",\n        data: papers.map((name, j) => ({ x: 1, y: paperY(j), name, degree: paperDegree[j] })),\n        backgroundColor: t.palette[1],\n        borderColor: t.pageBg,\n        borderWidth: 2,\n        pointRadius: (ctx) => radiusFor(ctx.raw.degree),\n        pointHoverRadius: (ctx) => radiusFor(ctx.raw.degree) + 3,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    layout: { padding: { top: 10, bottom: 10, left: 10, right: 10 } },\n    plugins: {\n      title: {\n        display: true,\n        text: \"network-bipartite · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n      },\n      legend: {\n        position: \"top\",\n        labels: { color: t.ink, font: { size: 16 }, usePointStyle: true },\n      },\n      tooltip: {\n        callbacks: {\n          label: (ctx) => `${ctx.raw.name} (degree ${ctx.raw.degree})`,\n        },\n      },\n    },\n    scales: {\n      x: { display: false, min: -0.45, max: 1.45 },\n      y: { display: false, min: -0.05, max: 1.05 },\n    },\n  },\n  plugins: [bipartiteLayout],\n});\n"}