Basic Stream Graph — Makie.jl

A streamgraph (also known as a stacked area chart with a centered baseline) displaying the composition of multiple categories over time with smooth, flowing curves. Unlike traditional stacked area charts, streamgraphs use a symmetric baseline centered around the x-axis, creating an organic, river-like appearance that emphasizes the overall shape and relative proportions of each category while minimizing the visual distortion of individual layers.

Basic Stream Graph rendered with Makie.jl

Renders

Julia source (Makie.jl)

# anyplot.ai
# streamgraph-basic: Basic Stream Graph
# Library: makie 0.21.9 | Julia 1.11.9
# Quality: 84/100 | Created: 2026-08-05

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, ALWAYS first series (Imprint palette)
    colorant"#C475FD",  # 2 — lavender
    colorant"#4467A3",  # 3 — blue
    colorant"#BD8233",  # 4 — ochre
]

# Data — monthly streaming hours (millions) by music genre, 2024-2025
genres = ["Pop", "Hip-Hop", "Rock", "Electronic"]
n_genres = length(genres)
n_months = 24

base_level = [420.0, 260.0, 340.0, 180.0]
trend      = [3.5, 6.5, -4.0, 9.0]
wave_amp   = [55.0, 35.0, 45.0, 65.0]
wave_phase = [0.4, 2.1, 1.0, 3.4]

raw_values = zeros(Float64, n_months, n_genres)
for g in 1:n_genres
    for t in 1:n_months
        seasonal = wave_amp[g] * sin(2π * t / 12 + wave_phase[g])
        noise = randn() * base_level[g] * 0.05
        raw_values[t, g] = max(0.0, base_level[g] + trend[g] * t + seasonal + noise)
    end
end

# Smooth interpolation (Catmull-Rom spline) so each genre's layer flows
# organically between the monthly samples instead of showing sharp knots.
steps_per_segment = 12
n_smooth = (n_months - 1) * steps_per_segment + 1
x_smooth = collect(range(1, n_months; length = n_smooth))
smooth_values = zeros(Float64, n_smooth, n_genres)

for g in 1:n_genres
    idx = 1
    for i in 1:(n_months - 1)
        p0 = raw_values[max(i - 1, 1), g]
        p1 = raw_values[i, g]
        p2 = raw_values[i + 1, g]
        p3 = raw_values[min(i + 2, n_months), g]
        last_step = i == n_months - 1 ? steps_per_segment : steps_per_segment - 1
        for s in 0:last_step
            u = s / steps_per_segment
            u2 = u * u
            u3 = u2 * u
            val = 0.5 * ((2p1) + (-p0 + p2) * u +
                         (2p0 - 5p1 + 4p2 - p3) * u2 +
                         (-p0 + 3p1 - 3p2 + p3) * u3)
            smooth_values[idx, g] = max(val, 0.0)
            idx += 1
        end
    end
end

# Symmetric ("silhouette") baseline centered on zero — the defining streamgraph trait
totals = vec(sum(smooth_values; dims = 2))
baseline = -totals ./ 2
cum = cumsum(smooth_values; dims = 2)
layer_lower = zeros(Float64, n_smooth, n_genres)
layer_upper = zeros(Float64, n_smooth, n_genres)
for g in 1:n_genres
    layer_lower[:, g] = baseline .+ (g == 1 ? zeros(n_smooth) : cum[:, g - 1])
    layer_upper[:, g] = baseline .+ cum[:, g]
end

# Plot
fig = Figure(
    size            = (1600, 900),
    fontsize        = 14,
    backgroundcolor = PAGE_BG,
)

ax = Axis(
    fig[1, 1];
    title              = "streamgraph-basic · julia · makie · anyplot.ai",
    titlesize          = 30,
    titlecolor         = INK,
    titlefont          = :bold,
    xlabel             = "Month",
    xlabelsize         = 16,
    xlabelcolor        = INK,
    ylabel             = "Streaming Hours (millions)",
    ylabelsize         = 16,
    ylabelcolor        = INK,
    xticklabelsize     = 12,
    xticklabelcolor    = INK_SOFT,
    xtickcolor         = INK_SOFT,
    backgroundcolor    = PAGE_BG,
    topspinevisible    = false,
    rightspinevisible  = false,
    leftspinevisible   = false,
    bottomspinecolor   = INK_SOFT,
    yticksvisible      = false,
    yticklabelsvisible = false,
    xgridvisible       = false,
    ygridvisible       = false,
)

for g in 1:n_genres
    band!(ax, x_smooth, layer_lower[:, g], layer_upper[:, g];
        color = IMPRINT_PALETTE[g])
end

# Thin background-colored strokes between adjacent bands sharpen layer
# separation where two saturated fills touch directly (style guide's
# optional outline pattern, applied here as inter-band edging).
for g in 1:(n_genres - 1)
    lines!(ax, x_smooth, layer_upper[:, g]; color = PAGE_BG, linewidth = 1.5)
end

month_names = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
               "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
xtick_positions = [1, 7, 13, 19, 24]
xtick_labels = String[]
for m in xtick_positions
    month_idx = mod1(m, 12)
    year = 2024 + div(m - 1, 12)
    push!(xtick_labels, "$(month_names[month_idx]) $(year)")
end
ax.xticks = (xtick_positions, xtick_labels)
xlims!(ax, 1 - 0.8, n_months + 0.8)

Legend(fig[1, 2],
    [PolyElement(color = IMPRINT_PALETTE[g]) for g in 1:n_genres],
    genres;
    framevisible = false,
    labelcolor   = INK_SOFT,
    labelsize    = 13,
)

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

Part of Basic Stream Graph on anyplot.ai.

Other implementations