Basic Density Plot — lets-plot

A density plot (also known as Kernel Density Estimation or KDE plot) visualizes the distribution of a continuous variable by smoothing the data into a continuous probability density curve. Unlike histograms which use discrete bins, density plots provide a smooth representation of the underlying distribution, making it easier to identify patterns such as skewness, modality, and overall shape.

Basic Density Plot rendered with lets-plot

Python source (lets-plot)

""" anyplot.ai
density-basic: Basic Density Plot
Library: letsplot 4.10.1 | Python 3.13.13
Quality: 89/100 | Updated: 2026-05-30
"""

import os

import numpy as np
import pandas as pd
from lets_plot import (
    LetsPlot,
    aes,
    element_blank,
    element_line,
    element_rect,
    element_text,
    geom_density,
    geom_segment,
    geom_text,
    geom_vline,
    ggplot,
    ggsize,
    labs,
    layer_tooltips,
    scale_x_continuous,
    scale_y_continuous,
    theme,
    theme_minimal,
)
from lets_plot.export import ggsave


LetsPlot.setup_html()

# Theme tokens — Imprint palette, theme-adaptive chrome
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"
BRAND = "#009E73"  # Imprint palette position 1 — always first series

# Data - Marathon finish times: trimodal distribution with realistic right skew
np.random.seed(42)
finish_minutes = np.concatenate(
    [
        np.random.normal(240, 25, 350),  # Main pack (~4 hour runners)
        np.random.normal(200, 15, 100),  # Competitive runners (~3:20)
        np.random.normal(300, 20, 50),  # Casual runners (~5 hours)
    ]
)
finish_minutes = np.clip(finish_minutes, 140, 400)
df = pd.DataFrame({"time": finish_minutes})

# Rug data: individual observations as small vertical ticks at the x-axis
rug_df = pd.DataFrame({"x": finish_minutes, "y0": 0.0, "y1": 0.0003})

# Peak annotation data for the three runner sub-populations
peaks_df = pd.DataFrame(
    {
        "x": [200, 240, 300],
        "y": [0.0065, 0.0110, 0.0030],
        "label": ["Elite\n~3:20", "Main Pack\n~4:00", "Casual\n~5:00"],
    }
)

# Title
title = "density-basic · python · letsplot · anyplot.ai"

# Chrome theme (theme-adaptive background, text, grid)
anyplot_theme = theme(
    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
    panel_background=element_rect(fill=PAGE_BG),
    panel_border=element_blank(),
    panel_grid_major_x=element_blank(),
    panel_grid_major_y=element_line(color=INK_SOFT, size=0.3),
    panel_grid_minor=element_blank(),
    axis_title=element_text(color=INK, size=12),
    axis_text=element_text(color=INK_SOFT, size=10),
    axis_ticks=element_blank(),
    plot_title=element_text(color=INK, size=16),
    legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
)

# Plot
plot = (
    ggplot(df, aes(x="time"))
    + geom_vline(data=peaks_df, mapping=aes(xintercept="x"), color=INK_SOFT, linetype="dashed", size=0.5, alpha=0.6)
    + geom_density(
        fill=BRAND,
        color=BRAND,
        alpha=0.35,
        size=1.5,
        kernel="gaussian",
        adjust=0.85,
        trim=True,
        tooltips=layer_tooltips().line("density|@..density.."),
    )
    + geom_segment(data=rug_df, mapping=aes(x="x", y="y0", xend="x", yend="y1"), color=BRAND, alpha=0.40, size=0.6)
    + geom_text(data=peaks_df, mapping=aes(x="x", y="y", label="label"), color=INK, size=3.5, vjust=0, hjust=0.5)
    + labs(x="Finish Time (minutes)", y="Density (×10⁻³)", title=title)
    + scale_x_continuous(breaks=list(range(150, 401, 50)))
    + scale_y_continuous(
        breaks=[0.002, 0.004, 0.006, 0.008, 0.010], labels=["2", "4", "6", "8", "10"], expand=[0.02, 0, 0.15, 0]
    )
    + theme_minimal()
    + anyplot_theme
    + ggsize(800, 450)
)

# Save — theme-suffixed filenames, scale=4 → 3200×1800 px
ggsave(plot, f"plot-{THEME}.png", path=".", scale=4)
ggsave(plot, f"plot-{THEME}.html", path=".")

Part of Basic Density Plot on anyplot.ai.

Other implementations