{"spec_id":"network-directed","library":"makie","language":"julia","code":"# anyplot.ai\n# network-directed: Directed Network Graph\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 91/100 | Created: 2026-09-05\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# --- Theme tokens ------------------------------------------------------------\nTHEME      = get(ENV, \"ANYPLOT_THEME\", \"light\")\nPAGE_BG    = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nINK        = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nINK_SOFT   = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nEDGE_COLOR = RGBAf(INK_SOFT.r, INK_SOFT.g, INK_SOFT.b, 0.55)\nIMPRINT_PALETTE = [\n    colorant\"#009E73\", colorant\"#C475FD\", colorant\"#4467A3\", colorant\"#BD8233\",\n    colorant\"#AE3030\", colorant\"#2ABCCD\", colorant\"#954477\", colorant\"#99B314\",\n]\n\n# --- Data: a software package dependency graph --------------------------------\n# Arrows point from a consumer to what it depends on / imports, exactly the\n# \"import direction\" application called out in the specification.\nnodes = [\n    \"webapp\", \"cli\",\n    \"api-client\", \"auth\", \"renderer\",\n    \"http\", \"config\", \"crypto\", \"cache\", \"svg-utils\",\n    \"json\",\n    \"logging\",\n]\n\nedges = [\n    (\"webapp\", \"api-client\"), (\"webapp\", \"auth\"), (\"webapp\", \"renderer\"),\n    (\"cli\", \"api-client\"), (\"cli\", \"auth\"), (\"cli\", \"logging\"),\n    (\"api-client\", \"http\"), (\"api-client\", \"config\"),\n    (\"auth\", \"crypto\"), (\"auth\", \"config\"), (\"auth\", \"cache\"),\n    (\"renderer\", \"svg-utils\"), (\"renderer\", \"config\"),\n    (\"http\", \"logging\"), (\"crypto\", \"logging\"), (\"cache\", \"logging\"),\n    (\"svg-utils\", \"json\"), (\"config\", \"logging\"), (\"json\", \"logging\"),\n]\n\n# --- Hierarchical layout ------------------------------------------------------\n# NetworkLayout.jl is not part of this catalog's Julia environment, so the\n# layer assignment is computed directly: each node's layer is the length of\n# the longest dependency chain reaching it, found by relaxing edges to a\n# fixpoint (a tiny Bellman-Ford variant — the dependency graph is a DAG, so\n# this always converges). Nodes with no incoming edges anchor layer 0.\nlayer = Dict(n => 0 for n in nodes)\nchanged = true\nwhile changed\n    global changed = false\n    for (src, dst) in edges\n        if layer[dst] < layer[src] + 1\n            layer[dst] = layer[src] + 1\n            global changed = true\n        end\n    end\nend\n\nn_layers = maximum(values(layer)) + 1\nlayer_nodes = [String[] for _ in 1:n_layers]\nfor n in nodes\n    push!(layer_nodes[layer[n]+1], n)\nend\n\nindegree = Dict(n => 0 for n in nodes)\nfor (_, dst) in edges\n    indegree[dst] += 1\nend\n\n# Barycenter crossing-minimization: repeatedly reorder each layer by the mean\n# position of its neighbors, alternating downward/upward sweeps (Sugiyama-style).\n# This is what pulls \"auth\"/\"renderer\" and their fan-out into straighter columns\n# instead of the crossing tangle the review flagged.\nneighbors = Dict(n => String[] for n in nodes)\nfor (src, dst) in edges\n    push!(neighbors[src], dst)\n    push!(neighbors[dst], src)\nend\n\norder_x = Dict{String,Float64}(n => Float64(j) for ns in layer_nodes for (j, n) in enumerate(ns))\nfor iter in 1:6\n    layer_order = isodd(iter) ? (1:n_layers) : reverse(1:n_layers)\n    for li in layer_order\n        ns = layer_nodes[li]\n        length(ns) <= 1 && continue\n        bary = Dict(n => begin\n            xs = [order_x[m] for m in neighbors[n]]\n            isempty(xs) ? order_x[n] : sum(xs) / length(xs)\n        end for n in ns)\n        sort!(ns, by=n -> bary[n])\n        for (j, n) in enumerate(ns)\n            order_x[n] = Float64(j)\n        end\n    end\nend\n\nlayer_spacing = 2.4\nnode_spacing = 2.2\npos = Dict{String,Point2f}()\nfor (li, ns) in enumerate(layer_nodes)\n    k = length(ns)\n    y = (n_layers - li) * layer_spacing\n    for (j, n) in enumerate(ns)\n        x = (j - (k + 1) / 2) * node_spacing\n        pos[n] = Point2f(x, y)\n    end\nend\n\nmarker_size(n) = 26.0f0 + 6.0f0 * indegree[n]\nnode_radius(n) = 0.16 + 0.006 * marker_size(n)  # data-space clearance so arrows stop at the node edge\n\ntier_labels = [\"Applications\", \"Services\", \"Infrastructure/utilities\", \"Data format\", \"Core\"]\nnode_color(n) = IMPRINT_PALETTE[min(layer[n] + 1, length(IMPRINT_PALETTE))]\n\n# --- Plot ----------------------------------------------------------------------\nfig = Figure(\n    size=(1200, 1200),\n    fontsize=14,\n    backgroundcolor=PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title=\"network-directed · julia · makie · anyplot.ai\",\n    titlesize=20,\n    titlecolor=INK,\n    backgroundcolor=PAGE_BG,\n    aspect=DataAspect(),\n)\nhidedecorations!(ax)\nhidespines!(ax)\nlimits!(ax, -5.2, 5.2, -1.0, 10.2)\n\n# Move `from` toward `to` by clearance `r` (data units) — keeps arrow shafts\n# and heads from disappearing under the node markers they connect.\nfunction pull_in(from::Point2f, to::Point2f, r)\n    d = to - from\n    u = d / hypot(d[1], d[2])\n    from + u * Float32(r)\nend\n\nfunction draw_arrow!(ax, src::String, dst::String; waypoint::Union{Point2f,Nothing}=nothing)\n    if waypoint === nothing\n        tail = pull_in(pos[src], pos[dst], node_radius(src))\n    else\n        tail = waypoint\n        start = pull_in(pos[src], waypoint, node_radius(src))\n        lines!(ax, [start, waypoint]; color=EDGE_COLOR, linewidth=2.0)\n    end\n    head = pull_in(pos[dst], tail, node_radius(dst))\n    arrows!(ax, [tail], [head - tail]; color=EDGE_COLOR, linewidth=2.0, arrowsize=15)\nend\n\n# The one long-range dependency (cli → logging) is routed around the middle\n# tiers with a dog-leg instead of a straight line, so it doesn't cut through\n# unrelated nodes — the \"curved edges to avoid overlap\" case from the spec.\nskip_edge = (\"cli\", \"logging\")\nwaypoint = Point2f(5.0, 4.8)\n\nfor (src, dst) in edges\n    if (src, dst) == skip_edge\n        draw_arrow!(ax, src, dst; waypoint=waypoint)\n    else\n        draw_arrow!(ax, src, dst)\n    end\nend\n\nfor n in nodes\n    scatter!(ax, [pos[n]]; color=node_color(n), markersize=marker_size(n), strokewidth=0)\nend\n\n# Labels sit below each node rather than inside it — several ids (\"api-client\",\n# \"svg-utils\") are wider than even the largest marker and would get clipped.\nfor n in nodes\n    label_pos = pos[n] - Point2f(0, node_radius(n) + 0.16)\n    text!(ax, label_pos; text=n, color=INK, fontsize=13, align=(:center, :top))\nend\n\nlegend_elements = [MarkerElement(color=IMPRINT_PALETTE[i], marker=:circle, markersize=14) for i in 1:n_layers]\nLegend(fig[2, 1], legend_elements, tier_labels[1:n_layers];\n    orientation=:horizontal, framevisible=false, labelcolor=INK, nbanks=2)\nrowgap!(fig.layout, 0)\nrowsize!(fig.layout, 2, Auto(0.05))\n\n# --- Save ------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit=2)\n"}