Loading Module…

πŸ“Š Matplotlib

32 topics • Click any card to expand

1. Line Plot

The most basic Matplotlib plot. Use plt.plot() for time series, trends, and continuous data. Always label axes and add a title.

Simple line plot
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2 * np.pi, 200)
y = np.sin(x)

plt.figure(figsize=(8, 4))
plt.plot(x, y, color='steelblue', linewidth=2, label='sin(x)')
plt.xlabel('x')
plt.ylabel('y')
plt.title('Sine Wave')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('line_simple.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved line_simple.png')
Multiple lines
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2 * np.pi, 200)

plt.figure(figsize=(8, 4))
plt.plot(x, np.sin(x),     label='sin(x)',    linewidth=2)
plt.plot(x, np.cos(x),     label='cos(x)',    linewidth=2, linestyle='--')
plt.plot(x, np.sin(2 * x), label='sin(2x)',   linewidth=1, alpha=0.7)
plt.xlabel('x')
plt.ylabel('y')
plt.title('Trig Functions')
plt.legend()
plt.axhline(0, color='gray', linewidth=0.8)
plt.tight_layout()
plt.savefig('line_multi.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved line_multi.png')
Line styles, markers, and fill_between
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(0)
x = np.linspace(0, 10, 50)
y1 = np.sin(x) + np.random.randn(50) * 0.1
y2 = np.cos(x) + np.random.randn(50) * 0.1
err = np.abs(np.random.randn(50) * 0.15)

fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(x, y1, 'o-', color='steelblue', markersize=4, linewidth=1.5, label='Signal A')
ax.plot(x, y2, 's--', color='tomato', markersize=4, linewidth=1.5, label='Signal B')
# Confidence band around Signal A
ax.fill_between(x, y1 - err, y1 + err, alpha=0.2, color='steelblue', label='Β±1Οƒ band')
ax.axhline(0, color='gray', linewidth=0.7, linestyle=':')
ax.set_xlabel('Time'); ax.set_ylabel('Amplitude')
ax.set_title('Line Styles, Markers & Confidence Band')
ax.legend(fontsize=9); ax.grid(True, alpha=0.25)
plt.tight_layout()
plt.savefig('line_styles.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved line_styles.png')
Step plot and errorbar
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(4)
x = np.arange(1, 13)
y = np.array([5, 8, 6, 10, 13, 11, 15, 14, 17, 16, 20, 22], dtype=float)
yerr = np.random.uniform(0.5, 2.0, 12)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

# Step plot β€” useful for histograms, digital signals, and discrete data
ax1.step(x, y, where='mid', color='steelblue', linewidth=2, label='Step (mid)')
ax1.step(x, y + 3, where='post', color='tomato', linewidth=2,
         linestyle='--', label='Step (post)')
ax1.fill_between(x, y, step='mid', alpha=0.15, color='steelblue')
ax1.set_xlabel('Month'); ax1.set_ylabel('Value')
ax1.set_title('Step Plot Variants')
ax1.legend(fontsize=9); ax1.grid(True, alpha=0.25)

# Errorbar plot β€” shows measurement uncertainty
ax2.errorbar(x, y, yerr=yerr, fmt='o-', color='#2ecc71', ecolor='gray',
             elinewidth=1.5, capsize=4, capthick=1.5, linewidth=2,
             markersize=6, markerfacecolor='white', markeredgewidth=2,
             label='Mean Β± std')
ax2.set_xlabel('Month'); ax2.set_ylabel('Measurement')
ax2.set_title('Errorbar Plot')
ax2.legend(fontsize=9); ax2.grid(True, alpha=0.25)

plt.tight_layout()
plt.savefig('line_step_errorbar.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved line_step_errorbar.png')
💼 Real-World: Website Traffic Trend
A marketing analyst plots 30-day daily visitor counts with a 7-day moving average to spot trends.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
days    = np.arange(1, 31)
traffic = 1000 + np.cumsum(np.random.randn(30) * 50) + np.random.randn(30) * 80
ma7     = np.convolve(traffic, np.ones(7)/7, mode='same')

fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(days, traffic, alpha=0.4, color='steelblue', label='Daily visitors')
ax.plot(days, ma7,     color='steelblue', linewidth=2.5, label='7-day MA')
ax.fill_between(days, traffic, alpha=0.1, color='steelblue')
ax.set_xlabel('Day of Month')
ax.set_ylabel('Visitors')
ax.set_title('Website Traffic β€” January 2024')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('traffic_trend.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved traffic_trend.png')
🏋️ Practice: Multiple Lines with Different Styles
Plot sin(x), sin(2x), and sin(3x) on the same axes over [0, 2Ο€]. Each line should use a different color, linestyle, and marker. Add a legend, axis labels, title, and a horizontal dashed line at y=0. Save the figure as 'practice_lines.png'.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 2 * np.pi, 200)

fig, ax = plt.subplots(figsize=(9, 4))

# TODO: plot sin(x) β€” solid line, circle markers, steelblue
# ax.plot(x, np.sin(x), ...)

# TODO: plot sin(2x) β€” dashed line, square markers, tomato
# ax.plot(x, np.sin(2*x), ...)

# TODO: plot sin(3x) β€” dotted line, triangle markers, green
# ax.plot(x, np.sin(3*x), ...)

# TODO: add horizontal dashed line at y=0
# ax.axhline(...)

# TODO: labels, title, legend, grid
ax.set_xlabel('x')
ax.set_ylabel('y')
# ax.set_title(...)
# ax.legend()
# ax.grid(...)

plt.tight_layout()
# TODO: plt.savefig('practice_lines.png', dpi=100, bbox_inches='tight')
plt.close()
print('Done')
✅ Practice Checklist
2. Bar Chart

Bar charts compare discrete categories. Use plt.bar() for vertical and plt.barh() for horizontal. Add value labels for clarity.

Vertical bar chart
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

categories = ['Python', 'JavaScript', 'Java', 'C++', 'Rust']
values     = [67.4, 63.6, 35.4, 24.1, 13.2]
colors     = ['#4C72B0','#DD8452','#55A868','#C44E52','#8172B2']

fig, ax = plt.subplots(figsize=(8, 5))
bars = ax.bar(categories, values, color=colors, edgecolor='white', linewidth=0.5)

# Add value labels on top
for bar, val in zip(bars, values):
    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
            f'{val}%', ha='center', va='bottom', fontsize=9)

ax.set_ylabel('Popularity (%)')
ax.set_title('Most Popular Programming Languages 2024')
ax.set_ylim(0, 80)
ax.grid(True, axis='y', alpha=0.3)
plt.tight_layout()
plt.savefig('bar_vertical.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved bar_vertical.png')
Grouped and stacked bar chart
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

quarters = ['Q1', 'Q2', 'Q3', 'Q4']
product_a = [120, 150, 180, 210]
product_b = [80,  95,  110, 130]
x = np.arange(len(quarters))
w = 0.35

fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# Grouped
axes[0].bar(x - w/2, product_a, w, label='Product A', color='#4C72B0')
axes[0].bar(x + w/2, product_b, w, label='Product B', color='#DD8452')
axes[0].set_xticks(x); axes[0].set_xticklabels(quarters)
axes[0].set_title('Grouped'); axes[0].legend()

# Stacked
axes[1].bar(quarters, product_a, label='Product A', color='#4C72B0')
axes[1].bar(quarters, product_b, bottom=product_a, label='Product B', color='#DD8452')
axes[1].set_title('Stacked'); axes[1].legend()

plt.tight_layout()
plt.savefig('bar_grouped_stacked.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved bar_grouped_stacked.png')
Horizontal bar chart with value labels
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

skills  = ['Python', 'SQL', 'Machine Learning', 'Data Viz', 'Statistics', 'Deep Learning']
scores  = [92, 85, 78, 70, 65, 55]
colors  = ['#4C72B0' if s >= 80 else '#DD8452' if s >= 65 else '#C44E52' for s in scores]

fig, ax = plt.subplots(figsize=(8, 5))
bars = ax.barh(skills, scores, color=colors, edgecolor='white', linewidth=0.5)

for bar, val in zip(bars, scores):
    ax.text(val + 0.5, bar.get_y() + bar.get_height()/2,
            f'{val}%', va='center', fontsize=9)

ax.set_xlabel('Proficiency Score (%)')
ax.set_title('Data Science Skill Assessment')
ax.set_xlim(0, 105)
ax.axvline(80, color='gray', linestyle='--', linewidth=1, alpha=0.6, label='Expert threshold')
ax.legend(fontsize=9); ax.grid(True, axis='x', alpha=0.3)
plt.tight_layout()
plt.savefig('bar_horizontal.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved bar_horizontal.png')
Bar chart with error bars and significance markers
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(21)
conditions = ['Control', 'Treatment A', 'Treatment B', 'Treatment C']
means  = np.array([45.2, 52.7, 61.3, 58.9])
errors = np.array([3.1,  4.2,  3.8,  4.5])   # standard deviations
colors = ['#8172B2', '#4C72B0', '#55A868', '#DD8452']

fig, ax = plt.subplots(figsize=(8, 5))
x = np.arange(len(conditions))

bars = ax.bar(x, means, yerr=errors, color=colors, edgecolor='white',
              linewidth=0.5, capsize=6, error_kw=dict(elinewidth=1.5, ecolor='black'))

# Add significance stars between bars
pairs = [(0, 2, '***'), (1, 2, '*')]   # (bar_i, bar_j, label)
y_max = (means + errors).max()
for i, j, sig in pairs:
    y = y_max + 4 + pairs.index((i, j, sig)) * 5
    ax.annotate('', xy=(j, y), xytext=(i, y),
                arrowprops=dict(arrowstyle='-', color='black', lw=1.2))
    ax.text((i + j) / 2, y + 0.3, sig, ha='center', va='bottom', fontsize=11)

# Value labels above each bar
for bar, m, e in zip(bars, means, errors):
    ax.text(bar.get_x() + bar.get_width()/2, m + e + 0.8,
            f'{m:.1f}', ha='center', va='bottom', fontsize=9)

ax.set_xticks(x); ax.set_xticklabels(conditions)
ax.set_ylabel('Response Score'); ax.set_title('Experimental Results with Error Bars')
ax.set_ylim(0, y_max + 18); ax.grid(True, axis='y', alpha=0.3)
plt.tight_layout()
plt.savefig('bar_errorbars.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved bar_errorbars.png')
💼 Real-World: Quarterly Sales by Region
A sales director compares quarterly revenue across four regions with a horizontal bar chart for a board presentation.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

regions = ['APAC', 'EMEA', 'LATAM', 'North America']
q1 = [2.1, 3.4, 1.2, 5.6]
q2 = [2.5, 3.8, 1.5, 6.1]
y  = np.arange(len(regions))
h  = 0.35

fig, ax = plt.subplots(figsize=(9, 4))
b1 = ax.barh(y + h/2, q1, h, label='Q1 2024', color='#4C72B0')
b2 = ax.barh(y - h/2, q2, h, label='Q2 2024', color='#55A868')

for bar in list(b1) + list(b2):
    w = bar.get_width()
    ax.text(w + 0.05, bar.get_y() + bar.get_height()/2,
            f'${w:.1f}M', va='center', fontsize=8)

ax.set_yticks(y); ax.set_yticklabels(regions)
ax.set_xlabel('Revenue (USD millions)')
ax.set_title('Q1 vs Q2 2024 Revenue by Region')
ax.legend(); ax.grid(True, axis='x', alpha=0.3)
ax.set_xlim(0, 8)
plt.tight_layout()
plt.savefig('bar_region.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved bar_region.png')
🏋️ Practice: Grouped Bar Chart
Create a grouped bar chart comparing sales of 3 products (A, B, C) across 4 quarters (Q1-Q4). Use distinct colors per product, offset the bars within each group, add value labels on top, and include a legend. Save as 'practice_grouped_bar.png'.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

quarters  = ['Q1', 'Q2', 'Q3', 'Q4']
product_a = [45, 60, 55, 80]
product_b = [30, 45, 70, 65]
product_c = [20, 35, 40, 55]

x = np.arange(len(quarters))
w = 0.25   # bar width

fig, ax = plt.subplots(figsize=(9, 5))

# TODO: plot three groups of bars, offset by -w, 0, +w
# bars_a = ax.bar(x - w, product_a, w, label='Product A', color='#4C72B0')
# bars_b = ax.bar(x,     product_b, w, label='Product B', color='#DD8452')
# bars_c = ax.bar(x + w, product_c, w, label='Product C', color='#55A868')

# TODO: add value labels on top of each bar
# for bars in [bars_a, bars_b, bars_c]:
#     for bar in bars:
#         ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
#                 str(int(bar.get_height())), ha='center', va='bottom', fontsize=8)

ax.set_xticks(x)
ax.set_xticklabels(quarters)
# TODO: ax.set_xlabel(...), ax.set_ylabel(...), ax.set_title(...)
# TODO: ax.legend()
ax.grid(True, axis='y', alpha=0.3)
plt.tight_layout()
# TODO: plt.savefig('practice_grouped_bar.png', dpi=100, bbox_inches='tight')
plt.close()
print('Done')
✅ Practice Checklist
3. Scatter Plot

Scatter plots reveal relationships between two numeric variables. Control color, size, and alpha to encode extra dimensions.

Basic scatter and color mapping
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
x = np.random.randn(200)
y = 0.7 * x + np.random.randn(200) * 0.6

fig, ax = plt.subplots(figsize=(6, 5))
sc = ax.scatter(x, y, c=y, cmap='RdYlGn', alpha=0.7, edgecolors='white', linewidth=0.3)
plt.colorbar(sc, label='y value')

# Trend line
m, b = np.polyfit(x, y, 1)
xline = np.linspace(x.min(), x.max(), 100)
ax.plot(xline, m * xline + b, 'k--', linewidth=1.5, label=f'y={m:.2f}x+{b:.2f}')

ax.set_xlabel('X'); ax.set_ylabel('Y')
ax.set_title('Scatter with Trend Line')
ax.legend()
plt.tight_layout()
plt.savefig('scatter_basic.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved scatter_basic.png')
Bubble chart (size as 3rd dimension)
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(7)
n = 20
x    = np.random.uniform(10, 100, n)
y    = np.random.uniform(5, 50, n)
size = np.random.uniform(50, 800, n)   # bubble size
color= np.random.rand(n)

fig, ax = plt.subplots(figsize=(7, 5))
sc = ax.scatter(x, y, s=size, c=color, cmap='viridis', alpha=0.6,
                edgecolors='white', linewidth=0.5)
plt.colorbar(sc, label='Category')
ax.set_xlabel('Revenue ($K)'); ax.set_ylabel('Profit Margin (%)')
ax.set_title('Product Portfolio β€” Bubble Chart')
plt.tight_layout()
plt.savefig('scatter_bubble.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved scatter_bubble.png')
Color-mapped scatter with categorical legend
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(3)
n_per_class = 80
classes = ['Class A', 'Class B', 'Class C']
centers = [(-2, -2), (0, 2), (3, 0)]
colors  = ['#e74c3c', '#3498db', '#2ecc71']

fig, ax = plt.subplots(figsize=(7, 6))
for cls, center, color in zip(classes, centers, colors):
    x = np.random.randn(n_per_class) * 0.8 + center[0]
    y = np.random.randn(n_per_class) * 0.8 + center[1]
    ax.scatter(x, y, c=color, label=cls, alpha=0.65,
               edgecolors='white', linewidth=0.4, s=50)

ax.set_xlabel('Feature 1'); ax.set_ylabel('Feature 2')
ax.set_title('Multi-Class Scatter Plot')
ax.legend(title='Class', framealpha=0.8)
ax.grid(True, alpha=0.2)
plt.tight_layout()
plt.savefig('scatter_classes.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved scatter_classes.png')
Hexbin density plot with colorbar and size scaling
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(17)
n = 5000
# Two overlapping Gaussian clusters
x = np.concatenate([np.random.randn(n) * 1.2 - 1,
                    np.random.randn(n) * 0.8 + 2])
y = np.concatenate([np.random.randn(n) * 1.5 + 0.5,
                    np.random.randn(n) * 1.0 - 1])

fig, axes = plt.subplots(1, 2, figsize=(13, 5))

# Hexbin β€” handles large datasets where individual points overlap
hb = axes[0].hexbin(x, y, gridsize=40, cmap='YlOrRd', mincnt=1)
plt.colorbar(hb, ax=axes[0], label='Count per bin')
axes[0].set_xlabel('X'); axes[0].set_ylabel('Y')
axes[0].set_title('Hexbin Density (gridsize=40)')

# Scatter with size proportional to local density (estimated via 2D hist)
H, xedges, yedges = np.histogram2d(x, y, bins=30)
xi = np.searchsorted(xedges, x, side='right').clip(1, H.shape[0]) - 1
yi = np.searchsorted(yedges, y, side='right').clip(1, H.shape[1]) - 1
density = H[xi, yi]

sc = axes[1].scatter(x[::5], y[::5], c=density[::5], s=density[::5] * 0.4 + 4,
                     cmap='plasma', alpha=0.5, edgecolors='none')
plt.colorbar(sc, ax=axes[1], label='Local density')
axes[1].set_xlabel('X'); axes[1].set_ylabel('Y')
axes[1].set_title('Scatter: Size & Color = Density')

plt.tight_layout()
plt.savefig('scatter_hexbin_density.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved scatter_hexbin_density.png')
💼 Real-World: Housing Price vs. Square Footage
A real estate analyst visualizes the relationship between house size and price, coloring by neighborhood.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(10)
neighborhoods = ['Downtown', 'Suburbs', 'Rural']
colors_map    = {'Downtown': '#e74c3c', 'Suburbs': '#3498db', 'Rural': '#2ecc71'}

fig, ax = plt.subplots(figsize=(9, 5))

for nbhd in neighborhoods:
    n    = 60
    sqft = np.random.normal({'Downtown':1200,'Suburbs':1800,'Rural':2500}[nbhd], 200, n)
    base = {'Downtown':600000,'Suburbs':350000,'Rural':180000}[nbhd]
    price= base + sqft * np.random.uniform(150,250) + np.random.randn(n) * 30000
    ax.scatter(sqft, price/1000, label=nbhd, alpha=0.6,
               color=colors_map[nbhd], edgecolors='white', linewidth=0.3, s=40)

ax.set_xlabel('Square Footage')
ax.set_ylabel('Price ($K)')
ax.set_title('House Price vs. Size by Neighborhood')
ax.legend(); ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('scatter_housing.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved scatter_housing.png')
🏋️ Practice: Color-Mapped Scatter with Colorbar
Generate 300 random (x, y) points where y = 0.5*x + noise. Color each point by its distance from the origin (sqrt(xΒ²+yΒ²)) using the 'plasma' colormap. Add a colorbar labeled 'Distance from origin', a regression line, axis labels, and a title. Save as 'practice_scatter.png'.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
x = np.random.randn(300)
y = 0.5 * x + np.random.randn(300) * 0.8

# TODO: compute distance from origin for each point
# dist = np.sqrt(x**2 + y**2)

fig, ax = plt.subplots(figsize=(7, 6))

# TODO: scatter with c=dist, cmap='plasma', alpha=0.7, edgecolors='white'
# sc = ax.scatter(x, y, c=dist, ...)
# TODO: plt.colorbar(sc, label='Distance from origin')

# TODO: fit and plot regression line
# m, b = np.polyfit(x, y, 1)
# xline = np.linspace(x.min(), x.max(), 100)
# ax.plot(xline, m * xline + b, 'k--', linewidth=1.5, label=f'fit')

ax.set_xlabel('X')
ax.set_ylabel('Y')
# TODO: ax.set_title(...)
# TODO: ax.legend()
plt.tight_layout()
# TODO: plt.savefig('practice_scatter.png', dpi=100, bbox_inches='tight')
plt.close()
print('Done')
✅ Practice Checklist
4. Histogram

Histograms show the distribution of a single variable. Control bins and density to compare distributions or estimate PDFs.

Basic histogram with density
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
data = np.random.normal(170, 10, 1000)   # heights in cm

fig, ax = plt.subplots(figsize=(8, 4))
ax.hist(data, bins=30, density=True, color='steelblue',
        edgecolor='white', linewidth=0.4, alpha=0.7, label='Data')

# Overlay normal PDF manually (no scipy needed)
xr = np.linspace(data.min(), data.max(), 200)
pdf = (1 / (data.std() * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((xr - data.mean()) / data.std())**2)
ax.plot(xr, pdf, color='navy', linewidth=2, label='Normal PDF')

ax.axvline(data.mean(), color='red', linestyle='--', label=f'Mean={data.mean():.1f}')
ax.set_xlabel('Height (cm)'); ax.set_ylabel('Density')
ax.set_title('Height Distribution'); ax.legend()
plt.tight_layout()
plt.savefig('hist_basic.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved hist_basic.png')
Overlapping histograms for comparison
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(0)
group_a = np.random.normal(100, 15, 500)
group_b = np.random.normal(115, 12, 500)

fig, ax = plt.subplots(figsize=(8, 4))
ax.hist(group_a, bins=30, alpha=0.6, label='Group A', color='steelblue', edgecolor='white')
ax.hist(group_b, bins=30, alpha=0.6, label='Group B', color='tomato',    edgecolor='white')
ax.axvline(group_a.mean(), color='steelblue', linestyle='--', linewidth=1.5)
ax.axvline(group_b.mean(), color='tomato',    linestyle='--', linewidth=1.5)
ax.set_xlabel('Score'); ax.set_ylabel('Count')
ax.set_title('Score Distribution by Group')
ax.legend()
plt.tight_layout()
plt.savefig('hist_compare.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved hist_compare.png')
Histogram with cumulative distribution
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(5)
data = np.random.exponential(scale=2.0, size=800)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4))

# Regular histogram
ax1.hist(data, bins=35, color='#DD8452', edgecolor='white', linewidth=0.4, alpha=0.8)
ax1.set_xlabel('Value'); ax1.set_ylabel('Count')
ax1.set_title('Exponential Distribution')
ax1.grid(True, axis='y', alpha=0.3)

# Cumulative histogram (ECDF style)
ax2.hist(data, bins=35, cumulative=True, density=True,
         color='#4C72B0', edgecolor='white', linewidth=0.4, alpha=0.7, label='ECDF')
# Overlay theoretical CDF: 1 - exp(-x/scale)
xr = np.linspace(0, data.max(), 200)
ax2.plot(xr, 1 - np.exp(-xr / 2.0), 'r-', linewidth=2, label='Theoretical CDF')
ax2.set_xlabel('Value'); ax2.set_ylabel('Cumulative Probability')
ax2.set_title('Cumulative Distribution')
ax2.legend(); ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('hist_cumulative.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved hist_cumulative.png')
Step histogram and density=True comparison
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(13)
data_a = np.random.normal(60, 12, 600)
data_b = np.random.normal(75, 10, 600)

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

# Filled histogram (counts)
axes[0].hist(data_a, bins=30, alpha=0.6, color='steelblue',
             edgecolor='white', label='Group A')
axes[0].hist(data_b, bins=30, alpha=0.6, color='tomato',
             edgecolor='white', label='Group B')
axes[0].set_title('Filled β€” Count'); axes[0].legend()
axes[0].set_xlabel('Value'); axes[0].set_ylabel('Count')
axes[0].grid(True, axis='y', alpha=0.3)

# Step histogram (unfilled outline)
axes[1].hist(data_a, bins=30, histtype='step', linewidth=2,
             color='steelblue', label='Group A')
axes[1].hist(data_b, bins=30, histtype='step', linewidth=2,
             color='tomato', label='Group B')
axes[1].set_title('Step β€” Count'); axes[1].legend()
axes[1].set_xlabel('Value'); axes[1].set_ylabel('Count')
axes[1].grid(True, axis='y', alpha=0.3)

# Density=True β€” normalised to probability density
axes[2].hist(data_a, bins=30, density=True, histtype='stepfilled',
             alpha=0.5, color='steelblue', edgecolor='steelblue',
             linewidth=1.5, label='Group A')
axes[2].hist(data_b, bins=30, density=True, histtype='stepfilled',
             alpha=0.5, color='tomato', edgecolor='tomato',
             linewidth=1.5, label='Group B')
# Overlay normal PDFs
for data, col in [(data_a, 'steelblue'), (data_b, 'tomato')]:
    xr = np.linspace(data.min(), data.max(), 200)
    pdf = (1 / (data.std() * np.sqrt(2*np.pi))) * np.exp(
          -0.5 * ((xr - data.mean()) / data.std())**2)
    axes[2].plot(xr, pdf, color=col, linewidth=2.5)
axes[2].set_title('Step-Filled β€” Density'); axes[2].legend()
axes[2].set_xlabel('Value'); axes[2].set_ylabel('Probability Density')
axes[2].grid(True, axis='y', alpha=0.3)

plt.suptitle('Histogram Style Comparison', fontsize=12, fontweight='bold')
plt.tight_layout()
plt.savefig('hist_styles_comparison.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved hist_styles_comparison.png')
💼 Real-World: Loan Application Risk Distribution
A credit risk analyst plots the distribution of applicant credit scores to set approval thresholds.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(3)
# Simulate credit score distribution (skewed left β€” more mid/high scores)
approved = np.random.normal(700, 60, 800).clip(580, 850)
rejected = np.random.normal(580, 50, 400).clip(300, 680)

fig, ax = plt.subplots(figsize=(9, 4))
ax.hist(approved, bins=30, alpha=0.6, color='#2ecc71', label='Approved', edgecolor='white')
ax.hist(rejected, bins=30, alpha=0.6, color='#e74c3c', label='Rejected', edgecolor='white')

# Threshold line
ax.axvline(620, color='black', linestyle='--', linewidth=2, label='Threshold: 620')
ax.set_xlabel('Credit Score'); ax.set_ylabel('Applicants')
ax.set_title('Credit Score Distribution by Decision')
ax.legend(); ax.grid(True, axis='y', alpha=0.3)

# Annotation
ax.annotate('High risk zone', xy=(550, 30), fontsize=9, color='#e74c3c')
ax.annotate('Low risk zone',  xy=(720, 60), fontsize=9, color='#2ecc71')
plt.tight_layout()
plt.savefig('hist_credit.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved hist_credit.png')
🏋️ Practice: Histogram with Density Curve
Generate 1000 samples from a normal distribution (mean=50, std=12). Plot a histogram with density=True (30 bins, steelblue). Overlay a manually computed normal PDF curve (no scipy). Add vertical lines for mean and Β±1 std. Label axes, add title and legend. Save as 'practice_hist.png'.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(7)
data = np.random.normal(50, 12, 1000)

fig, ax = plt.subplots(figsize=(8, 4))

# TODO: plot histogram with density=True, bins=30, steelblue
# ax.hist(data, bins=30, density=True, ...)

# TODO: compute and overlay normal PDF
# xr = np.linspace(data.min(), data.max(), 200)
# mu, sigma = data.mean(), data.std()
# pdf = (1 / (sigma * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((xr - mu) / sigma)**2)
# ax.plot(xr, pdf, color='navy', linewidth=2, label='Normal PDF')

# TODO: add vertical lines for mean and Β±1 std
# ax.axvline(mu, color='red', linestyle='--', label=f'Mean={mu:.1f}')
# ax.axvline(mu - sigma, color='orange', linestyle=':', label='-1Οƒ')
# ax.axvline(mu + sigma, color='orange', linestyle=':', label='+1Οƒ')

ax.set_xlabel('Value')
ax.set_ylabel('Density')
# TODO: ax.set_title(...)
# TODO: ax.legend()
plt.tight_layout()
# TODO: plt.savefig('practice_hist.png', dpi=100, bbox_inches='tight')
plt.close()
print('Done')
✅ Practice Checklist
5. Subplots

plt.subplots() creates a grid of axes in a single figure. Essential for dashboards and side-by-side comparisons.

2Γ—2 subplot grid
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
x = np.linspace(0, 10, 200)

fig, axes = plt.subplots(2, 2, figsize=(10, 8))

axes[0,0].plot(x, np.sin(x), color='steelblue')
axes[0,0].set_title('Line: sin(x)')

