Basic Icicle Chart — Makie.jl

An icicle chart displaying hierarchical data as adjacent rectangles in a layered structure, where each rectangle's size represents its value in the hierarchy. Unlike treemaps that nest rectangles, icicle charts stack them in rows (horizontal) or columns (vertical), making parent-child relationships explicitly visible through spatial adjacency. This layout excels at showing both the hierarchy levels and the proportional values simultaneously.

Basic Icicle Chart rendered with Makie.jl

Renders

Julia source (Makie.jl)

# anyplot.ai
# icicle-basic: Basic Icicle Chart
# Library: makie 0.21.9 | Julia 1.11.9
# Quality: 86/100 | Created: 2026-09-05

using CairoMakie
using Colors

# 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"

# Root band and the "other" leaf strip are data-bearing fills (they encode a
# value, not chrome), so -- unlike the usual theme-adaptive neutral/muted
# anchors -- they stay a single fixed hex in both renders.
const FIXED_NEUTRAL = colorant"#726F64"
const FIXED_MUTED   = colorant"#B0AA98"

const IMPRINT_PALETTE = [
    colorant"#009E73", colorant"#C475FD", colorant"#4467A3", colorant"#BD8233",
]

# Icicle layout helpers -- recursive tree partition (data-layout only; the
# plot itself stays top-level, top-down below, mirroring the treemap recipe).
function subtree_value(name, children, raw_value)
    kids = get(children, name, String[])
    isempty(kids) ? raw_value[name] : sum(subtree_value(k, children, raw_value) for k in kids)
end

function leaf_levels!(acc, name, level, children)
    kids = get(children, name, String[])
    if isempty(kids)
        push!(acc, level)
    else
        for k in kids
            leaf_levels!(acc, k, level + 1, children)
        end
    end
end

# Leaves with no children stretch down to the bottom row (no children to
# fill the space below them) -- the classic icicle-chart visual convention.
# `branch` is threaded down from the root's direct children so each rect
# already knows which top-level folder it belongs to (no separate parent
# lookup needed later for coloring).
function layout_icicle!(rects, name, x0, x1, level, max_depth, children, raw_value, branch)
    kids = get(children, name, String[])
    row_span = isempty(kids) ? (max_depth - level + 1) : 1
    push!(rects, (name = name, x0 = x0, x1 = x1, level = level, row_span = row_span, branch = branch))
    if !isempty(kids)
        total = subtree_value(name, children, raw_value)
        cx = x0
        for k in kids
            w = subtree_value(k, children, raw_value) / total * (x1 - x0)
            child_branch = level == 0 ? k : branch
            layout_icicle!(rects, k, cx, cx + w, level + 1, max_depth, children, raw_value, child_branch)
            cx += w
        end
    end
end

# Depth-within-branch tint via perceptually uniform LCHab lightness lift
# toward a fixed ceiling (never the theme background), so a given depth
# renders the identical color in both light and dark PNGs -- only the
# surrounding chrome is allowed to flip.
function tint_lightness(base::RGB, frac::Float64)
    lch      = convert(LCHab, base)
    target_l = min(lch.l + 28.0, 90.0)
    l        = lch.l + frac * (target_l - lch.l)
    convert(RGB, LCHab(l, lch.c, lch.h))
end

# Data ---------------------------------------------------------------------
# Source-tree file sizes (KB) for a small web app repository. Folder sizes
# are the sum of their contents, computed below rather than hard-coded.
tree = [
    ("project", "", 0.0),
    ("src", "project", 0.0),
    ("components", "src", 0.0),
    ("Button.jsx", "components", 12.0),
    ("Modal.jsx", "components", 18.0),
    ("Table.jsx", "components", 25.0),
    ("utils", "src", 0.0),
    ("format.js", "utils", 8.0),
    ("validate.js", "utils", 10.0),
    ("hooks", "src", 0.0),
    ("useAuth.js", "hooks", 14.0),
    ("styles", "src", 0.0),
    ("theme.css", "styles", 6.0),
    ("docs", "project", 0.0),
    ("guides", "docs", 0.0),
    ("setup.md", "guides", 15.0),
    ("deploy.md", "guides", 20.0),
    ("api", "docs", 0.0),
    ("reference.md", "api", 30.0),
    ("tests", "project", 0.0),
    ("unit", "tests", 0.0),
    ("button.test.js", "unit", 9.0),
    ("utils.test.js", "unit", 7.0),
    ("integration", "tests", 0.0),
    ("flow.test.js", "integration", 22.0),
    ("assets", "project", 0.0),
    ("images", "assets", 0.0),
    ("logo.png", "images", 45.0),
    ("banner.png", "images", 60.0),
    ("fonts", "assets", 0.0),
    ("inter.woff2", "fonts", 35.0),
    ("config.json", "project", 5.0),
]

