{"spec_id":"network-force-directed","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// network-force-directed: Force-Directed Graph\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-08-24\n\n//# anyplot-orientation: square\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: a software module dependency graph (in-memory, deterministic) ---\nconst nodeIds = [\n  \"core-utils\", \"config\", \"logger\",\n  \"db-client\", \"cache\", \"query-builder\", \"migration-runner\",\n  \"api-gateway\", \"auth-service\", \"rate-limiter\", \"validation\",\n  \"user-service\", \"billing-service\", \"notification-service\",\n  \"order-service\", \"inventory-service\", \"payment-processor\",\n  \"ui-components\", \"state-store\", \"router\", \"form-validator\", \"theme-engine\",\n  \"metrics-collector\", \"health-check\",\n];\n\nconst edgePairs = [\n  [\"api-gateway\", \"auth-service\"], [\"api-gateway\", \"rate-limiter\"],\n  [\"api-gateway\", \"validation\"], [\"api-gateway\", \"core-utils\"],\n  [\"auth-service\", \"db-client\"], [\"auth-service\", \"cache\"],\n  [\"auth-service\", \"core-utils\"], [\"auth-service\", \"logger\"],\n  [\"rate-limiter\", \"cache\"], [\"rate-limiter\", \"config\"],\n  [\"validation\", \"core-utils\"],\n  [\"user-service\", \"db-client\"], [\"user-service\", \"cache\"],\n  [\"user-service\", \"validation\"], [\"user-service\", \"logger\"],\n  [\"user-service\", \"core-utils\"],\n  [\"billing-service\", \"db-client\"], [\"billing-service\", \"payment-processor\"],\n  [\"billing-service\", \"notification-service\"], [\"billing-service\", \"logger\"],\n  [\"billing-service\", \"core-utils\"],\n  [\"payment-processor\", \"db-client\"], [\"payment-processor\", \"logger\"],\n  [\"payment-processor\", \"config\"],\n  [\"order-service\", \"db-client\"], [\"order-service\", \"inventory-service\"],\n  [\"order-service\", \"notification-service\"], [\"order-service\", \"user-service\"],\n  [\"order-service\", \"logger\"],\n  [\"inventory-service\", \"db-client\"], [\"inventory-service\", \"cache\"],\n  [\"inventory-service\", \"logger\"],\n  [\"notification-service\", \"config\"], [\"notification-service\", \"logger\"],\n  [\"db-client\", \"query-builder\"], [\"db-client\", \"migration-runner\"],\n  [\"db-client\", \"config\"], [\"db-client\", \"logger\"],\n  [\"cache\", \"config\"], [\"cache\", \"logger\"],\n  [\"query-builder\", \"core-utils\"], [\"migration-runner\", \"logger\"],\n  [\"ui-components\", \"theme-engine\"], [\"ui-components\", \"state-store\"],\n  [\"ui-components\", \"logger\"], [\"theme-engine\", \"config\"],\n  [\"router\", \"state-store\"], [\"router\", \"api-gateway\"],\n  [\"form-validator\", \"ui-components\"], [\"form-validator\", \"validation\"],\n  [\"state-store\", \"core-utils\"],\n  [\"metrics-collector\", \"logger\"], [\"metrics-collector\", \"config\"],\n  [\"health-check\", \"logger\"], [\"health-check\", \"db-client\"],\n  [\"health-check\", \"cache\"],\n];\n\n// --- Force-directed layout (Fruchterman-Reingold), computed in-memory ------\n// Highcharts core has no networkgraph module, so the physics simulation runs\n// here and the result is plotted as plain scatter points + line segments.\nlet lcgState = 42;\nconst nextRandom = () => {\n  lcgState = (lcgState * 1103515245 + 12345) & 0x7fffffff;\n  return lcgState / 0x7fffffff;\n};\n\nconst nodeCount = nodeIds.length;\nconst indexById = new Map(nodeIds.map((id, i) => [id, i]));\nconst edges = edgePairs.map(([a, b]) => [indexById.get(a), indexById.get(b)]);\nconst degree = new Array(nodeCount).fill(0);\nedges.forEach(([a, b]) => { degree[a] += 1; degree[b] += 1; });\n\nconst FIELD = 100;\nconst positions = nodeIds.map(() => ({\n  x: nextRandom() * FIELD - FIELD / 2,\n  y: nextRandom() * FIELD - FIELD / 2,\n}));\n\nconst k = 0.75 * Math.sqrt((FIELD * FIELD) / nodeCount);\nconst gravity = 0.09; // pull-to-center so loosely-connected nodes don't drift too far\nconst bound = FIELD * 0.38; // hard wall — keeps a single weak leaf from stretching the frame\nconst iterations = 500;\nlet temperature = FIELD / 10;\n\nfor (let iter = 0; iter < iterations; iter += 1) {\n  const disp = positions.map(() => ({ x: 0, y: 0 }));\n\n  // Repulsive force between every pair of nodes.\n  for (let i = 0; i < nodeCount; i += 1) {\n    for (let j = i + 1; j < nodeCount; j += 1) {\n      let dx = positions[i].x - positions[j].x;\n      let dy = positions[i].y - positions[j].y;\n      let dist = Math.sqrt(dx * dx + dy * dy) || 0.01;\n      const force = (k * k) / dist;\n      dx = (dx / dist) * force;\n      dy = (dy / dist) * force;\n      disp[i].x += dx; disp[i].y += dy;\n      disp[j].x -= dx; disp[j].y -= dy;\n    }\n  }\n\n  // Attractive force along each edge.\n  edges.forEach(([a, b]) => {\n    let dx = positions[a].x - positions[b].x;\n    let dy = positions[a].y - positions[b].y;\n    let dist = Math.sqrt(dx * dx + dy * dy) || 0.01;\n    const force = (dist * dist) / k;\n    dx = (dx / dist) * force;\n    dy = (dy / dist) * force;\n    disp[a].x -= dx; disp[a].y -= dy;\n    disp[b].x += dx; disp[b].y += dy;\n  });\n\n  // Weak centering force keeps loosely-connected branches from drifting away.\n  for (let i = 0; i < nodeCount; i += 1) {\n    disp[i].x -= positions[i].x * gravity;\n    disp[i].y -= positions[i].y * gravity;\n  }\n\n  // Apply capped displacement, cooling the temperature over time, then clamp\n  // to a hard boundary so no single weakly-attached node can stretch the frame.\n  for (let i = 0; i < nodeCount; i += 1) {\n    const dist = Math.sqrt(disp[i].x ** 2 + disp[i].y ** 2) || 0.01;\n    const capped = Math.min(dist, temperature);\n    positions[i].x = Math.max(-bound, Math.min(bound, positions[i].x + (disp[i].x / dist) * capped));\n    positions[i].y = Math.max(-bound, Math.min(bound, positions[i].y + (disp[i].y / dist) * capped));\n  }\n  temperature *= 0.99;\n}\n\n// Center on the bounding-box midpoint (robust to a single peripheral node —\n// centering on the mean would let one outlier skew the whole frame), then\n// frame both axes to the actual footprint (equal range keeps proportions).\nconst xs = positions.map((p) => p.x);\nconst ys = positions.map((p) => p.y);\nconst centerX = (Math.min(...xs) + Math.max(...xs)) / 2;\nconst centerY = (Math.min(...ys) + Math.max(...ys)) / 2;\npositions.forEach((p) => { p.x -= centerX; p.y -= centerY; });\n\nconst extent = Math.max(...positions.map((p) => Math.max(Math.abs(p.x), Math.abs(p.y)))) * 1.08;\n\nconst maxDegree = Math.max(...degree);\nconst hubThreshold = 6;\n\n// --- Chart -------------------------------------------------------------\nconst edgeSeriesData = [];\nedges.forEach(([a, b]) => {\n  edgeSeriesData.push({ x: positions[a].x, y: positions[a].y });\n  edgeSeriesData.push({ x: positions[b].x, y: positions[b].y });\n  edgeSeriesData.push(null);\n});\n\n// Radial gradient fill (light-to-brand-green) gives every node a subtle 3D\n// lift; hub nodes additionally get a heavier, ink-colored stroke so degree\n// reads through both size AND a second visual channel.\nconst nodeFill = {\n  radialGradient: { cx: 0.35, cy: 0.35, r: 0.75 },\n  stops: [\n    [0, Highcharts.color(t.palette[0]).brighten(0.3).get()],\n    [1, t.palette[0]],\n  ],\n};\n\nconst nodeSeriesData = nodeIds.map((id, i) => {\n  const isHub = degree[i] >= hubThreshold;\n  return {\n    x: positions[i].x,\n    y: positions[i].y,\n    name: id,\n    degree: degree[i],\n    marker: {\n      symbol: \"circle\",\n      radius: 9 + (degree[i] / maxDegree) * 12,\n      fillColor: nodeFill,\n      lineColor: isHub ? t.ink : t.pageBg,\n      lineWidth: isHub ? 2.5 : 1,\n    },\n    dataLabels: { enabled: isHub },\n  };\n});\n\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\", backgroundColor: \"transparent\", animation: false,\n    style: { fontFamily: \"inherit\" },\n  },\n  credits: { enabled: false },\n  title: {\n    text: \"network-force-directed · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Software module dependency graph — node size scales with degree\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: { visible: false, min: -extent, max: extent },\n  yAxis: { visible: false, min: -extent, max: extent, title: { text: null } },\n  legend: { enabled: false },\n  tooltip: {\n    formatter() {\n      return this.point.name\n        ? `<b>${this.point.name}</b><br/>connections: ${this.point.degree}`\n        : false;\n    },\n  },\n  plotOptions: { series: { animation: false } },\n  series: [\n    {\n      name: \"Dependencies\",\n      type: \"line\",\n      data: edgeSeriesData,\n      color: t.grid,\n      lineWidth: 1.5,\n      marker: { enabled: false },\n      enableMouseTracking: false,\n      connectNulls: false,\n    },\n    {\n      name: \"Modules\",\n      type: \"scatter\",\n      data: nodeSeriesData,\n      color: t.palette[0],\n      marker: { symbol: \"circle\", fillColor: nodeFill, lineColor: t.pageBg, lineWidth: 1 },\n      dataLabels: {\n        formatter() { return this.point.name; },\n        style: { color: t.ink, fontSize: \"13px\", fontWeight: \"500\", textOutline: \"none\" },\n        y: -12,\n        allowOverlap: false,\n      },\n    },\n  ],\n});\n"}