axes[0,1].bar(['A','B','C','D'], [23,45,12,67], color='#DD8452')
axes[0,1].set_title('Bar Chart')

axes[1,0].scatter(np.random.randn(100), np.random.randn(100), alpha=0.5)
axes[1,0].set_title('Scatter')

axes[1,1].hist(np.random.randn(500), bins=25, color='#55A868')
axes[1,1].set_title('Histogram')

# Global title and spacing
fig.suptitle('Dashboard Overview', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig('subplots_2x2.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved subplots_2x2.png')
Shared axes and different sizes
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(5)
dates  = np.arange(30)
price  = 100 + np.cumsum(np.random.randn(30))
volume = np.random.randint(1000, 5000, 30)

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6),
                                 sharex=True,
                                 gridspec_kw={'height_ratios': [3, 1]})
ax1.plot(dates, price, color='steelblue', linewidth=2)
ax1.set_ylabel('Price ($)'); ax1.set_title('Stock Price & Volume')
ax1.grid(True, alpha=0.3)

ax2.bar(dates, volume, color='gray', alpha=0.5)
ax2.set_ylabel('Volume'); ax2.set_xlabel('Day')

plt.tight_layout()
plt.savefig('subplots_shared.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved subplots_shared.png')
GridSpec for irregular subplot layouts
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np

np.random.seed(8)
fig = plt.figure(figsize=(11, 7))
gs  = gridspec.GridSpec(2, 3, figure=fig, hspace=0.35, wspace=0.3)

# Wide top plot spanning all 3 columns
ax_top = fig.add_subplot(gs[0, :])
x = np.linspace(0, 10, 200)
ax_top.plot(x, np.sin(x) * np.exp(-0.1*x), color='steelblue', linewidth=2)
ax_top.set_title('Wide Top: Damped Sine'); ax_top.grid(True, alpha=0.25)

# Three smaller bottom plots
for col, (title, color) in enumerate([('Scatter','#DD8452'),('Bar','#55A868'),('Hist','#8172B2')]):
    ax = fig.add_subplot(gs[1, col])
    if title == 'Scatter':
        ax.scatter(np.random.randn(50), np.random.randn(50), color=color, alpha=0.6, s=25)
    elif title == 'Bar':
        ax.bar(['A','B','C'], [4, 7, 5], color=color)
    else:
        ax.hist(np.random.randn(200), bins=20, color=color, edgecolor='white', linewidth=0.4)
    ax.set_title(title)

fig.suptitle('GridSpec Layout', fontsize=13, fontweight='bold')
plt.savefig('subplots_gridspec.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved subplots_gridspec.png')
subplot2grid for asymmetric dashboard layouts
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(22)
fig = plt.figure(figsize=(12, 7))

# subplot2grid(shape, loc, rowspan, colspan) β€” place cells in a grid manually
ax_main  = plt.subplot2grid((3, 4), (0, 0), rowspan=2, colspan=3)  # big left
ax_right = plt.subplot2grid((3, 4), (0, 3), rowspan=3, colspan=1)  # tall right
ax_bot1  = plt.subplot2grid((3, 4), (2, 0), rowspan=1, colspan=1)  # bottom row
ax_bot2  = plt.subplot2grid((3, 4), (2, 1), rowspan=1, colspan=1)
ax_bot3  = plt.subplot2grid((3, 4), (2, 2), rowspan=1, colspan=1)

# Main panel β€” time series
t = np.linspace(0, 4 * np.pi, 300)
ax_main.plot(t, np.sin(t), color='steelblue', linewidth=2, label='sin')
ax_main.plot(t, np.cos(t), color='tomato', linewidth=2, linestyle='--', label='cos')
ax_main.fill_between(t, np.sin(t), np.cos(t), alpha=0.08, color='gray')
ax_main.set_title('Main β€” Trig Functions'); ax_main.legend(fontsize=8)
ax_main.grid(True, alpha=0.25)

# Tall right panel β€” horizontal bars (KPIs)
kpis   = ['KPI A', 'KPI B', 'KPI C', 'KPI D', 'KPI E']
values = np.random.uniform(40, 95, 5)
colors = ['#2ecc71' if v >= 70 else '#e74c3c' for v in values]
ax_right.barh(kpis, values, color=colors, edgecolor='white', linewidth=0.5)
ax_right.set_xlim(0, 100)
ax_right.axvline(70, color='gray', linestyle='--', linewidth=0.8)
ax_right.set_title('KPIs', fontsize=9)
ax_right.tick_params(labelsize=7)

# Bottom three mini-panels
for ax, title, color in zip([ax_bot1, ax_bot2, ax_bot3],
                              ['Alpha', 'Beta', 'Gamma'],
                              ['#4C72B0', '#DD8452', '#55A868']):
    data = np.random.randn(120)
    ax.hist(data, bins=15, color=color, edgecolor='white', linewidth=0.3, alpha=0.8)
    ax.set_title(title, fontsize=8); ax.tick_params(labelsize=6)
    ax.grid(True, axis='y', alpha=0.3)

fig.suptitle('subplot2grid β€” Asymmetric Dashboard', fontsize=12, fontweight='bold')
plt.tight_layout()
plt.savefig('subplots_subplot2grid.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved subplots_subplot2grid.png')
💼 Real-World: ML Model Evaluation Dashboard
A data scientist creates a 4-panel figure showing loss curves, accuracy, confusion matrix, and prediction distribution.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
epochs    = np.arange(1, 51)
train_loss= 1.5 * np.exp(-0.06 * epochs) + np.random.randn(50) * 0.02
val_loss  = 1.5 * np.exp(-0.05 * epochs) + 0.05 + np.random.randn(50) * 0.03
train_acc = 1 - train_loss / 1.5 + 0.3
val_acc   = 1 - val_loss   / 1.5 + 0.28

fig, axes = plt.subplots(1, 3, figsize=(14, 4))

# Loss curves
axes[0].plot(epochs, train_loss, label='Train',  color='steelblue')
axes[0].plot(epochs, val_loss,   label='Val',    color='tomato', linestyle='--')
axes[0].set_title('Loss'); axes[0].set_xlabel('Epoch')
axes[0].legend(); axes[0].grid(True, alpha=0.3)

# Accuracy curves
axes[1].plot(epochs, train_acc.clip(0,1), label='Train', color='steelblue')
axes[1].plot(epochs, val_acc.clip(0,1),   label='Val',   color='tomato', linestyle='--')
axes[1].set_title('Accuracy'); axes[1].set_xlabel('Epoch')
axes[1].legend(); axes[1].grid(True, alpha=0.3)

# Prediction histogram
preds = np.random.beta(2, 2, 500)
axes[2].hist(preds, bins=25, color='#55A868', edgecolor='white')
axes[2].axvline(0.5, color='red', linestyle='--', label='Threshold')
axes[2].set_title('Prediction Scores'); axes[2].set_xlabel('Score')
axes[2].legend()

fig.suptitle('Model Evaluation Dashboard', fontweight='bold')
plt.tight_layout()
plt.savefig('subplots_ml_dashboard.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved subplots_ml_dashboard.png')
🏋️ Practice: 2x2 Subplot Grid
Create a 2x2 subplot figure. Top-left: line plot of cos(x) over [0, 4Ο€]. Top-right: scatter of 100 random points colored by angle. Bottom-left: bar chart of 5 random categories. Bottom-right: histogram of 500 standard normal samples. Add individual titles, a global suptitle, and save as 'practice_subplots.png'.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
fig, axes = plt.subplots(2, 2, figsize=(10, 8))

# Top-left: line plot of cos(x)
x = np.linspace(0, 4 * np.pi, 300)
# TODO: axes[0,0].plot(x, np.cos(x), ...)
# TODO: axes[0,0].set_title('...')

# Top-right: scatter of 100 random points, colored by angle
pts = np.random.randn(100, 2)
angles = np.arctan2(pts[:, 1], pts[:, 0])
# TODO: sc = axes[0,1].scatter(pts[:,0], pts[:,1], c=angles, cmap='hsv', alpha=0.7)
# TODO: plt.colorbar(sc, ax=axes[0,1])
# TODO: axes[0,1].set_title('...')

# Bottom-left: bar chart of 5 categories
cats   = ['A', 'B', 'C', 'D', 'E']
values = np.random.randint(10, 80, 5)
# TODO: axes[1,0].bar(cats, values, ...)
# TODO: axes[1,0].set_title('...')

# Bottom-right: histogram of 500 normal samples
data = np.random.randn(500)
# TODO: axes[1,1].hist(data, bins=25, ...)
# TODO: axes[1,1].set_title('...')

# TODO: fig.suptitle('2x2 Dashboard', fontsize=14, fontweight='bold')
plt.tight_layout()
# TODO: plt.savefig('practice_subplots.png', dpi=100, bbox_inches='tight')
plt.close()
print('Done')
✅ Practice Checklist
6. Figure Customization

Control colors, line styles, markers, fonts, spines, and tick formatting. Good styling makes charts publication-ready.

Markers, styles, colors, spines
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x  = np.arange(1, 8)
y1 = [3, 7, 5, 9, 6, 8, 10]
y2 = [1, 4, 3, 6, 4, 7, 8]

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, y1, 'o-', color='steelblue', markersize=8,
        linewidth=2, markerfacecolor='white', markeredgewidth=2, label='Series A')
ax.plot(x, y2, 's--', color='tomato',    markersize=7,
        linewidth=2, markerfacecolor='white', markeredgewidth=2, label='Series B')

# Remove top/right spines
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.set_xticks(x)
ax.set_xticklabels([f'Day {i}' for i in x], rotation=30, ha='right')
ax.set_title('Weekly Performance', fontsize=13, fontweight='bold', pad=12)
ax.legend(frameon=False); ax.grid(True, alpha=0.2)
plt.tight_layout()
plt.savefig('custom_markers.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved custom_markers.png')
Annotations and text
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(1)
x = np.linspace(0, 10, 100)
y = np.sin(x) * np.exp(-0.1 * x)

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(x, y, color='steelblue', linewidth=2)

# Peak annotation
peak_idx = np.argmax(y)
ax.annotate(f'Peak ({x[peak_idx]:.1f}, {y[peak_idx]:.2f})',
            xy=(x[peak_idx], y[peak_idx]),
            xytext=(x[peak_idx]+1.5, y[peak_idx]+0.1),
            arrowprops=dict(arrowstyle='->', color='red'),
            color='red', fontsize=9)

ax.axhline(0, color='gray', linewidth=0.8, linestyle='--')
ax.fill_between(x, y, where=(y > 0), alpha=0.15, color='steelblue')
ax.fill_between(x, y, where=(y < 0), alpha=0.15, color='tomato')
ax.set_title('Damped Sine Wave'); ax.set_xlabel('x')
plt.tight_layout()
plt.savefig('custom_annotations.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved custom_annotations.png')
Tick formatting and color-coded regions
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import numpy as np

np.random.seed(2)
months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
revenue = np.array([42, 38, 51, 47, 59, 65, 70, 62, 55, 68, 75, 88]) * 1000

fig, ax = plt.subplots(figsize=(11, 4))
ax.plot(range(12), revenue, 'o-', color='#1a73e8', linewidth=2.5,
        markersize=7, markerfacecolor='white', markeredgewidth=2)
ax.fill_between(range(12), revenue, alpha=0.1, color='#1a73e8')

# Format y-axis as currency
ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f'${v/1000:.0f}K'))

# Color background by quarter
colors = ['#fff9f0','#f0fff4','#f0f4ff','#fff0f4']
for q, color in enumerate(colors):
    ax.axvspan(q*3 - 0.5, q*3 + 2.5, alpha=0.25, color=color, zorder=0)

# Annotate max
best_idx = int(np.argmax(revenue))
ax.annotate(f'Best: ${revenue[best_idx]/1000:.0f}K', xy=(best_idx, revenue[best_idx]),
            xytext=(best_idx - 2, revenue[best_idx] + 4000),
            arrowprops=dict(arrowstyle='->', color='green'), color='green', fontsize=9)

ax.set_xticks(range(12)); ax.set_xticklabels(months)
ax.spines['top'].set_visible(False); ax.spines['right'].set_visible(False)
ax.set_title('Monthly Revenue with Quarter Shading', fontsize=13, fontweight='bold')
ax.grid(True, axis='y', alpha=0.25)
plt.tight_layout()
plt.savefig('custom_tick_format.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved custom_tick_format.png')
💼 Real-World: Executive KPI Summary Chart
A BI developer creates a polished, publication-ready monthly KPI chart with branded colors and annotations.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

months  = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
revenue = [4.2, 3.8, 5.1, 5.8, 6.2, 7.0, 6.5, 7.8, 8.1, 7.5, 9.2, 10.4]
target  = [5.0] * 12

fig, ax = plt.subplots(figsize=(11, 5))

ax.plot(months, revenue, 'o-', color='#1a73e8', linewidth=2.5,
        markersize=7, markerfacecolor='white', markeredgewidth=2.5, label='Actual Revenue')
ax.plot(months, target, '--', color='#ea4335', linewidth=1.5,
        alpha=0.7, label='Monthly Target ($5M)')
ax.fill_between(months, revenue, target,
                where=[r >= t for r, t in zip(revenue, target)],
                alpha=0.1, color='green', label='Above target')
ax.fill_between(months, revenue, target,
                where=[r < t for r, t in zip(revenue, target)],
                alpha=0.1, color='red', label='Below target')

# Annotate best month
best = months[revenue.index(max(revenue))]
ax.annotate(f'Best: ${max(revenue):.1f}M', xy=(best, max(revenue)),
            xytext=(best, max(revenue)+0.4),
            ha='center', fontsize=9, color='green', fontweight='bold')

ax.spines['top'].set_visible(False); ax.spines['right'].set_visible(False)
ax.set_ylabel('Revenue (USD millions)'); ax.set_title('2024 Monthly Revenue vs Target',
                                                       fontsize=14, fontweight='bold')
ax.legend(frameon=False, loc='upper left'); ax.grid(True, axis='y', alpha=0.2)
plt.tight_layout()
plt.savefig('custom_kpi.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved custom_kpi.png')
🏋️ Practice: Full Professional Chart
Plot monthly sales data for two products over 12 months. Customize: remove top/right spines, use circle and square markers with white fill, format y-axis as dollars (e.g. '$42K'), rotate x-tick labels, add an annotation arrow pointing to the peak month, and a shaded region between the two lines. Save as 'practice_custom.png'.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import numpy as np

months   = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
sales_a  = np.array([42, 38, 51, 47, 59, 65, 70, 62, 55, 68, 75, 88]) * 1000
sales_b  = np.array([30, 35, 40, 38, 45, 50, 55, 52, 48, 58, 62, 70]) * 1000
x        = np.arange(12)

fig, ax = plt.subplots(figsize=(11, 5))

# TODO: plot sales_a with 'o-' markers, steelblue, markerfacecolor='white'
# ax.plot(x, sales_a, 'o-', ...)

# TODO: plot sales_b with 's--' markers, tomato, markerfacecolor='white'
# ax.plot(x, sales_b, 's--', ...)

# TODO: shade region between the two lines
# ax.fill_between(x, sales_a, sales_b, alpha=0.1, color='gray')

# TODO: remove top/right spines
# ax.spines['top'].set_visible(False)
# ax.spines['right'].set_visible(False)

# TODO: format y-axis as '$XK'
# ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda v, _: f'${v/1000:.0f}K'))

# TODO: annotate peak of sales_a with arrow
# best = int(np.argmax(sales_a))
# ax.annotate(...)

ax.set_xticks(x)
ax.set_xticklabels(months, rotation=30, ha='right')
# TODO: ax.set_title(...), ax.legend(), ax.grid(...)
plt.tight_layout()
# TODO: plt.savefig('practice_custom.png', dpi=100, bbox_inches='tight')
plt.close()
print('Done')
✅ Practice Checklist
7. Pie & Donut Chart

Pie charts show part-to-whole relationships. Donut charts are a modern alternative. Avoid too many slices β€” use 5 or fewer.

Pie chart with explode
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

labels  = ['Python', 'JavaScript', 'Java', 'C++', 'Other']
sizes   = [30, 25, 20, 15, 10]
explode = [0.05, 0, 0, 0, 0]   # pull out the first slice
colors  = ['#4C72B0','#DD8452','#55A868','#C44E52','#8172B2']

fig, ax = plt.subplots(figsize=(7, 5))
wedges, texts, autotexts = ax.pie(
    sizes, labels=labels, explode=explode, colors=colors,
    autopct='%1.1f%%', startangle=140,
    wedgeprops=dict(edgecolor='white', linewidth=2)
)
for t in autotexts: t.set_fontsize(9)
ax.set_title('Language Market Share')
plt.tight_layout()
plt.savefig('pie_basic.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved pie_basic.png')
Donut chart
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

labels = ['Organic', 'Paid Search', 'Social', 'Email', 'Direct']
sizes  = [35, 25, 20, 12, 8]
colors = ['#2ecc71','#3498db','#e74c3c','#f39c12','#9b59b6']

fig, ax = plt.subplots(figsize=(7, 5))
wedges, texts, autotexts = ax.pie(
    sizes, labels=labels, colors=colors,
    autopct='%1.0f%%', startangle=90,
    wedgeprops=dict(width=0.5, edgecolor='white', linewidth=2)   # donut!
)
for t in autotexts: t.set_fontsize(9)
ax.text(0, 0, 'Traffic\nSources', ha='center', va='center',
        fontsize=11, fontweight='bold')
ax.set_title('Website Traffic Sources')
plt.tight_layout()
plt.savefig('pie_donut.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved pie_donut.png')
Nested donut / ring chart
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

# Outer ring: main categories
outer_labels = ['Engineering', 'Sales', 'Marketing', 'Other']
outer_sizes  = [40, 30, 20, 10]
outer_colors = ['#4C72B0', '#DD8452', '#55A868', '#C44E52']

# Inner ring: sub-breakdown for Engineering and Sales only (simplified)
inner_labels = ['FE', 'BE', 'DevOps', 'Inside', 'Field', 'Mkt-A', 'Mkt-B', 'Misc']
inner_sizes  = [15, 15, 10, 18, 12, 12, 8, 10]
inner_colors = ['#6B9BE8', '#8BB2F0', '#AACCFF',
                '#F0A875', '#F5C49A',
                '#7DC88A', '#A5DDB0',
                '#E88A8A']

fig, ax = plt.subplots(figsize=(8, 7))
ax.pie(outer_sizes, labels=outer_labels, colors=outer_colors,
       radius=1.0, startangle=90,
       wedgeprops=dict(width=0.35, edgecolor='white', linewidth=2),
       autopct='%1.0f%%', pctdistance=0.82)
ax.pie(inner_sizes, colors=inner_colors,
       radius=0.65, startangle=90,
       wedgeprops=dict(width=0.35, edgecolor='white', linewidth=1))
ax.set_title('Headcount β€” Nested Donut', fontsize=13, fontweight='bold', pad=15)
plt.tight_layout()
plt.savefig('pie_nested.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved pie_nested.png')
Waffle chart with matplotlib patches
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches

categories = ['Completed', 'In Progress', 'Blocked', 'Not Started']
values     = [45, 30, 10, 15]
colors     = ['#2e7d32', '#1565c0', '#b71c1c', '#9e9e9e']

total   = sum(values)
squares = [round(v / total * 100) for v in values]
squares[-1] += 100 - sum(squares)

grid = []
for color, count in zip(colors, squares):
    grid.extend([color] * count)

fig, ax = plt.subplots(figsize=(8, 8))
for i, color in enumerate(grid):
    row, col = divmod(i, 10)
    ax.add_patch(mpatches.FancyBboxPatch(
        (col * 1.1, (9 - row) * 1.1), 1.0, 1.0,
        boxstyle='round,pad=0.05', fc=color, ec='white', lw=2))

ax.set_xlim(-0.1, 11.1); ax.set_ylim(-0.1, 11.1)
ax.set_aspect('equal'); ax.axis('off')
ax.set_title('Project Task Status (Waffle Chart)', fontsize=13, pad=15)

legend_patches = [mpatches.Patch(fc=c, label=f'{l} ({v}%)')
                  for c, l, v in zip(colors, categories, squares)]
ax.legend(handles=legend_patches, loc='lower center',
          ncol=2, frameon=False, fontsize=11,
          bbox_to_anchor=(0.5, -0.05))
plt.tight_layout()
plt.savefig('waffle_chart.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved waffle_chart.png')
💼 Real-World: Budget Allocation Dashboard
A CFO uses a donut chart to present department budget allocation for a board presentation.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

depts  = ['Engineering', 'Sales', 'Marketing', 'Operations', 'HR', 'R&D']
budget = [32, 22, 18, 12, 8, 8]
colors = ['#1a73e8','#34a853','#fbbc04','#ea4335','#9c27b0','#00bcd4']
total  = sum(budget)

fig, ax = plt.subplots(figsize=(8, 6))
wedges, texts, autotexts = ax.pie(
    budget, labels=depts, colors=colors,
    autopct=lambda p: f'${p*total/100:.0f}M\n({p:.0f}%)',
    startangle=120,
    wedgeprops=dict(width=0.55, edgecolor='white', linewidth=2)
)
for t in autotexts: t.set_fontsize(8)
ax.text(0, 0, f'Total\n${total}M', ha='center', va='center',
        fontsize=12, fontweight='bold')
ax.set_title('FY2024 Budget Allocation by Department',
             fontsize=13, fontweight='bold', pad=20)
plt.tight_layout()
plt.savefig('pie_budget.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved pie_budget.png')
🏋️ Practice: Donut Chart
Create a donut chart (width=0.45) showing 5 product categories: Electronics 35%, Clothing 25%, Food 20%, Books 12%, Other 8%. Use a custom color palette, display percentages inside the wedges, add a center label showing 'Sales\n2024', and a title. Save as 'practice_donut.png'.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt

labels = ['Electronics', 'Clothing', 'Food', 'Books', 'Other']
sizes  = [35, 25, 20, 12, 8]
# TODO: choose 5 colors
colors = ['#4C72B0', '#DD8452', '#55A868', '#C44E52', '#8172B2']

fig, ax = plt.subplots(figsize=(7, 6))

# TODO: create donut chart using ax.pie with wedgeprops=dict(width=0.45, ...)
# wedges, texts, autotexts = ax.pie(
#     sizes, labels=labels, colors=colors,
#     autopct='%1.0f%%', startangle=90,
#     wedgeprops=dict(width=0.45, edgecolor='white', linewidth=2)
# )

# TODO: resize autopct text
# for t in autotexts: t.set_fontsize(9)

# TODO: add center label
# ax.text(0, 0, 'Sales\n2024', ha='center', va='center', fontsize=12, fontweight='bold')

# TODO: ax.set_title(...)
plt.tight_layout()
# TODO: plt.savefig('practice_donut.png', dpi=100, bbox_inches='tight')
plt.close()
print('Done')
✅ Practice Checklist
8. Heatmap with imshow/pcolor

Heatmaps encode a 2D matrix as color intensities. Great for correlation matrices, confusion matrices, and timeΓ—metric data.

Correlation heatmap
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
data = np.random.randn(100, 5)
labels = ['Age', 'Income', 'Score', 'Tenure', 'Spend']

corr = np.corrcoef(data.T)

fig, ax = plt.subplots(figsize=(6, 5))
im = ax.imshow(corr, cmap='RdBu_r', vmin=-1, vmax=1)
plt.colorbar(im, ax=ax)

ax.set_xticks(range(5)); ax.set_yticks(range(5))
ax.set_xticklabels(labels, rotation=45, ha='right')
ax.set_yticklabels(labels)

# Annotate values
for i in range(5):
    for j in range(5):
        ax.text(j, i, f'{corr[i,j]:.2f}', ha='center', va='center',
                fontsize=8, color='black' if abs(corr[i,j]) < 0.6 else 'white')

ax.set_title('Feature Correlation Matrix')
plt.tight_layout()
plt.savefig('heatmap_corr.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved heatmap_corr.png')
Calendar heatmap (day Γ— hour)
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(7)
# 7 days Γ— 24 hours activity matrix
activity = np.random.poisson(lam=5, size=(7, 24)).astype(float)
activity[1:5, 9:17] += 15   # weekday work hours spike

days  = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']
hours = [f'{h:02d}:00' for h in range(24)]

fig, ax = plt.subplots(figsize=(14, 4))
im = ax.imshow(activity, cmap='YlOrRd', aspect='auto')
plt.colorbar(im, label='Requests/hr')
ax.set_yticks(range(7)); ax.set_yticklabels(days)
ax.set_xticks(range(0, 24, 2)); ax.set_xticklabels(hours[::2], rotation=45, ha='right')
ax.set_title('API Request Heatmap β€” Weekday vs Weekend')
plt.tight_layout()
plt.savefig('heatmap_calendar.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved heatmap_calendar.png')
pcolormesh with diverging colormap
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(3)
# 10 assets x 12 months return matrix (%)
n_assets, n_months = 10, 12
returns = np.random.randn(n_assets, n_months) * 5   # % monthly returns
asset_names = [f'Asset {i+1}' for i in range(n_assets)]
month_names = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']

fig, ax = plt.subplots(figsize=(13, 5))
mesh = ax.pcolormesh(returns, cmap='RdYlGn', vmin=-12, vmax=12)
plt.colorbar(mesh, label='Monthly Return (%)', ax=ax)

# Annotate cells
for i in range(n_assets):
    for j in range(n_months):
        color = 'white' if abs(returns[i, j]) > 8 else 'black'
        ax.text(j + 0.5, i + 0.5, f'{returns[i,j]:+.1f}',
                ha='center', va='center', fontsize=7, color=color)

ax.set_xticks(np.arange(n_months) + 0.5); ax.set_xticklabels(month_names)
ax.set_yticks(np.arange(n_assets) + 0.5); ax.set_yticklabels(asset_names)
ax.set_title('Asset Monthly Returns Heatmap (%)', fontweight='bold')
plt.tight_layout()
plt.savefig('heatmap_pcolormesh.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved heatmap_pcolormesh.png')
Calendar heatmap (GitHub-style)
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy as np
import datetime

rng   = np.random.default_rng(42)
days  = 365
start = datetime.date(2024, 1, 1)
counts = rng.integers(0, 20, days)
for i in range(days):
    if (start + datetime.timedelta(i)).weekday() >= 5:
        counts[i] = max(0, int(counts[i] * 0.3))

padding = (start.weekday() + 1) % 7
padded  = np.concatenate([np.zeros(padding, dtype=int), counts])
n_pad   = 7 - len(padded) % 7 if len(padded) % 7 else 0
padded  = np.concatenate([padded, np.zeros(n_pad)])
grid    = padded.reshape(-1, 7).T

cmap = mcolors.LinearSegmentedColormap.from_list(
    'gh', ['#ebedf0','#9be9a8','#40c463','#30a14e','#216e39'])

fig, ax = plt.subplots(figsize=(16, 3))
im = ax.imshow(grid, cmap=cmap, aspect='auto', vmin=0, vmax=counts.max())
ax.set_yticks(range(7))
ax.set_yticklabels(['Sun','Mon','Tue','Wed','Thu','Fri','Sat'], fontsize=9)
ax.set_xticks([]); ax.set_title('2024 Activity Calendar', fontsize=12)
fig.colorbar(im, ax=ax, orientation='horizontal', fraction=0.02,
             pad=0.15, label='Activity count')
plt.tight_layout()
plt.savefig('calendar_heatmap.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved calendar_heatmap.png')
💼 Real-World: Confusion Matrix Visualization
A ML engineer visualizes the confusion matrix of a multi-class classifier to identify which classes are most confused.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

# Simulated confusion matrix: 5 classes
classes = ['Cat','Dog','Bird','Fish','Rabbit']
cm = np.array([
    [85,  5,  3,  2,  5],
    [ 4, 88,  2,  1,  5],
    [ 3,  2, 82,  8,  5],
    [ 1,  2,  6, 89,  2],
    [ 6,  4,  3,  2, 85],
])

fig, ax = plt.subplots(figsize=(7, 6))
im = ax.imshow(cm, cmap='Blues')
plt.colorbar(im, ax=ax)

