Basic Bubble Chart in Plotly (Python)

The same plot in 14 other libraries — Python: Altair, Bokeh, lets-plot, Matplotlib, 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 Plotly

Renders

Python source (Plotly)

""" anyplot.ai
bubble-basic: Basic Bubble Chart
Library: plotly 7.1.0 | Python 3.13.15
Quality: 92/100 | Updated: 2026-09-27
"""

import os

import numpy as np
import plotly.graph_objects as go


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

# Continuous colorscale — imprint_seq (brand green → blue, single-polarity)
imprint_seq = [[0.0, "#009E73"], [1.0, "#4467A3"]]

# Data — Tech companies: R&D investment vs. product-market-fit score
np.random.seed(42)
n = 62
rd_pct = np.random.uniform(4, 42, n)  # R&D spend as % of revenue
pmf = np.clip(rd_pct * 1.3 + np.random.normal(0, 17, n) + 18, 8, 98)
revenue_m = np.clip(np.abs(np.random.normal(750, 380, n)), 40, 2200)

# Anchor key data points for visual storytelling
rd_pct[0], pmf[0], revenue_m[0] = 39, 96, 2100  # R&D powerhouse
rd_pct[1], pmf[1], revenue_m[1] = 36, 91, 1800
rd_pct[2], pmf[2], revenue_m[2] = 33, 87, 1650
rd_pct[5], pmf[5], revenue_m[5] = 7, 24, 180  # underinvestors
rd_pct[6], pmf[6], revenue_m[6] = 5, 18, 120
rd_pct[10], pmf[10], revenue_m[10] = 22, 92, 920  # efficient innovator

# Bubble sizing via sizeref (Plotly's idiomatic area-based scaling).
# Max on-screen diameter capped at 34 and sizemin lowered to 5 so the dense
# low-R&D/low-PMF cluster overlaps less while the lowest-revenue bubbles
# stay clearly visible.
sizeref = 2.0 * float(revenue_m.max()) / (34.0**2)
cmin, cmax = float(revenue_m.min()), float(revenue_m.max())

# Legend swatch colors tinted along imprint_seq (green->blue) so each swatch
# matches what a real data bubble of that revenue would render.
imprint_seq_rgb = (np.array([0x00, 0x9E, 0x73]), np.array([0x44, 0x67, 0xA3]))


title = "R&D Investment vs. Product-Market Fit · bubble-basic · python · plotly · anyplot.ai"
title_size = max(11, round(16 * 67 / len(title))) if len(title) > 67 else 16

# Plot
fig = go.Figure()

fig.add_trace(
    go.Scatter(
        x=rd_pct,
        y=pmf,
        mode="markers",
        customdata=revenue_m,
        marker={
            "size": revenue_m,
            "sizemode": "area",
            "sizeref": sizeref,
            "sizemin": 5,
            "color": revenue_m,
            "cmin": cmin,
            "cmax": cmax,
            "colorscale": imprint_seq,
            "showscale": False,
            "opacity": 0.5,
            "line": {"width": 2, "color": PAGE_BG},
        },
        hovertemplate=(
            "<b>R&D Spend:</b> %{x:.1f}%<br>"
            "<b>PMF Score:</b> %{y:.0f}<br>"
            "<b>Revenue:</b> $%{customdata:.0f}M<extra></extra>"
        ),
        showlegend=False,
    )
)

# Size legend — three reference bubbles (anchored near the data's min/mid/max
# revenue), tinted along imprint_seq so the swatch color matches what a real
# data bubble of that revenue would render
for size_label in [100, 600, 2000]:
    t = max(0.0, min(1.0, (size_label - cmin) / (cmax - cmin)))
    r, g, b = (imprint_seq_rgb[0] + (imprint_seq_rgb[1] - imprint_seq_rgb[0]) * t).round().astype(int)
    fig.add_trace(
        go.Scatter(
            x=[None],
            y=[None],
            mode="markers",
            marker={
                "size": size_label,
                "sizemode": "area",
                "sizeref": sizeref,
                "sizemin": 5,
                "color": f"#{r:02X}{g:02X}{b:02X}",
                "opacity": 0.5,
                "line": {"width": 2, "color": PAGE_BG},
            },
            name=f"${size_label}M",
            showlegend=True,
        )
    )

