{"spec_id":"hive-basic","library":"makie","language":"julia","code":"# anyplot.ai\n# hive-basic: Basic Hive Plot\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 87/100 | Created: 2026-09-05\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# --- Theme tokens (see prompts/default-style-guide.md \"Theme-adaptive Chrome\") ----\nconst THEME    = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG  = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst INK      = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\",  # 1 — Core (brand green, always first series)\n    colorant\"#C475FD\",  # 2 — Utility (lavender)\n    colorant\"#4467A3\",  # 3 — Interface (blue)\n]\n\n# --- Data: a software module dependency network -----------------------------\n# 3 radial axes grouped by module type; node position along its axis encodes\n# in-network degree so structurally similar modules always land at the same\n# radius, making the plot directly comparable across versions/runs.\naxis_names = [\"Core\", \"Utility\", \"Interface\"]\nn_per_axis = 12\nn_nodes = n_per_axis * length(axis_names)\nnode_axis = repeat(1:length(axis_names), inner = n_per_axis)  # 1=Core 2=Utility 3=Interface\n\n# Dependency edges only connect modules on *different* axes — the classic\n# hive-plot convention where axis membership, not adjacency, tells the story.\n# Each edge also carries a strength `weight` (optional per spec), later shown\n# via line width/opacity — probabilities are kept modest to limit crossing\n# density near the Utility–Interface base.\nedges = Tuple{Int, Int, Float64}[]\nfor i in 1:n_nodes, j in (i + 1):n_nodes\n    if node_axis[i] != node_axis[j]\n        pair = sort([node_axis[i], node_axis[j]])\n        edge_probability = pair == [1, 2] ? 0.16 : pair == [2, 3] ? 0.12 : 0.06\n        if rand() < edge_probability\n            weight = 1.0 + 4.0 * rand()  # dependency strength, 1 (weak) – 5 (strong)\n            push!(edges, (i, j, weight))\n        end\n    end\nend\n\ndegree = zeros(Int, n_nodes)\nfor (source, target, _) in edges\n    degree[source] += 1\n    degree[target] += 1\nend\n\n# --- Radial layout (shared scale across all 3 axes) --------------------------\nconst MIN_R = 0.15\nconst MAX_R = 0.95\naxis_angles = deg2rad.([90.0, 210.0, 330.0])  # up, lower-left, lower-right\n\nnode_x = zeros(n_nodes)\nnode_y = zeros(n_nodes)\nfor axis_idx in 1:length(axis_names)\n    members = findall(==(axis_idx), node_axis)\n    ranked = sort(members, by = n -> (degree[n], n))  # low → high degree, stable tie-break\n    radii = range(MIN_R, MAX_R, length = length(ranked))\n    angle = axis_angles[axis_idx]\n    for (rank, n) in enumerate(ranked)\n        node_x[n] = radii[rank] * cos(angle)\n        node_y[n] = radii[rank] * sin(angle)\n    end\nend\n\n# --- Plot ---------------------------------------------------------------------\ntitle_text = \"hive-basic · julia · makie · anyplot.ai\"\n\nfig = Figure(\n    size = (1200, 1200),\n    fontsize = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title = title_text,\n    titlesize = 20,\n    titlecolor = INK,\n    backgroundcolor = PAGE_BG,\n    aspect = DataAspect(),\n)\nhidedecorations!(ax)\nhidespines!(ax)\nlimits!(ax, -1.2, 1.2, -1.2, 1.2)\n\n# Axis reference lines (subtle, structural — not data)\nfor angle in axis_angles\n    lines!(\n        ax, [0.0, MAX_R * cos(angle)], [0.0, MAX_R * sin(angle)];\n        color = (INK_SOFT, 0.5), linewidth = 1.5,\n    )\nend\n\n# Axis tip labels\nlabel_r = MAX_R + 0.14\nfor (i, angle) in enumerate(axis_angles)\n    text!(\n        ax, label_r * cos(angle), label_r * sin(angle);\n        text = axis_names[i], color = INK, fontsize = 16,\n        align = (:center, :center),\n    )\nend\n\n# Caption spelling out what radial position means — without it, the\n# center→tip degree ordering (the whole point of a hive plot) is invisible\ntext!(\n    ax, 0.0, -1.08;\n    text = \"position ∝ degree (center = low, tip = high)\",\n    color = INK_SOFT, fontsize = 13, align = (:center, :center),\n)\n\n# Edges — quadratic Bezier curves bowed toward the origin (never through\n# nodes on the third axis). Weak edges drawn first and kept faint/thin,\n# strong edges drawn last and bolder — both de-clutters the base region and\n# visualizes the optional edge-weight field via width/opacity.\nfor (source, target, weight) in sort(edges, by = e -> e[3])\n    angle_a = axis_angles[node_axis[source]]\n    angle_b = axis_angles[node_axis[target]]\n    control_angle = atan(sin(angle_a) + sin(angle_b), cos(angle_a) + cos(angle_b))\n    control_r = 0.32 * MAX_R\n    p0x, p0y = node_x[source], node_y[source]\n    p1x, p1y = control_r * cos(control_angle), control_r * sin(control_angle)\n    p2x, p2y = node_x[target], node_y[target]\n    t = range(0.0, 1.0, length = 30)\n    curve_x = @. (1 - t)^2 * p0x + 2 * (1 - t) * t * p1x + t^2 * p2x\n    curve_y = @. (1 - t)^2 * p0y + 2 * (1 - t) * t * p1y + t^2 * p2y\n    weight_frac = (weight - 1.0) / 4.0  # 0 (weak) .. 1 (strong)\n    edge_alpha = 0.14 + 0.34 * weight_frac\n    edge_width = 0.8 + 1.4 * weight_frac\n    lines!(ax, curve_x, curve_y; color = (INK_SOFT, edge_alpha), linewidth = edge_width)\nend\n\n# Nodes — colored by axis category (Imprint palette, first series always brand green)\nfor (i, name) in enumerate(axis_names)\n    members = findall(==(i), node_axis)\n    scatter!(\n        ax, node_x[members], node_y[members];\n        color = IMPRINT_PALETTE[i], markersize = 16,\n        strokewidth = 1, strokecolor = PAGE_BG, label = name,\n    )\nend\n\naxislegend(ax; position = :lt, framevisible = false, labelcolor = INK, patchsize = (18, 18))\n\n# --- Save -----------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}