ax.set_xticks(range(5)); ax.set_yticks(range(5))
ax.set_xticklabels(classes, rotation=45, ha='right')
ax.set_yticklabels(classes)
ax.set_xlabel('Predicted'); ax.set_ylabel('Actual')
ax.set_title('Confusion Matrix β€” Animal Classifier')

for i in range(5):
    for j in range(5):
        color = 'white' if cm[i,j] > 50 else 'black'
        ax.text(j, i, str(cm[i,j]), ha='center', va='center',
                fontsize=11, color=color, fontweight='bold')

# Overall accuracy
acc = cm.diagonal().sum() / cm.sum()
ax.set_xlabel(f'Predicted    (Accuracy: {acc:.1%})', fontsize=10)
plt.tight_layout()
plt.savefig('heatmap_confusion.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved heatmap_confusion.png')
🏋️ Practice: Correlation Matrix Heatmap
Generate a (200, 6) random dataset with manually introduced correlations between columns. Compute the 6x6 correlation matrix, display it as a heatmap using imshow with the 'coolwarm' colormap (vmin=-1, vmax=1), annotate each cell with its correlation value (2 decimal places), and rotate x-tick labels 45 degrees. Save as 'practice_heatmap.png'.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
n = 200
# Build correlated dataset
x1 = np.random.randn(n)
x2 = 0.8 * x1 + np.random.randn(n) * 0.4       # correlated with x1
x3 = -0.5 * x1 + np.random.randn(n) * 0.7      # negatively correlated
x4 = np.random.randn(n)                          # independent
x5 = 0.6 * x2 + np.random.randn(n) * 0.5       # correlated with x2
x6 = np.random.randn(n)                          # independent

data   = np.column_stack([x1, x2, x3, x4, x5, x6])
labels = ['X1', 'X2', 'X3', 'X4', 'X5', 'X6']

# TODO: corr = np.corrcoef(data.T)

fig, ax = plt.subplots(figsize=(7, 6))

# TODO: im = ax.imshow(corr, cmap='coolwarm', vmin=-1, vmax=1)
# TODO: plt.colorbar(im, ax=ax)

# TODO: set tick labels (rotated 45 for x-axis)
# ax.set_xticks(range(6)); ax.set_xticklabels(labels, rotation=45, ha='right')
# ax.set_yticks(range(6)); ax.set_yticklabels(labels)

# TODO: annotate each cell with corr value
# for i in range(6):
#     for j in range(6):
#         color = 'white' if abs(corr[i,j]) > 0.6 else 'black'
#         ax.text(j, i, f'{corr[i,j]:.2f}', ha='center', va='center', fontsize=8, color=color)

# TODO: ax.set_title(...)
plt.tight_layout()
# TODO: plt.savefig('practice_heatmap.png', dpi=100, bbox_inches='tight')
plt.close()
print('Done')
✅ Practice Checklist
9. Twin Axes & Secondary Y-Axis

twinx() creates a second y-axis sharing the same x-axis β€” essential when two variables have different scales.

Dual y-axis line + bar
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

months  = ['Jan','Feb','Mar','Apr','May','Jun']
revenue = [120, 135, 128, 155, 162, 180]
margin  = [22.1, 21.5, 23.0, 24.2, 23.8, 25.5]

fig, ax1 = plt.subplots(figsize=(9, 4))
ax2 = ax1.twinx()

bars = ax1.bar(months, revenue, color='steelblue', alpha=0.7, label='Revenue ($K)')
line = ax2.plot(months, margin, 'o-', color='tomato', linewidth=2.5,
                markersize=7, markerfacecolor='white', markeredgewidth=2, label='Margin %')

ax1.set_ylabel('Revenue ($K)',    color='steelblue')
ax2.set_ylabel('Gross Margin (%)', color='tomato')
ax1.tick_params(axis='y', labelcolor='steelblue')
ax2.tick_params(axis='y', labelcolor='tomato')

lines  = line
labels = [l.get_label() for l in lines]
ax1.legend(bars, ['Revenue ($K)'], loc='upper left')
ax2.legend(lines, labels, loc='lower right')

ax1.set_title('Revenue & Gross Margin')
plt.tight_layout()
plt.savefig('twin_bar_line.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved twin_bar_line.png')
Temperature and humidity dual axis
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(9)
hours    = np.arange(24)
temp     = 20 + 8 * np.sin((hours - 6) * np.pi / 12) + np.random.randn(24)
humidity = 60 - 20 * np.sin((hours - 6) * np.pi / 12) + np.random.randn(24) * 3

fig, ax1 = plt.subplots(figsize=(10, 4))
ax2 = ax1.twinx()

ax1.plot(hours, temp, color='#e74c3c', linewidth=2, label='Temp (Β°C)')
ax1.fill_between(hours, temp, alpha=0.1, color='#e74c3c')
ax2.plot(hours, humidity, color='#3498db', linewidth=2,
         linestyle='--', label='Humidity (%)')

ax1.set_xlabel('Hour of Day'); ax1.set_ylabel('Temperature (Β°C)', color='#e74c3c')
ax2.set_ylabel('Humidity (%)', color='#3498db')
ax1.set_title('24-Hour Weather Profile')
ax1.grid(True, alpha=0.2)
plt.tight_layout()
plt.savefig('twin_weather.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved twin_weather.png')
Three-axis plot with twiny and twinx
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(11)
months   = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
x        = np.arange(12)
revenue  = np.array([42, 38, 51, 47, 59, 65, 70, 62, 55, 68, 75, 88])
cost     = np.array([30, 29, 36, 34, 41, 44, 48, 43, 40, 46, 51, 59])
margin   = (revenue - cost) / revenue * 100

fig, ax1 = plt.subplots(figsize=(11, 5))
ax2 = ax1.twinx()

# Stacked area (revenue, cost on ax1)
ax1.fill_between(x, cost,    alpha=0.35, color='#C44E52', label='Cost ($K)')
ax1.fill_between(x, revenue, cost, alpha=0.35, color='#55A868', label='Profit ($K)')
ax1.plot(x, revenue, 'o-', color='#4C72B0', linewidth=2,
         markersize=5, markerfacecolor='white', markeredgewidth=1.5, label='Revenue ($K)')

# Margin % on secondary axis
ax2.plot(x, margin, 's--', color='#DD8452', linewidth=1.8,
         markersize=6, markerfacecolor='white', markeredgewidth=1.5, label='Margin %')
ax2.set_ylabel('Gross Margin (%)', color='#DD8452')
ax2.tick_params(axis='y', labelcolor='#DD8452')
ax2.set_ylim(0, 50)

ax1.set_xticks(x); ax1.set_xticklabels(months, rotation=30, ha='right')
ax1.set_ylabel('Amount ($K)', color='#4C72B0')
ax1.tick_params(axis='y', labelcolor='#4C72B0')
ax1.set_title('Revenue, Cost & Margin β€” Full Year', fontweight='bold')
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left', fontsize=8)
ax1.grid(True, axis='y', alpha=0.2)
plt.tight_layout()
plt.savefig('twin_three_axis.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved twin_three_axis.png')
Event annotation with axvspan and annotate
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(0)
t   = np.linspace(0, 365, 365)
revenue = 1000 + 2*t + 150*np.sin(2*np.pi*t/365) + rng.normal(0, 30, 365)
traffic = 5000 + 10*t + rng.normal(0, 200, 365)

fig, ax1 = plt.subplots(figsize=(12, 5))
ax2 = ax1.twinx()

l1, = ax1.plot(t, revenue, color='steelblue', lw=1.5, label='Revenue ($)')
l2, = ax2.plot(t, traffic, color='tomato',    lw=1.2, alpha=0.7, label='Traffic')

# Highlight events
events = [(90,  120, '#ffffcc', 'Summer\nPromo'),
          (200, 220, '#e8f5e9', 'Product\nLaunch'),
          (300, 340, '#fce4ec', 'Holiday\nSale')]
for start, end, color, label in events:
    ax1.axvspan(start, end, alpha=0.4, color=color)
    ax1.annotate(label, xy=((start+end)/2, revenue.max()*0.95),
                 ha='center', fontsize=9, fontweight='bold')

ax1.set(xlabel='Day of Year', ylabel='Revenue ($)', title='Revenue & Traffic with Event Annotations')
ax2.set_ylabel('Daily Traffic', color='tomato')
ax2.tick_params(axis='y', labelcolor='tomato')
ax1.legend(handles=[l1, l2], loc='upper left')
plt.tight_layout()
plt.savefig('twin_annotated.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved twin_annotated.png')
💼 Real-World: E-Commerce Sales & Conversion Rate
A growth analyst overlays daily order volume and conversion rate to find days where traffic didn't convert well.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(12)
days     = np.arange(1, 31)
orders   = (500 + np.cumsum(np.random.randn(30)*20) +
            np.random.randn(30)*30).clip(400, 900).astype(int)
cvr      = (3.5 + np.random.randn(30)*0.4 +
            np.sin(days/5) * 0.3).clip(2.5, 5.0)

fig, ax1 = plt.subplots(figsize=(12, 4))
ax2 = ax1.twinx()

ax1.bar(days, orders, color='#4C72B0', alpha=0.5, label='Orders')
ax2.plot(days, cvr, 'o-', color='#DD8452', linewidth=2,
         markersize=5, markerfacecolor='white', markeredgewidth=1.5,
         label='Conversion Rate (%)')

# Flag low-CVR days
low_cvr = days[cvr < 3.0]
for d in low_cvr:
    ax1.axvspan(d-0.5, d+0.5, color='red', alpha=0.1)

ax1.set_xlabel('Day of Month')
ax1.set_ylabel('Orders', color='#4C72B0')
ax2.set_ylabel('Conversion Rate (%)', color='#DD8452')
ax1.set_title('January 2024 β€” Orders & Conversion Rate')
ax1.legend(loc='upper left', frameon=False)
ax2.legend(loc='upper right', frameon=False)
plt.tight_layout()
plt.savefig('twin_ecommerce.png', dpi=100, bbox_inches='tight')
plt.close()
print('Saved twin_ecommerce.png')
🏋️ Practice: Dual-Axis Sales & Growth Rate Chart
Create a dual-axis chart for 12 months of data: bar chart of monthly revenue on the left y-axis (steelblue bars), and a line of month-over-month growth rate (%) on the right y-axis (tomato line with markers). Color-code the left y-axis label steelblue and right y-axis label tomato. Add legends for both axes. Save as 'practice_twin.png'.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

months  = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
revenue = np.array([42, 38, 51, 47, 59, 65, 70, 62, 55, 68, 75, 88])
# TODO: compute MoM growth rate (%)
# growth = np.concatenate([[0], np.diff(revenue) / revenue[:-1] * 100])

fig, ax1 = plt.subplots(figsize=(11, 5))
ax2 = ax1.twinx()

# TODO: ax1.bar(months, revenue, color='steelblue', alpha=0.7, label='Revenue ($K)')
# TODO: ax2.plot(months, growth, 'o-', color='tomato', ...)

# TODO: color y-axis labels
# ax1.set_ylabel('Revenue ($K)', color='steelblue')
# ax2.set_ylabel('MoM Growth (%)', color='tomato')
# ax1.tick_params(axis='y', labelcolor='steelblue')
# ax2.tick_params(axis='y', labelcolor='tomato')

# TODO: add a horizontal dashed line at growth=0 on ax2
# ax2.axhline(0, color='gray', linestyle='--', linewidth=0.8)

# TODO: legends for both axes
ax1.set_title('Monthly Revenue & Growth Rate')
plt.tight_layout()
# TODO: plt.savefig('practice_twin.png', dpi=100, bbox_inches='tight')
plt.close()
print('Done')
✅ Practice Checklist
10. Saving Figures & Style Sheets

Save plots as PNG, PDF, SVG, or EPS with savefig(). Use plt.style.use() or rcParams to apply consistent styling across all charts.

Saving figures with savefig
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import os

fig, ax = plt.subplots(figsize=(8, 4))
x = np.linspace(0, 10, 100)
ax.plot(x, np.sin(x), linewidth=2)
ax.set_title('Saved Figure Example')
ax.set_xlabel('x'); ax.set_ylabel('sin(x)')

# Save as PNG (high DPI for print)
fig.savefig('plot.png', dpi=150, bbox_inches='tight', facecolor='white')
# Save as PDF (vector β€” best for papers)
fig.savefig('plot.pdf', bbox_inches='tight')
# Save as SVG (scalable for web)
fig.savefig('plot.svg', bbox_inches='tight')

print('Saved: plot.png, plot.pdf, plot.svg')
plt.close()

for f in ['plot.png','plot.pdf','plot.svg']:
    if os.path.exists(f): os.remove(f)
print('Cleaned up.')
Style sheets and rcParams
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

print("Available styles (first 5):", plt.style.available[:5], "...")

# Apply a style
with plt.style.context('seaborn-v0_8-whitegrid'):
    fig, ax = plt.subplots(figsize=(8, 4))
    x = np.linspace(0, 10, 100)
    ax.plot(x, np.sin(x), linewidth=2, label='sin(x)')
    ax.plot(x, np.cos(x), linewidth=2, label='cos(x)')
    ax.set_title('Seaborn Whitegrid Style')
    ax.legend()
    plt.tight_layout()
    fig.savefig('style_whitegrid.png', dpi=100, bbox_inches='tight')
    plt.close()
    print('Saved style_whitegrid.png')

# Custom rcParams for global defaults
plt.rcParams.update({
    'font.size': 12,
    'axes.titlesize': 14,
    'figure.facecolor': 'white',
})
print('rcParams updated.')
Batch export: saving multiple figures to files
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import os

plt.rcParams.update({
    'font.size': 10,
    'axes.spines.top': False,
    'axes.spines.right': False,
    'grid.alpha': 0.3,
})

np.random.seed(42)
datasets = {
    'alpha': np.random.randn(300) * 2 + 5,
    'beta':  np.random.exponential(2, 300),
    'gamma': np.random.uniform(0, 10, 300),
}

saved = []
for name, data in datasets.items():
    fig, axes = plt.subplots(1, 2, figsize=(9, 3))

    axes[0].hist(data, bins=25, color='steelblue', edgecolor='white', linewidth=0.4)
    axes[0].set_title(f'{name.capitalize()} β€” Histogram')
    axes[0].axvline(data.mean(), color='red', linestyle='--', linewidth=1.2, label=f'mean={data.mean():.2f}')
    axes[0].legend(fontsize=8)

    axes[1].boxplot(data, vert=True, patch_artist=True,
                    boxprops=dict(facecolor='steelblue', alpha=0.5))
    axes[1].set_title(f'{name.capitalize()} β€” Boxplot')
    axes[1].set_ylabel('Value')

    fname = f'report_{name}.png'
    fig.tight_layout()
    fig.savefig(fname, dpi=120, bbox_inches='tight', facecolor='white')
    plt.close(fig)
    saved.append(fname)
    print(f'  Saved {fname}  ({os.path.getsize(fname)//1024} KB)')

# Clean up
for f in saved:
    if os.path.exists(f): os.remove(f)
print('Batch export complete.')
Animation with FuncAnimation (bouncing ball)
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np

fig, ax = plt.subplots(figsize=(6, 5))
ax.set_xlim(0, 10); ax.set_ylim(0, 10)
ax.set_aspect('equal'); ax.set_title('Bouncing Ball Animation')
ax.set_facecolor('#1a1a2e')

ball, = ax.plot([], [], 'o', color='#ffa600', ms=18)
trail_x, trail_y = [], []
trail, = ax.plot([], [], '-', color='#ffa600', alpha=0.3, lw=2)

x, y = 5.0, 8.0
vx, vy = 0.15, -0.12

def init():
    ball.set_data([], []); trail.set_data([], [])
    return ball, trail

def update(frame):
    global x, y, vx, vy
    x += vx; y += vy
    vy -= 0.005          # gravity
    if x <= 0 or x >= 10: vx *= -1
    if y <= 0: vy = abs(vy) * 0.92; y = 0
    trail_x.append(x); trail_y.append(y)
    if len(trail_x) > 40:
        trail_x.pop(0); trail_y.pop(0)
    ball.set_data([x], [y])
    trail.set_data(trail_x, trail_y)
    return ball, trail

ani = animation.FuncAnimation(fig, update, frames=80, init_func=init,
                               interval=40, blit=True)
ani.save('bouncing_ball.gif', writer='pillow', fps=25, dpi=80)
plt.close()
print('Saved bouncing_ball.gif')
💼 Real-World: Automated Report Figure Export
A data engineering pipeline generates standardized PNG charts nightly and attaches them to an email report.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import numpy as np
import os

# Apply consistent branding
plt.rcParams.update({
    'font.family':      'DejaVu Sans',
    'font.size':        10,
    'axes.titlesize':   12,
    'axes.titleweight': 'bold',
    'axes.spines.top':  False,
    'axes.spines.right':False,
    'grid.alpha':       0.3,
})

def save_kpi_chart(metric, values, labels, filename, color='steelblue'):
    fig, ax = plt.subplots(figsize=(8, 3))
    ax.plot(range(len(values)), values, 'o-', color=color,
            linewidth=2, markersize=6, markerfacecolor='white', markeredgewidth=2)
    ax.fill_between(range(len(values)), values, alpha=0.1, color=color)
    ax.set_xticks(range(len(labels)))
    ax.set_xticklabels(labels, rotation=30, ha='right')
    ax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda x,_: f'${x:,.0f}'))
    ax.set_title(metric); ax.grid(True)
    fig.tight_layout()
    fig.savefig(filename, dpi=120, bbox_inches='tight', facecolor='white')
    plt.close(fig)
    print(f'Saved {filename}  ({os.path.getsize(filename)//1024} KB)')

months = ['Jan','Feb','Mar','Apr','May','Jun']
rev    = [42000, 38500, 51000, 47200, 59800, 65600]
save_kpi_chart('Monthly Revenue', rev, months, 'revenue_report.png')

if os.path.exists('revenue_report.png'):
    os.remove('revenue_report.png')
    print('Cleaned up.')
🏋️ Practice: Saving to PNG and SVG
Create a figure with two subplots side-by-side: (left) a line plot of sin(x) styled with 'seaborn-v0_8-whitegrid', and (right) a bar chart of 5 categories. Apply rcParams to set font.size=11. Save the figure as both 'practice_output.png' (dpi=150) and 'practice_output.svg'. Print the file sizes. Then clean up both files.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
import os

# TODO: update rcParams (font.size=11, figure.facecolor='white')
# plt.rcParams.update({...})

x      = np.linspace(0, 2 * np.pi, 200)
cats   = ['A', 'B', 'C', 'D', 'E']
values = [23, 45, 17, 38, 29]

# TODO: use plt.style.context('seaborn-v0_8-whitegrid') to create the figure
# with plt.style.context('seaborn-v0_8-whitegrid'):
#     fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
#
#     # left: line plot of sin(x)
#     ax1.plot(x, np.sin(x), color='steelblue', linewidth=2)
#     ax1.set_title('sin(x)'); ax1.set_xlabel('x'); ax1.set_ylabel('y')
#
#     # right: bar chart
#     ax2.bar(cats, values, color='#DD8452', edgecolor='white')
#     ax2.set_title('Category Values'); ax2.set_ylabel('Count')
#
#     plt.tight_layout()
#
#     # TODO: save as PNG and SVG
#     # fig.savefig('practice_output.png', dpi=150, bbox_inches='tight', facecolor='white')
#     # fig.savefig('practice_output.svg', bbox_inches='tight')
#     plt.close()

# TODO: print file sizes
# for f in ['practice_output.png', 'practice_output.svg']:
#     print(f'{f}: {os.path.getsize(f)//1024} KB')

# TODO: clean up both files
# for f in ['practice_output.png', 'practice_output.svg']:
#     if os.path.exists(f): os.remove(f)
print('Done')
✅ Practice Checklist
11. 3D Plotting

Create 3D visualizations with Axes3D β€” surface plots, wireframes, 3D scatter plots, and line trajectories that reveal multi-dimensional structure.

3D scatter plot with color mapping
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np

np.random.seed(42)
x, y, z = np.random.randn(3, 100)
fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111, projection='3d')
sc = ax.scatter(x, y, z, c=z, cmap='plasma', s=50)
plt.colorbar(sc, ax=ax, label='Z value')
ax.set_xlabel('X'); ax.set_ylabel('Y'); ax.set_zlabel('Z')
ax.set_title('3D Scatter Plot')
plt.tight_layout()
plt.savefig('3d_scatter.png', dpi=80); plt.close()
print('Saved 3d_scatter.png')
Surface plot with meshgrid
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np

x = np.linspace(-3, 3, 50)
y = np.linspace(-3, 3, 50)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

fig = plt.figure(figsize=(8, 6))
ax = fig.add_subplot(111, projection='3d')
surf = ax.plot_surface(X, Y, Z, cmap='viridis', alpha=0.9)
plt.colorbar(surf, ax=ax, shrink=0.5, label='sin(r)')
ax.set_title('3D Surface: sin(sqrt(x^2+y^2))')
plt.tight_layout()
plt.savefig('surface3d.png', dpi=80); plt.close()
print('Saved surface3d.png')
Wireframe and contour overlay
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np

x = np.linspace(-2, 2, 30)
y = np.linspace(-2, 2, 30)
X, Y = np.meshgrid(x, y)
Z = X**2 - Y**2

fig = plt.figure(figsize=(12, 5))
ax1 = fig.add_subplot(121, projection='3d')
ax1.plot_wireframe(X, Y, Z, rstride=3, cstride=3, color='teal', linewidth=0.5)
ax1.set_title('Wireframe: x^2 - y^2')

ax2 = fig.add_subplot(122, projection='3d')
ax2.plot_surface(X, Y, Z, cmap='coolwarm', alpha=0.7)
ax2.contour(X, Y, Z, zdir='z', offset=Z.min(), cmap='coolwarm')
ax2.set_title('Surface + Contour')
plt.tight_layout()
plt.savefig('wireframe3d.png', dpi=80); plt.close()
print('Saved wireframe3d.png')
3D line trajectory
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np

t = np.linspace(0, 4*np.pi, 300)
x = np.sin(t)
y = np.cos(t)
z = t / (4*np.pi)

fig = plt.figure(figsize=(7, 6))
ax = fig.add_subplot(111, projection='3d')
for i in range(len(t)-1):
    ax.plot(x[i:i+2], y[i:i+2], z[i:i+2], color=plt.cm.plasma(z[i]), linewidth=2)
ax.set_xlabel('X'); ax.set_ylabel('Y'); ax.set_zlabel('Height')
ax.set_title('3D Helix Trajectory')
plt.tight_layout()
plt.savefig('helix3d.png', dpi=80); plt.close()
print('Saved helix3d.png')
🏋️ Practice: Rosenbrock Surface
Plot the Rosenbrock function f(x,y)=(1-x)^2 + 100(y-x^2)^2 as a 3D surface (clipped at 2500). Overlay a contour at z=0. Mark the global minimum at (1,1,0) with a red dot.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np

x = np.linspace(-2, 2, 100)
y = np.linspace(-1, 3, 100)
X, Y = np.meshgrid(x, y)
Z = np.clip((1-X)**2 + 100*(Y-X**2)**2, 0, 2500)

# TODO: create 3D surface plot
# TODO: mark minimum at (1, 1, 0)
# TODO: save to 'rosenbrock.png'
✅ Practice Checklist
12. Custom Styles & Themes

Make publication-quality figures with style sheets, rcParams, custom color cycles, and reusable theming functions.

Built-in style sheets
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

print('Available styles (sample):', plt.style.available[:8])

styles = ['seaborn-v0_8-darkgrid', 'ggplot', 'bmh']
x = np.linspace(0, 2*np.pi, 100)

fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for ax, style in zip(axes, styles):
    with plt.style.context(style):
        for i in range(3):
            ax.plot(x, np.sin(x + i*np.pi/3))
        ax.set_title(style.split('-')[-1])
plt.tight_layout()
plt.savefig('styles.png', dpi=80); plt.close()
print('Saved styles.png')
Custom rcParams dark theme
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

custom = {
    'figure.facecolor': '#1e1e2e', 'axes.facecolor': '#1e1e2e',
    'axes.edgecolor': '#6e6e8e', 'axes.labelcolor': '#cdd6f4',
    'xtick.color': '#cdd6f4', 'ytick.color': '#cdd6f4',
    'text.color': '#cdd6f4', 'grid.color': '#313244',
    'axes.prop_cycle': matplotlib.cycler('color', ['#89b4fa','#f38ba8','#a6e3a1','#fab387']),
    'font.size': 12,
}
plt.rcParams.update(custom)
x = np.linspace(0, 4*np.pi, 200)
fig, ax = plt.subplots(figsize=(9, 5))
for i, label in enumerate(['sin', 'cos', 'sin2']):
    y = np.sin(x)*np.sin(x) if i==2 else (np.sin(x) if i==0 else np.cos(x))
    ax.plot(x, y, linewidth=2, label=label)
ax.legend(facecolor='#313244'); ax.set_title('Dark Theme'); ax.grid(True)
plt.tight_layout()
plt.savefig('dark_theme.png', dpi=80, facecolor=fig.get_facecolor()); plt.close()
plt.rcParams.update(plt.rcParamsDefault)
print('Saved dark_theme.png')
Custom color cycles and grouped charts
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

PALETTE = ['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00']
plt.rcParams['axes.prop_cycle'] = matplotlib.cycler('color', PALETTE)

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
categories = ['A','B','C','D']; x = np.arange(len(categories)); width=0.25
for i, grp in enumerate(['G1','G2','G3']):
    axes[0].bar(x+i*width, np.random.randint(10,50,4), width, label=grp)
axes[0].set_xticks(x+width); axes[0].set_xticklabels(categories)
axes[0].legend(); axes[0].set_title('Custom Colors β€” Bars')

t = np.linspace(0,10,200)
for i in range(5):
    axes[1].plot(t, np.sin(t+i)*np.exp(-0.1*t), label=f'S{i+1}')
axes[1].legend(fontsize=8); axes[1].set_title('Custom Colors β€” Lines')
plt.tight_layout()
plt.savefig('colors.png', dpi=80); plt.close()
plt.rcParams.update(plt.rcParamsDefault)
print('Saved colors.png')
Reusable publication-style context manager
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
from contextlib import contextmanager

@contextmanager
def pub_style(w=8, h=5):
    params = {
        'font.size': 12, 'axes.titlesize': 14, 'axes.labelsize': 12,
        'lines.linewidth': 2, 'axes.spines.top': False, 'axes.spines.right': False,
    }
    with plt.style.context('seaborn-v0_8-whitegrid'):
        plt.rcParams.update(params)
        fig, ax = plt.subplots(figsize=(w, h))
        yield fig, ax
        plt.tight_layout()

x = np.linspace(0, 10, 200)
with pub_style() as (fig, ax):
    ax.plot(x, np.sin(x), label='sin(x)')
    ax.plot(x, np.cos(x), '--', label='cos(x)')
    ax.set_xlabel('x'); ax.set_ylabel('Amplitude')
    ax.set_title('Publication-Ready Figure'); ax.legend()
    fig.savefig('publication.png', bbox_inches='tight')
plt.close()
print('Saved publication.png')
🏋️ Practice: Themed Dashboard
Create a 2x2 subplot figure with a dark background. Include: scatter (colored by magnitude), multi-line chart, bar chart, and histogram. Use a consistent 4-color palette.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
# TODO: set dark rcParams
# TODO: 2x2 subplots
# TODO: top-left: scatter (x,y normal, color=magnitude)
# TODO: top-right: 3 sine waves
# TODO: bottom-left: 12-bar monthly sales
# TODO: bottom-right: histogram 500 samples, 30 bins
# TODO: suptitle and save to 'dark_dashboard.png'
✅ Practice Checklist
13. Annotations & Text Elements

Add rich annotations β€” arrows, text boxes, spans, and LaTeX math expressions β€” to communicate insights directly on the plot.

Arrow and text annotations
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 4*np.pi, 300)
y = np.sin(x)
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(x, y, 'steelblue', linewidth=2)

