How to Create a Bubble Chart in Matplotlib

Bubble charts are scatter plots on steroids. While a standard scatter plot shows the relationship between two variables using x and y coordinates, bubble charts add a third dimension by varying the...

Key Insights

  • Bubble charts extend scatter plots by encoding a third variable through marker size and optionally a fourth through color, making them ideal for multidimensional datasets like population demographics or sales performance metrics.
  • Proper bubble size scaling is critical—raw values often produce illegible charts, so multiply your size array by a constant factor (typically 50-500) and use the s parameter in plt.scatter().
  • Always include a colorbar for color-mapped data and consider adding a legend that shows what different bubble sizes represent, as size perception is less intuitive than position or color.

Introduction to Bubble Charts

Bubble charts are scatter plots on steroids. While a standard scatter plot shows the relationship between two variables using x and y coordinates, bubble charts add a third dimension by varying the size of each marker. You can even encode a fourth variable through color, creating a visualization that packs significant analytical power into a single chart.

Use bubble charts when you need to compare three or more variables simultaneously. Financial analysts use them to plot revenue versus profit margin with bubble size representing market cap. Public health researchers visualize disease prevalence against healthcare spending with population size as bubbles. Marketing teams analyze campaign performance across channels, budget, and conversion rates.

The key advantage over multiple separate charts is context. Seeing all dimensions together reveals patterns and outliers that would remain hidden in isolated two-dimensional plots. However, bubble charts require careful design—poorly scaled bubbles or overcrowded data points quickly become unreadable.

Basic Bubble Chart Setup

Matplotlib’s scatter() function handles bubble charts through its s parameter, which controls marker size. Start with the essential imports and basic data structure.

import matplotlib.pyplot as plt
import numpy as np

# Generate sample data
np.random.seed(42)
x = np.random.rand(20) * 100
y = np.random.rand(20) * 100
sizes = np.random.rand(20) * 1000

# Create basic bubble chart
plt.figure(figsize=(10, 6))
plt.scatter(x, y, s=sizes)
plt.xlabel('X Variable')
plt.ylabel('Y Variable')
plt.title('Basic Bubble Chart')
plt.show()

This creates a functional bubble chart, but it’s bare-bones. The s parameter accepts an array where each value represents the area of the corresponding marker in points squared. Notice we’re using values up to 1000—raw data values often need scaling to produce visible, distinguishable bubbles.

For data from pandas DataFrames, the process is identical:

import pandas as pd

df = pd.DataFrame({
    'metric_a': np.random.rand(20) * 100,
    'metric_b': np.random.rand(20) * 100,
    'size_metric': np.random.rand(20) * 50
})

plt.scatter(df['metric_a'], df['metric_b'], s=df['size_metric'] * 100)

Notice the multiplication by 100 on the size metric. This is your scaling factor—adjust it based on your data range and desired visual impact.

Customizing Bubble Appearance

Raw bubbles are just the start. Control over color, transparency, and edges transforms a basic chart into a professional visualization.

# Create sample data
x = np.random.rand(15) * 100
y = np.random.rand(15) * 100
sizes = np.random.rand(15) * 800

# Customized bubble chart
plt.figure(figsize=(10, 6))
plt.scatter(
    x, y, 
    s=sizes,
    c='steelblue',           # Single color for all bubbles
    alpha=0.6,               # Transparency (0-1)
    edgecolors='navy',       # Border color
    linewidths=2             # Border width
)
plt.xlabel('X Variable')
plt.ylabel('Y Variable')
plt.title('Customized Bubble Chart')
plt.grid(True, alpha=0.3)
plt.show()

The alpha parameter is crucial for overlapping bubbles. Values between 0.5 and 0.7 typically work well, allowing you to see through bubbles to identify underlying data points. Edge colors add definition, preventing bubbles from bleeding together visually.

Size scaling deserves emphasis. If your raw values range from 1 to 100, multiplying by 5-10 often works well. For values in the thousands, divide first, then multiply by a smaller factor. The goal is bubble areas between roughly 50 and 2000 points squared.

# Proper size scaling example
raw_sizes = np.array([1200, 3400, 890, 5600, 2100])
scaled_sizes = (raw_sizes / 100) * 50  # Normalize then scale

Adding Color Mapping and Colorbars

Encoding a fourth variable through color creates truly multidimensional visualizations. Matplotlib’s color mapping system makes this straightforward.

# Generate four-dimensional data
x = np.random.rand(25) * 100
y = np.random.rand(25) * 100
sizes = np.random.rand(25) * 600
colors = np.random.rand(25) * 100  # Fourth dimension

plt.figure(figsize=(10, 6))
scatter = plt.scatter(
    x, y,
    s=sizes,
    c=colors,                # Color values
    cmap='viridis',          # Color map
    alpha=0.6,
    edgecolors='black',
    linewidths=1
)

# Add colorbar
cbar = plt.colorbar(scatter)
cbar.set_label('Color Metric Value', rotation=270, labelpad=20)

plt.xlabel('X Variable')
plt.ylabel('Y Variable')
plt.title('Bubble Chart with Color Mapping')
plt.grid(True, alpha=0.3)
plt.show()

