Annotated Heatmap — Makie.jl

A heatmap with numeric values displayed inside each cell, combining color intensity with exact value labels. Essential for correlation matrices, confusion matrices, and any matrix visualization where both pattern recognition and precise values matter. Text color automatically contrasts with background for readability.

Annotated Heatmap rendered with Makie.jl

Renders

Julia source (Makie.jl)

# anyplot.ai
# heatmap-annotated: Annotated Heatmap
# Library: makie 0.21.9 | Julia 1.11.9
# Quality: 83/100 | Created: 2026-08-05

using CairoMakie
using Colors
using Random
using Statistics
using Printf

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 MIDPOINT = THEME == "light" ? colorant"#FAF8F1" : colorant"#1A1A17"

# Imprint diverging colormap — correlations have a meaningful zero midpoint
const ANYPLOT_DIV = cgrad([colorant"#AE3030", MIDPOINT, colorant"#4467A3"])

# Cell-annotation contrast colors — chosen per cell from the fill's own
# luminance, so text stays legible whether the cell lands on the saturated
# red/blue ends or the near-background midpoint of the diverging scale.
const DARK_TEXT  = colorant"#1A1A17"
const LIGHT_TEXT = colorant"#F0EFE8"

# --- Data ---------------------------------------------------------------
variables = [
    "Temperature", "Humidity", "Wind Speed", "Pressure",
    "Rainfall", "Solar Radiation", "Cloud Cover", "Visibility",
]
n_vars = length(variables)
n_samples = 200

cloud_cover = randn(n_samples) .* 15 .+ 50
temperature = randn(n_samples) .* 5 .+ 20
humidity = -0.6 .* temperature .+ randn(n_samples) .* 8 .+ 70
wind_speed = randn(n_samples) .* 3 .+ 15
pressure = -0.4 .* temperature .+ randn(n_samples) .* 5 .+ 1013
rainfall = 0.5 .* humidity .+ randn(n_samples) .* 10
solar_radiation = 0.7 .* temperature .- 0.4 .* cloud_cover .+ randn(n_samples) .* 10 .+ 200
visibility = -0.5 .* humidity .- 0.3 .* cloud_cover .+ randn(n_samples) .* 5 .+ 20

measurements = hcat(
    temperature, humidity, wind_speed, pressure,
    rainfall, solar_radiation, cloud_cover, visibility,
)
corr_matrix = cor(measurements)

# --- Plot -----------------------------------------------------------------
fig = Figure(
    resolution = (1200, 1200),
    fontsize = 14,
    backgroundcolor = PAGE_BG,
)

ax = Axis(
    fig[1, 1];
    title = "heatmap-annotated · julia · makie · anyplot.ai",
    titlesize = 20,
    titlecolor = INK,
    xticks = (1:n_vars, variables),
    yticks = (1:n_vars, variables),
    xticklabelrotation = pi / 4,
    xticklabelcolor = INK_SOFT,
    yticklabelcolor = INK_SOFT,
    xticklabelsize = 13,
    yticklabelsize = 13,
    backgroundcolor = PAGE_BG,
    topspinevisible = false,
    rightspinevisible = false,
    leftspinevisible = false,
    bottomspinevisible = false,
    xgridvisible = false,
    ygridvisible = false,
    aspect = DataAspect(),
    yreversed = true,
)

# Diagonal self-correlations (always 1.00) are redundant next to the
# off-diagonal relationships the matrix exists to reveal, so the fill mask
# swaps them for NaN and renders them in a muted neutral tone instead of
# solid colormap blue -- the value still prints, just visually quieted.
const NEUTRAL_FILL = RGBA(convert(RGB, INK_SOFT), 0.12)
display_matrix = copy(corr_matrix)
for i in 1:n_vars
    display_matrix[i, i] = NaN
end

hm = heatmap!(
    ax, 1:n_vars, 1:n_vars, display_matrix;
    colormap = ANYPLOT_DIV, colorrange = (-1, 1), nan_color = NEUTRAL_FILL,
)

# Subtle grid at cell boundaries keeps near-zero-correlation cells (whose
# fill sits close to the theme background by design) visually separated
# from the canvas without touching the data colors themselves.
for k in 0.5:(n_vars + 0.5)
    hlines!(ax, k; xmin = 0, xmax = 1, color = (INK_SOFT, 0.15), linewidth = 1)
    vlines!(ax, k; ymin = 0, ymax = 1, color = (INK_SOFT, 0.15), linewidth = 1)
end

const STRONG_THRESHOLD = 0.5
for i in 1:n_vars, j in 1:n_vars
    value = corr_matrix[i, j]
    is_diagonal = i == j
    is_strong = !is_diagonal && abs(value) >= STRONG_THRESHOLD

    if is_diagonal
        text_color = INK_SOFT
        label_fontsize = 13
    else
        fill_color = ANYPLOT_DIV[(value + 1) / 2]
        luminance = 0.299 * red(fill_color) + 0.587 * green(fill_color) + 0.114 * blue(fill_color)
        text_color = luminance > 0.5 ? DARK_TEXT : LIGHT_TEXT
        label_fontsize = 16
    end

    # A crisp outline calls out the strongest relationships (|r| >= 0.5) so
    # they read as the focal point instead of competing equally with weak
    # correlations for attention.
    if is_strong
        poly!(
            ax, Rect2f(i - 0.5, j - 0.5, 1, 1);
            color = :transparent, strokecolor = INK, strokewidth = 2.5,
        )
    end

    text!(
        ax, i, j;
        text = @sprintf("%.2f", value),
        align = (:center, :center),
        fontsize = label_fontsize,
        font = is_strong ? :bold : :regular,
        color = text_color,
    )
end

Colorbar(
    fig[1, 2], hm;
    label = "Correlation",
    labelsize = 14,
    labelcolor = INK,
    ticklabelsize = 12,
    ticklabelcolor = INK_SOFT,
    tickcolor = INK_SOFT,
)

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

Part of Annotated Heatmap on anyplot.ai.

Other implementations