2D Histogram Heatmap — lets-plot

A two-dimensional histogram that displays the joint distribution of two continuous variables as a heatmap with rectangular bins. Each bin's color intensity represents the frequency or count of data points falling within that region, making it ideal for revealing density patterns, clusters, and correlations in bivariate data. Unlike scatter plots that can become cluttered with large datasets, 2D histograms effectively summarize point density.

2D Histogram Heatmap rendered with lets-plot

Python source (lets-plot)

""" anyplot.ai
histogram-2d: 2D Histogram Heatmap
Library: letsplot 4.9.0 | Python 3.13.13
Quality: 90/100 | Updated: 2026-05-08
"""

import os
import shutil

import numpy as np
import pandas as pd
from lets_plot import *
from lets_plot import ggsave

LetsPlot.setup_html()

# Theme tokens
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
RULE = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)"

# Data - Bivariate normal distribution with correlation (2000 points)
np.random.seed(42)
n_points = 2000

# Simulating: Customer Age vs Annual Spending ($k)
mean = [42, 55]  # Average age 42, average spending $55k
cov = [[120, 60], [60, 200]]  # Positive correlation between age and spending

data = np.random.multivariate_normal(mean, cov, n_points)
age = np.clip(data[:, 0], 18, 75)  # Realistic age range
spending = np.clip(data[:, 1], 5, 120)  # Realistic spending range

df = pd.DataFrame({"age": age, "spending": spending})

# Plot - 2D histogram heatmap using geom_bin2d
plot = (
    ggplot(df, aes(x="age", y="spending"))
    + geom_bin2d(bins=[25, 25], alpha=0.95)
    + scale_fill_viridis(name="Count", option="viridis")
    + labs(x="Customer Age (years)", y="Annual Spending ($k)", title="histogram-2d · letsplot · anyplot.ai")
    + theme_minimal()
    + theme(
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_grid_major=element_line(color=RULE, size=0.3),
        panel_grid_minor=element_blank(),
        plot_title=element_text(size=24, color=INK, face="bold"),
        axis_title=element_text(size=20, color=INK),
        axis_text=element_text(size=16, color=INK_SOFT),
        axis_line=element_line(color=INK_SOFT, size=0.5),
        legend_title=element_text(size=18, color=INK),
        legend_text=element_text(size=16, color=INK_SOFT),
        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
    )
    + ggsize(1600, 900)
)

# Save
ggsave(plot, f"plot-{THEME}.png", scale=3)
ggsave(plot, f"plot-{THEME}.html")

# Move files from lets-plot-images to current directory
if os.path.exists(f"lets-plot-images/plot-{THEME}.png"):
    shutil.move(f"lets-plot-images/plot-{THEME}.png", f"plot-{THEME}.png")
if os.path.exists(f"lets-plot-images/plot-{THEME}.html"):
    shutil.move(f"lets-plot-images/plot-{THEME}.html", f"plot-{THEME}.html")
# Clean up empty directory
if os.path.exists("lets-plot-images") and not os.listdir("lets-plot-images"):
    shutil.rmtree("lets-plot-images")

Part of 2D Histogram Heatmap on anyplot.ai.

Other implementations