diff --git a/plots/circlepacking-basic/implementations/julia/makie.jl b/plots/circlepacking-basic/implementations/julia/makie.jl new file mode 100644 index 0000000000..ac385b723f --- /dev/null +++ b/plots/circlepacking-basic/implementations/julia/makie.jl @@ -0,0 +1,283 @@ +# anyplot.ai +# circlepacking-basic: Circle Packing Chart +# Library: makie 0.21.9 | Julia 1.11.9 +# Quality: 79/100 | Created: 2026-09-02 + +using CairoMakie +using Colors +using Random + +Random.seed!(42) + +# --- Theme tokens ------------------------------------------------------------- +const THEME = get(ENV, "ANYPLOT_THEME", "light") +const PAGE_BG = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17" +const INK = THEME == "light" ? colorant"#1A1A17" : colorant"#F0EFE8" +const INK_SOFT = THEME == "light" ? colorant"#4A4A44" : colorant"#B8B7B0" +const IMPRINT_PALETTE = [ + colorant"#009E73", # 1 — brand green + colorant"#C475FD", # 2 — lavender + colorant"#4467A3", # 3 — blue + colorant"#BD8233", # 4 — ochre +] + +# --- Data: disk storage broken down into folders and files -------------------- +struct LeafSpec + label::String + size_mb::Float64 +end + +struct SubcatSpec + label::String + leaves::Vector{LeafSpec} +end + +struct CategorySpec + label::String + subcats::Vector{SubcatSpec} +end + +function random_leaves(names, lo, hi) + return [LeafSpec(n, lo + rand() * (hi - lo)) for n in names] +end + +categories = [ + CategorySpec("Documents", [ + SubcatSpec("Reports", random_leaves(["Q1", "Q2", "Q3", "Q4"], 4.0, 60.0)), + SubcatSpec("Spreadsheets", random_leaves(["Budget", "Forecast", "Payroll"], 2.0, 40.0)), + SubcatSpec("Presentations", random_leaves(["Kickoff", "Roadmap"], 8.0, 90.0)), + ]), + CategorySpec("Media", [ + SubcatSpec("Photos", random_leaves(["Trip", "Family", "Events", "Pets"], 20.0, 320.0)), + SubcatSpec("Videos", random_leaves(["Vacation", "Tutorial"], 200.0, 1400.0)), + SubcatSpec("Audio", random_leaves(["Podcasts", "Music", "Voice Memos"], 15.0, 260.0)), + SubcatSpec("Design Files", random_leaves(["Logos", "Mockups"], 10.0, 150.0)), + ]), + CategorySpec("Code", [ + SubcatSpec("Frontend", random_leaves(["Components", "Styles", "Assets"], 3.0, 55.0)), + SubcatSpec("Backend", random_leaves(["API", "Services", "Migrations"], 3.0, 50.0)), + SubcatSpec("Scripts", random_leaves(["Automation", "CI"], 1.0, 20.0)), + SubcatSpec("Tests", random_leaves(["Unit", "Integration", "Fixtures"], 1.0, 30.0)), + ]), + CategorySpec("System", [ + SubcatSpec("Cache", random_leaves(["Browser", "Build", "Package"], 10.0, 200.0)), + SubcatSpec("Logs", random_leaves(["App", "Access", "Crash"], 2.0, 45.0)), + SubcatSpec("Config", random_leaves(["User", "Network"], 0.5, 6.0)), + SubcatSpec("Temp", random_leaves(["Downloads", "Swap", "Recovery"], 5.0, 90.0)), + ]), +] + +# --- Hierarchy node + recursive circle packing --------------------------------- +mutable struct PackNode + label::String + depth::Int + value::Float64 + category_idx::Int + children::Vector{PackNode} + rel_x::Float64 + rel_y::Float64 + abs_x::Float64 + abs_y::Float64 + r::Float64 +end + +PackNode(label, depth, category_idx) = + PackNode(label, depth, 0.0, category_idx, PackNode[], 0.0, 0.0, 0.0, 0.0, 0.0) + +# Position of a circle with radius r3, externally tangent to two placed circles. +function tangent_points(x1, y1, r1, x2, y2, r2, r3) + d = hypot(x2 - x1, y2 - y1) + R1, R2 = r1 + r3, r2 + r3 + if d < 1e-9 || d > R1 + R2 || d < abs(R1 - R2) + return Tuple{Float64,Float64}[] + end + a = (R1^2 - R2^2 + d^2) / (2d) + h2 = R1^2 - a^2 + h2 < 0 && return Tuple{Float64,Float64}[] + h = sqrt(h2) + xm = x1 + a * (x2 - x1) / d + ym = y1 + a * (y2 - y1) / d + ux, uy = -(y2 - y1) / d, (x2 - x1) / d + return [(xm + h * ux, ym + h * uy), (xm - h * ux, ym - h * uy)] +end + +# Greedy sibling packer: places circles (by descending radius) tangent to two +# already-placed neighbors, minimizing distance from the current centroid. +function pack_siblings(radii::Vector{Float64}) + n = length(radii) + n == 0 && return Float64[], Float64[] + xs, ys = zeros(n), zeros(n) + order = sortperm(radii, rev = true) + placed = Int[order[1]] + if n >= 2 + i2 = order[2] + xs[i2] = radii[order[1]] + radii[i2] + push!(placed, i2) + end + for k in 3:n + i = order[k] + r = radii[i] + best, best_dist = nothing, Inf + for ai in 1:length(placed), bi in (ai + 1):length(placed) + a, b = placed[ai], placed[bi] + for p in tangent_points(xs[a], ys[a], radii[a], xs[b], ys[b], radii[b], r) + ok = true + for c in placed + if hypot(p[1] - xs[c], p[2] - ys[c]) < radii[c] + r - 1e-6 + ok = false + break + end + end + if ok + dist = hypot(p[1], p[2]) + r + if dist < best_dist + best_dist, best = dist, p + end + end + end + end + if best === nothing + angle = 2pi * k / n + reach = sum(radii) + r + best = (reach * cos(angle), reach * sin(angle)) + end + xs[i], ys[i] = best + push!(placed, i) + end + return xs, ys +end + +# Bottom-up: pack each node's children, then set node.r to their enclosing +# circle (plus padding) and store each child's offset relative to this node. +function pack!(node::PackNode; padding_ratio = 0.10) + if isempty(node.children) + node.r = sqrt(node.value) + return + end + for c in node.children + pack!(c; padding_ratio = padding_ratio) + end + radii = [c.r for c in node.children] + xs, ys = pack_siblings(radii) + lefts = xs .- radii + rights = xs .+ radii + tops = ys .- radii + bottoms = ys .+ radii + cx = (minimum(lefts) + maximum(rights)) / 2 + cy = (minimum(tops) + maximum(bottoms)) / 2 + xs .-= cx + ys .-= cy + enclosing_r = maximum(hypot.(xs, ys) .+ radii) + node.r = enclosing_r * (1 + padding_ratio) + for (c, x, y) in zip(node.children, xs, ys) + c.rel_x, c.rel_y = x, y + end +end + +function locate!(node::PackNode, parent_x, parent_y) + node.abs_x = parent_x + node.rel_x + node.abs_y = parent_y + node.rel_y + for c in node.children + locate!(c, node.abs_x, node.abs_y) + end +end + +function collect_nodes!(node::PackNode, acc::Vector{PackNode}) + push!(acc, node) + for c in node.children + collect_nodes!(c, acc) + end +end + +# --- Build the tree ------------------------------------------------------------- +root = PackNode("Storage", 0, 0) +for (ci, cat) in enumerate(categories) + cat_node = PackNode(cat.label, 1, ci) + for sub in cat.subcats + sub_node = PackNode(sub.label, 2, ci) + for leaf in sub.leaves + leaf_node = PackNode(leaf.label, 3, ci) + leaf_node.value = leaf.size_mb + push!(sub_node.children, leaf_node) + end + push!(cat_node.children, sub_node) + end + push!(root.children, cat_node) +end + +pack!(root) +root.rel_x, root.rel_y = 0.0, 0.0 +locate!(root, 0.0, 0.0) + +# Rescale so the root circle lands on a fixed size in figure data units. +const TARGET_ROOT_R = 540.0 +scale = TARGET_ROOT_R / root.r +all_nodes = PackNode[] +collect_nodes!(root, all_nodes) +for node in all_nodes + node.abs_x *= scale + node.abs_y *= scale + node.r *= scale +end + +# --- Plot ------------------------------------------------------------------------ +fig = Figure( + size = (1200, 1200), + backgroundcolor = PAGE_BG, +) + +ax = Axis( + fig[1, 1]; + title = "circlepacking-basic · julia · makie · anyplot.ai", + titlesize = 30, + titlecolor = INK, + aspect = DataAspect(), + backgroundcolor = PAGE_BG, +) +hidedecorations!(ax) +hidespines!(ax) + +# Root: faint container circle showing the encompassing boundary. +poly!(ax, Circle(Point2f(root.abs_x, root.abs_y), root.r); + color = (PAGE_BG, 0.0), strokecolor = INK_SOFT, strokewidth = 1.5) + +fill_alpha = Dict(1 => 0.16, 2 => 0.38, 3 => 0.88) +for depth in 1:3 + for node in all_nodes + node.depth == depth || continue + base = IMPRINT_PALETTE[node.category_idx] + poly!(ax, Circle(Point2f(node.abs_x, node.abs_y), node.r); + color = (base, fill_alpha[depth]), strokecolor = PAGE_BG, strokewidth = 2.0) + end +end + +# Labels: categories placed just below the actual bottom of their own child +# cluster (not a fixed fraction of the category radius, which can collide +# with a child that happens to sit near the category's edge); subcategories +# at their own center, only when large relative to their own category (not +# the global root) so every category gets comparable coverage. +# +# Each tier is drawn with a single vectorized text!() call (positions/text as +# arrays) rather than one text!() per node: a per-node loop of individual +# text!() calls was silently dropping a subset of glyphs in CairoMakie even +# though their positions and the labeling threshold were correct. +category_r = Dict(node.category_idx => node.r for node in all_nodes if node.depth == 1) +label_margin = 0.05 * root.r + +cat_nodes = filter(n -> n.depth == 1, all_nodes) +cat_positions = [ + Point2f(n.abs_x, minimum(c.abs_y - c.r for c in n.children) - label_margin) for + n in cat_nodes +] +text!(ax, cat_positions; text = [n.label for n in cat_nodes], + align = (:center, :center), fontsize = 20, color = INK, font = :bold) + +sub_nodes = filter( + n -> n.depth == 2 && n.r >= 0.18 * category_r[n.category_idx], all_nodes, +) +sub_positions = [Point2f(n.abs_x, n.abs_y) for n in sub_nodes] +text!(ax, sub_positions; text = [n.label for n in sub_nodes], + align = (:center, :center), fontsize = 13, color = INK) + +# --- Save -------------------------------------------------------------------- +save("plot-$(THEME).png", fig; px_per_unit = 2) diff --git a/plots/circlepacking-basic/metadata/julia/makie.yaml b/plots/circlepacking-basic/metadata/julia/makie.yaml new file mode 100644 index 0000000000..b73dc63b6c --- /dev/null +++ b/plots/circlepacking-basic/metadata/julia/makie.yaml @@ -0,0 +1,259 @@ +library: makie +language: julia +specification_id: circlepacking-basic +created: '2026-09-02T15:36:20Z' +updated: '2026-09-02T16:39:04Z' +generated_by: claude-sonnet +workflow_run: 33648746263 +issue: 2498 +language_version: 1.11.9 +library_version: 0.21.9 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/circlepacking-basic/julia/makie/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/circlepacking-basic/julia/makie/plot-dark.png +preview_html_light: null +preview_html_dark: null +quality_score: 79 +review: + strengths: + - Genuine greedy circle-packing algorithm from scratch (tangent-point placement, + bottom-up enclosing-radius sizing) with no NetworkLayout.jl workaround + - Leaf circle area (not radius) scales with node.value via r = sqrt(value), satisfying + the spec's area-encoding requirement + - Imprint palette applied in canonical order; both renders keep data colors identical + and flip only chrome tokens, no dark-on-dark/light-on-light failures + - Root circle drawn as a faint outlined container encompassing all children with + visible padding, matching the spec note + - Depth-based fill alpha (0.16/0.38/0.88) creates a clear, deliberate three-tier + visual hierarchy + - Canvas lands exactly on 2400x2400; title format matches the mandated convention + exactly + weaknesses: + - 'CONFIRMED VIA INDEPENDENT RE-EXECUTION of the exact packing algorithm (same seed, + same code) that this is NOT a threshold problem: category_r ratios are Reports=0.438, + Spreadsheets=0.300, Presentations=0.414, Photos=0.346, Videos=0.563, Audio=0.198, + Design Files=0.188, Frontend=0.375, Backend=0.452, Scripts=0.289, Tests=0.329, + Cache=0.545, Logs=0.196, Config=0.097 (correctly excluded), Temp=0.360 -- 13 of + 14 subcategory nodes clear the 0.18 threshold, yet only 7 actually render as text + in the saved PNG (Reports, Presentations, Photos, Videos, Audio, Design Files, + Cache). The other 6 that clear the threshold (Spreadsheets, Frontend, Backend, + Scripts, Tests, Logs, Temp) are silently missing in BOTH themes.' + - This is the exact same drop pattern (Documents 2/3, Media 4/4, Code 0/4, System + 1/4) as attempt 2, meaning the attempt-2 fix (switching the per-node text!() loop + to a single vectorized text!(ax, positions; text=labels, ...) call) did not resolve + the underlying defect -- it produced an identical failure signature, so the root + cause is not the per-node-vs-vectorized call style. + - 'CQ-01: the implementation necessarily introduces a mutable struct (PackNode) + and five helper functions (tangent_points, pack_siblings, pack!, locate!, collect_nodes!) + to implement circle packing manually -- justified by the plot type but still a + real departure from the single flat-script ideal.' + image_description: |- + Light render (plot-light.png): + Background: warm off-white, matches #FAF8F1, not pure white. + Chrome: title "circlepacking-basic · julia · makie · anyplot.ai" bold dark ink, clearly readable; four bold category labels (Documents, Media, Code, System) below their clusters, all legible; a thin ink-soft stroke marks the faint root-container circle. + Data: Imprint palette in canonical order -- Documents=green (#009E73), Media=lavender, Code=blue, System=ochre -- with depth-based alpha (0.16/0.38/0.88) giving category/subcategory/leaf tiers. First series is confirmed brand green. + Subcategory labels: only 7 of the 13 subcategory circles that clear the 0.18x-category-radius labeling threshold actually show text -- Reports, Presentations, Photos, Videos, Audio, Design Files, Cache render; Spreadsheets, Frontend, Backend, Scripts, Tests, Logs, Temp are silently missing even though their circles are large enough and comparable in size to labeled peers (verified against independently recomputed packing ratios, not just visual estimate). + Legibility verdict: PASS for all text that is present; the missing-label defect is a content-completeness bug (scored under SC-02/DE-03/DQ-01/LM-01), not a contrast/theme failure. + + Dark render (plot-dark.png): + Background: warm near-black, matches #1A1A17, not pure black. + Chrome: title, category labels, and root-circle stroke correctly flip to light ink (#F0EFE8) and remain fully readable -- no dark-on-dark failures. + Data: identical hues to the light render -- only chrome flipped, as required. + Subcategory labels: the exact same 7-of-13 rendered / 6-missing pattern as the light render (Spreadsheets, Frontend, Backend, Scripts, Tests, Logs, Temp missing in both themes) -- confirmed via pixel-level crop inspection of the System and Code clusters at 2x zoom. + Legibility verdict: PASS for all text that is present; same completeness defect as light render. + criteria_checklist: + visual_quality: + score: 29 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 7 + max: 8 + passed: true + comment: All rendered text is legible in both themes; missing-label content + gap scored under SC-02/DE-03/DQ-01/LM-01 instead + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: No collisions between text, circles, or labels + - id: VQ-03 + name: Element Visibility + score: 6 + max: 6 + passed: true + comment: Circle sizes and depth-alpha tiers clearly distinguish hierarchy + levels + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Imprint palette, CVD-safe, no red-green-only signal + - id: VQ-05 + name: Layout & Canvas + score: 4 + max: 4 + passed: true + comment: Canvas gate passed exactly at 2400x2400, no clipping or overflow + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: No axes needed for this plot type; title descriptive + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'First series is #009E73, canonical order, correct theme-adaptive + backgrounds in both renders' + design_excellence: + score: 13 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 6 + max: 8 + passed: true + comment: Custom greedy tangent-circle packer and depth-based alpha layering + show real design intent + - id: DE-02 + name: Visual Refinement + score: 5 + max: 6 + passed: true + comment: Spines/decorations hidden, generous whitespace, subtle root-circle + stroke + - id: DE-03 + name: Data Storytelling + score: 2 + max: 6 + passed: false + comment: 6 of 13 qualifying subcategory labels are missing, undercutting the + chart's core job of showing hierarchy to the viewer -- Code cluster shows + 0/4 labels + spec_compliance: + score: 12 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: Correct circle-packing chart + - id: SC-02 + name: Required Features + score: 1 + max: 4 + passed: false + comment: '"Display labels for larger circles" fails for the majority of circles + that pass their own labeling threshold -- same unresolved defect as attempt + 2' + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: Leaf circle area (not radius) scales with value via r = sqrt(value) + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 + passed: true + comment: Title matches mandated convention exactly + data_quality: + score: 12 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 3 + max: 6 + passed: false + comment: ~62 nodes across 4 levels is within spec range, but most are functionally + indistinguishable since their labels never render + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Disk-storage folder hierarchy, plausible and neutral + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Sensible MB-scale values for file sizes + code_quality: + score: 8 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 2 + max: 3 + passed: false + comment: Struct + 5 helper functions justified by the plot type but a real + departure from flat-script ideal + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: Random.seed!(42) + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: Only CairoMakie, Colors, Random used + - id: CQ-04 + name: Code Elegance + score: 1 + max: 2 + passed: false + comment: The explicitly-recommended vectorized text!() fix from attempt 2 + review did not resolve the flagged correctness bug -- a real, still-open + correctness gap + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Saves plot-{THEME}.png via save() with px_per_unit=2 + library_mastery: + score: 5 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 2 + max: 5 + passed: false + comment: Correct use of poly!/text!/DataAspect()/hidedecorations!/hidespines!, + but the vectorized text!() call still silently drops most sub-labels across + two different call styles + - id: LM-02 + name: Distinctive Features + score: 3 + max: 5 + passed: true + comment: Hand-rolled tangent-circle packing algorithm remains genuinely distinctive + engineering + verdict: REJECTED +impl_tags: + dependencies: [] + techniques: + - annotations + patterns: + - data-generation + - iteration-over-groups + dataprep: [] + styling: + - alpha-blending + - minimal-chrome