{"spec_id":"network-directed","library":"d3","language":"javascript","code":"// anyplot.ai\n// network-directed: Directed Network Graph\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 130, right: 90, bottom: 45, left: 90 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\nconst radius = 26;\n\n// --- Data: a software package dependency graph (in-memory, deterministic) --\n// Four build tiers, left-to-right; arrows point from a package to the\n// package it imports (its dependency).\nconst tiers = [\n  { name: \"Entry points\", color: t.palette[0], ids: [\"app\", \"cli\", \"worker\"] },\n  { name: \"Services\", color: t.palette[1], ids: [\"api\", \"admin\", \"scheduler\"] },\n  { name: \"Middleware\", color: t.palette[2], ids: [\"auth\", \"cache\", \"queue\"] },\n  { name: \"Foundation\", color: t.palette[3], ids: [\"db\", \"config\", \"logging\", \"utils\"] },\n];\n\nconst rawEdges = [\n  [\"app\", \"api\"], [\"app\", \"admin\"],\n  [\"cli\", \"scheduler\"], [\"cli\", \"admin\"],\n  [\"worker\", \"scheduler\"], [\"worker\", \"queue\"],\n  [\"api\", \"auth\"], [\"api\", \"cache\"],\n  [\"admin\", \"auth\"], [\"admin\", \"db\"],\n  [\"scheduler\", \"queue\"], [\"scheduler\", \"db\"],\n  [\"auth\", \"db\"], [\"auth\", \"config\"],\n  [\"cache\", \"config\"], [\"cache\", \"logging\"],\n  [\"queue\", \"config\"], [\"queue\", \"utils\"],\n];\n\n// Weight each edge by its target's in-degree: packages many others depend on\n// (\"db\", \"config\") read as thicker, more prominent arrows — a data-driven\n// stand-in for the spec's optional edge-weight attribute.\nconst inDegree = d3.rollup(rawEdges, (v) => v.length, ([, target]) => target);\nconst weightScale = d3\n  .scaleLinear()\n  .domain([1, d3.max(inDegree.values())])\n  .range([2, 3.2])\n  .clamp(true);\nconst edges = rawEdges.map(([source, target]) => ({\n  source,\n  target,\n  weight: weightScale(inDegree.get(target)),\n}));\n\n// --- Layout: fixed layered columns; every tier's node row spans the same\n// fraction of the plot height regardless of node count, so all four columns\n// use the canvas evenly instead of the shortest tier leaving slack below it.\nconst yScale = d3.scaleLinear().domain([0, 1]).range([margin.top + ih * 0.08, margin.top + ih * 0.92]);\n\nconst nodeById = new Map();\ntiers.forEach((tier, ti) => {\n  const x = margin.left + ti * (iw / (tiers.length - 1));\n  tier.ids.forEach((id, i) => {\n    const frac = tier.ids.length > 1 ? i / (tier.ids.length - 1) : 0.5;\n    nodeById.set(id, { id, x, y: yScale(frac), tier: ti, color: tier.color });\n  });\n});\n\n// Trim a quadratic curve to the circle boundary at both ends so the\n// arrowhead lands right at the node edge instead of under the fill.\nfunction edgePath(source, target, bow) {\n  const mx = (source.x + target.x) / 2;\n  const my = (source.y + target.y) / 2 + bow;\n  const d1x = mx - source.x, d1y = my - source.y;\n  const len1 = Math.hypot(d1x, d1y) || 1;\n  const sx = source.x + (d1x / len1) * radius;\n  const sy = source.y + (d1y / len1) * radius;\n  const d2x = target.x - mx, d2y = target.y - my;\n  const len2 = Math.hypot(d2x, d2y) || 1;\n  const ex = target.x - (d2x / len2) * (radius + 2);\n  const ey = target.y - (d2y / len2) * (radius + 2);\n  return `M${sx},${sy} Q${mx},${my} ${ex},${ey}`;\n}\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\nsvg\n  .append(\"defs\")\n  .append(\"marker\")\n  .attr(\"id\", \"arrowhead\")\n  .attr(\"viewBox\", \"0 0 10 10\")\n  .attr(\"refX\", 9)\n  .attr(\"refY\", 5)\n  .attr(\"markerWidth\", 8)\n  .attr(\"markerHeight\", 8)\n  .attr(\"orient\", \"auto\")\n  .append(\"path\")\n  .attr(\"d\", \"M0,0 L10,5 L0,10 Z\")\n  .attr(\"fill\", t.inkSoft);\n\n// --- Edges (drawn first, so nodes sit on top) --------------------------------\nsvg\n  .selectAll(\"path.edge\")\n  .data(edges)\n  .join(\"path\")\n  .attr(\"class\", \"edge\")\n  .attr(\"d\", (d, i) => {\n    const source = nodeById.get(d.source);\n    const target = nodeById.get(d.target);\n    const skipsTier = target.tier - source.tier > 1;\n    const bow = skipsTier ? (i % 2 === 0 ? 60 : -60) : 0;\n    return edgePath(source, target, bow);\n  })\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-opacity\", 0.85)\n  .attr(\"stroke-width\", (d) => d.weight)\n  .attr(\"marker-end\", \"url(#arrowhead)\");\n\n// --- Nodes --------------------------------------------------------------------\nconst node = svg\n  .selectAll(\"g.node\")\n  .data([...nodeById.values()])\n  .join(\"g\")\n  .attr(\"class\", \"node\")\n  .attr(\"transform\", (d) => `translate(${d.x},${d.y})`);\n\nnode\n  .append(\"circle\")\n  .attr(\"r\", radius)\n  .attr(\"fill\", (d) => d.color)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 3);\n\nnode\n  .append(\"text\")\n  .attr(\"y\", radius + 22)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"13px\")\n  .style(\"font-weight\", 500)\n  .text((d) => d.id);\n\n// --- Tier headers (double as the color legend) -------------------------------\nsvg\n  .selectAll(\"text.tier\")\n  .data(tiers)\n  .join(\"text\")\n  .attr(\"class\", \"tier\")\n  .attr(\"x\", (d, i) => margin.left + i * (iw / (tiers.length - 1)))\n  .attr(\"y\", margin.top - 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", (d) => d.color)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", 700)\n  .style(\"letter-spacing\", \"0.5px\")\n  .text((d) => d.name.toUpperCase());\n\n// --- Title ---------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", 600)\n  .text(\"network-directed · javascript · d3 · anyplot.ai\");\n"}