{"spec_id":"network-force-directed","library":"makie","language":"julia","code":"# anyplot.ai\n# network-force-directed: Force-Directed Graph\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 90/100 | Created: 2026-07-01\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\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\",\n    colorant\"#C475FD\",\n    colorant\"#4467A3\",\n    colorant\"#BD8233\",\n    colorant\"#AE3030\",\n    colorant\"#2ABCCD\",\n    colorant\"#954477\",\n    colorant\"#99B314\",\n]\n\n# Data: academic research collaboration network (4 departments, 32 researchers)\nn_nodes    = 32\ndept_names = [\"Machine Learning\", \"Networks\", \"Databases\", \"Systems\"]\nnode_dept  = vcat([fill(d, 8) for d in 1:4]...)\n\nedges = Tuple{Int,Int}[]\n\n# Dense within-department edges\nfor d in 1:4\n    members = findall(==(d), node_dept)\n    for i in members, j in members\n        if i < j && rand() < 0.65\n            push!(edges, (i, j))\n        end\n    end\nend\n\n# Sparse cross-department bridges\nfor (d1, d2) in [(1, 2), (2, 3), (3, 4), (1, 3), (1, 4), (2, 4)]\n    m1 = findall(==(d1), node_dept)\n    m2 = findall(==(d2), node_dept)\n    for _ in 1:2\n        push!(edges, (rand(m1), rand(m2)))\n    end\nend\n\nunique!(edges)\n\ndegree = zeros(Int, n_nodes)\nfor (u, v) in edges\n    degree[u] += 1\n    degree[v] += 1\nend\n\n# Force-directed layout — Fruchterman-Reingold algorithm\npos_x = randn(n_nodes) .* 3.0\npos_y = randn(n_nodes) .* 3.0\n\nk = sqrt(100.0 / n_nodes)  # ideal spring length\n\nfor iter in 0:299\n    t_step = max(1.0 * 0.97^iter, 0.005)  # cooling schedule\n\n    dx = zeros(n_nodes)\n    dy = zeros(n_nodes)\n\n    # Repulsive forces between all node pairs\n    for i in 1:n_nodes, j in 1:n_nodes\n        if i != j\n            δx = pos_x[i] - pos_x[j]\n            δy = pos_y[i] - pos_y[j]\n            d  = max(sqrt(δx^2 + δy^2), 1e-4)\n            f  = k^2 / d\n            dx[i] += δx / d * f\n            dy[i] += δy / d * f\n        end\n    end\n\n    # Attractive forces along edges\n    for (u, v) in edges\n        δx = pos_x[u] - pos_x[v]\n        δy = pos_y[u] - pos_y[v]\n        d  = max(sqrt(δx^2 + δy^2), 1e-4)\n        f  = d^2 / k\n        dx[u] -= δx / d * f\n        dy[u] -= δy / d * f\n        dx[v] += δx / d * f\n        dy[v] += δy / d * f\n    end\n\n    # Update positions clipped to temperature\n    for i in 1:n_nodes\n        disp = sqrt(dx[i]^2 + dy[i]^2)\n        if disp > 0\n            pos_x[i] += dx[i] / disp * min(disp, t_step)\n            pos_y[i] += dy[i] / disp * min(disp, t_step)\n        end\n    end\nend\n\n# Normalise to [0.05, 0.95]\npos_x = 0.05 .+ 0.90 .* (pos_x .- minimum(pos_x)) ./ (maximum(pos_x) - minimum(pos_x))\npos_y = 0.05 .+ 0.90 .* (pos_y .- minimum(pos_y)) ./ (maximum(pos_y) - minimum(pos_y))\n\nnode_colors = [IMPRINT_PALETTE[d] for d in node_dept]\nnode_sizes  = 12.0 .+ degree .* 2.5\n\n# Title — 51 chars, below the 67-char baseline so no scaling needed\nconst TITLE        = \"network-force-directed · julia · makie · anyplot.ai\"\nconst TITLE_SIZE   = 22\n\n# Figure\nfig = Figure(\n    size            = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title              = TITLE,\n    titlesize          = TITLE_SIZE,\n    titlecolor         = INK,\n    backgroundcolor    = PAGE_BG,\n    topspinevisible    = false,\n    rightspinevisible  = false,\n    leftspinevisible   = false,\n    bottomspinevisible = false,\n    xgridvisible       = false,\n    ygridvisible       = false,\n    xticksvisible      = false,\n    yticksvisible      = false,\n    xticklabelsvisible = false,\n    yticklabelsvisible = false,\n)\n\nlimits!(ax, 0, 1, 0, 1)\n\n# Draw edges\nfor (u, v) in edges\n    lines!(ax, [pos_x[u], pos_x[v]], [pos_y[u], pos_y[v]];\n           color = (INK_SOFT, 0.25), linewidth = 1.0)\nend\n\n# Draw nodes (size scales with degree)\nscatter!(ax, pos_x, pos_y;\n         color       = node_colors,\n         markersize  = node_sizes,\n         strokewidth = 1.5,\n         strokecolor = PAGE_BG)\n\n# Legend\nlegend_elems = [\n    MarkerElement(\n        color       = IMPRINT_PALETTE[i],\n        marker      = :circle,\n        markersize  = 16,\n        strokewidth = 0,\n    )\n    for i in 1:4\n]\n\nLegend(\n    fig[1, 2], legend_elems, dept_names;\n    title        = \"Department\",\n    titlesize    = 13,\n    titlecolor   = INK,\n    labelsize    = 12,\n    labelcolor   = INK,\n    framevisible    = true,\n    framecolor      = (INK_SOFT, 0.3),\n    backgroundcolor = ELEVATED_BG,\n)\n\ncolsize!(fig.layout, 1, Relative(0.82))\n\n# Annotation: explain the node-size encoding\ntext!(ax, 0.01, 0.01; text = \"Node size ∝ degree (connections)\",\n      fontsize = 10, color = INK_SOFT, align = (:left, :bottom))\n\n# Save\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}