{"spec_id":"network-directed","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// network-directed: Directed Network Graph\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data: a software module import graph, laid out in dependency tiers ----\n// Arrows point from an importing module to the module it imports. Node radius\n// encodes in-degree (how many other modules import it), so the shared\n// foundation modules read as visual hubs.\nconst TIERS = [\n  {\n    name: \"Entry points\",\n    color: t.palette[0],\n    nodes: [\n      { id: \"cli\", name: \"cli\", y: 1, indeg: 0 },\n      { id: \"server\", name: \"server\", y: 2, indeg: 0 },\n      { id: \"worker\", name: \"worker\", y: 3, indeg: 0 },\n    ],\n  },\n  {\n    name: \"Orchestration\",\n    color: t.palette[1],\n    nodes: [\n      { id: \"router\", name: \"router\", y: 1, indeg: 2 },\n      { id: \"scheduler\", name: \"scheduler\", y: 2, indeg: 1 },\n      { id: \"api_gateway\", name: \"api_gateway\", y: 3, indeg: 1 },\n    ],\n  },\n  {\n    name: \"Services\",\n    color: t.palette[2],\n    nodes: [\n      { id: \"auth\", name: \"auth\", y: 0.2, indeg: 3 },\n      { id: \"database\", name: \"database\", y: 1.4, indeg: 3 },\n      { id: \"cache\", name: \"cache\", y: 2.6, indeg: 2 },\n      { id: \"queue\", name: \"queue\", y: 3.8, indeg: 2 },\n    ],\n  },\n  {\n    name: \"Foundation\",\n    color: t.palette[3],\n    nodes: [\n      { id: \"logger\", name: \"logger\", y: 1, indeg: 7 },\n      { id: \"config\", name: \"config\", y: 2, indeg: 3 },\n      { id: \"utils\", name: \"utils\", y: 3, indeg: 3 },\n    ],\n  },\n];\n\n// Third element is the call-frequency weight (spec's optional `weight`\n// attribute), rendered as edge line thickness.\nconst EDGES = [\n  [\"cli\", \"router\", 3],\n  [\"cli\", \"config\", 2],\n  [\"server\", \"router\", 3],\n  [\"server\", \"auth\", 4],\n  [\"server\", \"api_gateway\", 3],\n  [\"worker\", \"scheduler\", 3],\n  [\"worker\", \"queue\", 4],\n  [\"worker\", \"config\", 2],\n  [\"router\", \"auth\", 4],\n  [\"router\", \"database\", 3],\n  [\"router\", \"logger\", 5],\n  [\"scheduler\", \"queue\", 3],\n  [\"scheduler\", \"database\", 3],\n  [\"scheduler\", \"logger\", 5],\n  [\"api_gateway\", \"auth\", 4],\n  [\"api_gateway\", \"cache\", 3],\n  [\"api_gateway\", \"logger\", 5],\n  [\"auth\", \"database\", 4],\n  [\"auth\", \"cache\", 3],\n  [\"auth\", \"logger\", 5],\n  [\"database\", \"logger\", 5],\n  [\"database\", \"config\", 2],\n  [\"cache\", \"logger\", 4],\n  [\"queue\", \"logger\", 4],\n  [\"queue\", \"utils\", 3],\n  [\"logger\", \"utils\", 2],\n  [\"config\", \"utils\", 2],\n];\n\nconst nodeRadius = (indeg) => 14 + indeg * 3;\n\n// --- Chart -------------------------------------------------------------\nconst series = TIERS.map((tier, i) => ({\n  name: tier.name,\n  color: tier.color,\n  data: tier.nodes.map((n) => ({\n    id: n.id,\n    x: i * 3.6,\n    y: n.y,\n    name: n.name,\n    marker: { radius: nodeRadius(n.indeg), lineColor: t.pageBg, lineWidth: 2 },\n    dataLabels: { format: n.name, y: -(nodeRadius(n.indeg) + 10) },\n  })),\n}));\n\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"scatter\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: \"inherit\" },\n    events: {\n      load() {\n        drawEdges(this);\n      },\n    },\n  },\n  credits: { enabled: false },\n  title: {\n    text: \"network-directed · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  subtitle: {\n    text: \"Module import graph — arrows point from importer to dependency\",\n    style: { color: t.inkSoft, fontSize: \"14px\" },\n  },\n  xAxis: {\n    min: -1.5,\n    max: 12.3,\n    lineWidth: 0,\n    tickLength: 0,\n    gridLineWidth: 0,\n    labels: { enabled: false },\n    title: { text: null },\n  },\n  yAxis: {\n    min: -0.3,\n    max: 4.3,\n    lineWidth: 0,\n    tickLength: 0,\n    gridLineWidth: 0,\n    labels: { enabled: false },\n    title: { text: null },\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: { enabled: false },\n  plotOptions: {\n    series: {\n      animation: false,\n      dataLabels: {\n        enabled: true,\n        style: {\n          color: t.ink,\n          fontSize: \"14px\",\n          fontWeight: \"normal\",\n          textOutline: \"none\",\n        },\n      },\n    },\n  },\n  series,\n});\n\n// Edges are drawn once with the core SVGRenderer, behind the node markers —\n// the core `highcharts` bundle has no networkgraph module (add-on only), so\n// node-link edges with arrowheads are composed by hand from point coordinates.\n// Deterministic per-edge signed offset (source/target ids -> stable hash) used\n// both to bow the edge slightly and to fan out where it lands on a crowded\n// target's perimeter, so parallel/converging paths stay distinguishable.\nfunction edgeHash(fromId, toId) {\n  const key = `${fromId}>${toId}`;\n  let h = 0;\n  for (let i = 0; i < key.length; i++) h = (h * 31 + key.charCodeAt(i)) | 0;\n  return h;\n}\n\nfunction drawEdges(c) {\n  const group = c.renderer.g(\"edges\").attr({ zIndex: 1 }).add();\n  const strokeColor = t.inkSoft;\n\n  const incomingTotal = {};\n  EDGES.forEach(([, toId]) => {\n    incomingTotal[toId] = (incomingTotal[toId] || 0) + 1;\n  });\n  const incomingSeen = {};\n\n  EDGES.forEach(([fromId, toId, weight]) => {\n    const from = c.get(fromId);\n    const to = c.get(toId);\n    if (!from || !to) return;\n\n    const sx = c.plotLeft + from.plotX;\n    const sy = c.plotTop + from.plotY;\n    const tx = c.plotLeft + to.plotX;\n    const ty = c.plotTop + to.plotY;\n\n    const dx = tx - sx;\n    const dy = ty - sy;\n    const dist = Math.sqrt(dx * dx + dy * dy) || 1;\n    const ux = dx / dist;\n    const uy = dy / dist;\n\n    // Fan incoming arrows around the target node's perimeter instead of all\n    // landing on the same point (VQ-03: converging edges were hard to tell\n    // apart at hub nodes like \"logger\" / \"database\").\n    const total = incomingTotal[toId];\n    const seen = incomingSeen[toId] || 0;\n    incomingSeen[toId] = seen + 1;\n    const spread = Math.min(0.7, (total - 1) * 0.16);\n    const fanOffset = total > 1 ? -spread / 2 + (spread / (total - 1)) * seen : 0;\n    const baseAngleAtTarget = Math.atan2(sy - ty, sx - tx) + fanOffset;\n\n    const rFrom = from.marker.radius + 2;\n    const rTo = to.marker.radius + 4;\n    const x1 = sx + ux * rFrom;\n    const y1 = sy + uy * rFrom;\n    const x2 = tx + Math.cos(baseAngleAtTarget) * rTo;\n    const y2 = ty + Math.sin(baseAngleAtTarget) * rTo;\n\n    // Slight deterministic curvature so overlapping straight paths through\n    // the dense middle tiers stay traceable (DE-03), echoing the spec's own\n    // \"consider curved edges\" guidance.\n    const curve = (Math.abs(edgeHash(fromId, toId)) % 14) - 7;\n    const midX = (x1 + x2) / 2 - uy * curve;\n    const midY = (y1 + y2) / 2 + ux * curve;\n\n    const lineWidth = 1 + (weight || 1) * 0.6;\n    drawArrow(c.renderer, group, x1, y1, midX, midY, x2, y2, strokeColor, lineWidth);\n  });\n\n  c.series.forEach((s) => s.group.toFront());\n  c.series.forEach((s) => s.dataLabelsGroup && s.dataLabelsGroup.toFront());\n}\n\nfunction drawArrow(renderer, group, x1, y1, cx, cy, x2, y2, color, lineWidth) {\n  // Tangent of the quadratic curve at its endpoint points from the control\n  // point to the endpoint — use it to orient the arrowhead correctly.\n  const angle = Math.atan2(y2 - cy, x2 - cx);\n  const headLen = 14;\n  const headAngle = Math.PI / 7;\n\n  const shaftEndX = x2 - Math.cos(angle) * headLen * 0.6;\n  const shaftEndY = y2 - Math.sin(angle) * headLen * 0.6;\n\n  renderer\n    .path([\"M\", x1, y1, \"Q\", cx, cy, shaftEndX, shaftEndY])\n    .attr({\n      stroke: color,\n      \"stroke-width\": lineWidth,\n      \"stroke-linecap\": \"round\",\n      fill: \"none\",\n      opacity: 0.7,\n    })\n    .add(group);\n\n  const p1x = x2 - Math.cos(angle - headAngle) * headLen;\n  const p1y = y2 - Math.sin(angle - headAngle) * headLen;\n  const p2x = x2 - Math.cos(angle + headAngle) * headLen;\n  const p2y = y2 - Math.sin(angle + headAngle) * headLen;\n\n  renderer\n    .path([\"M\", x2, y2, \"L\", p1x, p1y, \"L\", p2x, p2y, \"Z\"])\n    .attr({ fill: color, stroke: \"none\", opacity: 0.9 })\n    .add(group);\n}\n"}