# Annotate maximum
max_idx = y.argmax()
ax.annotate('Global Max', xy=(x[max_idx], y[max_idx]),
            xytext=(x[max_idx]+1, 0.6),
            arrowprops=dict(arrowstyle='->', color='red', lw=2),
            fontsize=11, color='red', fontweight='bold')

# Annotate minimum
min_idx = y.argmin()
ax.annotate('Global Min', xy=(x[min_idx], y[min_idx]),
            xytext=(x[min_idx]-1.5, -0.6),
            arrowprops=dict(arrowstyle='->', color='orange', lw=2),
            fontsize=11, color='orange')

ax.set_title('sin(x) with Annotations'); ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('annotated_sine.png', dpi=80); plt.close()
print('Saved annotated_sine.png')
Text boxes and callout styles
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 200)
fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(x, np.exp(-0.3*x)*np.sin(x*2), linewidth=2)

box_styles = [('round,pad=0.3', 'lightblue', 'navy'),
              ('round4,pad=0.4', 'lightyellow', 'darkorange'),
              ('sawtooth,pad=0.3', 'lightgreen', 'darkgreen')]

for i, (style, fc, ec) in enumerate(box_styles):
    ax.text(2+i*3, 0.5-i*0.3, f'Style: {style.split(",")[0]}',
            fontsize=10, ha='center',
            bbox=dict(boxstyle=style, facecolor=fc, edgecolor=ec, alpha=0.9))

ax.set_title('Text Box Styles'); ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('textboxes.png', dpi=80); plt.close()
print('Saved textboxes.png')
axvspan and axhspan for region highlighting
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
dates = np.arange(60)
prices = 100 + np.cumsum(np.random.randn(60))

fig, ax = plt.subplots(figsize=(11, 5))
ax.plot(dates, prices, 'steelblue', linewidth=1.5)

# Shade event regions
ax.axvspan(10, 20, color='red',   alpha=0.15, label='Crash period')
ax.axvspan(35, 45, color='green', alpha=0.15, label='Recovery period')
ax.axhline(prices.mean(), color='gray', linestyle='--', linewidth=1, label='Mean price')

# Annotate events
ax.text(15, ax.get_ylim()[1]*0.98, 'Crash', ha='center', color='darkred', fontsize=10)
ax.text(40, ax.get_ylim()[1]*0.98, 'Recovery', ha='center', color='darkgreen', fontsize=10)

ax.legend(); ax.set_xlabel('Day'); ax.set_ylabel('Price')
ax.set_title('Price Series with Event Annotations')
plt.tight_layout()
plt.savefig('event_regions.png', dpi=80); plt.close()
print('Saved event_regions.png')
LaTeX math in labels and annotations
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-3, 3, 300)
mu, sigma = 0, 1
y = (1/(sigma*np.sqrt(2*np.pi))) * np.exp(-0.5*((x-mu)/sigma)**2)

fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(x, y, 'steelblue', linewidth=2.5)
ax.fill_between(x, y, where=(x>=-1)&(x<=1), alpha=0.3, color='steelblue', label=r'$\pm 1\sigma$ (68.3%)')

# LaTeX in axis labels
ax.set_xlabel(r'$x$', fontsize=14)
ax.set_ylabel(r'$f(x) = \frac{1}{\sigma\sqrt{2\pi}} e^{-\frac{1}{2}\left(\frac{x-\mu}{\sigma}\right)^2}$', fontsize=11)
ax.set_title(r'Normal Distribution $\mathcal{N}(\mu=0, \sigma=1)$', fontsize=13)

# LaTeX annotation
ax.annotate(r'$\mu = 0$', xy=(0, y.max()), xytext=(1.2, y.max()*0.9),
            arrowprops=dict(arrowstyle='->'), fontsize=12)
ax.legend(fontsize=11)
plt.tight_layout()
plt.savefig('latex_plot.png', dpi=80); plt.close()
print('Saved latex_plot.png')
🏋️ Practice: Annotated Confidence Interval
Plot a regression line y=2x+1 with noise. Add shaded 95% confidence band, annotate the slope with an arrow and LaTeX label, and shade the extrapolation region (x>8) in red.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
x = np.linspace(0, 10, 50)
y = 2*x + 1 + np.random.randn(50) * 2
x_fit = np.linspace(0, 12, 200)
y_fit = 2*x_fit + 1

fig, ax = plt.subplots(figsize=(9, 5))
ax.scatter(x, y, alpha=0.6, label='Data')
ax.plot(x_fit, y_fit, 'r-', label='Fit')
# TODO: add shaded CI band (y_fit +/- 2 units)
# TODO: shade extrapolation x>8 with axvspan
# TODO: annotate slope with arrow and r'$\hat{\beta}_1 = 2$'
# TODO: save to 'ci_plot.png'
✅ Practice Checklist
14. Animations & GIF Export

FuncAnimation β€” rolling sine wave
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np

fig, ax = plt.subplots(figsize=(7, 3))
x = np.linspace(0, 2*np.pi, 200)
line, = ax.plot(x, np.sin(x))
ax.set_ylim(-1.3, 1.3)

def update(frame):
    line.set_ydata(np.sin(x + frame * 0.15))
    return line,

ani = animation.FuncAnimation(fig, update, frames=40, interval=60, blit=True)
try:
    ani.save('sine_anim.gif', writer='pillow', fps=15)
    print('Saved sine_anim.gif')
except Exception as e:
    print(f'pillow not installed ({e}) β€” animation built ok')
plt.close()
Random walk particle animation
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np

np.random.seed(42)
n = 20
pos = np.random.randn(n, 2)
fig, ax = plt.subplots(figsize=(5, 5))
sc = ax.scatter(pos[:, 0], pos[:, 1], c=range(n), cmap='hsv', s=50)
ax.set_xlim(-5, 5); ax.set_ylim(-5, 5)
ax.set_title('Random Walk Particles')

def update(frame):
    global pos
    pos += np.random.randn(n, 2) * 0.15
    pos = np.clip(pos, -4.5, 4.5)
    sc.set_offsets(pos)
    return sc,

ani = animation.FuncAnimation(fig, update, frames=50, interval=80, blit=True)
try:
    ani.save('particles.gif', writer='pillow', fps=12)
    print('Saved particles.gif')
except Exception as e:
    print(f'pillow not found ({e})')
plt.close()
Dual-subplot animation (sin & cos)
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4))
x = np.linspace(0, 2*np.pi, 200)
l1, = ax1.plot(x, np.sin(x), 'b-')
l2, = ax2.plot(x, np.cos(x), 'r-')
ax1.set_title('sin(x+t)'); ax2.set_title('cos(x-t)')

def update(frame):
    t = frame * 0.12
    l1.set_ydata(np.sin(x + t))
    l2.set_ydata(np.cos(x - t))
    return l1, l2

ani = animation.FuncAnimation(fig, update, frames=50, blit=True)
try:
    ani.save('dual_anim.gif', writer='pillow', fps=15)
    print('Saved dual_anim.gif')
except:
    print('Animation created (pillow needed to save)')
plt.close()
ArtistAnimation with histogram frames
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np

fig, ax = plt.subplots(figsize=(6, 4))
np.random.seed(0)
frames = []
for i in range(10):
    data = np.random.randn(50) + i * 0.5
    hist = ax.hist(data, bins=20, range=(-3, 9),
                  color=plt.cm.viridis(i/10), alpha=0.7)
    title = ax.text(0.5, 1.01, f'Shift = {i*0.5:.1f}',
                   transform=ax.transAxes, ha='center')
    frames.append(hist[2].tolist() + [title])
ani = animation.ArtistAnimation(fig, frames, interval=300, blit=True)
try:
    ani.save('hist_anim.gif', writer='pillow', fps=3)
    print('Saved hist_anim.gif')
except:
    print('ArtistAnimation built (pillow needed to save)')
plt.close()
🏋️ Practice: Bouncing Ball Animation
Animate a ball under gravity (g=9.8). Start at y=5, vy=0. Update position each frame with dt=0.05. Reverse vy on bounce at y=0. Save 100 frames to 'bounce.gif'.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import numpy as np

fig, ax = plt.subplots(figsize=(4, 6))
ax.set_xlim(0, 1); ax.set_ylim(-0.2, 5.5)
ball, = ax.plot([0.5], [5], 'bo', ms=15)
dt = 0.05; g = 9.8
y, vy = 5.0, 0.0

# TODO: define update(frame) that applies gravity, reverses on bounce
# TODO: FuncAnimation for 100 frames
# TODO: save to 'bounce.gif' with pillow
✅ Practice Checklist
15. Publication-Quality Figures

Style sheets comparison
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 4*np.pi, 200)
styles = ['seaborn-v0_8-paper', 'ggplot', 'bmh']
fig, axes = plt.subplots(1, 3, figsize=(13, 4))
for ax, style in zip(axes, styles):
    with plt.style.context(style):
        ax.plot(x, np.sin(x), label='sin')
        ax.plot(x, np.cos(x), label='cos')
        ax.set_title(style.split('_')[-1])
        ax.legend(fontsize=8)
fig.tight_layout()
fig.savefig('styles_compare.png', dpi=120, bbox_inches='tight')
print('Saved styles_compare.png')
plt.close()
LaTeX math in axis labels and titles
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots(figsize=(6, 4))
x = np.linspace(0.1, 3, 200)
ax.plot(x, np.exp(-x**2), label=r'$f(x) = e^{-x^2}$', lw=2)
ax.plot(x, 1/(1+x**2), label=r'$g(x) = \frac{1}{1+x^2}$', lw=2, ls='--')
ax.set_xlabel(r'$x$ (normalized)', fontsize=12)
ax.set_ylabel(r'$f(x)$', fontsize=12)
ax.set_title(r'Gaussian vs Lorentzian decay', fontsize=13)
ax.legend(fontsize=11); ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig('latex_labels.png', dpi=150, bbox_inches='tight')
print('Saved latex_labels.png')
plt.close()
Shared-axes multi-panel figure
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
fig, axes = plt.subplots(2, 3, figsize=(12, 7), sharex=True, sharey=True)
for i, ax in enumerate(axes.flat):
    data = np.random.normal(i, 1.2, 80)
    ax.hist(data, bins=18, edgecolor='k', alpha=0.7)
    ax.axvline(data.mean(), color='red', ls='--', lw=1.2)
    ax.set_title(f'Group {i+1} (ΞΌ={i})', fontsize=10)
fig.suptitle('Shared-Axis Multi-Panel', fontsize=14, fontweight='bold')
fig.tight_layout()
fig.savefig('multi_panel.png', dpi=120, bbox_inches='tight')
print('Saved multi_panel.png')
plt.close()
High-DPI export with serif fonts
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

plt.rcParams.update({'font.family': 'serif', 'font.size': 10,
                     'axes.linewidth': 1.2, 'xtick.major.width': 1.2})
np.random.seed(42)
groups = ['Control', 'Drug A', 'Drug B']
means = [2.1, 3.4, 2.9]; stds = [0.3, 0.4, 0.35]
fig, ax = plt.subplots(figsize=(4.5, 3.5))
bars = ax.bar(groups, means, yerr=stds, capsize=5,
              color=['#4878D0','#EE854A','#6ACC65'], edgecolor='k', lw=0.8)
for b, m in zip(bars, means):
    ax.text(b.get_x()+b.get_width()/2, b.get_height()+0.06,
            f'{m:.1f}', ha='center', fontsize=9)
ax.set_ylabel(r'Response ($ΞΌmol/L)', fontsize=11)
ax.set_title('Treatment Effect', fontsize=12)
ax.spines[['top','right']].set_visible(False)
fig.tight_layout()
fig.savefig('publication_fig.png', dpi=300, bbox_inches='tight')
print('Saved publication_fig.png at 300 DPI')
plt.close()
🏋️ Practice: 2-Panel with Inset Zoom
Plot y = e^{-0.1t}*sin(2t) on linear and log scales. Add an inset zoom on t=[0,2] in the linear panel. Use seaborn-v0_8-paper style, serif fonts, and export at 300 DPI.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import inset_axes, mark_inset
import numpy as np

t = np.linspace(0, 20, 500)
y = np.exp(-0.1*t) * np.sin(2*t)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 4))
# TODO: ax1 linear plot + inset zoom t in [0,2]
# TODO: ax2 semilogy plot
# TODO: seaborn-v0_8-paper style, 300 DPI export
✅ Practice Checklist
16. Custom Colormaps & Color Science

LinearSegmentedColormap from color list
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy as np

colors = ['#1a1a2e','#16213e','#0f3460','#e94560']
cmap = mcolors.LinearSegmentedColormap.from_list('dark_red', colors, N=256)
np.random.seed(0)
data = np.random.randn(40, 40).cumsum(axis=0)
fig, ax = plt.subplots(figsize=(6, 4))
im = ax.imshow(data, cmap=cmap, aspect='auto')
plt.colorbar(im, ax=ax, label='Value')
ax.set_title('Custom Dark-Red Colormap')
fig.savefig('custom_cmap.png', dpi=120, bbox_inches='tight')
print('Saved custom_cmap.png')
plt.close()
TwoSlopeNorm for asymmetric diverging maps
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy as np

np.random.seed(42)
data = np.random.randn(20, 20) * 3 + 1
vmax = max(abs(data.min()), abs(data.max()))
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4))
im1 = ax1.imshow(data, cmap='RdBu_r', vmin=-vmax, vmax=vmax)
ax1.set_title('Symmetric Norm'); plt.colorbar(im1, ax=ax1)
norm = mcolors.TwoSlopeNorm(vmin=data.min(), vcenter=0, vmax=data.max())
im2 = ax2.imshow(data, cmap='RdBu_r', norm=norm)
ax2.set_title('TwoSlopeNorm'); plt.colorbar(im2, ax=ax2)
fig.tight_layout()
fig.savefig('diverging_norm.png', dpi=120, bbox_inches='tight')
print('Saved diverging_norm.png')
plt.close()
Discrete colorbar with BoundaryNorm
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy as np

bounds = [0, 1, 2, 3, 4, 5]
cmap = plt.get_cmap('RdYlGn', len(bounds)-1)
norm = mcolors.BoundaryNorm(bounds, cmap.N)
levels = ['None','Low','Med','High','Max']
np.random.seed(7)
data = np.random.randint(0, 5, (8, 10))
fig, ax = plt.subplots(figsize=(9, 5))
im = ax.imshow(data, cmap=cmap, norm=norm)
cbar = plt.colorbar(im, ax=ax, boundaries=bounds, ticks=[0.5,1.5,2.5,3.5,4.5])
cbar.set_ticklabels(levels)
for i in range(data.shape[0]):
    for j in range(data.shape[1]):
        ax.text(j, i, levels[data[i,j]][:3],
                ha='center', va='center', fontsize=8)
ax.set_title('Discrete Risk Heatmap')
fig.savefig('discrete_cmap.png', dpi=120, bbox_inches='tight')
print('Saved discrete_cmap.png')
plt.close()
Perceptually uniform colormap comparison
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

cmaps = ['viridis', 'plasma', 'inferno', 'cividis']
x = y = np.linspace(-np.pi, np.pi, 200)
X, Y = np.meshgrid(x, y)
Z = np.sin(X) * np.cos(Y)
fig, axes = plt.subplots(1, 4, figsize=(14, 3.5))
for ax, cm in zip(axes, cmaps):
    im = ax.imshow(Z, cmap=cm, aspect='auto')
    ax.set_title(cm)
    plt.colorbar(im, ax=ax, fraction=0.046)
fig.suptitle('Perceptually Uniform Colormaps', fontsize=12)
fig.tight_layout()
fig.savefig('perceptual_cmaps.png', dpi=120, bbox_inches='tight')
print('Saved perceptual_cmaps.png')
plt.close()
🏋️ Practice: Custom Terrain Colormap
Create a colormap: blue β†’ green β†’ yellow β†’ brown β†’ white. Apply to a sin(X)*cos(Y) surface. Add a colorbar with 5 ticks labeled: Sea, Lowland, Hills, Mountain, Snow.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy as np

colors = ['#1a6b9a','#2ecc71','#f1c40f','#8b5e3c','#f5f5f5']
cmap = mcolors.LinearSegmentedColormap.from_list('terrain_custom', colors)
x = y = np.linspace(-3, 3, 100)
X, Y = np.meshgrid(x, y)
Z = np.sin(X) + np.cos(Y)
# TODO: imshow with custom cmap
# TODO: colorbar with 5 labeled ticks
# TODO: save 'terrain.png' at 150 DPI
✅ Practice Checklist
17. Error Bars & Confidence Intervals

Visualize measurement uncertainty with errorbar(), fill_between() for confidence bands, and asymmetric errors.

Basic symmetric error bars
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(0)
x = np.arange(1, 8)
y = np.array([2.3, 3.1, 2.8, 4.5, 3.9, 5.2, 4.8])
yerr = np.random.uniform(0.2, 0.6, len(x))

fig, ax = plt.subplots(figsize=(8, 4))
ax.errorbar(x, y, yerr=yerr, fmt='o-', color='steelblue',
            ecolor='lightsteelblue', elinewidth=2, capsize=5,
            capthick=2, label='Mean Β± SD')
ax.set_xlabel('Experiment')
ax.set_ylabel('Value')
ax.set_title('Error Bars β€” Symmetric')
ax.legend()
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig('errorbars_sym.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved errorbars_sym.png')
Asymmetric error bars
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(1)
x = np.arange(5)
y = np.array([1.5, 2.8, 2.2, 3.6, 3.0])
yerr_low  = np.array([0.3, 0.5, 0.4, 0.6, 0.3])
yerr_high = np.array([0.5, 0.3, 0.6, 0.2, 0.7])

fig, ax = plt.subplots(figsize=(7, 4))
ax.errorbar(x, y, yerr=[yerr_low, yerr_high],
            fmt='s--', color='tomato', ecolor='lightcoral',
            elinewidth=2, capsize=6, label='Median [IQR]')
ax.set_xticks(x)
ax.set_xticklabels([f'Group {i+1}' for i in x])
ax.set_title('Asymmetric Error Bars')
ax.legend()
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig('errorbars_asym.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved errorbars_asym.png')
Confidence band with fill_between
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(2)
x = np.linspace(0, 10, 100)
y_true = np.sin(x)
noise = np.random.randn(100) * 0.3
y_mean = y_true + noise * 0.2
y_lower = y_mean - 1.96 * 0.3
y_upper = y_mean + 1.96 * 0.3

fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(x, y_mean, color='steelblue', linewidth=2, label='Mean')
ax.fill_between(x, y_lower, y_upper, alpha=0.2, color='steelblue',
                label='95% CI')
ax.plot(x, y_true, 'k--', linewidth=1, alpha=0.6, label='True')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title('Confidence Interval Band')
ax.legend()
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig('conf_band.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved conf_band.png')
Multiple series with shaded bands
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
x = np.linspace(0, 5, 80)
colors = ['steelblue', 'tomato', 'seagreen']
labels = ['Model A', 'Model B', 'Model C']
offsets = [0, 0.5, 1.0]

fig, ax = plt.subplots(figsize=(9, 5))
for c, lab, off in zip(colors, labels, offsets):
    mu = np.sin(x + off)
    sd = 0.15 + 0.05 * np.abs(np.cos(x))
    ax.plot(x, mu, color=c, linewidth=2, label=lab)
    ax.fill_between(x, mu - sd, mu + sd, color=c, alpha=0.15)
ax.set_xlabel('Time')
ax.set_ylabel('Score')
ax.set_title('Model Comparison with Confidence Bands')
ax.legend()
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig('multi_band.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved multi_band.png')
💼 Real-World: Clinical Trial Results
Visualize drug-trial results with asymmetric confidence intervals for 4 dosage groups, a reference line at placebo, and significance brackets.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(7)
groups = ['Placebo', '10mg', '25mg', '50mg']
means  = [0.0, 1.2, 2.8, 3.5]
low    = [0.0, 0.3, 0.5, 0.4]
high   = [0.0, 0.5, 0.6, 0.8]

fig, ax = plt.subplots(figsize=(7, 5))
x = np.arange(len(groups))
ax.bar(x, means, color=['#aaa','#5ab4d6','#2171b5','#084594'],
       alpha=0.85, width=0.5)
ax.errorbar(x, means, yerr=[low, high],
            fmt='none', ecolor='black', elinewidth=2, capsize=8, capthick=2)
ax.axhline(0, color='gray', linewidth=0.8, linestyle='--')
ax.set_xticks(x); ax.set_xticklabels(groups)
ax.set_ylabel('Effect Size vs Baseline')
ax.set_title('Clinical Trial: Dose-Response', fontweight='bold')
for xi, m, h in zip(x[1:], means[1:], high[1:]):
    ax.text(xi, m + h + 0.05, '*' if m < 3 else '***',
            ha='center', fontsize=14, color='darkred')
fig.tight_layout()
fig.savefig('clinical_trial.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved clinical_trial.png')
🏋️ Practice: Error Bar Practice
Generate 6 groups with random means (2-8) and errors. Plot horizontal error bars (ax.errorbar with fmt='o', xerr=...) with capsize=5. Color bars by quartile (low=green, mid=orange, high=red). Add a vertical reference line at x=5.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(10)
groups = [f'Group {i}' for i in range(1, 7)]
means  = np.random.uniform(2, 8, 6)
errors = np.random.uniform(0.3, 1.2, 6)
# TODO: horizontal errorbar plot
# TODO: color by quartile
# TODO: vertical reference line at x=5
# TODO: save 'hbar_errors.png'
✅ Practice Checklist
18. Box Plots & Violin Plots

Compare distributions across groups with boxplot() for quartile summaries and violinplot() for full density shapes. Combine both for richer insights.

Side-by-side box plots
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(0)
data = [np.random.normal(loc, 1.0, 80) for loc in [2, 3.5, 2.8, 4.2, 3.0]]
labels = ['A', 'B', 'C', 'D', 'E']

fig, ax = plt.subplots(figsize=(8, 5))
bp = ax.boxplot(data, labels=labels, patch_artist=True, notch=True,
                medianprops=dict(color='white', linewidth=2))
colors = ['#4c72b0','#dd8452','#55a868','#c44e52','#8172b2']
for patch, color in zip(bp['boxes'], colors):
    patch.set_facecolor(color)
    patch.set_alpha(0.8)
ax.set_xlabel('Group')
ax.set_ylabel('Value')
ax.set_title('Notched Box Plots by Group')
ax.grid(True, axis='y', alpha=0.3)
fig.tight_layout()
fig.savefig('boxplot.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved boxplot.png')
Violin plot with overlaid box
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(1)
data = [np.concatenate([np.random.normal(0, 1, 60),
                         np.random.normal(3, 0.5, 20)])
        for _ in range(4)]
labels = ['Q1', 'Q2', 'Q3', 'Q4']

fig, ax = plt.subplots(figsize=(8, 5))
parts = ax.violinplot(data, positions=range(1, 5), showmedians=True,
                       showextrema=True)
for pc in parts['bodies']:
    pc.set_facecolor('#4c72b0')
    pc.set_alpha(0.7)
ax.boxplot(data, positions=range(1, 5), widths=0.1,
           patch_artist=True,
           boxprops=dict(facecolor='white', linewidth=1),
           medianprops=dict(color='red', linewidth=2),
           whiskerprops=dict(linewidth=1),
           capprops=dict(linewidth=1),
           flierprops=dict(markersize=3))
ax.set_xticks(range(1, 5)); ax.set_xticklabels(labels)
ax.set_title('Violin + Box Plot Overlay')
ax.grid(True, axis='y', alpha=0.3)
fig.tight_layout()
fig.savefig('violin_box.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved violin_box.png')
Grouped box plots with hue
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(2)
n = 60
months = ['Jan', 'Feb', 'Mar', 'Apr']
treatments = ['Control', 'Treatment']
x_pos = np.array([0, 1, 2, 3])
width = 0.35

fig, ax = plt.subplots(figsize=(9, 5))
for i, (trt, color) in enumerate(zip(treatments, ['#4c72b0','#dd8452'])):
    data = [np.random.normal(2 + i * 0.8 + j * 0.3, 0.7, n) for j in range(4)]
    bp = ax.boxplot(data, positions=x_pos + (i - 0.5) * width,
                    widths=width * 0.85, patch_artist=True,
                    medianprops=dict(color='white', linewidth=2))
    for patch in bp['boxes']:
        patch.set_facecolor(color); patch.set_alpha(0.75)
ax.set_xticks(x_pos); ax.set_xticklabels(months)
ax.set_xlabel('Month'); ax.set_ylabel('Score')
ax.set_title('Grouped Box Plots: Control vs Treatment')
handles = [plt.Rectangle((0,0),1,1, color=c, alpha=0.75) for c in ['#4c72b0','#dd8452']]
ax.legend(handles, treatments)
ax.grid(True, axis='y', alpha=0.3)
fig.tight_layout()
fig.savefig('grouped_box.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved grouped_box.png')
Half-violin with jitter (raincloud style)
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
groups = ['Low', 'Mid', 'High']
data = [np.random.normal(m, 0.8, 80) for m in [1.5, 3.0, 4.5]]
colors = ['#5ab4d6', '#f4a261', '#e76f51']

fig, ax = plt.subplots(figsize=(8, 5))
for i, (d, c) in enumerate(zip(data, colors)):
    parts = ax.violinplot([d], positions=[i], showmedians=False,
                          showextrema=False)
    for pc in parts['bodies']:
        pc.set_facecolor(c); pc.set_alpha(0.6)
        # half violin: mask right side
        verts = pc.get_paths()[0].vertices
        verts[:, 0] = np.clip(verts[:, 0], -np.inf, i)
        pc.get_paths()[0].vertices = verts
    # jitter
    jitter = np.random.uniform(-0.05, 0.05, len(d))
    ax.scatter(i + 0.05 + jitter, d, alpha=0.4, s=15, color=c)
    ax.hlines(np.median(d), i - 0.3, i + 0.1, colors='black', linewidth=2)
ax.set_xticks(range(3)); ax.set_xticklabels(groups)
ax.set_title('Raincloud Plot (Half Violin + Jitter)')
ax.grid(True, axis='y', alpha=0.3)
fig.tight_layout()
fig.savefig('raincloud.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved raincloud.png')
💼 Real-World: Product Quality Distribution
QA team needs to compare defect rates across 5 production lines for 3 shifts. Use grouped notched box plots with color coding, outlier markers, and a horizontal threshold line.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(99)
lines = ['L1','L2','L3','L4','L5']
shifts = ['Morning','Afternoon','Night']
colors = ['#2196f3','#ff9800','#9c27b0']
x_pos = np.arange(5)
width = 0.25

fig, ax = plt.subplots(figsize=(11, 5))
for si, (shift, col) in enumerate(zip(shifts, colors)):
    data = [np.random.exponential(1 + si * 0.5 + li * 0.2, 50) for li in range(5)]
    bp = ax.boxplot(data, positions=x_pos + (si-1)*width, widths=width*0.85,
                    patch_artist=True, notch=True,
                    medianprops=dict(color='white', linewidth=2),
                    flierprops=dict(marker='x', color=col, markersize=5))
    for patch in bp['boxes']:
        patch.set_facecolor(col); patch.set_alpha(0.75)
ax.axhline(3.0, color='red', linestyle='--', linewidth=1.5, label='Threshold')
ax.set_xticks(x_pos); ax.set_xticklabels(lines)
ax.set_xlabel('Production Line'); ax.set_ylabel('Defects per 100 units')
ax.set_title('Quality Control: Defects by Line & Shift', fontweight='bold')
handles = [plt.Rectangle((0,0),1,1,color=c,alpha=0.75) for c in colors] +           [plt.Line2D([0],[0],color='red',linestyle='--')]
ax.legend(handles, shifts + ['Threshold'], ncol=4)
ax.grid(True, axis='y', alpha=0.3)
fig.tight_layout()
fig.savefig('qc_boxplot.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved qc_boxplot.png')
🏋️ Practice: Distribution Comparison Practice
Create 4 groups of data: bimodal (two Gaussians mixed), right-skewed (exponential), uniform, and heavy-tailed (Student-t df=2). Plot violin plots in a 2x2 subplot grid. Add the mean as a diamond marker and IQR as a vertical line on each.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(5)
bimodal  = np.concatenate([np.random.normal(-2,0.5,100), np.random.normal(2,0.5,100)])
skewed   = np.random.exponential(1.5, 200)
uniform  = np.random.uniform(-3, 3, 200)
heavy    = np.random.standard_t(df=2, size=200)
datasets = [bimodal, skewed, uniform, heavy]
titles   = ['Bimodal', 'Right-Skewed', 'Uniform', 'Heavy-Tailed']

