{"spec_id":"network-basic","library":"makie","language":"julia","code":"# anyplot.ai\n# network-basic: Basic Network Graph\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 95/100 | Created: 2026-07-24\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# --- Theme tokens -----------------------------------------------------------\nconst THEME       = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG     = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst ELEVATED_BG = THEME == \"light\" ? colorant\"#FFFDF6\" : colorant\"#242420\"\nconst INK         = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT    = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\n\n# Imprint categorical palette (first 4 positions — module layers)\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\",  # 1 — Core\n    colorant\"#C475FD\",  # 2 — Data\n    colorant\"#4467A3\",  # 3 — API\n    colorant\"#BD8233\",  # 4 — UI\n]\nconst LAYER_NAMES = [\"Core\", \"Data\", \"API\", \"UI\"]\n\n# --- Data: software module dependency graph ---------------------------------\nmodule_names = [\n    \"Auth\", \"Config\", \"Logger\", \"Cache\", \"EventBus\",\n    \"UserRepo\", \"OrderRepo\", \"PaymentRepo\", \"Schema\", \"Migrations\",\n    \"UserAPI\", \"OrderAPI\", \"PaymentAPI\", \"SearchAPI\", \"NotifyAPI\", \"Gateway\",\n    \"Dashboard\", \"Checkout\", \"Profile\", \"Admin\", \"Reports\", \"MobileApp\",\n]\nlayer = [\n    1, 1, 1, 1, 1,\n    2, 2, 2, 2, 2,\n    3, 3, 3, 3, 3, 3,\n    4, 4, 4, 4, 4, 4,\n]\nn = length(module_names)\n\n# Third element is coupling strength: 1=light, 2=normal, 3=critical dependency\nedges = [\n    # Core internals\n    (1, 2, 2), (1, 3, 2), (2, 4, 1), (1, 5, 2),\n    # Data depends on Core\n    (6, 1, 3), (6, 4, 1), (7, 1, 2), (8, 1, 3), (9, 4, 1), (10, 9, 2),\n    # API depends on Data + Core\n    (11, 6, 3), (11, 1, 2), (12, 7, 3), (13, 8, 3), (14, 6, 2), (14, 7, 1), (15, 5, 2),\n    (16, 11, 2), (16, 12, 2), (16, 13, 2),\n    # UI depends on API\n    (17, 16, 3), (17, 11, 1), (18, 12, 3), (18, 13, 3), (19, 11, 2),\n    (20, 16, 2), (21, 14, 1), (21, 12, 2), (22, 16, 2),\n]\n\ndegrees = zeros(Int, n)\nfor (a, b, _) in edges\n    degrees[a] += 1\n    degrees[b] += 1\nend\n\n# --- Force-directed layout (Fruchterman-Reingold, hand-rolled — no\n#     NetworkLayout.jl, which is not installed in the CI runtime) -----------\nangles = range(0, 2π; length = n + 1)[1:n] .+ randn(n) .* 0.15\nradius = 0.4\npositions = hcat(0.5 .+ radius .* cos.(angles), 0.5 .+ radius .* sin.(angles))\n\nk = sqrt(1.0 / n)\niterations = 200\nfor iter in 1:iterations\n    disp = zeros(n, 2)\n    for i in 1:n, j in 1:n\n        i == j && continue\n        dx, dy = positions[i, 1] - positions[j, 1], positions[i, 2] - positions[j, 2]\n        dist = max(hypot(dx, dy), 0.01)\n        force = k^2 / dist\n        disp[i, 1] += (dx / dist) * force\n        disp[i, 2] += (dy / dist) * force\n    end\n    for (a, b, _) in edges\n        dx, dy = positions[a, 1] - positions[b, 1], positions[a, 2] - positions[b, 2]\n        dist = max(hypot(dx, dy), 0.01)\n        force = dist^2 / k\n        disp[a, 1] -= (dx / dist) * force\n        disp[a, 2] -= (dy / dist) * force\n        disp[b, 1] += (dx / dist) * force\n        disp[b, 2] += (dy / dist) * force\n    end\n    temperature = 0.1 * (1 - iter / iterations)\n    for i in 1:n\n        dn = hypot(disp[i, 1], disp[i, 2])\n        if dn > 0\n            step = min(dn, temperature) / dn\n            positions[i, 1] += disp[i, 1] * step\n            positions[i, 2] += disp[i, 2] * step\n        end\n    end\nend\n\npos_min = vec(minimum(positions; dims = 1))\npos_max = vec(maximum(positions; dims = 1))\npositions = (positions .- pos_min') ./ (pos_max' .- pos_min') .* 0.86 .+ 0.07\n\n# --- Plot ---------------------------------------------------------------\ntitle_text = \"Software Module Dependencies · network-basic · julia · makie · anyplot.ai\"\n\nfig = Figure(\n    resolution      = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title            = title_text,\n    titlesize        = 18,\n    titlecolor       = INK,\n    backgroundcolor  = PAGE_BG,\n)\nhidedecorations!(ax)\nhidespines!(ax)\nxlims!(ax, -0.05, 1.05)\nylims!(ax, -0.11, 1.05)\n\nnode_colors = [IMPRINT_PALETTE[layer[i]] for i in 1:n]\nnode_sizes = [26.0 + degrees[i] * 5.0 for i in 1:n]\n\n# Edges drawn per-segment so linewidth/alpha can be keyed to coupling strength\n# (weight 1=light, 2=normal, 3=critical) — a Makie-distinctive per-edge encoding\n# rather than a single batched style for every dependency.\nedge_points = Vector{Point2f}(undef, 2 * length(edges))\nedge_widths = Vector{Float32}(undef, 2 * length(edges))\nedge_colors = Vector{RGBAf}(undef, 2 * length(edges))\nfor (idx, (a, b, w)) in enumerate(edges)\n    edge_points[2idx - 1] = Point2f(positions[a, 1], positions[a, 2])\n    edge_points[2idx]     = Point2f(positions[b, 1], positions[b, 2])\n    width = 1.2 + w * 0.75\n    alpha = 0.28 + w * 0.11\n    edge_widths[2idx - 1] = width\n    edge_widths[2idx]     = width\n    edge_colors[2idx - 1] = RGBAf(INK_SOFT.r, INK_SOFT.g, INK_SOFT.b, alpha)\n    edge_colors[2idx]     = RGBAf(INK_SOFT.r, INK_SOFT.g, INK_SOFT.b, alpha)\nend\nlinesegments!(ax, edge_points; color = edge_colors, linewidth = edge_widths)\n\n# Small arrowhead per edge, pulled back from the target node's rim, so the\n# undirected line segments above read as directed \"depends on\" relationships.\npx_per_data_unit = 1150.0\narrow_positions = Vector{Point2f}(undef, length(edges))\narrow_rotations = Vector{Float32}(undef, length(edges))\nfor (idx, (a, b, _)) in enumerate(edges)\n    pa, pb = positions[a, :], positions[b, :]\n    dvec = pb .- pa\n    dist = max(hypot(dvec[1], dvec[2]), 1e-6)\n    dir = dvec ./ dist\n    pullback = node_sizes[b] / 2 / px_per_data_unit + 0.012\n    arrow_positions[idx] = Point2f((pb .- dir .* pullback)...)\n    arrow_rotations[idx] = atan(dir[2], dir[1]) - Float32(pi / 2)\nend\nscatter!(\n    ax, arrow_positions;\n    marker      = :utriangle,\n    markersize  = 10,\n    rotation    = arrow_rotations,\n    color       = RGBAf(INK_SOFT.r, INK_SOFT.g, INK_SOFT.b, 0.7),\n    strokewidth = 0,\n)\n\nscatter!(\n    ax, positions[:, 1], positions[:, 2];\n    color       = node_colors,\n    markersize  = node_sizes,\n    strokecolor = PAGE_BG,\n    strokewidth = 2.5,\n)\n\n# Emphasize the top hub module(s) with an outer ring beyond size alone, giving\n# the layout a clear architectural-bottleneck focal point.\nmax_degree = maximum(degrees)\nhub_indices = findall(==(max_degree), degrees)\nlength(hub_indices) > 2 && (hub_indices = hub_indices[1:2])\nscatter!(\n    ax, positions[hub_indices, 1], positions[hub_indices, 2];\n    marker      = :circle,\n    markersize  = [node_sizes[i] + 16.0 for i in hub_indices],\n    color       = :transparent,\n    strokecolor = INK,\n    strokewidth = 2.2,\n)\n\nhub_set = Set(hub_indices)\nlabel_offsets = [(0.0f0, -(node_sizes[i] / 2 + (i in hub_set ? 8.0 : 0.0) + 9)) for i in 1:n]\ntext!(\n    ax, positions[:, 1], positions[:, 2];\n    text      = module_names,\n    align     = (:center, :top),\n    fontsize  = 12,\n    color     = INK,\n    offset    = label_offsets,\n)\n\nfor (i, name) in enumerate(LAYER_NAMES)\n    scatter!(ax, [NaN], [NaN]; color = IMPRINT_PALETTE[i], markersize = 20, label = name)\nend\naxislegend(ax, \"Layer\"; position = :lt, backgroundcolor = ELEVATED_BG, labelcolor = INK_SOFT, titlecolor = INK)\n\ntext!(\n    ax, 0.5, -0.065;\n    text     = \"Edge width/opacity ∝ coupling strength · arrows point toward the depended-upon module\",\n    align    = (:center, :top),\n    fontsize = 11,\n    color    = INK_SOFT,\n)\n\n# --- Save -------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}