{"spec_id":"icicle-basic","library":"makie","language":"julia","code":"# anyplot.ai\n# icicle-basic: Basic Icicle Chart\n# Library: makie 0.21.9 | Julia 1.11.9\n# Quality: 86/100 | Created: 2026-09-05\n\nusing CairoMakie\nusing Colors\n\n# Theme tokens -----------------------------------------------------------------\nconst THEME    = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG  = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst INK      = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\n\n# Root band and the \"other\" leaf strip are data-bearing fills (they encode a\n# value, not chrome), so -- unlike the usual theme-adaptive neutral/muted\n# anchors -- they stay a single fixed hex in both renders.\nconst FIXED_NEUTRAL = colorant\"#726F64\"\nconst FIXED_MUTED   = colorant\"#B0AA98\"\n\nconst IMPRINT_PALETTE = [\n    colorant\"#009E73\", colorant\"#C475FD\", colorant\"#4467A3\", colorant\"#BD8233\",\n]\n\n# Icicle layout helpers -- recursive tree partition (data-layout only; the\n# plot itself stays top-level, top-down below, mirroring the treemap recipe).\nfunction subtree_value(name, children, raw_value)\n    kids = get(children, name, String[])\n    isempty(kids) ? raw_value[name] : sum(subtree_value(k, children, raw_value) for k in kids)\nend\n\nfunction leaf_levels!(acc, name, level, children)\n    kids = get(children, name, String[])\n    if isempty(kids)\n        push!(acc, level)\n    else\n        for k in kids\n            leaf_levels!(acc, k, level + 1, children)\n        end\n    end\nend\n\n# Leaves with no children stretch down to the bottom row (no children to\n# fill the space below them) -- the classic icicle-chart visual convention.\n# `branch` is threaded down from the root's direct children so each rect\n# already knows which top-level folder it belongs to (no separate parent\n# lookup needed later for coloring).\nfunction layout_icicle!(rects, name, x0, x1, level, max_depth, children, raw_value, branch)\n    kids = get(children, name, String[])\n    row_span = isempty(kids) ? (max_depth - level + 1) : 1\n    push!(rects, (name = name, x0 = x0, x1 = x1, level = level, row_span = row_span, branch = branch))\n    if !isempty(kids)\n        total = subtree_value(name, children, raw_value)\n        cx = x0\n        for k in kids\n            w = subtree_value(k, children, raw_value) / total * (x1 - x0)\n            child_branch = level == 0 ? k : branch\n            layout_icicle!(rects, k, cx, cx + w, level + 1, max_depth, children, raw_value, child_branch)\n            cx += w\n        end\n    end\nend\n\n# Depth-within-branch tint via perceptually uniform LCHab lightness lift\n# toward a fixed ceiling (never the theme background), so a given depth\n# renders the identical color in both light and dark PNGs -- only the\n# surrounding chrome is allowed to flip.\nfunction tint_lightness(base::RGB, frac::Float64)\n    lch      = convert(LCHab, base)\n    target_l = min(lch.l + 28.0, 90.0)\n    l        = lch.l + frac * (target_l - lch.l)\n    convert(RGB, LCHab(l, lch.c, lch.h))\nend\n\n# Data ---------------------------------------------------------------------\n# Source-tree file sizes (KB) for a small web app repository. Folder sizes\n# are the sum of their contents, computed below rather than hard-coded.\ntree = [\n    (\"project\", \"\", 0.0),\n    (\"src\", \"project\", 0.0),\n    (\"components\", \"src\", 0.0),\n    (\"Button.jsx\", \"components\", 12.0),\n    (\"Modal.jsx\", \"components\", 18.0),\n    (\"Table.jsx\", \"components\", 25.0),\n    (\"utils\", \"src\", 0.0),\n    (\"format.js\", \"utils\", 8.0),\n    (\"validate.js\", \"utils\", 10.0),\n    (\"hooks\", \"src\", 0.0),\n    (\"useAuth.js\", \"hooks\", 14.0),\n    (\"styles\", \"src\", 0.0),\n    (\"theme.css\", \"styles\", 6.0),\n    (\"docs\", \"project\", 0.0),\n    (\"guides\", \"docs\", 0.0),\n    (\"setup.md\", \"guides\", 15.0),\n    (\"deploy.md\", \"guides\", 20.0),\n    (\"api\", \"docs\", 0.0),\n    (\"reference.md\", \"api\", 30.0),\n    (\"tests\", \"project\", 0.0),\n    (\"unit\", \"tests\", 0.0),\n    (\"button.test.js\", \"unit\", 9.0),\n    (\"utils.test.js\", \"unit\", 7.0),\n    (\"integration\", \"tests\", 0.0),\n    (\"flow.test.js\", \"integration\", 22.0),\n    (\"assets\", \"project\", 0.0),\n    (\"images\", \"assets\", 0.0),\n    (\"logo.png\", \"images\", 45.0),\n    (\"banner.png\", \"images\", 60.0),\n    (\"fonts\", \"assets\", 0.0),\n    (\"inter.woff2\", \"fonts\", 35.0),\n    (\"config.json\", \"project\", 5.0),\n]\n\nchildren  = Dict{String,Vector{String}}()\nraw_value = Dict{String,Float64}()\nfor (name, parent, value) in tree\n    raw_value[name] = value\n    if !isempty(parent)\n        push!(get!(children, parent, String[]), name)\n    end\nend\n\nroot = \"project\"\nleaf_lvls = Int[]\nleaf_levels!(leaf_lvls, root, 0, children)\nmax_depth = maximum(leaf_lvls)\ntotal_kb  = subtree_value(root, children, raw_value)\n\nrects = NamedTuple[]\nlayout_icicle!(rects, root, 0.0, total_kb, 0, max_depth, children, raw_value, \"\")\n\n# Branch color: the four top-level folders each own a hue; the lone\n# root-level file (no folder of its own) reads as \"other\" via the muted\n# anchor. Deeper nodes within a branch lighten toward a shared ceiling so\n# depth reads as a lightness ramp without spending extra hues.\nbranch_color = Dict(\n    \"src\"         => IMPRINT_PALETTE[1],\n    \"docs\"        => IMPRINT_PALETTE[2],\n    \"tests\"       => IMPRINT_PALETTE[3],\n    \"assets\"      => IMPRINT_PALETTE[4],\n    \"config.json\" => FIXED_MUTED,\n)\n\nrect_colors = Vector{RGB}(undef, length(rects))\nfor (i, r) in enumerate(rects)\n    if r.name == root\n        rect_colors[i] = FIXED_NEUTRAL\n    else\n        base = branch_color[r.branch]\n        frac = max_depth > 1 ? (r.level - 1) / (max_depth - 1) : 0.0\n        rect_colors[i] = tint_lightness(base, frac)\n    end\nend\n\n# Title scaled to fit if it ever runs past the mandated-title baseline.\ntitle_text    = \"icicle-basic · julia · makie · anyplot.ai\"\ntitle_default = 20\ntitle_size = length(title_text) > 67 ?\n    max(round(Int, title_default * 67 / length(title_text)), 14) :\n    title_default\n\n# Plot -----------------------------------------------------------------------\nfig = Figure(resolution = (1600, 900), backgroundcolor = PAGE_BG)\n\nax = Axis(\n    fig[1, 1];\n    title           = title_text,\n    titlesize       = title_size,\n    titlecolor      = INK,\n    backgroundcolor = PAGE_BG,\n)\nhidedecorations!(ax)\nhidespines!(ax)\n\nrows = max_depth + 1  # total row bands, root at the top, deepest leaves at the bottom\nfor (i, r) in enumerate(rects)\n    y0 = rows - (r.level + r.row_span)\n    y1 = rows - r.level\n    poly!(ax, Rect2f(r.x0, y0, r.x1 - r.x0, y1 - y0);\n        color       = rect_colors[i],\n        strokecolor = PAGE_BG,\n        strokewidth = 2.5,\n    )\nend\n\n# Labels: name + size, only where the tile is wide enough to hold the text\n# without overflowing (spec: hide labels on small nodes). The root band\n# spans the full width and always fits.\nfor (i, r) in enumerate(rects)\n    w      = r.x1 - r.x0\n    val    = subtree_value(r.name, children, raw_value)\n    label  = r.name == root ?\n        \"$(r.name) · $(round(Int, val)) KB total\" :\n        \"$(r.name)\\n$(round(Int, val)) KB\"\n    maxlen = maximum(length, split(label, '\\n'))\n    if w >= max(0.032 * total_kb, 0.0055 * total_kb * maxlen)\n        y0 = rows - (r.level + r.row_span)\n        y1 = rows - r.level\n        c  = rect_colors[i]\n        text!(ax, r.x0 + w / 2, (y0 + y1) / 2;\n            text          = label,\n            align         = (:center, :center),\n            justification = :center,\n            color         = 0.2126 * red(c) + 0.7152 * green(c) + 0.0722 * blue(c) > 0.5 ?\n                colorant\"#1A1A17\" : colorant\"#FAF8F1\",\n            fontsize      = 13,\n            font          = r.name == root ? :bold : :regular,\n        )\n    end\nend\n\nxlims!(ax, 0, total_kb)\nylims!(ax, 0, rows)\n\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}