fig, axes = plt.subplots(2, 2, figsize=(9, 7))
for ax, d, title in zip(axes.flat, datasets, titles):
    ax.violinplot([d], showmedians=True, showextrema=True)
    # TODO: add mean as diamond marker
    # TODO: label the title
    pass
fig.suptitle('Distribution Shapes Comparison', fontweight='bold')
fig.tight_layout()
fig.savefig('dist_shapes.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved dist_shapes.png')
✅ Practice Checklist
19. Contour Plots

Use contour() for lines and contourf() for filled regions to display 2D scalar fields, decision boundaries, and topographic data.

Basic contour and contourf
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = y = np.linspace(-3, 3, 200)
X, Y = np.meshgrid(x, y)
Z = np.sin(X) * np.cos(Y)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4))
# Filled contour
cf = ax1.contourf(X, Y, Z, levels=20, cmap='RdBu_r')
fig.colorbar(cf, ax=ax1, label='Z')
ax1.set_title('contourf β€” filled')

# Line contour with labels
cs = ax2.contour(X, Y, Z, levels=15, cmap='RdBu_r')
ax2.clabel(cs, inline=True, fontsize=8, fmt='%.1f')
ax2.set_title('contour β€” lines with labels')

for ax in (ax1, ax2):
    ax.set_xlabel('x'); ax.set_ylabel('y')
fig.tight_layout()
fig.savefig('contour_basic.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved contour_basic.png')
Decision boundary visualization
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.colors import ListedColormap

np.random.seed(0)
X_cls = np.random.randn(200, 2)
y_cls = ((X_cls[:,0]**2 + X_cls[:,1]**2) < 1.5).astype(int)

xx, yy = np.meshgrid(np.linspace(-3,3,300), np.linspace(-3,3,300))
r = xx**2 + yy**2
zz = (r < 1.5).astype(float)

fig, ax = plt.subplots(figsize=(6, 5))
ax.contourf(xx, yy, zz, alpha=0.3, cmap=ListedColormap(['#ff7f7f','#7fbfff']))
ax.contour(xx, yy, zz, colors='black', linewidths=1.5)
colors_pt = ['#cc0000' if yi else '#0055aa' for yi in y_cls]
ax.scatter(X_cls[:,0], X_cls[:,1], c=colors_pt, s=30, edgecolors='k', linewidths=0.5)
ax.set_title('Circular Decision Boundary')
ax.set_xlabel('Feature 1'); ax.set_ylabel('Feature 2')
fig.tight_layout()
fig.savefig('decision_boundary.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved decision_boundary.png')
Topographic map with hillshading
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.colors import LightSource

x = y = np.linspace(0, 4*np.pi, 300)
X, Y = np.meshgrid(x, y)
Z = np.sin(X/2) * np.cos(Y/3) + 0.5*np.sin(X + Y)

ls = LightSource(azdeg=315, altdeg=45)
hillshade = ls.hillshade(Z, vert_exag=1.5)

fig, ax = plt.subplots(figsize=(8, 6))
ax.imshow(hillshade, cmap='gray', origin='lower', alpha=0.6)
cf = ax.contourf(Z, levels=25, cmap='terrain', alpha=0.7, origin='lower')
cs = ax.contour(Z, levels=10, colors='k', linewidths=0.5, alpha=0.5, origin='lower')
fig.colorbar(cf, ax=ax, label='Elevation')
ax.set_title('Topographic Map with Hillshading')
fig.tight_layout()
fig.savefig('topo_map.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved topo_map.png')
Contour overlay on scatter
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from scipy.stats import gaussian_kde

np.random.seed(3)
x = np.concatenate([np.random.normal(0,1,150), np.random.normal(3,0.8,100)])
y = np.concatenate([np.random.normal(0,1,150), np.random.normal(2,0.8,100)])

# KDE over a grid
xi = np.linspace(x.min()-1, x.max()+1, 150)
yi = np.linspace(y.min()-1, y.max()+1, 150)
Xi, Yi = np.meshgrid(xi, yi)
k = gaussian_kde(np.vstack([x, y]))
Zi = k(np.vstack([Xi.ravel(), Yi.ravel()])).reshape(Xi.shape)

fig, ax = plt.subplots(figsize=(7, 6))
ax.scatter(x, y, s=15, alpha=0.4, color='steelblue')
cf = ax.contourf(Xi, Yi, Zi, levels=10, cmap='Blues', alpha=0.5)
ax.contour(Xi, Yi, Zi, levels=10, colors='navy', linewidths=0.8, alpha=0.7)
fig.colorbar(cf, ax=ax, label='Density')
ax.set_title('KDE Contour Overlay on Scatter')
fig.tight_layout()
fig.savefig('kde_contour.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved kde_contour.png')
💼 Real-World: Loss Landscape Visualization
Plot the loss surface of a simplified neural network (Z = (X-1)^2 + 2*(Y+0.5)^2 + 0.5*sin(3X)) with a filled contour, gradient-descent path overlay, and start/end markers.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-2, 3, 300)
y = np.linspace(-2.5, 1.5, 300)
X, Y = np.meshgrid(x, y)
Z = (X-1)**2 + 2*(Y+0.5)**2 + 0.5*np.sin(3*X)

# Simulated gradient-descent path
path_x = [-1.5]
path_y = [-2.0]
lr = 0.1
for _ in range(40):
    gx = 2*(path_x[-1]-1) + 1.5*np.cos(3*path_x[-1])
    gy = 4*(path_y[-1]+0.5)
    path_x.append(path_x[-1] - lr*gx)
    path_y.append(path_y[-1] - lr*gy)

fig, ax = plt.subplots(figsize=(8, 6))
cf = ax.contourf(X, Y, Z, levels=30, cmap='viridis')
fig.colorbar(cf, ax=ax, label='Loss')
ax.contour(X, Y, Z, levels=15, colors='white', linewidths=0.5, alpha=0.4)
ax.plot(path_x, path_y, 'w-o', markersize=4, linewidth=1.5, label='GD path')
ax.plot(path_x[0], path_y[0], 'rs', markersize=10, label='Start')
ax.plot(path_x[-1], path_y[-1], 'r*', markersize=14, label='End')
ax.set_xlabel('w1'); ax.set_ylabel('w2')
ax.set_title('Loss Landscape & Gradient Descent', fontweight='bold')
ax.legend(facecolor='#222')
fig.tight_layout()
fig.savefig('loss_landscape.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved loss_landscape.png')
🏋️ Practice: Contour Practice
Create Z = cos(sqrt(X^2 + Y^2)) for X,Y in [-6,6]. Plot: (1) filled contour with 'plasma' colormap, (2) white contour lines at 10 levels, (3) colorbar labeled 'Amplitude'. Add a red star marker at (0,0) where the function is maximum.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = y = np.linspace(-6, 6, 300)
X, Y = np.meshgrid(x, y)
Z = np.cos(np.sqrt(X**2 + Y**2))

fig, ax = plt.subplots(figsize=(7, 6))
# TODO: contourf with plasma cmap
# TODO: white contour lines, 10 levels
# TODO: colorbar labeled 'Amplitude'
# TODO: red star marker at (0, 0)
# TODO: save 'ripple.png'
plt.close()
✅ Practice Checklist
20. Polar Plots

Use polar projections for directional data, radar charts, and rose diagrams. Access the polar axes with subplot_kw={'projection':'polar'}.

Basic polar line and fill
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

theta = np.linspace(0, 2*np.pi, 300)
r = 1 + 0.5 * np.cos(3*theta)

fig, ax = plt.subplots(figsize=(6, 6), subplot_kw={'projection': 'polar'})
ax.plot(theta, r, color='steelblue', linewidth=2)
ax.fill(theta, r, color='steelblue', alpha=0.2)
ax.set_title('Rose Curve: 1 + 0.5Β·cos(3ΞΈ)', pad=20)
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig('polar_rose.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved polar_rose.png')
Wind rose bar chart
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(5)
n_dirs = 16
theta_bars = np.linspace(0, 2*np.pi, n_dirs, endpoint=False)
radii = np.abs(np.random.randn(n_dirs)) * 10 + 5
width = 2*np.pi / n_dirs

fig, ax = plt.subplots(figsize=(7, 7), subplot_kw={'projection': 'polar'})
bars = ax.bar(theta_bars, radii, width=width, bottom=0,
              color=plt.cm.hsv(theta_bars / (2*np.pi)), alpha=0.8, edgecolor='white')
ax.set_theta_direction(-1)
ax.set_theta_zero_location('N')
dirs = ['N','NNE','NE','ENE','E','ESE','SE','SSE',
        'S','SSW','SW','WSW','W','WNW','NW','NNW']
ax.set_xticks(theta_bars)
ax.set_xticklabels(dirs, fontsize=8)
ax.set_title('Wind Rose Diagram', pad=20, fontweight='bold')
fig.tight_layout()
fig.savefig('wind_rose.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved wind_rose.png')
Radar / spider chart
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

categories = ['Speed','Strength','Defense','Agility','Intelligence','Stamina']
N = len(categories)
angles = np.linspace(0, 2*np.pi, N, endpoint=False).tolist()
angles += angles[:1]  # close the loop

player_a = [8, 6, 7, 9, 5, 7]
player_b = [5, 9, 8, 4, 7, 6]
for v in (player_a, player_b):
    v.append(v[0])

fig, ax = plt.subplots(figsize=(7, 7), subplot_kw={'projection': 'polar'})
ax.plot(angles, player_a, 'o-', color='steelblue', linewidth=2, label='Player A')
ax.fill(angles, player_a, color='steelblue', alpha=0.2)
ax.plot(angles, player_b, 's-', color='tomato', linewidth=2, label='Player B')
ax.fill(angles, player_b, color='tomato', alpha=0.2)
ax.set_xticks(angles[:-1])
ax.set_xticklabels(categories, fontsize=10)
ax.set_ylim(0, 10)
ax.set_title('Player Stats Radar Chart', pad=20, fontweight='bold')
ax.legend(loc='upper right', bbox_to_anchor=(1.3, 1.1))
fig.tight_layout()
fig.savefig('radar.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved radar.png')
Polar scatter with colormap
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(3)
n = 200
theta = np.random.uniform(0, 2*np.pi, n)
r = np.random.exponential(2, n)
c = theta / (2*np.pi)

fig, ax = plt.subplots(figsize=(7, 7), subplot_kw={'projection': 'polar'})
sc = ax.scatter(theta, r, c=c, cmap='hsv', s=40, alpha=0.7)
fig.colorbar(sc, ax=ax, label='Direction (normalized)', pad=0.1)
ax.set_title('Polar Scatter β€” Exponential Radii', pad=20)
fig.tight_layout()
fig.savefig('polar_scatter.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved polar_scatter.png')
💼 Real-World: Sales Direction Analysis
Visualize monthly sales volume by compass direction for a logistics company. Use a wind-rose style bar chart with bars colored by volume (viridis colormap) and annotated with top 3 directions.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(21)
n_dirs = 12
months = ['Jan','Feb','Mar','Apr','May','Jun',
          'Jul','Aug','Sep','Oct','Nov','Dec']
theta_bars = np.linspace(0, 2*np.pi, n_dirs, endpoint=False)
sales = np.abs(np.random.randn(n_dirs)) * 50 + 80
width = 2*np.pi / n_dirs

fig, ax = plt.subplots(figsize=(8, 8), subplot_kw={'projection': 'polar'})
norm = plt.Normalize(sales.min(), sales.max())
colors = plt.cm.viridis(norm(sales))
bars = ax.bar(theta_bars, sales, width=width, bottom=5,
              color=colors, alpha=0.85, edgecolor='white')
ax.set_theta_zero_location('N')
ax.set_theta_direction(-1)
ax.set_xticks(theta_bars); ax.set_xticklabels(months, fontsize=9)
ax.set_title('Monthly Sales by Direction', pad=20, fontweight='bold')
# annotate top 3
top3 = np.argsort(sales)[-3:]
for idx in top3:
    ax.annotate(f'{sales[idx]:.0f}',
                xy=(theta_bars[idx], sales[idx]+8),
                ha='center', fontsize=9, color='gold', fontweight='bold')
sm = plt.cm.ScalarMappable(cmap='viridis', norm=norm)
fig.colorbar(sm, ax=ax, label='Sales Volume', pad=0.1)
fig.tight_layout()
fig.savefig('sales_polar.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved sales_polar.png')
🏋️ Practice: Polar Plot Practice
Create a radar chart with 8 categories (Communication, Analysis, Design, Coding, Testing, DevOps, Leadership, Creativity). Plot two engineers' scores (randomly generated 1-10). Close the polygon, fill with alpha=0.2, and add a legend. Save as 'skills_radar.png'.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

categories = ['Communication','Analysis','Design','Coding',
              'Testing','DevOps','Leadership','Creativity']
N = len(categories)
np.random.seed(42)
eng1 = np.random.randint(4, 10, N).tolist()
eng2 = np.random.randint(3, 10, N).tolist()
angles = np.linspace(0, 2*np.pi, N, endpoint=False).tolist()
# TODO: close the polygon (append first element to angles, eng1, eng2)
# TODO: polar subplot
# TODO: plot and fill both engineers
# TODO: set tick labels to categories
# TODO: save 'skills_radar.png'
✅ Practice Checklist
21. Stacked Bar & Area Charts

Show part-to-whole relationships over categories or time with stacked bar charts and area plots using stackplot().

Stacked bar chart
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

categories = ['Q1','Q2','Q3','Q4']
product_a = [120, 145, 160, 180]
product_b = [90,  110, 95,  130]
product_c = [60,  75,  80,  95]
x = np.arange(len(categories))
colors = ['#4c72b0','#dd8452','#55a868']

fig, ax = plt.subplots(figsize=(8, 5))
ax.bar(x, product_a, label='Product A', color=colors[0], width=0.5)
ax.bar(x, product_b, bottom=product_a, label='Product B', color=colors[1], width=0.5)
bottom_c = [a+b for a,b in zip(product_a, product_b)]
ax.bar(x, product_c, bottom=bottom_c, label='Product C', color=colors[2], width=0.5)
ax.set_xticks(x); ax.set_xticklabels(categories)
ax.set_ylabel('Revenue ($K)')
ax.set_title('Quarterly Revenue by Product β€” Stacked')
ax.legend(loc='upper left')
ax.grid(True, axis='y', alpha=0.3)
fig.tight_layout()
fig.savefig('stacked_bar.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved stacked_bar.png')
100% stacked bar (normalized)
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

categories = ['North','South','East','West','Central']
a = np.array([30, 45, 20, 60, 35])
b = np.array([50, 30, 55, 25, 40])
c = np.array([20, 25, 25, 15, 25])
total = a + b + c
a_p, b_p, c_p = a/total*100, b/total*100, c/total*100
x = np.arange(len(categories))

fig, ax = plt.subplots(figsize=(9, 5))
ax.bar(x, a_p, label='Tier 1', color='#4c72b0', width=0.55)
ax.bar(x, b_p, bottom=a_p, label='Tier 2', color='#dd8452', width=0.55)
ax.bar(x, c_p, bottom=a_p+b_p, label='Tier 3', color='#55a868', width=0.55)
for xi, (ap, bp, cp) in enumerate(zip(a_p, b_p, c_p)):
    ax.text(xi, ap/2, f'{ap:.0f}%', ha='center', va='center', fontsize=9, color='white', fontweight='bold')
    ax.text(xi, ap+bp/2, f'{bp:.0f}%', ha='center', va='center', fontsize=9, color='white', fontweight='bold')
    ax.text(xi, ap+bp+cp/2, f'{cp:.0f}%', ha='center', va='center', fontsize=9, fontweight='bold')
ax.set_xticks(x); ax.set_xticklabels(categories)
ax.set_ylabel('Percentage'); ax.set_ylim(0,100)
ax.set_title('100% Stacked Bar β€” Market Share by Region')
ax.legend(loc='upper right')
fig.tight_layout()
fig.savefig('stacked_100.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved stacked_100.png')
Stack area chart with stackplot()
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(0)
months = np.arange(1, 13)
direct   = np.array([200,210,225,240,260,280,270,290,310,295,320,350])
organic  = np.array([80, 95, 100,110,125,140,135,150,160,155,175,190])
referral = np.array([40, 45, 50, 55, 60, 65, 70, 68, 75, 72, 80, 90])
labels_s = ['Direct','Organic','Referral']
colors_s = ['#4c72b0','#55a868','#dd8452']

fig, ax = plt.subplots(figsize=(9, 5))
ax.stackplot(months, direct, organic, referral,
             labels=labels_s, colors=colors_s, alpha=0.8)
ax.set_xlabel('Month'); ax.set_ylabel('Sessions (K)')
ax.set_title('Website Traffic by Source β€” Stacked Area')
ax.set_xticks(months)
ax.set_xticklabels(['Jan','Feb','Mar','Apr','May','Jun',
                    'Jul','Aug','Sep','Oct','Nov','Dec'], rotation=30)
ax.legend(loc='upper left'); ax.grid(True, alpha=0.2)
fig.tight_layout()
fig.savefig('stackplot.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved stackplot.png')
Stream graph (centered stackplot)
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(7)
x = np.linspace(0, 10, 100)
n_series = 5
ys = [np.abs(np.random.randn(100)).cumsum() * 0.3 + np.random.uniform(1,3)
      for _ in range(n_series)]
baseline = -np.array(ys).sum(axis=0) / 2  # center

fig, ax = plt.subplots(figsize=(10, 5))
cmap = plt.cm.Set2
ax.stackplot(x, *ys, baseline='sym',
             colors=[cmap(i/n_series) for i in range(n_series)],
             alpha=0.85)
ax.set_title('Stream Graph (Symmetric Baseline)')
ax.set_xlabel('Time'); ax.set_ylabel('Magnitude')
ax.grid(True, alpha=0.2)
fig.tight_layout()
fig.savefig('streamgraph.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved streamgraph.png')
💼 Real-World: Energy Mix Dashboard
Show a country's electricity generation mix (Solar, Wind, Hydro, Gas, Coal) over 12 months as a 100% stacked area chart. Annotate the month with highest renewable share.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(15)
months = np.arange(12)
solar = 20 + 15*np.sin(np.linspace(0, np.pi, 12)) + np.random.randn(12)*2
wind  = 18 + 8*np.cos(np.linspace(0, 2*np.pi, 12)) + np.random.randn(12)*2
hydro = np.full(12, 22.0) + np.random.randn(12)
gas   = 25 - 5*np.sin(np.linspace(0, np.pi, 12)) + np.random.randn(12)
coal  = 100 - solar - wind - hydro - gas
sources = np.vstack([solar, wind, hydro, gas, coal])
sources = np.clip(sources, 1, None)
pct = sources / sources.sum(axis=0) * 100

fig, ax = plt.subplots(figsize=(10, 5))
mnames = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
ax.stackplot(months, *pct,
             labels=['Solar','Wind','Hydro','Gas','Coal'],
             colors=['#f9c74f','#90be6d','#43aa8b','#f3722c','#555'],
             alpha=0.9)
ren = pct[:3].sum(axis=0)
best = ren.argmax()
ax.annotate(f'Peak renewable
{ren[best]:.1f}%',
            xy=(best, 50), xytext=(best+1, 70),
            arrowprops=dict(arrowstyle='->', color='white'),
            fontsize=9, color='white',
            bbox=dict(boxstyle='round', fc='#333'))
ax.set_xticks(months); ax.set_xticklabels(mnames)
ax.set_ylim(0,100); ax.set_ylabel('Share (%)')
ax.set_title('Energy Mix by Month', fontweight='bold')
ax.legend(loc='lower right', ncol=5, fontsize=8)
fig.tight_layout()
fig.savefig('energy_mix.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved energy_mix.png')
🏋️ Practice: Stacked Chart Practice
Create weekly budget allocation data for 5 weeks across categories: Rent, Food, Transport, Entertainment, Savings (make up reasonable values). Plot a stacked bar chart and below it a 100% stacked bar showing the proportion. Use a shared x-axis in a 2x1 subplot.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

weeks = ['Wk1','Wk2','Wk3','Wk4','Wk5']
rent    = np.array([800, 800, 800, 800, 800])
food    = np.array([200, 220, 180, 240, 210])
transport = np.array([80, 90, 75, 85, 95])
entertainment = np.array([100, 60, 120, 80, 50])
savings = np.array([150, 200, 180, 130, 220])

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(9, 8), sharex=True)
x = np.arange(len(weeks))
# TODO: stacked bar on ax1
# TODO: 100% stacked bar on ax2
# TODO: legend, labels, title
fig.tight_layout()
fig.savefig('budget_stacked.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved budget_stacked.png')
✅ Practice Checklist
22. Step Plots & Eventplot

Use step() and drawstyle='steps-*' for discrete/piecewise data, stairs() for histograms, and eventplot() for spike-train and event sequence data.

step() and stairs() basics
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = np.arange(0, 10)
y = np.array([2, 3, 3, 5, 4, 6, 5, 7, 6, 8])

fig, axes = plt.subplots(1, 3, figsize=(12, 4))
for ax, where, title in zip(axes,
                             ['pre','mid','post'],
                             ['steps-pre','steps-mid','steps-post']):
    ax.step(x, y, where=where, color='steelblue', linewidth=2)
    ax.scatter(x, y, color='steelblue', zorder=5)
    ax.set_title(title); ax.grid(True, alpha=0.3)
fig.suptitle('Step Plot Variants', fontweight='bold')
fig.tight_layout()
fig.savefig('step_variants.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved step_variants.png')
Cumulative step (ECDF-style)
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(0)
data = np.sort(np.random.normal(5, 1.5, 200))
ecdf_y = np.arange(1, len(data)+1) / len(data)

fig, ax = plt.subplots(figsize=(8, 4))
ax.step(data, ecdf_y, where='post', color='steelblue', linewidth=2, label='ECDF')
ax.axhline(0.5, color='red', linestyle='--', linewidth=1, label='Median')
ax.axvline(np.median(data), color='red', linestyle='--', linewidth=1)
ax.set_xlabel('Value'); ax.set_ylabel('Cumulative Probability')
ax.set_title('Empirical CDF (step-post)')
ax.legend(); ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig('ecdf_step.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved ecdf_step.png')
Eventplot: neural spike trains
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(1)
n_neurons = 5
spike_trains = [np.sort(np.random.uniform(0, 1, np.random.randint(10, 30)))
                for _ in range(n_neurons)]
colors_ev = plt.cm.tab10(np.linspace(0, 0.5, n_neurons))

fig, ax = plt.subplots(figsize=(10, 4))
ax.eventplot(spike_trains, colors=colors_ev,
             lineoffsets=range(1, n_neurons+1),
             linelengths=0.7, linewidths=1.5)
ax.set_xlabel('Time (s)')
ax.set_yticks(range(1, n_neurons+1))
ax.set_yticklabels([f'Neuron {i}' for i in range(1, n_neurons+1)])
ax.set_title('Neural Spike Train Raster Plot')
ax.set_xlim(0, 1)
ax.grid(True, axis='x', alpha=0.3)
fig.tight_layout()
fig.savefig('spike_raster.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved spike_raster.png')
Step with fill for discrete signal
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(2)
t = np.arange(0, 50)
signal = np.where(np.random.rand(50) > 0.6, 1, 0)
signal[10:15] = 1; signal[30:38] = 1  # force some pulses

fig, ax = plt.subplots(figsize=(10, 3))
ax.step(t, signal, where='post', color='steelblue', linewidth=1.5)
ax.fill_between(t, signal, step='post', alpha=0.2, color='steelblue')
ax.set_ylim(-0.1, 1.4)
ax.set_xlabel('Time (ms)'); ax.set_ylabel('State')
ax.set_title('Digital Signal β€” Step Fill')
ax.set_yticks([0, 1]); ax.set_yticklabels(['OFF', 'ON'])
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig('digital_signal.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved digital_signal.png')
💼 Real-World: System Event Log Visualization
Visualize server events across 5 services over 60 seconds: each service fires random events. Use eventplot with different colors per service and add a shaded incident window (t=20–35).
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(8)
services = ['API','DB','Cache','Auth','Queue']
events = [np.sort(np.random.uniform(0, 60, np.random.randint(8, 25)))
          for _ in services]
colors_svc = ['#4c72b0','#dd8452','#55a868','#c44e52','#9467bd']

fig, ax = plt.subplots(figsize=(11, 4))
ax.eventplot(events, colors=colors_svc,
             lineoffsets=range(1, len(services)+1),
             linelengths=0.6, linewidths=2)
ax.axvspan(20, 35, color='red', alpha=0.1, label='Incident window')
ax.axvline(20, color='red', linestyle='--', linewidth=1)
ax.axvline(35, color='red', linestyle='--', linewidth=1)
ax.set_xlabel('Time (s)'); ax.set_xlim(0, 60)
ax.set_yticks(range(1, len(services)+1)); ax.set_yticklabels(services)
ax.set_title('Service Event Log β€” 60s Window', fontweight='bold')
ax.legend(loc='upper right')
ax.grid(True, axis='x', alpha=0.3)
fig.tight_layout()
fig.savefig('event_log.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved event_log.png')
🏋️ Practice: Step Plot Practice
Simulate a 3-state machine (IDLE=0, RUNNING=1, ERROR=2) over 80 time steps using random transitions. Plot it with step(where='post') and fill_between for each state using different colors. Add a legend for the three states.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(11)
t = np.arange(80)
state = np.zeros(80, dtype=int)
for i in range(1, 80):
    if np.random.rand() < 0.1:
        state[i] = np.random.choice([0, 1, 2])
    else:
        state[i] = state[i-1]

fig, ax = plt.subplots(figsize=(11, 3))
# TODO: step plot with post
# TODO: fill_between for state=0 (blue), 1 (green), 2 (red)
# TODO: add legend, labels, title
# TODO: save 'state_machine.png'
plt.close()
✅ Practice Checklist
23. Log Scale & Symlog

Use set_xscale/set_yscale with 'log', 'symlog', or 'logit' to handle data spanning many orders of magnitude.

Log-log plot: power-law relationship
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = np.logspace(0, 4, 100)
y_power = 2.5 * x**1.7
noise = np.random.lognormal(0, 0.1, 100)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4))
ax1.plot(x, y_power * noise, 'o', markersize=4, alpha=0.6, color='steelblue')
ax1.set_title('Linear Scale'); ax1.set_xlabel('x'); ax1.set_ylabel('y')
ax1.grid(True, alpha=0.3)

ax2.loglog(x, y_power * noise, 'o', markersize=4, alpha=0.6, color='steelblue')
ax2.loglog(x, y_power, 'r--', linewidth=2, label=r'y = 2.5 x^{1.7}')
ax2.set_title('Log-Log Scale'); ax2.set_xlabel('x'); ax2.set_ylabel('y')
ax2.legend(); ax2.grid(True, which='both', alpha=0.3)
fig.suptitle('Power-Law: Linear vs Log-Log', fontweight='bold')
fig.tight_layout()
fig.savefig('loglog.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved loglog.png')
Semilog: exponential decay
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

t = np.linspace(0, 10, 200)
decay_fast = np.exp(-0.8 * t)
decay_slow = np.exp(-0.2 * t)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4))
for ax, scale, title in [(ax1, 'linear', 'Linear Y'), (ax2, 'log', 'Log Y')]:
    ax.plot(t, decay_fast, label='Fast (k=0.8)', linewidth=2)
    ax.plot(t, decay_slow, label='Slow (k=0.2)', linewidth=2, linestyle='--')
    ax.set_yscale(scale)
    ax.set_xlabel('Time'); ax.set_ylabel('Concentration')
    ax.set_title(title); ax.legend(); ax.grid(True, which='both', alpha=0.3)
