{"spec_id":"network-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// network-basic: Basic Network Graph\n// Library: highcharts 12.6.0 | JavaScript 22.23.1\n// Quality: 90/100 | Created: 2026-07-24\n//# anyplot-orientation: square\n\n// Only the core Highcharts bundle is loaded (no `networkgraph` module), so node\n// positions are computed here with a Fruchterman-Reingold force-directed layout\n// and drawn as a `line` series (edges) plus per-community `scatter` series\n// (nodes) — the same approach anyone would take without the add-on module.\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: a small friendship network across three communities -------------\nconst NODES = [\n  { id: \"Ava\", group: \"Work\" },\n  { id: \"Liam\", group: \"Work\" },\n  { id: \"Mia\", group: \"Work\" },\n  { id: \"Noah\", group: \"Work\" },\n  { id: \"Ivy\", group: \"Work\" },\n  { id: \"Ethan\", group: \"Work\" },\n  { id: \"Zoe\", group: \"Work\" },\n  { id: \"Leo\", group: \"College\" },\n  { id: \"Nora\", group: \"College\" },\n  { id: \"Kai\", group: \"College\" },\n  { id: \"Luna\", group: \"College\" },\n  { id: \"Finn\", group: \"College\" },\n  { id: \"Maya\", group: \"College\" },\n  { id: \"Owen\", group: \"College\" },\n  { id: \"Ruby\", group: \"Family\" },\n  { id: \"Theo\", group: \"Family\" },\n  { id: \"Sara\", group: \"Family\" },\n  { id: \"Milo\", group: \"Family\" },\n  { id: \"Ella\", group: \"Family\" },\n  { id: \"Jack\", group: \"Family\" },\n];\n\n// Third element is tie strength (1-5) — bridge edges between communities are\n// weak acquaintance ties, within-community edges skew toward closer bonds.\nconst EDGES = [\n  [\"Ava\", \"Liam\", 4], [\"Ava\", \"Mia\", 5], [\"Ava\", \"Noah\", 3], [\"Liam\", \"Mia\", 4], [\"Liam\", \"Ivy\", 2],\n  [\"Mia\", \"Noah\", 3], [\"Noah\", \"Ethan\", 3], [\"Ivy\", \"Zoe\", 4], [\"Ethan\", \"Zoe\", 2], [\"Ava\", \"Ethan\", 3],\n  [\"Leo\", \"Nora\", 3], [\"Leo\", \"Kai\", 5], [\"Nora\", \"Luna\", 2], [\"Kai\", \"Luna\", 4], [\"Kai\", \"Finn\", 3],\n  [\"Luna\", \"Maya\", 3], [\"Finn\", \"Owen\", 2], [\"Maya\", \"Owen\", 4], [\"Leo\", \"Maya\", 3],\n  [\"Ruby\", \"Theo\", 5], [\"Ruby\", \"Sara\", 4], [\"Theo\", \"Milo\", 3], [\"Sara\", \"Ella\", 4], [\"Milo\", \"Jack\", 3],\n  [\"Ella\", \"Jack\", 5], [\"Ruby\", \"Jack\", 4],\n  [\"Ava\", \"Leo\", 1], [\"Mia\", \"Ruby\", 2], [\"Kai\", \"Theo\", 1],\n];\n\n// Degree (connection count) drives node size.\nconst degree = {};\nNODES.forEach((node) => { degree[node.id] = 0; });\nEDGES.forEach(([a, b]) => { degree[a] += 1; degree[b] += 1; });\nconst maxDegree = Math.max(...Object.values(degree));\n\n// --- Force-directed layout (Fruchterman-Reingold, fixed-seed LCG) ----------\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst AREA = 100;\nconst idealDistance = Math.sqrt((AREA * AREA) / NODES.length);\nconst pos = {};\nNODES.forEach((node, i) => {\n  const angle = (i / NODES.length) * 2 * Math.PI;\n  pos[node.id] = {\n    x: 40 * Math.cos(angle) + (rand() - 0.5) * 6,\n    y: 40 * Math.sin(angle) + (rand() - 0.5) * 6,\n  };\n});\n\nlet temperature = AREA / 10;\nfor (let iter = 0; iter < 300; iter += 1) {\n  const disp = {};\n  NODES.forEach((node) => { disp[node.id] = { x: 0, y: 0 }; });\n\n  for (let i = 0; i < NODES.length; i += 1) {\n    for (let j = i + 1; j < NODES.length; j += 1) {\n      const a = NODES[i].id;\n      const b = NODES[j].id;\n      const dx = pos[a].x - pos[b].x;\n      const dy = pos[a].y - pos[b].y;\n      const dist = Math.max(Math.sqrt(dx * dx + dy * dy), 0.01);\n      const force = (idealDistance * idealDistance) / dist;\n      disp[a].x += (dx / dist) * force;\n      disp[a].y += (dy / dist) * force;\n      disp[b].x -= (dx / dist) * force;\n      disp[b].y -= (dy / dist) * force;\n    }\n  }\n\n  EDGES.forEach(([a, b]) => {\n    const dx = pos[a].x - pos[b].x;\n    const dy = pos[a].y - pos[b].y;\n    const dist = Math.max(Math.sqrt(dx * dx + dy * dy), 0.01);\n    const force = (dist * dist) / idealDistance;\n    disp[a].x -= (dx / dist) * force;\n    disp[a].y -= (dy / dist) * force;\n    disp[b].x += (dx / dist) * force;\n    disp[b].y += (dy / dist) * force;\n  });\n\n  NODES.forEach((node) => {\n    const dx = disp[node.id].x;\n    const dy = disp[node.id].y;\n    const dist = Math.max(Math.sqrt(dx * dx + dy * dy), 0.01);\n    const capped = Math.min(dist, temperature);\n    pos[node.id].x += (dx / dist) * capped;\n    pos[node.id].y += (dy / dist) * capped;\n  });\n  temperature *= 0.97;\n}\n\n// Normalize the free-floating layout into a fixed, centered frame — the\n// repulsion/attraction forces above have no boundary, so raw coordinates can\n// land anywhere; rescale to a known extent before drawing.\nlet minX = Infinity;\nlet maxX = -Infinity;\nlet minY = Infinity;\nlet maxY = -Infinity;\nNODES.forEach((node) => {\n  minX = Math.min(minX, pos[node.id].x);\n  maxX = Math.max(maxX, pos[node.id].x);\n  minY = Math.min(minY, pos[node.id].y);\n  maxY = Math.max(maxY, pos[node.id].y);\n});\nconst centerX = (minX + maxX) / 2;\nconst centerY = (minY + maxY) / 2;\nconst halfExtent = Math.max(maxX - minX, maxY - minY) / 2;\nNODES.forEach((node) => {\n  pos[node.id].x = ((pos[node.id].x - centerX) / halfExtent) * 50;\n  pos[node.id].y = ((pos[node.id].y - centerY) / halfExtent) * 50;\n});\n\n// --- Chart -------------------------------------------------------------------\nconst GROUPS = [\"Work\", \"College\", \"Family\"];\nconst GROUP_COLOR = { Work: t.palette[0], College: t.palette[1], Family: t.palette[2] };\n// Deliberate per-group marker shapes (not left to Highcharts' default symbol\n// cycling) — a second, colorblind-safe channel redundant with community color.\nconst GROUP_SYMBOL = { Work: \"diamond\", College: \"square\", Family: \"triangle\" };\n// Hub threshold: nodes in the top degree band get bolder, larger labels — a\n// per-point dataLabels override, a distinctly Highcharts (not library-agnostic)\n// way to spotlight structurally important nodes without a second series.\nconst HUB_DEGREE = maxDegree - 1;\n\n// Node radius uses a squared term on degree so hub nodes create a clear focal\n// point instead of blending into the leaf nodes.\nfunction nodeRadius(id) {\n  return 6 + degree[id] ** 2;\n}\n\n// Tie-strength tiers (weak/medium/strong) become three separate edge series so\n// lineWidth + opacity can vary by weight — a plain single \"line\" series can't\n// vary per-segment, so splitting by tier is the idiomatic Highcharts route.\nconst EDGE_TIERS = [\n  { key: \"weak\", test: (w) => w <= 2, lineWidth: 1, alpha: 0.18 },\n  { key: \"medium\", test: (w) => w === 3, lineWidth: 1.75, alpha: 0.32 },\n  { key: \"strong\", test: (w) => w >= 4, lineWidth: 2.75, alpha: 0.5 },\n];\n\nconst edgeSeries = EDGE_TIERS.map((tier) => {\n  const data = [];\n  EDGES.filter(([, , w]) => tier.test(w)).forEach(([a, b]) => {\n    data.push([pos[a].x, pos[a].y]);\n    data.push([pos[b].x, pos[b].y]);\n    data.push(null);\n  });\n  return {\n    type: \"line\",\n    name: `Connections (${tier.key})`,\n    data,\n    color: t.grid.replace(/[\\d.]+\\)$/, `${tier.alpha})`),\n    lineWidth: tier.lineWidth,\n    marker: { enabled: false },\n    enableMouseTracking: false,\n    showInLegend: false,\n    zIndex: 0,\n  };\n});\n\nconst nodeSeries = GROUPS.map((group) => ({\n  type: \"scatter\",\n  name: group,\n  color: GROUP_COLOR[group],\n  marker: { symbol: GROUP_SYMBOL[group], lineColor: t.pageBg, lineWidth: 1 },\n  data: NODES.filter((node) => node.group === group).map((node) => {\n    const radius = nodeRadius(node.id);\n    const isHub = degree[node.id] >= HUB_DEGREE;\n    return {\n      x: pos[node.id].x,\n      y: pos[node.id].y,\n      name: node.id,\n      marker: { radius },\n      dataLabels: {\n        y: -(radius + 6),\n        style: {\n          fontSize: isHub ? \"15px\" : \"13px\",\n          fontWeight: isHub ? \"700\" : \"normal\",\n        },\n      },\n    };\n  }),\n  dataLabels: {\n    enabled: true,\n    format: \"{point.name}\",\n    allowOverlap: false,\n    style: { color: t.ink, fontWeight: \"normal\", textOutline: \"none\" },\n  },\n}));\n\nHighcharts.chart(\"container\", {\n  chart: {\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  title: {\n    text: \"network-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Friendship network across three communities — node size = number of connections\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: { visible: false, min: -58, max: 58 },\n  yAxis: { visible: false, min: -58, max: 58, 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 },\n    scatter: { states: { hover: { enabled: false } } },\n  },\n  series: [...edgeSeries, ...nodeSeries],\n});\n"}