The cmap parameter accepts any Matplotlib colormap. viridis is an excellent default—it’s perceptually uniform and colorblind-friendly. Other good options include plasma, cividis, and coolwarm for diverging data.

The colorbar is essential for interpretation. Without it, viewers can’t decode what the colors mean. The set_label() method adds a descriptive label, with rotation=270 and labelpad positioning it properly on the right side.

Advanced Formatting and Labels

Publication-ready charts need proper labeling, legends, and formatting. Here’s a complete example with all the bells and whistles.

# Generate comprehensive data
np.random.seed(42)
x = np.random.rand(20) * 100
y = np.random.rand(20) * 100
sizes = np.random.rand(20) * 800 + 100
colors = np.random.rand(20) * 100

plt.figure(figsize=(12, 7))
scatter = plt.scatter(
    x, y,
    s=sizes,
    c=colors,
    cmap='plasma',
    alpha=0.6,
    edgecolors='black',
    linewidths=1.5
)

# Colorbar
cbar = plt.colorbar(scatter, pad=0.02)
cbar.set_label('Performance Score', rotation=270, labelpad=25, fontsize=11)

# Labels and title
plt.xlabel('Cost ($)', fontsize=12, fontweight='bold')
plt.ylabel('Revenue ($)', fontsize=12, fontweight='bold')
plt.title('Product Performance Analysis', fontsize=14, fontweight='bold', pad=20)

# Grid
plt.grid(True, alpha=0.3, linestyle='--', linewidth=0.5)

# Size legend (manual)
legend_sizes = [100, 400, 800]
legend_labels = ['Small', 'Medium', 'Large']
legend_bubbles = []

for size in legend_sizes:
    legend_bubbles.append(plt.scatter([], [], s=size, c='gray', alpha=0.6, edgecolors='black'))

plt.legend(legend_bubbles, legend_labels, scatterpoints=1, title='Market Size', 
           loc='upper left', frameon=True, fontsize=10)

plt.tight_layout()
plt.show()

The manual legend for bubble sizes requires creating invisible scatter plots with representative sizes. This gives viewers a reference for interpreting bubble dimensions—critical since size perception is less precise than position.

Real-World Example: Country Statistics

Let’s apply these concepts to a realistic dataset analyzing countries by GDP, life expectancy, and population.

import pandas as pd

# Sample country data
data = {
    'country': ['USA', 'China', 'India', 'Germany', 'Brazil', 'Japan', 'Nigeria', 'Bangladesh'],
    'gdp_per_capita': [65000, 10500, 2100, 46000, 8900, 40000, 2200, 1960],
    'life_expectancy': [78, 77, 70, 81, 75, 84, 54, 72],
    'population_millions': [331, 1440, 1380, 83, 213, 126, 206, 165],
    'continent': ['Americas', 'Asia', 'Asia', 'Europe', 'Americas', 'Asia', 'Africa', 'Asia']
}

df = pd.DataFrame(data)

# Color mapping for continents
continent_colors = {'Asia': 0, 'Europe': 1, 'Americas': 2, 'Africa': 3}
df['color_code'] = df['continent'].map(continent_colors)

plt.figure(figsize=(12, 7))
scatter = plt.scatter(
    df['gdp_per_capita'],
    df['life_expectancy'],
    s=df['population_millions'] * 2,  # Scale population for visibility
    c=df['color_code'],
    cmap='tab10',
    alpha=0.6,
    edgecolors='black',
    linewidths=1.5
)

plt.xlabel('GDP per Capita ($)', fontsize=12, fontweight='bold')
plt.ylabel('Life Expectancy (years)', fontsize=12, fontweight='bold')
plt.title('Country Statistics: GDP, Life Expectancy, and Population', 
          fontsize=14, fontweight='bold', pad=20)

# Add country labels
for idx, row in df.iterrows():
    plt.annotate(row['country'], 
                (row['gdp_per_capita'], row['life_expectancy']),
                xytext=(5, 5), textcoords='offset points', fontsize=9)

plt.grid(True, alpha=0.3, linestyle='--')
plt.tight_layout()
plt.show()

This example demonstrates real-world complexity: three quantitative variables plus categorical continent data. The annotations help identify specific countries, though in crowded charts, you might selectively label only outliers or points of interest.

Best Practices and Common Pitfalls

Scale your bubbles appropriately. Too small and they’re invisible; too large and they overlap excessively. Start with a multiplier around 100 and adjust based on your data range. Test with your actual data distribution, not just averages.

Limit your data points. Bubble charts become unreadable beyond 30-50 points. If you have more data, consider filtering to show only the most relevant items or using interactive visualizations with tooltips.

Choose colorblind-friendly colormaps. Avoid red-green combinations. Stick with viridis, plasma, cividis, or use distinct markers instead of colors when possible.

Don’t encode critical information solely through size. Size perception is imprecise compared to position. Use size for supporting context, not your primary message.

Add reference bubbles in your legend. Show what small, medium, and large bubbles represent with actual size examples, not just text labels.

Consider logarithmic scales for data spanning orders of magnitude. GDP and population data often benefit from log scales on one or both axes.

Bubble charts excel at revealing patterns in complex datasets, but they require thoughtful design. Follow these guidelines and your visualizations will communicate insights clearly rather than confusing your audience.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.