fig.suptitle('Exponential Decay on Linear vs Semilog', fontweight='bold')
fig.tight_layout()
fig.savefig('semilog.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved semilog.png')
Symlog: signed data spanning zero
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(0)
x = np.linspace(-1000, 1000, 500)
y = x + np.random.randn(500) * 50

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4))
ax1.scatter(x, y, s=8, alpha=0.5, color='steelblue')
ax1.set_title('Linear Scale')

ax2.scatter(x, y, s=8, alpha=0.5, color='steelblue')
ax2.set_xscale('symlog', linthresh=10)
ax2.set_yscale('symlog', linthresh=10)
ax2.set_title('Symlog Scale (linthresh=10)')

for ax in (ax1, ax2):
    ax.axhline(0, color='gray', linewidth=0.8)
    ax.axvline(0, color='gray', linewidth=0.8)
    ax.grid(True, which='both', alpha=0.3)
    ax.set_xlabel('X'); ax.set_ylabel('Y')
fig.tight_layout()
fig.savefig('symlog.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved symlog.png')
Log scale with minor grid and custom ticks
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

import matplotlib.ticker as ticker

f = np.logspace(1, 5, 300)  # 10 Hz to 100 kHz
gain_db = -20 * np.log10(1 + (f/1000)**2)  # simple LP filter

fig, ax = plt.subplots(figsize=(9, 4))
ax.semilogx(f, gain_db, color='steelblue', linewidth=2)
ax.axhline(-3, color='red', linestyle='--', linewidth=1, label='-3 dB cutoff')
ax.axvline(1000, color='red', linestyle='--', linewidth=1)
ax.set_xlabel('Frequency (Hz)')
ax.set_ylabel('Gain (dB)')
ax.set_title('Bode Plot β€” Low-Pass Filter')
ax.grid(True, which='both', alpha=0.3)
ax.xaxis.set_major_formatter(ticker.FuncFormatter(
    lambda x, _: f'{x/1000:.0f}k' if x >= 1000 else f'{x:.0f}'))
ax.legend()
fig.tight_layout()
fig.savefig('bode.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved bode.png')
💼 Real-World: Server Response Time Analysis
Plot server response time distribution (lognormal data, 10k samples) on both linear and log-x histograms side by side. Overlay the theoretical lognormal PDF. Mark the 95th and 99th percentiles.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
mu, sigma = 5.5, 0.8   # lognormal params
data = np.random.lognormal(mu, sigma, 10000)  # ms

p95, p99 = np.percentile(data, [95, 99])

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
bins = 80

ax1.hist(data, bins=bins, color='steelblue', alpha=0.7, density=True)
for p, lab, col in [(p95,'p95','orange'),(p99,'p99','red')]:
    ax1.axvline(p, color=col, linestyle='--', linewidth=2, label=f'{lab}: {p:.0f}ms')
ax1.set_title('Linear Scale'); ax1.legend(); ax1.grid(True, alpha=0.3)

ax2.hist(data, bins=np.logspace(np.log10(data.min()), np.log10(data.max()), bins),
         color='steelblue', alpha=0.7, density=True)
x_pdf = np.logspace(np.log10(data.min()), np.log10(data.max()), 300)
pdf = (1/(x_pdf * sigma * np.sqrt(2*np.pi))) * np.exp(-(np.log(x_pdf)-mu)**2/(2*sigma**2))
ax2.plot(x_pdf, pdf, 'r-', linewidth=2, label='Lognormal PDF')
ax2.set_xscale('log')
for p, lab, col in [(p95,'p95','orange'),(p99,'p99','red')]:
    ax2.axvline(p, color=col, linestyle='--', linewidth=2)
ax2.set_title('Log-X Scale'); ax2.legend(); ax2.grid(True, which='both', alpha=0.3)

for ax in (ax1, ax2):
    ax.set_xlabel('Response Time (ms)'); ax.set_ylabel('Density')
fig.suptitle('Server Response Time Distribution', fontweight='bold')
fig.tight_layout()
fig.savefig('response_time.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved response_time.png')
🏋️ Practice: Log Scale Practice
Generate data: y = 3 * x^(-1.5) for x in [1, 10000] with lognormal noise. Plot on: (1) linear scale, (2) log-log scale with a fitted power-law line, (3) symlog scale. Arrange in a 1x3 figure. Use np.polyfit on log-transformed data to get the exponent.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(6)
x = np.logspace(0, 4, 200)
y = 3 * x**(-1.5) * np.random.lognormal(0, 0.15, 200)

fig, axes = plt.subplots(1, 3, figsize=(13, 4))
titles = ['Linear', 'Log-Log', 'Symlog']
scales = [('linear','linear'), ('log','log'), ('symlog','symlog')]

for ax, title, (xs, ys) in zip(axes, titles, scales):
    ax.scatter(x, y, s=10, alpha=0.5)
    ax.set_xscale(xs); ax.set_yscale(ys)
    ax.set_title(title); ax.grid(True, which='both', alpha=0.3)
    # TODO: on log-log, add fitted power-law line using np.polyfit

fig.suptitle('Power Law on Three Scales', fontweight='bold')
fig.tight_layout()
fig.savefig('powerlaw_scales.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved powerlaw_scales.png')
✅ Practice Checklist
24. GridSpec & Complex Layouts

Go beyond plt.subplots() using GridSpec for unequal column/row spans, subplot_mosaic() for named panels, and add_axes() for insets.

GridSpec: unequal column widths
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.gridspec import GridSpec

np.random.seed(0)
fig = plt.figure(figsize=(11, 5))
gs = GridSpec(2, 3, figure=fig, width_ratios=[2,1,1], hspace=0.4, wspace=0.3)

ax_main = fig.add_subplot(gs[:, 0])   # spans both rows, col 0
ax_tr   = fig.add_subplot(gs[0, 1])
ax_br   = fig.add_subplot(gs[1, 1])
ax_tall = fig.add_subplot(gs[:, 2])

x = np.random.randn(200); y = np.random.randn(200)
ax_main.scatter(x, y, s=15, alpha=0.5, color='steelblue')
ax_main.set_title('Main Scatter')
ax_tr.hist(x, bins=20, color='steelblue', alpha=0.7); ax_tr.set_title('X dist')
ax_br.hist(y, bins=20, orientation='horizontal', color='tomato', alpha=0.7)
ax_br.set_title('Y dist')
ax_tall.boxplot([x, y], labels=['X','Y'], patch_artist=True)
ax_tall.set_title('Box')

fig.suptitle('GridSpec Complex Layout', fontweight='bold')
fig.savefig('gridspec.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved gridspec.png')
subplot_mosaic: named panels
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(1)
layout = [['A', 'A', 'B'],
          ['C', 'D', 'B']]

fig, axd = plt.subplot_mosaic(layout, figsize=(11, 6),
                               gridspec_kw={'hspace':0.35,'wspace':0.3})

x = np.linspace(0, 10, 100)
axd['A'].plot(x, np.sin(x), color='steelblue'); axd['A'].set_title('A: Wide Line')
axd['B'].imshow(np.random.rand(20,20), cmap='viridis', aspect='auto'); axd['B'].set_title('B: Tall Image')
axd['C'].bar(['x','y','z'], [3,7,5], color=['#4c72b0','#dd8452','#55a868']); axd['C'].set_title('C: Bar')
axd['D'].scatter(*np.random.randn(2,50), s=20, alpha=0.6); axd['D'].set_title('D: Scatter')

fig.suptitle('subplot_mosaic β€” Named Panels', fontweight='bold')
fig.savefig('mosaic.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved mosaic.png')
Nested GridSpec for subgrid
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.gridspec import GridSpec, GridSpecFromSubplotSpec

np.random.seed(2)
fig = plt.figure(figsize=(11, 6))
outer = GridSpec(1, 2, figure=fig, wspace=0.3)

# Left: single scatter
ax_left = fig.add_subplot(outer[0])
ax_left.scatter(*np.random.randn(2, 80), s=20, alpha=0.5)
ax_left.set_title('Left Panel')

# Right: 2x2 subgrid
inner = GridSpecFromSubplotSpec(2, 2, subplot_spec=outer[1], hspace=0.4, wspace=0.3)
for i in range(4):
    ax = fig.add_subplot(inner[i])
    ax.plot(np.random.randn(30).cumsum(), linewidth=1.5)
    ax.set_title(f'R{i+1}', fontsize=9)

fig.suptitle('Nested GridSpec', fontweight='bold')
fig.savefig('nested_gs.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved nested_gs.png')
Inset axis with add_axes
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(3)
x = np.linspace(0, 10, 300)
y = np.sin(x) * np.exp(-0.2*x) + np.random.randn(300)*0.05

fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(x, y, color='steelblue', linewidth=1.5)
ax.set_xlabel('x'); ax.set_ylabel('y')
ax.set_title('Main Plot with Inset Zoom')

# Inset: zoom [0, 1.5]
ax_inset = ax.inset_axes([0.55, 0.55, 0.42, 0.38])
ax_inset.plot(x, y, color='steelblue', linewidth=1.5)
ax_inset.set_xlim(0, 1.5); ax_inset.set_ylim(-0.1, 1.05)
ax_inset.set_title('Zoom [0,1.5]', fontsize=8)
ax_inset.tick_params(labelsize=7)

ax.indicate_inset_zoom(ax_inset, edgecolor='black')
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig('inset_axis.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved inset_axis.png')
💼 Real-World: ML Model Comparison Dashboard
Build a 3-panel dashboard: (1) large training-curve plot (loss vs epoch for 3 models), (2) confusion-matrix heatmap, (3) ROC curve β€” using GridSpec with a 2:1 width ratio for the first column.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.gridspec import GridSpec

np.random.seed(42)
fig = plt.figure(figsize=(13, 5))
gs = GridSpec(2, 2, figure=fig, width_ratios=[1.8, 1], hspace=0.4, wspace=0.35)

ax_curve = fig.add_subplot(gs[:, 0])
ax_cm    = fig.add_subplot(gs[0, 1])
ax_roc   = fig.add_subplot(gs[1, 1])

# Training curves
epochs = np.arange(1, 31)
for name, color, offset in [('CNN','#4c72b0',0), ('RNN','#dd8452',0.3), ('MLP','#55a868',0.5)]:
    loss = 2.5*np.exp(-0.15*epochs) + offset*np.exp(-0.1*epochs) + np.random.randn(30)*0.04
    ax_curve.plot(epochs, loss, color=color, linewidth=2, label=name)
ax_curve.set_xlabel('Epoch'); ax_curve.set_ylabel('Loss')
ax_curve.set_title('Training Loss'); ax_curve.legend(); ax_curve.grid(True, alpha=0.3)

# Confusion matrix
cm = np.array([[45,5,2],[3,38,4],[1,2,50]])
im = ax_cm.imshow(cm, cmap='Blues')
for i in range(3):
    for j in range(3):
        ax_cm.text(j, i, cm[i,j], ha='center', va='center',
                   color='white' if cm[i,j] > 30 else 'black', fontsize=10)
ax_cm.set_title('Confusion Matrix', fontsize=9)

# ROC
fpr = np.linspace(0, 1, 100)
tpr = np.sqrt(fpr) * 0.92
ax_roc.plot(fpr, tpr, color='steelblue', linewidth=2, label='AUC=0.92')
ax_roc.plot([0,1],[0,1],'k--',linewidth=0.8)
ax_roc.set_xlabel('FPR', fontsize=8); ax_roc.set_ylabel('TPR', fontsize=8)
ax_roc.set_title('ROC Curve', fontsize=9); ax_roc.legend(fontsize=8)

fig.suptitle('ML Model Comparison Dashboard', fontweight='bold')
fig.savefig('ml_dashboard.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved ml_dashboard.png')
🏋️ Practice: GridSpec Practice
Create a 3x3 GridSpec layout where: cell (0,0) spans 2 columns (title/text placeholder), row 1 has 3 equal plots (sin, cos, tan clipped), and row 2 spans all 3 columns as a wide bar chart. Use fig.add_subplot() with appropriate slicing.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.gridspec import GridSpec

fig = plt.figure(figsize=(12, 8))
gs = GridSpec(3, 3, figure=fig, hspace=0.4, wspace=0.3)
x = np.linspace(0, 2*np.pi, 200)

# Row 0: spans 3 cols β€” title area
ax_title = fig.add_subplot(gs[0, :])
ax_title.text(0.5, 0.5, 'GridSpec Practice Dashboard', ha='center', va='center',
              fontsize=14, fontweight='bold', transform=ax_title.transAxes)
ax_title.axis('off')

# TODO: Row 1: three plots (sin, cos, tan clipped to [-5,5])
# TODO: Row 2: wide bar chart spanning all 3 cols
# TODO: save 'gridspec_practice.png'
plt.close()
✅ Practice Checklist
25. Hexbin & 2D Density Plots

Use hexbin() for large scatter datasets, hist2d() for rectangular binning, and KDE-based density coloring to visualize joint distributions.

hexbin with colorbar
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(0)
n = 5000
x = np.random.normal(0, 1.5, n)
y = 0.6*x + np.random.normal(0, 1, n)

