Basic Bubble Chart in lets-plot (Python)

The same plot in 14 other libraries — Python: Altair, Bokeh, Matplotlib, Plotly, plotnine, Pygal, Seaborn; R: ggplot2; Julia: Makie.jl; JavaScript: Chart.js, D3.js, Apache ECharts, Highcharts, MUI X Charts. Compare all 15 side by side: Basic Bubble Chart in Python, R, Julia and JavaScript.

A bubble chart extending scatter plots by adding a third dimension through bubble size. Each point's position shows two variables (x, y) while the bubble size represents a third quantitative variable. This visualization is excellent for understanding relationships between three numerical variables simultaneously, revealing patterns that would be hidden in traditional 2D scatter plots.

Basic Bubble Chart rendered with lets-plot

Renders

Python source (lets-plot)

""" anyplot.ai
bubble-basic: Basic Bubble Chart
Library: letsplot 4.11.0 | Python 3.13.15
Quality: 87/100 | Updated: 2026-09-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_point,
    ggplot,
    ggsave,
    ggsize,
    ggtb,
    guide_legend,
    guides,
    labs,
    layer_tooltips,
    scale_fill_manual,
    scale_size_area,
    scale_x_continuous,
    theme,
    theme_minimal,
)


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"
RULE = "rgba(26,26,23,0.15)" if THEME == "light" else "rgba(240,239,232,0.15)"

# Imprint palette — 8 hues, theme-independent, hybrid-v3 sort
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]

# Data - sports analytics: team payroll vs. win rate, bubble size = average attendance
np.random.seed(42)

conferences = ["Eastern", "Western"]
win_pct_offset = {"Eastern": 0.0, "Western": 5.0}
n_per_conference = 35

rows = []
for conference in conferences:
    payroll_millions = np.random.uniform(90, 190, n_per_conference)
    win_pct = np.clip(
        18 + win_pct_offset[conference] + 0.28 * (payroll_millions - 90) + np.random.normal(0, 9, n_per_conference),
        15,
        78,
    )
    attendance_thousands = np.clip(np.random.normal(28, 7, n_per_conference), 12, 45)
    for payroll, win, attendance in zip(payroll_millions, win_pct, attendance_thousands, strict=True):
        rows.append({"payroll": payroll, "win_pct": win, "attendance": attendance, "conference": conference})

df = pd.DataFrame(rows)

# Plot
plot = (
    ggplot(df, aes(x="payroll", y="win_pct", size="attendance", fill="conference"))
    + geom_point(
        shape=21,
        color=INK_SOFT,
        stroke=0.4,
        alpha=0.55,
        tooltips=layer_tooltips()
        .format("payroll", "${.0f}M")
        .format("win_pct", "{.1f}%")
        .format("attendance", "{.1f}K")
        .line("@conference")
        .line("Payroll|@payroll")
        .line("Win Rate|@win_pct")
        .line("Avg. Attendance|@attendance"),
    )
    + scale_size_area(max_size=13, name="Avg. Attendance (K)", breaks=[15, 25, 35, 45])
    + scale_fill_manual(values=IMPRINT_PALETTE[:2], name="Conference")
    + scale_x_continuous(expand=[0.02, 5])
    + guides(
        fill=guide_legend(nrow=1, override_aes={"size": 7}),
        size=guide_legend(nrow=1, override_aes={"fill": IMPRINT_PALETTE[0], "color": INK_SOFT, "alpha": 0.55}),
    )
    + labs(x="Team Payroll (Million USD)", y="Win Rate (%)", title="bubble-basic · python · letsplot · anyplot.ai")
    + theme_minimal()
    + theme(
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        # color must match fill explicitly: an unset color on panel_background
        # renders a visible default border once a legend is present, even
        # with panel_border=element_blank() (lets-plot 4.11.0 quirk).
        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_border=element_blank(),
        axis_title=element_text(size=13, color=INK),
        axis_text=element_text(size=10, color=INK_SOFT),
        plot_title=element_text(size=18, color=INK),
        plot_margin=[30, 20, 20, 20],
        legend_title=element_text(size=10, color=INK),
        legend_text=element_text(size=10, color=INK_SOFT),
        # color must match fill: legend frames are removed per the
        # decoration-removal checklist (no visible box border).
        legend_background=element_rect(fill=ELEVATED_BG, color=ELEVATED_BG),
        panel_grid_major=element_line(size=0.3, color=RULE),
        panel_grid_minor=element_blank(),
        legend_position="bottom",
        legend_box="horizontal",
        legend_spacing=20,
    )
    + ggsize(800, 450)
)

# Save: static PNG without the interactive toolbar, HTML with letsplot's
# distinctive pan/zoom toolbar (ggtb) layered on top for the interactive export
ggsave(plot, f"plot-{THEME}.png", path=".", scale=4)
ggsave(plot + ggtb(), f"plot-{THEME}.html", path=".")

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/bubble-basic/letsplot/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": "bubble-basic",
  "language": "python",
  "library": "letsplot",
  "page": "https://anyplot.ai/bubble-basic/python/letsplot",
  "hub": "https://anyplot.ai/bubble-basic",
  "code_json": "https://api.anyplot.ai/specs/bubble-basic/letsplot/code",
  "spec_json": "https://api.anyplot.ai/specs/bubble-basic",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/bubble-basic/python/letsplot/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/bubble-basic/python/letsplot/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/bubble-basic/python/letsplot/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/bubble-basic/python/letsplot/plot-dark.html",
  "quality_score": 87.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Basic Bubble Chart on anyplot.ai.

Other implementations