{"spec_id":"network-bipartite","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// network-bipartite: Bipartite Network Graph\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-05\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Deterministic tiny LCG (no seeded RNG in the browser) ------------------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\nfunction randInt(min, max) {\n  return Math.floor(rand() * (max - min + 1)) + min;\n}\n\n// --- Data: user -> product purchase network ---------------------------------\nconst users = [\n  \"Ortiz, M.\",\n  \"Bennett, R.\",\n  \"Kaur, S.\",\n  \"Fischer, T.\",\n  \"Diallo, A.\",\n  \"Nakamura, Y.\",\n  \"Reyes, C.\",\n  \"Novak, P.\",\n  \"Haddad, L.\",\n  \"Sørensen, E.\",\n  \"Okafor, N.\",\n  \"Petrova, I.\",\n];\nconst products = [\n  \"Wireless Mouse\",\n  \"USB-C Hub\",\n  \"Mech. Keyboard\",\n  \"Webcam\",\n  \"Desk Lamp\",\n  \"Monitor Stand\",\n  \"Headset\",\n  \"Laptop Sleeve\",\n  \"Portable SSD\",\n  \"Cable Organizer\",\n];\n\nconst edges = [];\nusers.forEach((u) => {\n  const n = randInt(2, 5);\n  const picked = new Set();\n  while (picked.size < n) picked.add(randInt(0, products.length - 1));\n  picked.forEach((pi) => {\n    edges.push({ source: u, target: products[pi], weight: randInt(1, 5) });\n  });\n});\n\n// --- Node degree + two-column layout ----------------------------------------\nconst degreeA = Object.fromEntries(users.map((u) => [u, 0]));\nconst degreeB = Object.fromEntries(products.map((p) => [p, 0]));\nedges.forEach((e) => {\n  degreeA[e.source] += 1;\n  degreeB[e.target] += 1;\n});\n\nfunction layout(names, x) {\n  const n = names.length;\n  return names.map((name, i) => ({\n    name,\n    x,\n    y: n === 1 ? 0.5 : 1 - i / (n - 1),\n  }));\n}\nconst nodesA = layout(users, 0.06);\nconst nodesB = layout(products, 0.94);\nconst posOf = {};\nnodesA.forEach((d) => (posOf[d.name] = d));\nnodesB.forEach((d) => (posOf[d.name] = d));\n\nconst maxDegA = Math.max(...Object.values(degreeA));\nconst maxDegB = Math.max(...Object.values(degreeB));\nfunction nodeRadius(deg, maxDeg) {\n  return 7 + (deg / maxDeg) * 12;\n}\n\n// --- Hub node: highest-degree node across both sets, highlighted below ------\nlet hubNode = null;\nlet hubDegree = -1;\nnodesA.forEach((d) => {\n  if (degreeA[d.name] > hubDegree) {\n    hubDegree = degreeA[d.name];\n    hubNode = { ...d, isUser: true };\n  }\n});\nnodesB.forEach((d) => {\n  if (degreeB[d.name] > hubDegree) {\n    hubDegree = degreeB[d.name];\n    hubNode = { ...d, isUser: false };\n  }\n});\nconst hubColor = hubNode.isUser ? t.palette[0] : t.palette[1];\nconst hubRadius = hubNode.isUser\n  ? nodeRadius(degreeA[hubNode.name], maxDegA)\n  : nodeRadius(degreeB[hubNode.name], maxDegB);\n\n// --- Edge series: one gently bowed spline per edge (drawn beneath nodes) ----\n// A slight perpendicular bend (alternating by index) separates edges that\n// would otherwise stack into straight overlapping segments in the crossing\n// zone, and doubles as a distinctive Highcharts touch beyond a plain line.\nconst edgeSeries = edges.map((e, i) => {\n  const s = posOf[e.source];\n  const dst = posOf[e.target];\n  const mx = (s.x + dst.x) / 2;\n  const my = (s.y + dst.y) / 2;\n  const dx = dst.x - s.x;\n  const dy = dst.y - s.y;\n  const len = Math.hypot(dx, dy) || 1;\n  const bend = (0.03 * (((i % 3) - 1) || 1)) / (0.6 + e.weight / 5);\n  const cx = mx + (-dy / len) * bend;\n  const cy = my + (dx / len) * bend;\n  return {\n    type: \"spline\",\n    data: [\n      [s.x, s.y],\n      [cx, cy],\n      [dst.x, dst.y],\n    ],\n    color: t.inkSoft,\n    opacity: 0.08 + (e.weight / 5) * 0.3,\n    lineWidth: 0.8 + (e.weight / 5) * 1.8,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n    showInLegend: false,\n  };\n});\n\n// --- Node series: scatter, color = set membership, size = degree -----------\nconst userSeries = {\n  type: \"scatter\",\n  name: \"Users\",\n  color: t.palette[0],\n  data: nodesA.map((d) => ({\n    x: d.x,\n    y: d.y,\n    name: d.name,\n    marker: { radius: nodeRadius(degreeA[d.name], maxDegA) },\n  })),\n  marker: { lineColor: t.pageBg, lineWidth: 1.5 },\n  dataLabels: {\n    enabled: true,\n    format: \"{point.name}\",\n    align: \"right\",\n    x: -14,\n    style: { color: t.inkSoft, fontSize: \"13px\", textOutline: \"none\" },\n  },\n};\n\nconst productSeries = {\n  type: \"scatter\",\n  name: \"Products\",\n  color: t.palette[1],\n  data: nodesB.map((d) => ({\n    x: d.x,\n    y: d.y,\n    name: d.name,\n    marker: { radius: nodeRadius(degreeB[d.name], maxDegB) },\n  })),\n  marker: { lineColor: t.pageBg, lineWidth: 1.5 },\n  dataLabels: {\n    enabled: true,\n    format: \"{point.name}\",\n    align: \"left\",\n    x: 14,\n    style: { color: t.inkSoft, fontSize: \"13px\", textOutline: \"none\" },\n  },\n};\n\n// --- Chart -------------------------------------------------------------------\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      // Distinctive Highcharts touch: a renderer-drawn double halo gives the\n      // busiest hub a clear focal point instead of relying on size alone.\n      load() {\n        const chart = this;\n        const px = chart.xAxis[0].toPixels(hubNode.x);\n        const py = chart.yAxis[0].toPixels(hubNode.y);\n\n        [hubRadius + 6, hubRadius + 12].forEach((r, i) => {\n          chart.renderer\n            .circle(px, py, r)\n            .attr({\n              fill: \"none\",\n              stroke: hubColor,\n              \"stroke-width\": 1.5,\n              opacity: i === 0 ? 0.55 : 0.25,\n              zIndex: 6,\n            })\n            .add();\n        });\n      },\n    },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"network-bipartite · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"User–product purchase network · node size encodes degree, edge weight encodes purchase frequency\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: { visible: false, min: -0.32, max: 1.32 },\n  yAxis: { visible: false, min: -0.06, max: 1.06, title: { text: null } },\n  legend: {\n    enabled: true,\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: { enabled: false },\n  plotOptions: {\n    series: { animation: false, states: { hover: { enabled: false } } },\n  },\n  series: [...edgeSeries, userSeries, productSeries],\n});\n"}