children  = Dict{String,Vector{String}}()
raw_value = Dict{String,Float64}()
for (name, parent, value) in tree
    raw_value[name] = value
    if !isempty(parent)
        push!(get!(children, parent, String[]), name)
    end
end

root = "project"
leaf_lvls = Int[]
leaf_levels!(leaf_lvls, root, 0, children)
max_depth = maximum(leaf_lvls)
total_kb  = subtree_value(root, children, raw_value)

rects = NamedTuple[]
layout_icicle!(rects, root, 0.0, total_kb, 0, max_depth, children, raw_value, "")

# Branch color: the four top-level folders each own a hue; the lone
# root-level file (no folder of its own) reads as "other" via the muted
# anchor. Deeper nodes within a branch lighten toward a shared ceiling so
# depth reads as a lightness ramp without spending extra hues.
branch_color = Dict(
    "src"         => IMPRINT_PALETTE[1],
    "docs"        => IMPRINT_PALETTE[2],
    "tests"       => IMPRINT_PALETTE[3],
    "assets"      => IMPRINT_PALETTE[4],
    "config.json" => FIXED_MUTED,
)

rect_colors = Vector{RGB}(undef, length(rects))
for (i, r) in enumerate(rects)
    if r.name == root
        rect_colors[i] = FIXED_NEUTRAL
    else
        base = branch_color[r.branch]
        frac = max_depth > 1 ? (r.level - 1) / (max_depth - 1) : 0.0
        rect_colors[i] = tint_lightness(base, frac)
    end
end

# Title scaled to fit if it ever runs past the mandated-title baseline.
title_text    = "icicle-basic · julia · makie · anyplot.ai"
title_default = 20
title_size = length(title_text) > 67 ?
    max(round(Int, title_default * 67 / length(title_text)), 14) :
    title_default

# Plot -----------------------------------------------------------------------
fig = Figure(resolution = (1600, 900), backgroundcolor = PAGE_BG)

ax = Axis(
    fig[1, 1];
    title           = title_text,
    titlesize       = title_size,
    titlecolor      = INK,
    backgroundcolor = PAGE_BG,
)
hidedecorations!(ax)
hidespines!(ax)

rows = max_depth + 1  # total row bands, root at the top, deepest leaves at the bottom
for (i, r) in enumerate(rects)
    y0 = rows - (r.level + r.row_span)
    y1 = rows - r.level
    poly!(ax, Rect2f(r.x0, y0, r.x1 - r.x0, y1 - y0);
        color       = rect_colors[i],
        strokecolor = PAGE_BG,
        strokewidth = 2.5,
    )
end

# Labels: name + size, only where the tile is wide enough to hold the text
# without overflowing (spec: hide labels on small nodes). The root band
# spans the full width and always fits.
for (i, r) in enumerate(rects)
    w      = r.x1 - r.x0
    val    = subtree_value(r.name, children, raw_value)
    label  = r.name == root ?
        "$(r.name) · $(round(Int, val)) KB total" :
        "$(r.name)\n$(round(Int, val)) KB"
    maxlen = maximum(length, split(label, '\n'))
    if w >= max(0.032 * total_kb, 0.0055 * total_kb * maxlen)
        y0 = rows - (r.level + r.row_span)
        y1 = rows - r.level
        c  = rect_colors[i]
        text!(ax, r.x0 + w / 2, (y0 + y1) / 2;
            text          = label,
            align         = (:center, :center),
            justification = :center,
            color         = 0.2126 * red(c) + 0.7152 * green(c) + 0.0722 * blue(c) > 0.5 ?
                colorant"#1A1A17" : colorant"#FAF8F1",
            fontsize      = 13,
            font          = r.name == root ? :bold : :regular,
        )
    end
end

xlims!(ax, 0, total_kb)
ylims!(ax, 0, rows)

save("plot-$(THEME).png", fig; px_per_unit = 2)

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/icicle-basic/makie/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "icicle-basic",
  "language": "julia",
  "library": "makie",
  "page": "https://anyplot.ai/icicle-basic/julia/makie",
  "hub": "https://anyplot.ai/icicle-basic",
  "code_json": "https://api.anyplot.ai/specs/icicle-basic/makie/code",
  "spec_json": "https://api.anyplot.ai/specs/icicle-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/icicle-basic/julia/makie/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/icicle-basic/julia/makie/plot-dark.png",
  "quality_score": 86.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Basic Icicle Chart on anyplot.ai.

Other implementations