fig.update_layout(
    autosize=False,
    title={"text": title, "font": {"size": title_size, "color": INK}, "x": 0.5, "xanchor": "center"},
    xaxis={
        "title": {"text": "R&D Spend (% of Revenue)", "font": {"size": 12, "color": INK}},
        "tickfont": {"size": 10, "color": INK_SOFT},
        "gridcolor": GRID,
        "gridwidth": 1,
        "linecolor": INK_SOFT,
        "zeroline": False,
        "ticks": "",
    },
    yaxis={
        "title": {"text": "Product-Market-Fit Score", "font": {"size": 12, "color": INK}},
        "tickfont": {"size": 10, "color": INK_SOFT},
        "gridcolor": GRID,
        "gridwidth": 1,
        "linecolor": INK_SOFT,
        "zeroline": False,
        "ticks": "",
    },
    paper_bgcolor=PAGE_BG,
    plot_bgcolor=PAGE_BG,
    font={"color": INK, "family": "Lato, 'DejaVu Sans', sans-serif"},
    legend={
        "title": {"text": "Revenue", "font": {"size": 10, "color": INK}},
        "font": {"size": 10, "color": INK_SOFT},
        "bgcolor": ELEVATED_BG,
        "bordercolor": INK_SOFT,
        "borderwidth": 0.5,
        "x": 0.02,
        "y": 0.98,
        "xanchor": "left",
        "yanchor": "top",
    },
    margin={"l": 80, "r": 40, "t": 80, "b": 60},
    # Subtle zone highlight around the R&D-powerhouse cluster (indices 0-2:
    # high R&D spend, high PMF, high revenue) — a third layer of visual
    # hierarchy beyond the two point-specific callouts below.
    shapes=[
        {
            "type": "rect",
            "x0": 29,
            "x1": 41,
            "y0": 83,
            "y1": 99,
            "line": {"color": INK_SOFT, "width": 1, "dash": "dot"},
            "fillcolor": GRID,
            "layer": "below",
        }
    ],
    annotations=[
        {
            "x": 35,
            "y": 99,
            "yshift": 6,
            "text": "R&D powerhouses",
            "font": {"size": 9, "color": INK_SOFT},
            "showarrow": False,
            "xanchor": "center",
            "yanchor": "bottom",
        },
        {
            "x": rd_pct[10],
            "y": pmf[10],
            "text": "Efficient innovator<br>(low R&D, high PMF)",
            "font": {"size": 10, "color": INK},
            "showarrow": True,
            "arrowhead": 2,
            "arrowcolor": INK_SOFT,
            "ax": 40,
            "ay": -40,
            "bgcolor": ELEVATED_BG,
            "bordercolor": INK_SOFT,
            "borderwidth": 0.5,
            "borderpad": 4,
        },
        {
            "x": rd_pct[6],
            "y": pmf[6],
            "text": "Underinvested laggard<br>(low R&D, low PMF)",
            "font": {"size": 10, "color": INK},
            "showarrow": True,
            "arrowhead": 2,
            "arrowcolor": INK_SOFT,
            "ax": -50,
            "ay": 40,
            "bgcolor": ELEVATED_BG,
            "bordercolor": INK_SOFT,
            "borderwidth": 0.5,
            "borderpad": 4,
        },
    ],
)

# Save
fig.write_image(f"plot-{THEME}.png", width=800, height=450, scale=4)
fig.write_html(f"plot-{THEME}.html", include_plotlyjs="cdn")

Retrieve this implementation

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

Part of Basic Bubble Chart on anyplot.ai.

Other implementations