fig, ax = plt.subplots(figsize=(7, 5))
hb = ax.hexbin(x, y, gridsize=40, cmap='YlOrRd', mincnt=1)
cb = fig.colorbar(hb, ax=ax, label='Count')
ax.set_xlabel('X'); ax.set_ylabel('Y')
ax.set_title('Hexbin β€” 5000 Points')
fig.tight_layout()
fig.savefig('hexbin.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved hexbin.png')
2D histogram with hist2d
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(1)
n = 3000
x = np.concatenate([np.random.normal(-2, 0.8, n//2), np.random.normal(2, 0.8, n//2)])
y = np.concatenate([np.random.normal(-1, 1.0, n//2), np.random.normal(1, 1.0, n//2)])

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4))
h, xedges, yedges, img = ax1.hist2d(x, y, bins=40, cmap='Blues')
fig.colorbar(img, ax=ax1, label='Count')
ax1.set_title('hist2d')

h2, xe, ye, img2 = ax2.hist2d(x, y, bins=40, cmap='Blues',
                                norm=plt.matplotlib.colors.LogNorm())
fig.colorbar(img2, ax=ax2, label='Log Count')
ax2.set_title('hist2d (log scale)')

for ax in (ax1, ax2):
    ax.set_xlabel('X'); ax.set_ylabel('Y')
fig.tight_layout()
fig.savefig('hist2d.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved hist2d.png')
Scatter colored by KDE density
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from scipy.stats import gaussian_kde

np.random.seed(2)
n = 2000
x = np.random.multivariate_normal([0,0], [[1,0.7],[0.7,1]], n)[:,0]
y = np.random.multivariate_normal([0,0], [[1,0.7],[0.7,1]], n)[:,1]

xy = np.vstack([x, y])
kde = gaussian_kde(xy)
density = kde(xy)
idx = density.argsort()

fig, ax = plt.subplots(figsize=(7, 6))
sc = ax.scatter(x[idx], y[idx], c=density[idx], cmap='inferno', s=10, alpha=0.8)
fig.colorbar(sc, ax=ax, label='Density')
ax.set_xlabel('X'); ax.set_ylabel('Y')
ax.set_title('Scatter Colored by KDE Density')
fig.tight_layout()
fig.savefig('kde_scatter.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved kde_scatter.png')
Marginal distributions with shared axes
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.gridspec import GridSpec

np.random.seed(3)
n = 1000
x = np.random.normal(0, 1, n)
y = x * 0.8 + np.random.normal(0, 0.6, n)

fig = plt.figure(figsize=(8, 8))
gs = GridSpec(2, 2, width_ratios=[4,1], height_ratios=[1,4],
              hspace=0.05, wspace=0.05)
ax_main = fig.add_subplot(gs[1, 0])
ax_top  = fig.add_subplot(gs[0, 0], sharex=ax_main)
ax_side = fig.add_subplot(gs[1, 1], sharey=ax_main)

ax_main.hexbin(x, y, gridsize=35, cmap='Blues', mincnt=1)
ax_top.hist(x, bins=40, color='steelblue', alpha=0.7)
ax_side.hist(y, bins=40, orientation='horizontal', color='tomato', alpha=0.7)

plt.setp(ax_top.get_xticklabels(), visible=False)
plt.setp(ax_side.get_yticklabels(), visible=False)
ax_main.set_xlabel('X'); ax_main.set_ylabel('Y')
ax_top.set_title('Hexbin with Marginal Distributions', fontweight='bold')
fig.savefig('marginal.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved marginal.png')
💼 Real-World: Customer Purchase Patterns
Visualize 20,000 e-commerce transactions (basket_size vs revenue) using hexbin with log-color scale. Add marginal histograms on top and right for each axis. Annotate the high-density region.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.gridspec import GridSpec

np.random.seed(55)
n = 20000
basket = np.random.lognormal(1.5, 0.6, n)
revenue = basket * np.random.uniform(15, 80, n) + np.random.normal(0, 20, n)
revenue = np.clip(revenue, 1, None)

fig = plt.figure(figsize=(9, 9))
gs = GridSpec(2, 2, width_ratios=[4,1], height_ratios=[1,4],
              hspace=0.05, wspace=0.05)
ax = fig.add_subplot(gs[1,0])
ax_top = fig.add_subplot(gs[0,0], sharex=ax)
ax_side = fig.add_subplot(gs[1,1], sharey=ax)

hb = ax.hexbin(basket, revenue, gridsize=50, cmap='YlOrRd', mincnt=1,
               norm=plt.matplotlib.colors.LogNorm())
fig.colorbar(hb, ax=ax, label='Log Count')

ax_top.hist(basket, bins=50, color='#f4a261', alpha=0.8)
ax_side.hist(revenue, bins=50, orientation='horizontal', color='#e76f51', alpha=0.8)
plt.setp(ax_top.get_xticklabels(), visible=False)
plt.setp(ax_side.get_yticklabels(), visible=False)

ax.set_xlabel('Basket Size (items)'); ax.set_ylabel('Revenue ($)')
ax.annotate('Peak density', xy=(5, 200), xytext=(12, 500),
            arrowprops=dict(arrowstyle='->', color='black'),
            fontsize=9, fontweight='bold')
ax_top.set_title('Customer Purchase Patterns', fontweight='bold')
fig.savefig('purchase_density.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved purchase_density.png')
🏋️ Practice: Density Plot Practice
Generate 3000 points from a mixture of 3 bivariate Gaussians at (0,0), (3,3), (-2,3). Plot using hexbin (gridsize=30, cmap='plasma'). Overlay contour lines from a KDE. Add a colorbar and axis labels.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from scipy.stats import gaussian_kde

np.random.seed(7)
centers = [(0,0), (3,3), (-2,3)]
n_each = 1000
pts = np.vstack([np.random.multivariate_normal(c, np.eye(2), n_each) for c in centers])
x, y = pts[:,0], pts[:,1]

fig, ax = plt.subplots(figsize=(7, 6))
# TODO: hexbin with plasma cmap
# TODO: KDE contour overlay
# TODO: colorbar, axis labels
# TODO: save 'mixture_density.png'
plt.close()
✅ Practice Checklist
26. Patch Artists & Custom Shapes

Draw custom geometric shapes with matplotlib.patches: Rectangle, Circle, Ellipse, Polygon, FancyArrow, and Arc for annotations and diagrams.

Basic patches: Rectangle, Circle, Ellipse
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

import matplotlib.patches as mpatches

fig, ax = plt.subplots(figsize=(8, 6))
ax.set_xlim(0, 10); ax.set_ylim(0, 8)
ax.set_aspect('equal')

rect = mpatches.Rectangle((1, 1), 2.5, 1.5, linewidth=2,
                            edgecolor='steelblue', facecolor='lightblue', alpha=0.8)
circ = mpatches.Circle((6, 4), radius=1.5, linewidth=2,
                         edgecolor='tomato', facecolor='lightsalmon', alpha=0.8)
ellip = mpatches.Ellipse((4, 6), width=3, height=1.2, angle=30,
                           linewidth=2, edgecolor='seagreen', facecolor='lightgreen', alpha=0.8)

for patch in [rect, circ, ellip]:
    ax.add_patch(patch)

ax.text(2.25, 1.75, 'Rectangle', ha='center', fontsize=9)
ax.text(6, 4, 'Circle', ha='center', fontsize=9)
ax.text(4, 6, 'Ellipse', ha='center', fontsize=9)
ax.set_title('Basic Patch Artists')
ax.grid(True, alpha=0.2)
fig.tight_layout()
fig.savefig('patches_basic.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved patches_basic.png')
Polygon and FancyArrow
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

import matplotlib.patches as mpatches

fig, ax = plt.subplots(figsize=(8, 6))
ax.set_xlim(0, 10); ax.set_ylim(0, 8)
ax.set_aspect('equal')

# Star polygon
n = 5
outer = np.array([[np.cos(2*np.pi*i/n - np.pi/2), np.sin(2*np.pi*i/n - np.pi/2)]
                   for i in range(n)]) * 2.0 + [3, 4]
inner = np.array([[np.cos(2*np.pi*i/n + np.pi/n - np.pi/2),
                   np.sin(2*np.pi*i/n + np.pi/n - np.pi/2)]
                   for i in range(n)]) * 0.8 + [3, 4]
verts = np.empty((2*n, 2))
verts[0::2] = outer; verts[1::2] = inner
star = mpatches.Polygon(verts, closed=True, facecolor='gold', edgecolor='orange', linewidth=2)
ax.add_patch(star)

arrow = mpatches.FancyArrow(5.5, 4, 2, 0, width=0.3,
                              head_width=0.7, head_length=0.5,
                              facecolor='steelblue', edgecolor='navy')
ax.add_patch(arrow)

arc = mpatches.Arc((8.5, 2), 2, 2, angle=0, theta1=30, theta2=270,
                    color='tomato', linewidth=2.5)
ax.add_patch(arc)

ax.set_title('Polygon, FancyArrow, Arc')
ax.grid(True, alpha=0.2)
fig.tight_layout()
fig.savefig('patches_advanced.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved patches_advanced.png')
Annotate with FancyBboxPatch callouts
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

import matplotlib.patches as mpatches

fig, ax = plt.subplots(figsize=(9, 5))
np.random.seed(0)
x = np.linspace(0, 10, 100)
y = np.sin(x) + np.random.randn(100)*0.1
ax.plot(x, y, color='steelblue', linewidth=2)

# Highlight a region
highlight = mpatches.FancyBboxPatch((3.0, -0.3), 2.0, 0.6,
    boxstyle='round,pad=0.1', linewidth=2,
    edgecolor='gold', facecolor='yellow', alpha=0.3)
ax.add_patch(highlight)
ax.annotate('Peak region', xy=(4, 0.8), xytext=(6.5, 1.3),
            arrowprops=dict(arrowstyle='->', color='darkred', lw=2),
            fontsize=11, color='darkred', fontweight='bold',
            bbox=dict(boxstyle='round,pad=0.3', facecolor='lightyellow', edgecolor='gold'))

ax.set_title('Annotations with FancyBboxPatch')
ax.set_xlabel('x'); ax.set_ylabel('y')
ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig('fancy_annotate.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved fancy_annotate.png')
Pipeline / flowchart diagram
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

import matplotlib.patches as mpatches

fig, ax = plt.subplots(figsize=(11, 4))
ax.set_xlim(0, 11); ax.set_ylim(0, 4)
ax.set_aspect('equal'); ax.axis('off')

boxes = [
    (0.5, 1.5, 'Data
Ingestion', '#4c72b0'),
    (3.0, 1.5, 'Clean &
Transform', '#dd8452'),
    (5.5, 1.5, 'Feature
Engineering', '#55a868'),
    (8.0, 1.5, 'Model
Training', '#c44e52'),
]
for x, y, label, color in boxes:
    box = mpatches.FancyBboxPatch((x, y), 2, 1,
        boxstyle='round,pad=0.1', facecolor=color, edgecolor='white',
        linewidth=2, alpha=0.9)
    ax.add_patch(box)
    ax.text(x+1, y+0.5, label, ha='center', va='center',
            color='white', fontsize=9, fontweight='bold')

for i in range(len(boxes)-1):
    x_start = boxes[i][0] + 2
    x_end   = boxes[i+1][0]
    y_mid   = 2.0
    ax.annotate('', xy=(x_end, y_mid), xytext=(x_start, y_mid),
                arrowprops=dict(arrowstyle='->', color='gray', lw=2))

ax.set_title('ML Pipeline Diagram', fontsize=13, fontweight='bold', y=0.95)
fig.tight_layout()
fig.savefig('pipeline.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved pipeline.png')
💼 Real-World: Architecture Diagram
Draw a 3-tier architecture diagram: Client (circle), Load Balancer (diamond/hexagon), 3 App Servers (rounded rectangles), Database (cylinder-style ellipse). Use FancyArrow for connections and add labels.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

import matplotlib.patches as mpatches

fig, ax = plt.subplots(figsize=(11, 6))
ax.set_xlim(0, 12); ax.set_ylim(0, 7)
ax.axis('off')

# Client
circ = mpatches.Circle((1, 3.5), 0.7, facecolor='#4c72b0', edgecolor='white', lw=2)
ax.add_patch(circ); ax.text(1, 3.5, 'Client', ha='center', va='center', color='white', fontsize=8, fontweight='bold')

# Load balancer
lb = mpatches.FancyBboxPatch((2.8, 2.8), 2, 1.4, boxstyle='round,pad=0.15',
    facecolor='#dd8452', edgecolor='white', lw=2)
ax.add_patch(lb); ax.text(3.8, 3.5, 'Load
Balancer', ha='center', va='center', color='white', fontsize=8, fontweight='bold')

# App servers
for i, (y, col) in enumerate(zip([1.2, 3.5, 5.8], ['#55a868','#55a868','#55a868'])):
    srv = mpatches.FancyBboxPatch((6.5, y), 1.8, 1.0, boxstyle='round,pad=0.1',
        facecolor=col, edgecolor='white', lw=2)
    ax.add_patch(srv)
    ax.text(7.4, y+0.5, f'App {i+1}', ha='center', va='center', color='white', fontsize=8, fontweight='bold')
    ax.annotate('', xy=(6.5, y+0.5), xytext=(4.8, 3.5),
                arrowprops=dict(arrowstyle='->', color='gray', lw=1.5))

# DB
db = mpatches.Ellipse((10.5, 3.5), 1.4, 0.9, facecolor='#c44e52', edgecolor='white', lw=2)
ax.add_patch(db); ax.text(10.5, 3.5, 'DB', ha='center', va='center', color='white', fontsize=9, fontweight='bold')
for y in [1.7, 3.5, 6.3]:
    ax.annotate('', xy=(9.8, 3.5), xytext=(8.3, y+0.5 if y != 3.5 else y),
                arrowprops=dict(arrowstyle='->', color='gray', lw=1.2))

ax.annotate('', xy=(2.8, 3.5), xytext=(1.7, 3.5),
            arrowprops=dict(arrowstyle='->', color='gray', lw=2))
ax.set_title('3-Tier Architecture Diagram', fontsize=13, fontweight='bold')
fig.tight_layout()
fig.savefig('architecture.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved architecture.png')
🏋️ Practice: Custom Shape Practice
Draw a Venn diagram of 3 overlapping circles with labels A, B, C and intersection labels (A∩B, B∩C, A∩C, A∩B∩C). Use Circle patches with alpha=0.4 and contrasting colors. Place text annotations in each region.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

import matplotlib.patches as mpatches

fig, ax = plt.subplots(figsize=(7, 6))
ax.set_xlim(0, 7); ax.set_ylim(0, 7)
ax.set_aspect('equal'); ax.axis('off')

# Three overlapping circles
circles = [
    mpatches.Circle((2.8, 4.2), 2, facecolor='#4c72b0', alpha=0.35, edgecolor='navy', lw=2),
    mpatches.Circle((4.2, 4.2), 2, facecolor='#dd8452', alpha=0.35, edgecolor='darkred', lw=2),
    mpatches.Circle((3.5, 2.5), 2, facecolor='#55a868', alpha=0.35, edgecolor='darkgreen', lw=2),
]
for c in circles: ax.add_patch(c)

# TODO: add text labels A, B, C in non-overlapping regions
# TODO: add intersection labels A∩B, B∩C, A∩C, A∩B∩C
# TODO: add title and save 'venn3.png'
plt.close()
✅ Practice Checklist
27. Quiver & Streamplot

Visualize 2D vector fields with quiver() for arrow grids and streamplot() for continuous flow lines. Used for fluid dynamics, electric fields, and gradient maps.

Basic quiver plot
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = y = np.linspace(-2, 2, 15)
X, Y = np.meshgrid(x, y)
U = -Y      # velocity components
V =  X

fig, ax = plt.subplots(figsize=(6, 6))
q = ax.quiver(X, Y, U, V, np.sqrt(U**2+V**2),
              cmap='coolwarm', scale=30, pivot='mid')
fig.colorbar(q, ax=ax, label='Speed')
ax.set_title('Rotational Vector Field')
ax.set_xlabel('x'); ax.set_ylabel('y')
ax.set_aspect('equal')
ax.grid(True, alpha=0.2)
fig.tight_layout()
fig.savefig('quiver_basic.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved quiver_basic.png')
Streamplot with speed coloring
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-3, 3, 100)
y = np.linspace(-2, 2, 80)
X, Y = np.meshgrid(x, y)
U = 1 - X**2
V = -Y

speed = np.sqrt(U**2 + V**2)

fig, ax = plt.subplots(figsize=(9, 5))
strm = ax.streamplot(X, Y, U, V, color=speed, cmap='plasma',
                      linewidth=1.5, density=1.5, arrowsize=1.2)
fig.colorbar(strm.lines, ax=ax, label='Speed')
ax.set_xlabel('x'); ax.set_ylabel('y')
ax.set_title('Streamplot: Flow Field Colored by Speed')
ax.set_aspect('equal')
ax.grid(True, alpha=0.2)
fig.tight_layout()
fig.savefig('streamplot.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved streamplot.png')
Gradient field of a scalar function
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = y = np.linspace(-3, 3, 50)
X, Y = np.meshgrid(x, y)
Z = np.exp(-(X**2 + Y**2)/2)  # 2D Gaussian
dZdX, dZdY = np.gradient(Z, x, y)

fig, axes = plt.subplots(1, 2, figsize=(12, 5))
cf = axes[0].contourf(X, Y, Z, levels=20, cmap='viridis')
fig.colorbar(cf, ax=axes[0], label='f(x,y)')
axes[0].set_title('Scalar Field: 2D Gaussian')

# Subsample for quiver
step = 4
axes[1].contourf(X, Y, Z, levels=20, cmap='viridis', alpha=0.5)
axes[1].quiver(X[::step,::step], Y[::step,::step],
               dZdX[::step,::step], dZdY[::step,::step],
               color='white', scale=10, alpha=0.9)
axes[1].set_title('Gradient Field (quiver)')

for ax in axes:
    ax.set_xlabel('x'); ax.set_ylabel('y'); ax.set_aspect('equal')
fig.tight_layout()
fig.savefig('gradient_field.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved gradient_field.png')
Electric dipole field with streamplot
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-4, 4, 200)
y = np.linspace(-3, 3, 150)
X, Y = np.meshgrid(x, y)

# Two point charges: +1 at (-1,0), -1 at (1,0)
def field(q, x0, y0, X, Y):
    dx, dy = X - x0, Y - y0
    r3 = (dx**2 + dy**2)**1.5
    r3 = np.where(r3 < 0.1, 0.1, r3)
    return q*dx/r3, q*dy/r3

Ex1, Ey1 = field(+1, -1, 0, X, Y)
Ex2, Ey2 = field(-1, +1, 0, X, Y)
Ex = Ex1 + Ex2; Ey = Ey1 + Ey2
speed = np.sqrt(Ex**2 + Ey**2)

fig, ax = plt.subplots(figsize=(8, 6))
strm = ax.streamplot(X, Y, Ex, Ey, color=np.log1p(speed),
                      cmap='coolwarm', density=1.5, linewidth=1.2)
ax.plot(-1, 0, 'bo', markersize=12, label='+q')
ax.plot(+1, 0, 'r^', markersize=12, label='-q')
fig.colorbar(strm.lines, ax=ax, label='log(|E|+1)')
ax.set_xlim(-4,4); ax.set_ylim(-3,3)
ax.set_title('Electric Dipole Field Lines')
ax.legend(); ax.set_aspect('equal')
fig.tight_layout()
fig.savefig('dipole_field.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved dipole_field.png')
💼 Real-World: Ocean Current Visualization
Simulate a simplified ocean surface current field with a gyre (circular) pattern plus a northward drift. Plot streamlines colored by speed, add coastline patches, and label the gyre center.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-5, 5, 150)
y = np.linspace(-4, 4, 120)
X, Y = np.meshgrid(x, y)

# Gyre + drift
U = -Y * np.exp(-(X**2 + Y**2)/8) + 0.2
V =  X * np.exp(-(X**2 + Y**2)/8) + 0.05*np.cos(Y)
speed = np.sqrt(U**2 + V**2)

fig, ax = plt.subplots(figsize=(10, 7))
strm = ax.streamplot(X, Y, U, V, color=speed, cmap='ocean_r',
                      density=2.0, linewidth=1.5, arrowsize=1.1)
fig.colorbar(strm.lines, ax=ax, label='Current Speed (m/s)')

# Simulated coastline patches
import matplotlib.patches as mp
coast = mp.Rectangle((3.5,-4), 1.5, 8, facecolor='#c2a267', edgecolor='none', zorder=5)
ax.add_patch(coast)
ax.text(4.25, 0, 'Coast', ha='center', rotation=90, fontsize=10,
        fontweight='bold', color='#5a3e1b', zorder=6)

ax.plot(0, 0, 'w*', markersize=14, label='Gyre center', zorder=7)
ax.set_xlim(-5,5); ax.set_ylim(-4,4)
ax.set_xlabel('Longitude'); ax.set_ylabel('Latitude')
ax.set_title('Ocean Surface Current Gyre', fontweight='bold')
ax.legend(loc='upper left')
fig.tight_layout()
fig.savefig('ocean_current.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved ocean_current.png')
🏋️ Practice: Vector Field Practice
Create a saddle-point vector field: U = X, V = -Y for X,Y in [-2,2]. Plot (1) quiver and (2) streamplot side by side. Color arrows/lines by speed. Mark the equilibrium point at (0,0) with a star. What type of fixed point is this?
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

x = y = np.linspace(-2, 2, 20)
X, Y = np.meshgrid(x, y)
U =  X
V = -Y
speed = np.sqrt(U**2 + V**2)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
# TODO: quiver on ax1, colored by speed
# TODO: streamplot on ax2, colored by speed
# TODO: mark (0,0) with a star on both
# TODO: add colorbar, labels, titles
# Hint: saddle point β€” unstable in x, stable in y
fig.tight_layout()
fig.savefig('saddle_field.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved saddle_field.png')
✅ Practice Checklist
28. Broken Axis & Dual Axis

Handle datasets with extreme outliers using a broken y-axis (two subplots with different ylim), and compare two unrelated scales with twinx()/twiny().

Broken y-axis with diagonal break markers
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(0)
x = np.arange(10)
y = np.array([2, 3, 4, 3, 5, 4, 6, 5, 4, 3], dtype=float)
y[4] = 95   # outlier

fig, (ax_top, ax_bot) = plt.subplots(2, 1, figsize=(8, 6),
    sharex=True, gridspec_kw={'hspace': 0.08, 'height_ratios': [1, 3]})

ax_top.bar(x, y, color='steelblue', alpha=0.8)
ax_bot.bar(x, y, color='steelblue', alpha=0.8)

ax_top.set_ylim(85, 100)
ax_bot.set_ylim(0, 10)
ax_top.spines['bottom'].set_visible(False)
ax_bot.spines['top'].set_visible(False)
ax_top.tick_params(bottom=False)

# Break markers
d = 0.015
kwargs = dict(transform=ax_top.transAxes, color='k', clip_on=False, linewidth=1.5)
ax_top.plot((-d, +d), (-d, +d), **kwargs)
ax_top.plot((1-d, 1+d), (-d, +d), **kwargs)
kwargs.update(transform=ax_bot.transAxes)
ax_bot.plot((-d, +d), (1-d, 1+d), **kwargs)
ax_bot.plot((1-d, 1+d), (1-d, 1+d), **kwargs)

ax_bot.set_xlabel('Category')
fig.text(0.04, 0.5, 'Value', va='center', rotation='vertical')
fig.suptitle('Broken Y-Axis β€” Outlier Handling', fontweight='bold')
fig.savefig('broken_axis.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved broken_axis.png')
twinx: temperature and precipitation
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

months = np.arange(1, 13)
temp = np.array([2,3,8,14,19,24,27,26,20,14,7,3], dtype=float)
precip = np.array([60,50,55,45,55,70,95,110,80,65,70,65], dtype=float)
month_labels = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']

fig, ax1 = plt.subplots(figsize=(10, 5))
ax2 = ax1.twinx()

ax1.bar(months, precip, color='steelblue', alpha=0.5, label='Precipitation')
ax2.plot(months, temp, 'ro-', linewidth=2, markersize=6, label='Temperature')

ax1.set_xlabel('Month')
ax1.set_ylabel('Precipitation (mm)', color='steelblue')
ax2.set_ylabel('Temperature (Β°C)', color='tomato')
ax1.tick_params(axis='y', labelcolor='steelblue')
ax2.tick_params(axis='y', labelcolor='tomato')
ax1.set_xticks(months); ax1.set_xticklabels(month_labels, rotation=30)

lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1+lines2, labels1+labels2, loc='upper left')
ax1.set_title('Climate Chart β€” twinx()', fontweight='bold')
ax1.grid(True, alpha=0.2)
fig.tight_layout()
fig.savefig('twinx_climate.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved twinx_climate.png')
twiny: two x-axes (Hz and ms)
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

freq = np.linspace(10, 1000, 200)   # Hz
period_ms = 1000 / freq              # milliseconds
gain = 1 / (1 + (freq/100)**2)

fig, ax1 = plt.subplots(figsize=(9, 4))
ax2 = ax1.twiny()

ax1.semilogx(freq, gain, color='steelblue', linewidth=2)
ax1.set_xlabel('Frequency (Hz)', color='steelblue')
ax1.tick_params(axis='x', labelcolor='steelblue')
ax1.set_ylabel('Gain')

# Top axis: period in ms (nonuniform tick positions in frequency space)
tick_freq = [10, 20, 50, 100, 200, 500, 1000]
tick_labels = [f'{1000/f:.0f}' for f in tick_freq]
ax2.set_xscale('log')
ax2.set_xlim(ax1.get_xlim())
ax2.set_xticks(tick_freq)
ax2.set_xticklabels(tick_labels)
ax2.set_xlabel('Period (ms)', color='tomato')
ax2.tick_params(axis='x', labelcolor='tomato')

ax1.set_title('Low-Pass Filter β€” Dual Frequency / Period Axes', fontweight='bold', y=1.15)
ax1.grid(True, which='both', alpha=0.3)
fig.tight_layout()
fig.savefig('twiny_filter.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved twiny_filter.png')
Multi-panel with broken and twin axes
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(5)
t = np.arange(24)
load = np.random.uniform(200, 400, 24)
load[12] = 950  # midday spike
price = 20 + 0.05 * load + np.random.randn(24) * 3

fig = plt.figure(figsize=(11, 7))
ax_t = fig.add_axes([0.1, 0.55, 0.8, 0.22])
ax_b = fig.add_axes([0.1, 0.10, 0.8, 0.40], sharex=ax_t)
ax_r = ax_b.twinx()

# broken axis
ax_t.bar(t, load, color='tomato', alpha=0.7); ax_t.set_ylim(800, 1050)
ax_b.bar(t, load, color='tomato', alpha=0.7); ax_b.set_ylim(0, 500)
ax_t.spines['bottom'].set_visible(False); ax_b.spines['top'].set_visible(False)
ax_t.tick_params(bottom=False)

ax_r.plot(t, price, 'b-o', markersize=4, linewidth=1.5)
ax_r.set_ylabel('Price ($/MWh)', color='steelblue')
ax_r.tick_params(axis='y', labelcolor='steelblue')
ax_b.set_xlabel('Hour of Day')
ax_b.set_ylabel('Load (MW)', color='tomato')
ax_b.tick_params(axis='y', labelcolor='tomato')
fig.suptitle('Grid Load & Price: Broken + Twin Axes', fontweight='bold', y=0.98)
fig.savefig('broken_twin.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved broken_twin.png')
💼 Real-World: Stock Price & Volume Dashboard
Create a financial chart: top panel shows candlestick-style daily range (high-low as bar, open-close as body using broken axis to exclude an extreme day), bottom panel uses twinx for price (line) and volume (bar).
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(33)
days = np.arange(20)
opens  = 100 + np.random.randn(20).cumsum()
closes = opens + np.random.randn(20) * 0.5
highs  = np.maximum(opens, closes) + np.abs(np.random.randn(20))*0.8
lows   = np.minimum(opens, closes) - np.abs(np.random.randn(20))*0.8
volume = np.random.randint(100000, 500000, 20).astype(float)
volume[9] = 1800000  # volume spike

fig, (ax_top, ax_bot) = plt.subplots(2, 1, figsize=(11,6),
    sharex=True, gridspec_kw={'hspace':0.1,'height_ratios':[2,1]})
ax_vol = ax_bot.twinx()

# Candlestick style
for d in days:
    color = 'seagreen' if closes[d] >= opens[d] else 'tomato'
    ax_top.plot([d,d], [lows[d], highs[d]], color='gray', linewidth=1)
    ax_top.bar(d, abs(closes[d]-opens[d]), bottom=min(opens[d],closes[d]),
               color=color, width=0.6, alpha=0.9)

ax_bot.bar(days, volume, color='steelblue', alpha=0.5, label='Volume')
ax_vol.plot(days, closes, 'k-', linewidth=1.5, label='Close')
ax_vol.set_ylabel('Price ($)')
ax_bot.set_ylabel('Volume')
ax_top.set_ylabel('Price ($)')
ax_top.set_title('Stock Price & Volume Dashboard', fontweight='bold')
ax_bot.set_xlabel('Day')
fig.tight_layout()
fig.savefig('stock_dashboard.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved stock_dashboard.png')
🏋️ Practice: Dual Axis Practice
Simulate monthly website users (in thousands, growing from 10 to 80 over 12 months) and server costs (in $, growing from 500 to 2000). Plot users as a filled area (fill_between) on left axis and costs as a step line on right axis (twinx). Use contrasting colors and add a legend.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

months = np.arange(1, 13)
users = np.linspace(10, 80, 12) + np.random.randn(12)*3
costs = np.linspace(500, 2000, 12) + np.random.randn(12)*50
month_labels = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']

fig, ax1 = plt.subplots(figsize=(10, 5))
ax2 = ax1.twinx()
# TODO: fill_between for users on ax1
# TODO: step line for costs on ax2
# TODO: contrasting colors, labels, legend, title
# TODO: save 'users_costs.png'
plt.close()
✅ Practice Checklist
29. Image Processing with imshow

Use imshow() for displaying arrays as images, applying colormaps, performing simple transformations, and visualizing feature maps from neural networks.

Display and compare colormaps
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.colors import Normalize

np.random.seed(0)
img = np.random.randn(40, 40).cumsum(axis=1)

cmaps_show = ['gray', 'viridis', 'plasma', 'RdBu_r']
fig, axes = plt.subplots(1, 4, figsize=(14, 3.5))
for ax, cmap in zip(axes, cmaps_show):
    im = ax.imshow(img, cmap=cmap, aspect='auto')
    plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
    ax.set_title(cmap, fontsize=10)
    ax.axis('off')
fig.suptitle('Same Array β€” Different Colormaps', fontweight='bold')
fig.tight_layout()
fig.savefig('colormaps_compare.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved colormaps_compare.png')
Image transformations: flip, rotate, crop
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(1)
img = np.random.rand(60, 80)
img[20:40, 30:60] = 0.85  # bright rectangle

transforms = {
    'Original': img,
    'Flipped H': np.fliplr(img),
    'Rotated 45': np.rot90(img),
    'Cropped': img[10:50, 20:70],
}

fig, axes = plt.subplots(1, 4, figsize=(14, 3.5))
for ax, (title, arr) in zip(axes, transforms.items()):
    ax.imshow(arr, cmap='gray', vmin=0, vmax=1)
    ax.set_title(title, fontsize=10); ax.axis('off')
fig.suptitle('Image Transformations', fontweight='bold')
fig.tight_layout()
fig.savefig('img_transforms.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved img_transforms.png')
Visualize CNN feature maps
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(42)
n_filters = 8
feature_maps = [np.random.randn(28, 28) for _ in range(n_filters)]
for i, fm in enumerate(feature_maps):
    # Simulate different filter responses
    x = y = np.linspace(-3, 3, 28)
    X, Y = np.meshgrid(x, y)
    feature_maps[i] = np.sin(X*(i+1)*0.5) * np.cos(Y*(i+1)*0.3) + np.random.randn(28,28)*0.2

fig, axes = plt.subplots(2, 4, figsize=(12, 6))
for ax, fm in zip(axes.flat, feature_maps):
    im = ax.imshow(fm, cmap='RdBu_r', aspect='auto')
    plt.colorbar(im, ax=ax, fraction=0.046)
    ax.axis('off')
fig.suptitle('CNN Feature Maps (Layer 1)', fontweight='bold')
fig.tight_layout()
fig.savefig('feature_maps.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved feature_maps.png')
Overlay: masks, contours, and annotations
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(7)
h, w = 60, 80
bg = np.random.randn(h, w)
signal_x, signal_y = 50, 30
for dy in range(-10, 11):
    for dx in range(-15, 16):
        if dx**2/225 + dy**2/100 < 1:
            bg[signal_y+dy, signal_x+dx] += 3.0

fig, axes = plt.subplots(1, 3, figsize=(14, 4))
# Raw image
axes[0].imshow(bg, cmap='gray'); axes[0].set_title('Raw'); axes[0].axis('off')

# Thresholded mask
mask = (bg > 2.0).astype(float)
axes[1].imshow(bg, cmap='gray')
axes[1].imshow(mask, cmap='Reds', alpha=0.5)
axes[1].set_title('Overlay Mask'); axes[1].axis('off')

# Contour overlay
axes[2].imshow(bg, cmap='gray')
axes[2].contour(bg, levels=[1.5, 2.5], colors=['yellow','red'], linewidths=1.5)
axes[2].annotate('Signal', xy=(signal_x, signal_y), xytext=(signal_x+12, signal_y-12),
                 arrowprops=dict(arrowstyle='->', color='cyan'),
                 color='cyan', fontsize=10, fontweight='bold')
axes[2].set_title('Contour Overlay'); axes[2].axis('off')
fig.suptitle('Image Segmentation Visualization', fontweight='bold')
fig.tight_layout()
fig.savefig('img_overlay.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved img_overlay.png')
💼 Real-World: Medical Image Analysis Dashboard
Simulate a 2D MRI slice (layered Gaussians) and visualize it: (1) raw grayscale, (2) threshold mask in red overlay, (3) gradient magnitude for edge detection, (4) pseudo-color with contour at 50% max intensity.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from scipy.ndimage import gaussian_filter

np.random.seed(42)
h, w = 80, 100
img = np.zeros((h, w))
for cx, cy, r, v in [(50,40,15,1.0),(35,30,8,0.7),(65,50,10,0.8),(45,60,6,0.6)]:
    y_idx, x_idx = np.ogrid[:h,:w]
    img += v * np.exp(-((x_idx-cx)**2 + (y_idx-cy)**2)/(2*r**2))
img = gaussian_filter(img + np.random.randn(h,w)*0.05, sigma=1.5)

fig, axes = plt.subplots(1, 4, figsize=(14, 4))

axes[0].imshow(img, cmap='gray'); axes[0].set_title('Raw MRI Slice'); axes[0].axis('off')

mask = img > img.max()*0.5
axes[1].imshow(img, cmap='gray')
axes[1].imshow(np.ma.masked_where(~mask, img), cmap='Reds', alpha=0.6, vmin=0, vmax=1)
axes[1].set_title('Threshold Mask'); axes[1].axis('off')

gy, gx = np.gradient(img)
grad_mag = np.sqrt(gx**2 + gy**2)
axes[2].imshow(grad_mag, cmap='hot'); axes[2].set_title('Gradient Magnitude'); axes[2].axis('off')

axes[3].imshow(img, cmap='plasma')
axes[3].contour(img, levels=[img.max()*0.5], colors='white', linewidths=2)
axes[3].set_title('Pseudo-color + Contour'); axes[3].axis('off')

fig.suptitle('MRI Analysis Dashboard', fontweight='bold')
fig.tight_layout()
fig.savefig('mri_dashboard.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved mri_dashboard.png')
🏋️ Practice: Image Processing Practice
Create a 50x50 checkerboard pattern (alternating 0 and 1 in 5x5 blocks). Display it in a 1x3 subplot: (1) original with 'gray' cmap, (2) with 'hot' cmap and colorbar, (3) with a Gaussian blur applied (use np.convolve or loop-based blur). Add titles.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

# Build checkerboard
size, block = 50, 5
board = np.zeros((size, size))
for i in range(0, size, block):
    for j in range(0, size, block):
        if (i//block + j//block) % 2 == 0:
            board[i:i+block, j:j+block] = 1

fig, axes = plt.subplots(1, 3, figsize=(11, 4))
# TODO: original gray cmap
# TODO: hot cmap with colorbar
# TODO: blurred version (use gaussian_filter from scipy.ndimage)
# TODO: add titles and axis('off')
# TODO: save 'checkerboard.png'
plt.close()
✅ Practice Checklist
30. Statistical Plots

Create publication-quality statistical visualizations: regression plots, residual diagnostics, Q-Q plots, correlation matrices, and bootstrapped confidence intervals.

Scatter with linear regression and confidence band
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(0)
n = 80
x = np.linspace(0, 10, n)
y = 2.5*x + 1.0 + np.random.randn(n)*3

# Manual OLS
coeffs = np.polyfit(x, y, 1)
poly = np.poly1d(coeffs)
y_pred = poly(x)
residuals = y - y_pred
se = np.std(residuals) * np.sqrt(1/n + (x - x.mean())**2 / ((x - x.mean())**2).sum())
t_val = 1.99  # ~95% CI for n=80

fig, ax = plt.subplots(figsize=(8, 5))
ax.scatter(x, y, s=25, alpha=0.6, color='steelblue', label='Data')
ax.plot(x, y_pred, 'r-', linewidth=2, label=f'y={coeffs[0]:.2f}x+{coeffs[1]:.2f}')
ax.fill_between(x, y_pred - t_val*se, y_pred + t_val*se,
                alpha=0.2, color='red', label='95% CI')
ax.set_xlabel('X'); ax.set_ylabel('Y')
ax.set_title('Linear Regression with Confidence Band')
ax.legend(); ax.grid(True, alpha=0.3)
fig.tight_layout()
fig.savefig('regression_plot.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved regression_plot.png')
Q-Q plot for normality check
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from scipy import stats

np.random.seed(1)
fig, axes = plt.subplots(1, 3, figsize=(13, 4))

datasets = {
    'Normal': np.random.normal(0, 1, 200),
    'Right-Skewed': np.random.exponential(1, 200),
    'Heavy-Tailed': np.random.standard_t(df=3, size=200),
}

for ax, (name, data) in zip(axes, datasets.items()):
    qq = stats.probplot(data, dist='norm')
    theo, sample = qq[0]
    ax.scatter(theo, sample, s=15, alpha=0.6, color='steelblue')
    ax.plot(theo, theo * qq[1][0] + qq[1][1], 'r-', linewidth=1.5, label='Ideal')
    ax.set_title(f'Q-Q Plot: {name}')
    ax.set_xlabel('Theoretical Quantiles')
    ax.set_ylabel('Sample Quantiles')
    ax.grid(True, alpha=0.3); ax.legend(fontsize=8)
fig.tight_layout()
fig.savefig('qq_plots.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved qq_plots.png')
Correlation matrix heatmap
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(2)
n = 200
a = np.random.randn(n)
b = 0.8*a + np.random.randn(n)*0.6
c = -0.5*a + np.random.randn(n)*0.8
d = np.random.randn(n)
e = 0.6*b + 0.4*d + np.random.randn(n)*0.5

data_mat = np.vstack([a,b,c,d,e]).T
labels = ['Feature A','Feature B','Feature C','Feature D','Feature E']
corr = np.corrcoef(data_mat.T)

fig, ax = plt.subplots(figsize=(7, 6))
im = ax.imshow(corr, cmap='RdBu_r', vmin=-1, vmax=1)
plt.colorbar(im, ax=ax, label='Pearson r')
ax.set_xticks(range(5)); ax.set_xticklabels(labels, rotation=30, ha='right', fontsize=9)
ax.set_yticks(range(5)); ax.set_yticklabels(labels, fontsize=9)
for i in range(5):
    for j in range(5):
        c_val = corr[i,j]
        ax.text(j, i, f'{c_val:.2f}', ha='center', va='center', fontsize=8,
                color='white' if abs(c_val) > 0.5 else 'black')
ax.set_title('Correlation Matrix', fontweight='bold')
fig.tight_layout()
fig.savefig('corr_matrix.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved corr_matrix.png')
Bootstrapped confidence interval
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(3)
data = np.random.exponential(2, 100)

n_boot = 2000
boot_means = [np.mean(np.random.choice(data, len(data), replace=True))
              for _ in range(n_boot)]

ci_lo, ci_hi = np.percentile(boot_means, [2.5, 97.5])
true_mean = np.mean(data)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(11, 4))
ax1.hist(data, bins=25, color='steelblue', alpha=0.7, density=True)
ax1.axvline(true_mean, color='red', linewidth=2, label=f'Mean={true_mean:.2f}')
ax1.set_title('Original Data (Exponential)'); ax1.legend(); ax1.grid(True, alpha=0.3)

ax2.hist(boot_means, bins=40, color='seagreen', alpha=0.7, density=True)
ax2.axvline(ci_lo, color='red', linestyle='--', linewidth=2, label=f'95% CI [{ci_lo:.2f}, {ci_hi:.2f}]')
ax2.axvline(ci_hi, color='red', linestyle='--', linewidth=2)
ax2.axvline(true_mean, color='black', linewidth=2, label='Sample mean')
ax2.set_title('Bootstrap Distribution of Mean')
ax2.legend(fontsize=8); ax2.grid(True, alpha=0.3)
fig.suptitle('Bootstrapped 95% Confidence Interval', fontweight='bold')
fig.tight_layout()
fig.savefig('bootstrap_ci.png', dpi=120, bbox_inches='tight')
plt.close()
print('Saved bootstrap_ci.png')
💼 Real-World: Regression Diagnostics Panel
Fit a polynomial regression (degree 3) on noisy data. Show a 2x2 diagnostic panel: (1) fitted curve on data, (2) residuals vs fitted values, (3) Q-Q plot of residuals, (4) histogram of residuals with normal overlay.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from scipy import stats

np.random.seed(99)
x = np.linspace(0, 10, 100)
y_true = 0.2*x**3 - 3*x**2 + 10*x + 5
y = y_true + np.random.randn(100)*5

coeffs = np.polyfit(x, y, 3)
poly = np.poly1d(coeffs)
y_hat = poly(x)
resid = y - y_hat

fig, axes = plt.subplots(2, 2, figsize=(11, 8))

# Fitted curve
axes[0,0].scatter(x, y, s=20, alpha=0.6, color='steelblue', label='Data')
axes[0,0].plot(x, y_hat, 'r-', linewidth=2, label='Poly-3 fit')
axes[0,0].set_title('Fitted Curve'); axes[0,0].legend(); axes[0,0].grid(True, alpha=0.3)

# Residuals vs fitted
axes[0,1].scatter(y_hat, resid, s=15, alpha=0.6, color='steelblue')
axes[0,1].axhline(0, color='red', linestyle='--', linewidth=1.5)
axes[0,1].set_xlabel('Fitted'); axes[0,1].set_ylabel('Residuals')
axes[0,1].set_title('Residuals vs Fitted'); axes[0,1].grid(True, alpha=0.3)

# Q-Q plot
qq = stats.probplot(resid, dist='norm')
theo, samp = qq[0]
axes[1,0].scatter(theo, samp, s=15, alpha=0.6, color='steelblue')
axes[1,0].plot(theo, theo*qq[1][0]+qq[1][1], 'r-', linewidth=1.5)
axes[1,0].set_title('Q-Q Plot of Residuals')
axes[1,0].set_xlabel('Theoretical'); axes[1,0].set_ylabel('Sample')
axes[1,0].grid(True, alpha=0.3)

# Residual histogram
axes[1,1].hist(resid, bins=20, density=True, color='steelblue', alpha=0.7)
rx = np.linspace(resid.min(), resid.max(), 100)
axes[1,1].plot(rx, stats.norm.pdf(rx, resid.mean(), resid.std()), 'r-', linewidth=2)
axes[1,1].set_title('Residual Distribution'); axes[1,1].grid(True, alpha=0.3)

fig.suptitle('Regression Diagnostic Panel', fontweight='bold')
fig.tight_layout()
fig.savefig('regression_diagnostics.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved regression_diagnostics.png')
🏋️ Practice: Statistical Plots Practice
Generate 5 groups of 50 samples each from distributions with means [1,2,3,4,5] and std=1. Create a 1x2 figure: (1) box plot of all 5 groups with mean markers (diamond), (2) violin plot overlaid with individual jitter points (alpha=0.3, s=15). Use matching colors across panels.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(20)
groups = [np.random.normal(mu, 1, 50) for mu in range(1, 6)]
labels = [f'G{i}' for i in range(1, 6)]
colors = ['#4c72b0','#dd8452','#55a868','#c44e52','#9467bd']

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
# TODO: boxplot on ax1 with mean diamond markers
# TODO: violinplot on ax2 with jitter scatter overlay
# TODO: matching colors, labels, titles
# TODO: save 'group_stats.png'
plt.close()
✅ Practice Checklist
31. Multi-Figure Export & Backends

Export single figures at various DPIs, save multiple pages to PDF with PdfPages, create figure collections, and control backends for headless rendering.

PdfPages: multi-page PDF report
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.backends.backend_pdf import PdfPages

np.random.seed(0)
pdf_path = 'multi_page_report.pdf'

with PdfPages(pdf_path) as pdf:
    # Page 1: line plot
    fig, ax = plt.subplots(figsize=(8, 5))
    x = np.linspace(0, 10, 200)
    ax.plot(x, np.sin(x), color='steelblue', linewidth=2)
    ax.set_title('Page 1: Sine Wave')
    ax.grid(True, alpha=0.3)
    fig.tight_layout()
    pdf.savefig(fig); plt.close()

    # Page 2: bar chart
    fig, ax = plt.subplots(figsize=(8, 5))
    vals = np.random.randint(10, 100, 8)
    ax.bar(range(8), vals, color='tomato', alpha=0.8)
    ax.set_title('Page 2: Bar Chart')
    fig.tight_layout()
    pdf.savefig(fig); plt.close()

    # Page 3: scatter
    fig, ax = plt.subplots(figsize=(8, 5))
    ax.scatter(*np.random.randn(2, 200), s=20, alpha=0.5)
    ax.set_title('Page 3: Scatter')
    fig.tight_layout()
    pdf.savefig(fig); plt.close()

    d = pdf.infodict()
    d['Title'] = 'Data Science Report'
    d['Author'] = 'matplotlib'

print(f'Saved {pdf_path} (3 pages)')
Export PNG at multiple DPIs
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(1)
x = np.linspace(0, 2*np.pi, 200)

fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(x, np.sin(x), linewidth=2, color='steelblue', label='sin')
ax.plot(x, np.cos(x), linewidth=2, color='tomato', linestyle='--', label='cos')
ax.legend(); ax.set_title('Multi-DPI Export Test')
ax.grid(True, alpha=0.3)
fig.tight_layout()

for dpi in [72, 150, 300]:
    fname = f'export_dpi{dpi}.png'
    fig.savefig(fname, dpi=dpi, bbox_inches='tight')
    print(f'Saved {fname} at {dpi} DPI')
plt.close()
SVG export for web/vector graphics
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

import matplotlib
matplotlib.rcParams['svg.fonttype'] = 'none'  # editable text in SVG

np.random.seed(2)
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
x = np.linspace(-3, 3, 100)
axes[0].plot(x, stats_curve := 1/(1+np.exp(-x)), color='steelblue', linewidth=2)
axes[0].axhline(0.5, color='red', linestyle='--', linewidth=1)
axes[0].set_title('Sigmoid Function'); axes[0].grid(True, alpha=0.3)
axes[0].set_xlabel('x'); axes[0].set_ylabel('sigma(x)')

np.random.seed(2)
data = np.random.randn(100)
axes[1].hist(data, bins=20, color='seagreen', alpha=0.7, density=True)
axes[1].set_title('Normal Distribution'); axes[1].grid(True, alpha=0.3)

fig.suptitle('SVG Export Example', fontweight='bold')
fig.tight_layout()
fig.savefig('vector_export.svg', format='svg', bbox_inches='tight')
fig.savefig('vector_export.png', dpi=150, bbox_inches='tight')
print('Saved vector_export.svg and vector_export.png')
plt.close()
Figure with custom metadata and tight layout
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(3)
fig = plt.figure(figsize=(10, 7))
fig.set_facecolor('#0f1117')

ax1 = fig.add_subplot(2, 2, (1, 2))  # top row, spans both cols
ax2 = fig.add_subplot(2, 2, 3)
ax3 = fig.add_subplot(2, 2, 4)

x = np.linspace(0, 10, 300)
ax1.plot(x, np.sin(x)*np.exp(-0.1*x), color='#58a6ff', linewidth=2)
ax1.set_facecolor('#1c2128'); ax1.tick_params(colors='white')
for sp in ax1.spines.values(): sp.set_color('#30363d')
ax1.set_title('Damped Oscillation', color='white')

ax2.hist(np.random.randn(500), bins=25, color='#79c0ff', alpha=0.8)
ax2.set_facecolor('#1c2128'); ax2.tick_params(colors='white')
for sp in ax2.spines.values(): sp.set_color('#30363d')
ax2.set_title('Distribution', color='white')

ax3.scatter(*np.random.randn(2,100), s=15, alpha=0.6, color='#ffa657')
ax3.set_facecolor('#1c2128'); ax3.tick_params(colors='white')
for sp in ax3.spines.values(): sp.set_color('#30363d')
ax3.set_title('Scatter', color='white')

fig.suptitle('Dark Theme Dashboard', color='white', fontweight='bold', fontsize=14)
fig.tight_layout()
fig.savefig('dark_dashboard.png', dpi=150, bbox_inches='tight',
            facecolor=fig.get_facecolor())
plt.close()
print('Saved dark_dashboard.png')
💼 Real-World: Automated Report Generation
Generate a 4-page PDF report: page 1 cover with title text and logo placeholder, page 2 multi-panel KPI summary, page 3 trend analysis, page 4 summary table rendered as a matplotlib table.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.backends.backend_pdf import PdfPages

np.random.seed(77)

with PdfPages('automated_report.pdf') as pdf:
    # Page 1: cover
    fig = plt.figure(figsize=(8.5, 11))
    fig.patch.set_facecolor('#1c2128')
    ax = fig.add_axes([0,0,1,1]); ax.axis('off')
    ax.text(0.5,0.65,'Data Science
Quarterly Report', ha='center', va='center',
            fontsize=28, color='white', fontweight='bold', transform=ax.transAxes)
    ax.text(0.5,0.45,'Q1 2025 | Generated by Matplotlib', ha='center',
            fontsize=13, color='#8b949e', transform=ax.transAxes)
    pdf.savefig(fig, facecolor=fig.get_facecolor()); plt.close()

    # Page 2: KPI panel
    fig, axes = plt.subplots(1,3, figsize=(11,5))
    kpis = [('Revenue','$2.4M','+12%','#4c72b0'),
            ('Users','84K','+8%','#55a868'),
            ('NPS','72','+5pt','#dd8452')]
    for ax,(label,val,delta,col) in zip(axes,kpis):
        ax.set_facecolor(col); ax.axis('off')
        ax.text(0.5,0.65,val,ha='center',fontsize=28,fontweight='bold',
                color='white',transform=ax.transAxes)
        ax.text(0.5,0.35,f'{label}
{delta}',ha='center',fontsize=11,
                color='white',transform=ax.transAxes)
    fig.suptitle('Key Performance Indicators',fontweight='bold')
    fig.tight_layout(); pdf.savefig(fig); plt.close()

    # Page 3: trends
    fig, ax = plt.subplots(figsize=(10,5))
    months = np.arange(12)
    for i,(name,col) in enumerate([('Revenue','#4c72b0'),('Costs','tomato'),('Margin','seagreen')]):
        y = np.cumsum(np.random.randn(12)*0.5) + 5 + i*1.5
        ax.plot(months, y, color=col, linewidth=2, label=name, marker='o', markersize=4)
    ax.set_title('12-Month Trend Analysis'); ax.legend(); ax.grid(True, alpha=0.3)
    fig.tight_layout(); pdf.savefig(fig); plt.close()

    # Page 4: table
    fig, ax = plt.subplots(figsize=(10,4))
    ax.axis('off')
    cols = ['Region','Q1','Q2','Q3','Q4','Total']
    rows = [['North','$1.2M','$1.4M','$1.3M','$1.6M','$5.5M'],
            ['South','$0.9M','$1.0M','$1.1M','$1.3M','$4.3M'],
            ['East','$0.7M','$0.8M','$0.9M','$1.0M','$3.4M'],
            ['West','$1.1M','$1.2M','$1.4M','$1.5M','$5.2M']]
    tbl = ax.table(cellText=rows, colLabels=cols, loc='center', cellLoc='center')
    tbl.auto_set_font_size(False); tbl.set_fontsize(10); tbl.scale(1.2,1.8)
    ax.set_title('Regional Revenue Summary', fontweight='bold', pad=20)
    fig.tight_layout(); pdf.savefig(fig); plt.close()

print('Saved automated_report.pdf (4 pages)')
🏋️ Practice: Export Practice
Create a figure with 3 subplots (line, bar, scatter). Save it as: (1) PNG at 72 DPI, (2) PNG at 300 DPI, (3) SVG vector. Then use PdfPages to save it as a 2-page PDF where page 1 is the 3-panel figure and page 2 is just the scatter zoomed in with a title overlay.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.backends.backend_pdf import PdfPages

np.random.seed(8)
x = np.linspace(0, 10, 100)
fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(13, 4))
ax1.plot(x, np.sin(x)); ax1.set_title('Line')
ax2.bar(range(5), np.random.randint(1,10,5)); ax2.set_title('Bar')
ax3.scatter(*np.random.randn(2,50), s=20, alpha=0.6); ax3.set_title('Scatter')
fig.tight_layout()

# TODO: save PNG at 72 DPI
# TODO: save PNG at 300 DPI
# TODO: save SVG
# TODO: save 2-page PDF with PdfPages
plt.close()
print('Done')
✅ Practice Checklist
32. Dashboard Composition

Compose production-quality dashboards by combining GridSpec layouts, dual axes, custom patches, annotation, and multi-figure export. Design for clarity, color accessibility, and print quality.

KPI summary dashboard with mixed chart types
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.gridspec import GridSpec
import matplotlib.patches as mpatches

np.random.seed(42)
fig = plt.figure(figsize=(14, 9), facecolor='#0f1117')
gs = GridSpec(3, 4, figure=fig, hspace=0.45, wspace=0.4)

def dark_ax(ax, title=''):
    ax.set_facecolor('#1c2128')
    for sp in ax.spines.values(): sp.set_color('#30363d')
    ax.tick_params(colors='#c9d1d9', labelsize=8)
    if title: ax.set_title(title, color='white', fontsize=9, fontweight='bold')
    return ax

# Top row: KPI boxes (4 mini panels)
kpis = [('Revenue', '$2.4M', '+12%', '#4c72b0'),
        ('DAU',     '84K',   '+8%',  '#55a868'),
        ('Conv%',   '3.7%',  '+0.3', '#dd8452'),
        ('NPS',     '72',    '+5',   '#c44e52')]
for col, (label, val, delta, color) in enumerate(kpis):
    ax = fig.add_subplot(gs[0, col])
    ax.set_facecolor(color); ax.axis('off')
    ax.text(0.5, 0.6, val, ha='center', va='center', fontsize=18,
            fontweight='bold', color='white', transform=ax.transAxes)
    ax.text(0.5, 0.2, f'{label}  {delta}', ha='center', fontsize=9,
            color='white', transform=ax.transAxes)

# Mid-left: trend line (spans 2 cols)
ax_trend = dark_ax(fig.add_subplot(gs[1, :2]), 'Monthly Revenue Trend')
months = np.arange(12)
rev = 1.5 + np.cumsum(np.random.randn(12)*0.08) + np.linspace(0,0.9,12)
ax_trend.plot(months, rev, color='#58a6ff', linewidth=2)
ax_trend.fill_between(months, rev.min(), rev, alpha=0.15, color='#58a6ff')
ax_trend.set_xticks(months)
ax_trend.set_xticklabels(['J','F','M','A','M','J','J','A','S','O','N','D'],
                          color='#8b949e', fontsize=7)

# Mid-right: bar chart (spans 2 cols)
ax_bar = dark_ax(fig.add_subplot(gs[1, 2:]), 'Revenue by Region')
regions = ['North','South','East','West']
vals = [2.4, 1.8, 1.3, 2.1]
colors_r = ['#4c72b0','#55a868','#dd8452','#c44e52']
ax_bar.bar(regions, vals, color=colors_r, alpha=0.85, width=0.6)
for i, (r, v) in enumerate(zip(regions, vals)):
    ax_bar.text(i, v+0.05, f'${v}M', ha='center', fontsize=8, color='white')

# Bottom: scatter + right-side donut
ax_scatter = dark_ax(fig.add_subplot(gs[2, :3]), 'User Engagement')
n = 300
sessions = np.random.lognormal(1, 0.5, n)
revenue_pts = sessions * np.random.uniform(5, 30, n)
sc = ax_scatter.scatter(sessions, revenue_pts, c=np.log(sessions),
                         cmap='plasma', s=20, alpha=0.6)
ax_scatter.set_xlabel('Sessions', color='#8b949e', fontsize=8)
ax_scatter.set_ylabel('Revenue ($)', color='#8b949e', fontsize=8)

ax_pie = dark_ax(fig.add_subplot(gs[2, 3]), 'Traffic Mix')
wedges, _ = ax_pie.pie([35,30,20,15], colors=['#4c72b0','#55a868','#dd8452','#8172b2'],
                        startangle=90, wedgeprops=dict(width=0.5))
ax_pie.legend(wedges, ['Direct','Organic','Paid','Ref'], loc='lower center',
              fontsize=7, labelcolor='white', facecolor='#1c2128',
              bbox_to_anchor=(0.5,-0.15), ncol=2)
ax_pie.axis('off'); ax_pie.set_facecolor('#1c2128')

fig.suptitle('Business Intelligence Dashboard', color='white',
             fontsize=15, fontweight='bold', y=0.98)
fig.savefig('bi_dashboard.png', dpi=150, bbox_inches='tight',
            facecolor=fig.get_facecolor())
plt.close()
print('Saved bi_dashboard.png')
Scientific dashboard: experiment results
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.gridspec import GridSpec
from scipy import stats

np.random.seed(7)
fig = plt.figure(figsize=(13, 8))
gs = GridSpec(2, 3, hspace=0.38, wspace=0.35)

# Time series with confidence band
ax1 = fig.add_subplot(gs[0, :2])
t = np.linspace(0, 20, 200)
signal = np.sin(t) * np.exp(-0.1*t)
noise = np.random.randn(200)*0.15
measured = signal + noise
upper = signal + 0.3; lower = signal - 0.3
ax1.fill_between(t, lower, upper, alpha=0.2, color='steelblue', label='95% CI')
ax1.plot(t, signal, 'steelblue', linewidth=2, label='True')
ax1.plot(t, measured, 'k.', markersize=3, alpha=0.4, label='Measured')
ax1.set_title('Damped Oscillation β€” Experiment A'); ax1.legend(fontsize=8)
ax1.set_xlabel('Time (s)'); ax1.grid(True, alpha=0.3)

# Q-Q
ax2 = fig.add_subplot(gs[0, 2])
qq = stats.probplot(measured - signal)
ax2.scatter(qq[0][0], qq[0][1], s=10, alpha=0.6, color='steelblue')
ax2.plot(qq[0][0], qq[0][0]*qq[1][0]+qq[1][1], 'r-')
ax2.set_title('Residual Q-Q'); ax2.grid(True, alpha=0.3)

# Frequency spectrum
ax3 = fig.add_subplot(gs[1, :2])
fft = np.abs(np.fft.rfft(measured))
freq = np.fft.rfftfreq(len(measured), d=t[1]-t[0])
ax3.semilogy(freq[1:], fft[1:], color='tomato', linewidth=1.5)
ax3.axvline(1/(2*np.pi), color='navy', linestyle='--', linewidth=1.5,
            label=f'Fundamental {1/(2*np.pi):.3f} Hz')
ax3.set_xlabel('Frequency (Hz)'); ax3.set_ylabel('Amplitude')
ax3.set_title('FFT Spectrum'); ax3.legend(fontsize=8); ax3.grid(True, which='both', alpha=0.3)

# Phase portrait
ax4 = fig.add_subplot(gs[1, 2])
vel = np.gradient(measured, t)
ax4.plot(measured, vel, 'purple', alpha=0.6, linewidth=0.8)
ax4.set_xlabel('Displacement'); ax4.set_ylabel('Velocity')
ax4.set_title('Phase Portrait'); ax4.grid(True, alpha=0.3)

fig.suptitle('Experiment Dashboard β€” Damped Oscillator', fontweight='bold', fontsize=13)
fig.savefig('science_dashboard.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved science_dashboard.png')
Financial OHLC chart with indicators
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

np.random.seed(21)
n = 60
dates = np.arange(n)
close = 100 + np.cumsum(np.random.randn(n)*1.2)
high  = close + np.abs(np.random.randn(n))*1.5
low   = close - np.abs(np.random.randn(n))*1.5
opens = close + np.random.randn(n)*0.5
volume = np.random.randint(500, 2000, n).astype(float)

sma20 = np.convolve(close, np.ones(20)/20, mode='valid')
sma_x = dates[19:]
bb_mid = sma20
bb_std = np.array([close[i-20:i].std() for i in range(20, n)])
bb_up = bb_mid + 2*bb_std; bb_lo = bb_mid - 2*bb_std

from matplotlib.gridspec import GridSpec
fig = plt.figure(figsize=(12, 8))
gs = GridSpec(3, 1, height_ratios=[3, 1, 1], hspace=0.12)
ax_price = fig.add_subplot(gs[0]); ax_vol = fig.add_subplot(gs[1], sharex=ax_price)
ax_rsi = fig.add_subplot(gs[2], sharex=ax_price)

for d in dates:
    color = 'seagreen' if close[d] >= opens[d] else 'tomato'
    ax_price.plot([d,d],[low[d],high[d]], color='gray', linewidth=0.8)
    ax_price.bar(d, abs(close[d]-opens[d]), bottom=min(close[d],opens[d]),
                 color=color, width=0.6)
ax_price.plot(sma_x, sma20, 'navy', linewidth=1.5, label='SMA20')
ax_price.fill_between(sma_x, bb_lo, bb_up, alpha=0.1, color='blue', label='BBΒ±2Οƒ')
ax_price.set_ylabel('Price'); ax_price.legend(fontsize=8)
ax_price.set_title('OHLC with Bollinger Bands', fontweight='bold')

ax_vol.bar(dates, volume, color='steelblue', alpha=0.6)
ax_vol.set_ylabel('Volume')

delta = np.diff(close); up = np.where(delta>0,delta,0); down = np.where(delta<0,-delta,0)
rs = np.convolve(up,np.ones(14)/14,'valid') / (np.convolve(down,np.ones(14)/14,'valid')+1e-9)
rsi = 100 - 100/(1+rs)
ax_rsi.plot(dates[14:], rsi, color='purple', linewidth=1.5)
ax_rsi.axhline(70, color='red', linestyle='--', linewidth=1)
ax_rsi.axhline(30, color='green', linestyle='--', linewidth=1)
ax_rsi.fill_between(dates[14:], 30, 70, alpha=0.05, color='gray')
ax_rsi.set_ylabel('RSI'); ax_rsi.set_ylim(0,100)
ax_rsi.set_xlabel('Day')

plt.setp(ax_price.get_xticklabels(), visible=False)
plt.setp(ax_vol.get_xticklabels(), visible=False)
fig.savefig('financial_chart.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved financial_chart.png')
Accessible dashboard: colorblind-safe palette
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

# Colorblind-safe palette (Wong 2011)
CB_COLORS = ['#000000','#E69F00','#56B4E9','#009E73',
             '#F0E442','#0072B2','#D55E00','#CC79A7']

np.random.seed(99)
fig, axes = plt.subplots(2, 2, figsize=(11, 8))
axes = axes.flat

# 1. Line plot
ax = axes[0]
x = np.linspace(0, 10, 100)
for i, col in enumerate(CB_COLORS[:4]):
    ax.plot(x, np.sin(x + i*np.pi/4), color=col, linewidth=2,
            label=f'Series {i+1}', linestyle=['-','--','-.',':'][i])
ax.set_title('Line Plot β€” CB Safe'); ax.legend(fontsize=8); ax.grid(True, alpha=0.3)

# 2. Bar chart
ax = axes[1]
vals = np.random.uniform(2, 9, 5)
ax.bar(range(5), vals, color=CB_COLORS[:5], alpha=0.9, width=0.6,
       hatch=['', '//', 'xx', '..', '\\'])  # hatch for print accessibility
ax.set_title('Bar with Hatch Patterns'); ax.grid(True, axis='y', alpha=0.3)

# 3. Scatter
ax = axes[2]
for i in range(4):
    pts = np.random.randn(30, 2)
    ax.scatter(pts[:,0], pts[:,1], color=CB_COLORS[i+1], s=40, alpha=0.8,
               marker=['o','s','^','D'][i], label=f'Class {i+1}')
ax.set_title('Scatter β€” Multiple Markers'); ax.legend(fontsize=8)

# 4. Filled area
ax = axes[3]
x_a = np.linspace(0, 8, 80)
for i, col in enumerate(CB_COLORS[1:5]):
    y = np.sin(x_a + i) + i*0.8
    ax.plot(x_a, y, color=col, linewidth=2)
    ax.fill_between(x_a, 0, y, color=col, alpha=0.15)
ax.set_title('Area Chart β€” CB Safe'); ax.grid(True, alpha=0.3)

fig.suptitle('Colorblind-Safe Dashboard (Wong Palette)', fontweight='bold', fontsize=13)
fig.tight_layout()
fig.savefig('accessible_dashboard.png', dpi=150, bbox_inches='tight')
plt.close()
print('Saved accessible_dashboard.png')
💼 Real-World: Executive Analytics Deck
Build a 5-panel executive dashboard: title banner, revenue trend with target line, geographic bar chart, user funnel (horizontal bars decreasing), and a summary table. Export at 200 DPI.
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.gridspec import GridSpec
import matplotlib.patches as mpatches

np.random.seed(88)
fig = plt.figure(figsize=(14, 10), facecolor='white')
gs = GridSpec(3, 3, figure=fig, hspace=0.45, wspace=0.35)

# Banner
ax_banner = fig.add_subplot(gs[0, :])
ax_banner.set_facecolor('#1c2128'); ax_banner.axis('off')
ax_banner.text(0.5, 0.6, 'Executive Dashboard β€” Q4 2024',
               ha='center', fontsize=18, fontweight='bold', color='white',
               transform=ax_banner.transAxes)
ax_banner.text(0.5, 0.2, 'Prepared by Analytics Team | Confidential',
               ha='center', fontsize=11, color='#8b949e',
               transform=ax_banner.transAxes)

# Revenue trend
ax_rev = fig.add_subplot(gs[1, :2])
months = np.arange(12)
rev = 1.0 + np.cumsum(np.random.randn(12)*0.05) + np.linspace(0,0.8,12)
target = np.linspace(1.0, 2.0, 12)
ax_rev.plot(months, rev, 'steelblue', linewidth=2.5, marker='o', markersize=5, label='Actual')
ax_rev.plot(months, target, 'r--', linewidth=1.5, label='Target')
ax_rev.fill_between(months, rev, target, where=(rev>=target),
                    alpha=0.15, color='green', label='Ahead')
ax_rev.fill_between(months, rev, target, where=(rev<target),
                    alpha=0.15, color='red', label='Behind')
ax_rev.set_title('Monthly Revenue vs Target', fontweight='bold')
ax_rev.legend(fontsize=8); ax_rev.grid(True, alpha=0.3)

# Regional bar
ax_geo = fig.add_subplot(gs[1, 2])
regions = ['APAC','EMEA','AMER','LATAM']
rev_r = [4.2, 3.1, 5.8, 1.9]
ax_geo.barh(regions, rev_r, color=['#4c72b0','#dd8452','#55a868','#c44e52'], alpha=0.85)
for i,(r,v) in enumerate(zip(regions,rev_r)):
    ax_geo.text(v+0.05, i, f'${v}M', va='center', fontsize=9)
ax_geo.set_title('Revenue by Region', fontweight='bold')
ax_geo.grid(True, axis='x', alpha=0.3)

# Funnel
ax_funnel = fig.add_subplot(gs[2, :2])
stages = ['Visitors','Signups','Activated','Paid','Retained']
counts = [100000, 18000, 9000, 2800, 1900]
colors_f = ['#4c72b0','#5e82c0','#7498d0','#8aaee0','#a0c4f0']
ax_funnel.barh(stages[::-1], counts[::-1], color=colors_f, alpha=0.85)
for i,(s,c) in enumerate(zip(stages[::-1],counts[::-1])):
    ax_funnel.text(c+500, i, f'{c:,}', va='center', fontsize=9)
ax_funnel.set_title('User Acquisition Funnel', fontweight='bold')
ax_funnel.grid(True, axis='x', alpha=0.3)

# Table
ax_tbl = fig.add_subplot(gs[2, 2])
ax_tbl.axis('off')
cols = ['Metric','Q3','Q4']
rows = [['Revenue','$18.2M','$22.1M'],
        ['Users','76K','84K'],
        ['NPS','67','72']]
tbl = ax_tbl.table(cellText=rows, colLabels=cols, loc='center', cellLoc='center')
tbl.auto_set_font_size(False); tbl.set_fontsize(9); tbl.scale(1.1,1.5)
ax_tbl.set_title('Summary', fontweight='bold', pad=15)

fig.savefig('executive_deck.png', dpi=200, bbox_inches='tight',
            facecolor=fig.get_facecolor())
plt.close()
print('Saved executive_deck.png')
🏋️ Practice: Dashboard Practice
Build your own 3-panel dashboard on a topic of your choice (e.g., fitness tracking, stock portfolio, weather). Requirements: (1) use GridSpec with at least one spanning panel, (2) include a dual-axis twinx or broken axis, (3) use at least 3 different chart types, (4) add a title banner, (5) save at 150 DPI.
Starter Code
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.gridspec import GridSpec

np.random.seed(42)
fig = plt.figure(figsize=(13, 9))
gs = GridSpec(3, 3, figure=fig, hspace=0.4, wspace=0.35)

# TODO: Banner row (gs[0, :])
# TODO: Main chart spanning 2 cols (gs[1, :2])
# TODO: Side chart (gs[1, 2])
# TODO: Bottom-left chart (gs[2, :2])
# TODO: Bottom-right chart or table (gs[2, 2])
# TODO: At least one twinx or broken axis
# TODO: Title and tight_layout
# TODO: save 'my_dashboard.png' at 150 DPI
plt.close()
print('Dashboard saved!')
✅ Practice Checklist