Skip to content

GridSpec

GridSpec provides fine-grained control over subplot layout, enabling complex arrangements with varying cell sizes and spans.

Basic GridSpec

Create a grid and add subplots to each cell.

1. Import GridSpec

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np

2. Create Grid

x = np.linspace(0, 3, 50)
y1 = x ** 2
y2 = np.sqrt(x)

fig = plt.figure()
gs = gridspec.GridSpec(2, 3)  # 2 rows, 3 columns

3. Add Subplots

for i, g in enumerate(gs):
    ax = fig.add_subplot(g)
    if i % 2 == 0:
        ax.plot(x, y1)
    else:
        ax.plot(x, y2)

fig.tight_layout()
plt.show()

Size Ratios

Control relative sizes of rows and columns.

1. Height Ratios

fig = plt.figure()
gs = gridspec.GridSpec(2, 3, height_ratios=[2, 1])  # Top row twice as tall

for i, g in enumerate(gs):
    ax = fig.add_subplot(g)
    ax.set_title(f'Cell {i}')

fig.tight_layout()
plt.show()

2. Width Ratios

fig = plt.figure()
gs = gridspec.GridSpec(2, 3, width_ratios=[1, 2, 1])  # Middle column wider

for i, g in enumerate(gs):
    ax = fig.add_subplot(g)

fig.tight_layout()
plt.show()

3. Combined Ratios

fig = plt.figure()
gs = gridspec.GridSpec(2, 3, 
                       height_ratios=[2, 1], 
                       width_ratios=[1, 2, 1])

for i, g in enumerate(gs):
    ax = fig.add_subplot(g)
    ax.set_title(f'Cell {i}')

fig.tight_layout()
plt.show()

Spanning Cells

Use slicing to span multiple rows or columns.

1. Span Columns

fig = plt.figure(figsize=(10, 6))
gs = gridspec.GridSpec(3, 3)

# Full top row
ax1 = fig.add_subplot(gs[0, :])  # Row 0, all columns
ax1.set_title('Top Row (spans all columns)')

fig.tight_layout()
plt.show()

2. Span Rows

fig = plt.figure(figsize=(10, 6))
gs = gridspec.GridSpec(3, 3)

# Left column, rows 1-2
ax2 = fig.add_subplot(gs[1:, 0])  # Rows 1-2, column 0
ax2.set_title('Left (spans 2 rows)')

fig.tight_layout()
plt.show()

3. Span Block

fig = plt.figure(figsize=(10, 6))
gs = gridspec.GridSpec(3, 3)

ax1 = fig.add_subplot(gs[0, :])
ax1.set_title('Top Row')

ax2 = fig.add_subplot(gs[1:, 0])
ax2.set_title('Left Column')

ax3 = fig.add_subplot(gs[1:, 1:])  # 2x2 block
ax3.set_title('Right Block (2x2)')

x = np.linspace(0, 10, 100)
ax1.plot(x, np.sin(x))
ax2.plot(x, np.cos(x))
ax3.plot(x, np.tan(x))
ax3.set_ylim(-5, 5)

fig.tight_layout()
plt.show()

Complex Layouts

Create sophisticated multi-panel figures.

1. Dashboard Layout

fig = plt.figure(figsize=(12, 8))
gs = gridspec.GridSpec(4, 4)

# Main plot: upper-left 3x3
ax_main = fig.add_subplot(gs[:3, :3])
ax_main.set_title('Main Plot')

# Right sidebar: 3 small plots
ax_right1 = fig.add_subplot(gs[0, 3])
ax_right2 = fig.add_subplot(gs[1, 3])
ax_right3 = fig.add_subplot(gs[2, 3])

# Bottom bar: full width
ax_bottom = fig.add_subplot(gs[3, :])
ax_bottom.set_title('Timeline')

2. Add Data

x = np.linspace(0, 10, 100)
ax_main.plot(x, np.sin(x), 'b-', linewidth=2)
ax_main.set_xlabel('x')
ax_main.set_ylabel('y')

ax_right1.hist(np.random.randn(100), bins=20)
ax_right2.hist(np.random.randn(100), bins=20)
ax_right3.hist(np.random.randn(100), bins=20)

ax_bottom.plot(x, np.random.randn(100).cumsum())

fig.tight_layout()
plt.show()

3. Scientific Figure Layout

fig = plt.figure(figsize=(10, 8))
gs = gridspec.GridSpec(2, 2, height_ratios=[3, 1], width_ratios=[2, 1])

ax_main = fig.add_subplot(gs[0, 0])
ax_hist = fig.add_subplot(gs[0, 1])
ax_resid = fig.add_subplot(gs[1, 0])
ax_stats = fig.add_subplot(gs[1, 1])

fig.tight_layout()
plt.show()

Nested GridSpec

Create grids within grids for hierarchical layouts.

1. Outer Grid

fig = plt.figure(figsize=(12, 6))
outer_gs = gridspec.GridSpec(1, 2, figure=fig)

ax_left = fig.add_subplot(outer_gs[0])
ax_left.set_title('Left Panel')

2. Inner Grid

inner_gs = gridspec.GridSpecFromSubplotSpec(2, 2, subplot_spec=outer_gs[1])

ax1 = fig.add_subplot(inner_gs[0, 0])
ax2 = fig.add_subplot(inner_gs[0, 1])
ax3 = fig.add_subplot(inner_gs[1, 0])
ax4 = fig.add_subplot(inner_gs[1, 1])

3. Complete Example

fig = plt.figure(figsize=(12, 6))
outer_gs = gridspec.GridSpec(1, 2, figure=fig)

ax_left = fig.add_subplot(outer_gs[0])
ax_left.set_title('Left Panel')

inner_gs = gridspec.GridSpecFromSubplotSpec(2, 2, subplot_spec=outer_gs[1])
ax1 = fig.add_subplot(inner_gs[0, 0])
ax2 = fig.add_subplot(inner_gs[0, 1])
ax3 = fig.add_subplot(inner_gs[1, 0])
ax4 = fig.add_subplot(inner_gs[1, 1])

x = np.linspace(0, 5, 50)
ax_left.plot(x, x**2)
ax1.plot(x, np.sin(x))
ax2.plot(x, np.cos(x))
ax3.plot(x, np.exp(x/5))
ax4.plot(x, np.log(x + 1))

fig.tight_layout()
plt.show()

GridSpec Parameters

Configure spacing and position.

1. Spacing Parameters

gs = gridspec.GridSpec(2, 3,
                       wspace=0.3,   # Width space between subplots
                       hspace=0.3)   # Height space between subplots

2. Position Parameters

gs = gridspec.GridSpec(2, 3,
                       left=0.1,     # Left edge position
                       right=0.9,    # Right edge position
                       bottom=0.1,   # Bottom edge position
                       top=0.9)      # Top edge position

3. Full Parameter List

gs = gridspec.GridSpec(
    nrows=2,                    # Number of rows
    ncols=3,                    # Number of columns
    figure=fig,                 # Figure to attach to
    left=0.1,
    right=0.9,
    bottom=0.1,
    top=0.9,
    wspace=0.2,
    hspace=0.2,
    width_ratios=[1, 2, 1],
    height_ratios=[2, 1]
)

Runnable Example: gridspec_tutorial.py

"""
Matplotlib Tutorial - Advanced Level
====================================
Topic: Complex Subplot Layouts and GridSpec
Author: Educational Python Course
Level: Advanced

Learning Objectives:
-------------------
1. Master complex subplot arrangements
2. Use GridSpec for flexible layouts
3. Create subplots with different sizes
4. Share axes between subplots
5. Create nested subplots
6. Build dashboard-style visualizations

Prerequisites:
-------------
- Complete understanding of basic subplots
- Mastery of axes array indexing
- Intermediate plotting skills
"""

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np

# ============================================================================
# SECTION 1: Review - Basic Subplots
# ============================================================================

if __name__ == "__main__":

    """
    Quick review of plt.subplots() for equal-sized subplots
    """

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

    for i, ax in enumerate(axes.flatten()):
        x = np.linspace(0, 10, 100)
        ax.plot(x, np.sin((i+1)*x))
        ax.set_title(f'Subplot {i+1}')

    plt.suptitle('Basic Subplots: Equal Sizes', fontsize=16, fontweight='bold')
    plt.tight_layout()
    plt.show()

    # ============================================================================
    # SECTION 2: Sharing Axes - Linked Plots
    # ============================================================================

    """
    sharex and sharey parameters link axes across subplots
    This is useful when comparing data with the same scale
    """

    x = np.linspace(0, 10, 100)

    # Share x-axis
    fig, axes = plt.subplots(3, 1, figsize=(10, 8), sharex=True)

    axes[0].plot(x, np.sin(x))
    axes[0].set_ylabel('sin(x)')
    axes[0].set_title('Shared x-axis: All plots use same x-range')

    axes[1].plot(x, np.cos(x))
    axes[1].set_ylabel('cos(x)')

    axes[2].plot(x, np.tan(x))
    axes[2].set_ylabel('tan(x)')
    axes[2].set_ylim(-3, 3)
    axes[2].set_xlabel('x')  # Only bottom plot has x-label

    # Notice: when you zoom on one plot, all plots zoom together in x
    plt.tight_layout()
    plt.show()

    # Share both x and y axes
    fig, axes = plt.subplots(2, 2, figsize=(10, 8), sharex=True, sharey=True)

    for i, ax in enumerate(axes.flatten()):
        x = np.linspace(0, 10, 100)
        ax.plot(x, np.sin(x + i))
        ax.set_title(f'Plot {i+1}')

    # Only need labels on edge plots
    axes[1, 0].set_xlabel('x')
    axes[1, 1].set_xlabel('x')
    axes[0, 0].set_ylabel('y')
    axes[1, 0].set_ylabel('y')

    plt.suptitle('Shared x and y axes', fontsize=16)
    plt.tight_layout()
    plt.show()

    # ============================================================================
    # SECTION 3: Introduction to GridSpec - The Power Tool
    # ============================================================================

    """
    GridSpec allows creating subplots with different sizes and positions
    Think of it as a grid layout manager

    Key concept: Define a grid (rows × columns), then assign subplots to 
    portions of that grid
    """

    # Create a 3×3 grid
    fig = plt.figure(figsize=(12, 10))
    gs = gridspec.GridSpec(3, 3, figure=fig)

    # Subplot spanning multiple cells
    # gs[row_start:row_end, col_start:col_end]

    # Top row: one large plot
    ax1 = fig.add_subplot(gs[0, :])  # Row 0, all columns
    ax1.plot(np.linspace(0, 10, 100), np.sin(np.linspace(0, 10, 100)))
    ax1.set_title('Large plot spanning entire top row')
    ax1.set_ylabel('sin(x)')

    # Middle row: two plots
    ax2 = fig.add_subplot(gs[1, :2])  # Row 1, columns 0-1
    ax2.plot(np.linspace(0, 10, 100), np.cos(np.linspace(0, 10, 100)))
    ax2.set_title('Middle-left plot (2/3 width)')
    ax2.set_ylabel('cos(x)')

    ax3 = fig.add_subplot(gs[1, 2])  # Row 1, column 2
    ax3.hist(np.random.randn(1000), bins=20)
    ax3.set_title('Middle-right (1/3 width)')

    # Bottom row: three small plots
    ax4 = fig.add_subplot(gs[2, 0])
    ax4.scatter(np.random.rand(50), np.random.rand(50))
    ax4.set_title('Bottom-left')

    ax5 = fig.add_subplot(gs[2, 1])
    ax5.scatter(np.random.rand(50), np.random.rand(50))
    ax5.set_title('Bottom-middle')

    ax6 = fig.add_subplot(gs[2, 2])
    ax6.scatter(np.random.rand(50), np.random.rand(50))
    ax6.set_title('Bottom-right')

    plt.suptitle('GridSpec: Different Subplot Sizes', fontsize=16, fontweight='bold')
    plt.tight_layout()
    plt.show()

    print("=" * 70)
    print("GRIDSPEC INDEXING")
    print("=" * 70)
    print("gs[0, :]     → Row 0, all columns")
    print("gs[:, 0]     → All rows, column 0")
    print("gs[1:3, 0:2] → Rows 1-2, columns 0-1")
    print("gs[0, 0]     → Single cell at row 0, col 0")

    # ============================================================================
    # SECTION 4: GridSpec with Spacing Control
    # ============================================================================

    """
    GridSpec allows fine control over spacing between subplots
    """

    fig = plt.figure(figsize=(12, 10))

    # Create GridSpec with custom spacing
    gs = gridspec.GridSpec(
        3, 3,
        figure=fig,
        wspace=0.4,  # Width space between columns
        hspace=0.4,  # Height space between rows
        left=0.1,    # Left margin
        right=0.9,   # Right margin
        top=0.9,     # Top margin
        bottom=0.1   # Bottom margin
    )

    # Create subplots
    for i in range(3):
        for j in range(3):
            ax = fig.add_subplot(gs[i, j])
            ax.text(0.5, 0.5, f'({i},{j})', 
                    ha='center', va='center', fontsize=16,
                    transform=ax.transAxes)
            ax.set_xticks([])
            ax.set_yticks([])

    plt.suptitle('GridSpec with Custom Spacing', fontsize=16, fontweight='bold', y=0.95)
    plt.show()

    # ============================================================================
    # SECTION 5: Dashboard-Style Layout
    # ============================================================================

    """
    Create a dashboard with one large main plot and smaller auxiliary plots
    """

    # Generate data
    np.random.seed(42)
    x = np.linspace(0, 10, 1000)
    y = np.sin(x) + 0.1 * np.random.randn(1000)

    fig = plt.figure(figsize=(14, 10))
    gs = gridspec.GridSpec(3, 3, figure=fig, hspace=0.3, wspace=0.3)

    # Main plot: large, spanning 2×2
    ax_main = fig.add_subplot(gs[:2, :2])
    ax_main.plot(x, y, 'b-', alpha=0.5, linewidth=0.5)
    ax_main.plot(x, np.sin(x), 'r-', linewidth=2, label='True signal')
    ax_main.set_xlabel('Time', fontsize=12)
    ax_main.set_ylabel('Signal', fontsize=12)
    ax_main.set_title('Main Plot: Time Series with Noise', fontsize=14, fontweight='bold')
    ax_main.legend()
    ax_main.grid(True, alpha=0.3)

    # Histogram of values (right side)
    ax_hist = fig.add_subplot(gs[:2, 2])
    ax_hist.hist(y, bins=50, orientation='horizontal', alpha=0.7)
    ax_hist.set_xlabel('Frequency', fontsize=10)
    ax_hist.set_title('Distribution', fontsize=12)
    ax_hist.grid(True, alpha=0.3)

    # Power spectrum (bottom left)
    ax_fft = fig.add_subplot(gs[2, 0])
    fft_vals = np.abs(np.fft.fft(y))
    fft_freq = np.fft.fftfreq(len(y), x[1] - x[0])
    ax_fft.plot(fft_freq[:len(fft_freq)//2], fft_vals[:len(fft_vals)//2])
    ax_fft.set_xlabel('Frequency', fontsize=10)
    ax_fft.set_ylabel('Power', fontsize=10)
    ax_fft.set_title('Frequency Domain', fontsize=12)
    ax_fft.grid(True, alpha=0.3)

    # Statistics (bottom middle)
    ax_stats = fig.add_subplot(gs[2, 1])
    stats_text = f"""
    Statistics:
    Mean: {np.mean(y):.3f}
    Std:  {np.std(y):.3f}
    Min:  {np.min(y):.3f}
    Max:  {np.max(y):.3f}
    """
    ax_stats.text(0.1, 0.5, stats_text, fontsize=12, family='monospace',
                  transform=ax_stats.transAxes, verticalalignment='center')
    ax_stats.set_xlim(0, 1)
    ax_stats.set_ylim(0, 1)
    ax_stats.axis('off')

    # Correlation plot (bottom right)
    ax_corr = fig.add_subplot(gs[2, 2])
    ax_corr.scatter(y[:-1], y[1:], alpha=0.3, s=1)
    ax_corr.set_xlabel('y(t)', fontsize=10)
    ax_corr.set_ylabel('y(t+1)', fontsize=10)
    ax_corr.set_title('Lag-1 Autocorr', fontsize=12)
    ax_corr.grid(True, alpha=0.3)

    plt.suptitle('Data Analysis Dashboard', fontsize=16, fontweight='bold')
    plt.show()

    # ============================================================================
    # SECTION 6: Nested GridSpecs
    # ============================================================================

    """
    You can create grids within grids for even more complex layouts
    """

    fig = plt.figure(figsize=(14, 10))

    # Outer grid: 2 rows, 2 columns
    outer_gs = gridspec.GridSpec(2, 2, figure=fig, wspace=0.3, hspace=0.3)

    # Top-left: Single plot
    ax1 = fig.add_subplot(outer_gs[0, 0])
    ax1.plot(np.linspace(0, 10, 100), np.sin(np.linspace(0, 10, 100)))
    ax1.set_title('Top-Left: Single Plot', fontweight='bold')

    # Top-right: Nested grid (2×2)
    inner_gs_tr = gridspec.GridSpecFromSubplotSpec(2, 2, subplot_spec=outer_gs[0, 1],
                                                    wspace=0.1, hspace=0.1)
    for i in range(2):
        for j in range(2):
            ax = fig.add_subplot(inner_gs_tr[i, j])
            ax.plot(np.random.rand(20))
            ax.set_title(f'TR-{i}{j}', fontsize=9)
            ax.tick_params(labelsize=8)

    # Bottom-left: Nested grid (3×1)
    inner_gs_bl = gridspec.GridSpecFromSubplotSpec(3, 1, subplot_spec=outer_gs[1, 0],
                                                    hspace=0.4)
    for i in range(3):
        ax = fig.add_subplot(inner_gs_bl[i, 0])
        ax.hist(np.random.randn(100), bins=15)
        ax.set_title(f'Bottom-Left {i+1}', fontsize=10)

    # Bottom-right: Single large plot
    ax2 = fig.add_subplot(outer_gs[1, 1])
    x = np.random.randn(500)
    y = np.random.randn(500)
    ax2.scatter(x, y, alpha=0.5)
    ax2.set_title('Bottom-Right: Scatter Plot', fontweight='bold')

    plt.suptitle('Nested GridSpecs: Grids within Grids', fontsize=16, fontweight='bold')
    plt.show()

    # ============================================================================
    # SECTION 7: Using subplot2grid - Alternative Syntax
    # ============================================================================

    """
    subplot2grid is an alternative to GridSpec with simpler syntax for some cases
    """

    fig = plt.figure(figsize=(12, 8))

    # subplot2grid(grid_shape, location, rowspan=1, colspan=1)

    # Create a 3×3 grid and place subplots
    ax1 = plt.subplot2grid((3, 3), (0, 0), colspan=3)  # Top row, span 3 cols
    ax1.plot(np.linspace(0, 10, 100), np.sin(np.linspace(0, 10, 100)))
    ax1.set_title('Top: colspan=3')

    ax2 = plt.subplot2grid((3, 3), (1, 0), colspan=2)  # Middle, span 2 cols
    ax2.plot(np.linspace(0, 10, 100), np.cos(np.linspace(0, 10, 100)))
    ax2.set_title('Middle-left: colspan=2')

    ax3 = plt.subplot2grid((3, 3), (1, 2), rowspan=2)  # Right side, span 2 rows
    ax3.hist(np.random.randn(1000), bins=30, orientation='horizontal')
    ax3.set_title('Right: rowspan=2')

    ax4 = plt.subplot2grid((3, 3), (2, 0))
    ax4.scatter(np.random.rand(50), np.random.rand(50))
    ax4.set_title('Bottom-left')

    ax5 = plt.subplot2grid((3, 3), (2, 1))
    ax5.scatter(np.random.rand(50), np.random.rand(50))
    ax5.set_title('Bottom-middle')

    plt.suptitle('subplot2grid: Simpler Syntax', fontsize=16, fontweight='bold')
    plt.tight_layout()
    plt.show()

    # ============================================================================
    # SECTION 8: Width and Height Ratios
    # ============================================================================

    """
    Control relative sizes of rows and columns
    """

    fig = plt.figure(figsize=(12, 8))

    # Create GridSpec with custom ratios
    # width_ratios: relative widths of columns
    # height_ratios: relative heights of rows
    gs = gridspec.GridSpec(
        2, 3,
        figure=fig,
        width_ratios=[1, 2, 1],   # Middle column is twice as wide
        height_ratios=[2, 1],     # Top row is twice as tall
        wspace=0.3,
        hspace=0.3
    )

    # Create subplots
    positions = [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)]
    for i, (row, col) in enumerate(positions):
        ax = fig.add_subplot(gs[row, col])
        ax.text(0.5, 0.5, f'({row},{col})\nRatio demo',
                ha='center', va='center', fontsize=14,
                transform=ax.transAxes)
        ax.set_title(f'Plot {i+1}')

    plt.suptitle('Custom Width and Height Ratios', fontsize=16, fontweight='bold')
    plt.show()

    print("=" * 70)
    print("WIDTH AND HEIGHT RATIOS")
    print("=" * 70)
    print("width_ratios=[1, 2, 1]  → columns have relative widths 1:2:1")
    print("height_ratios=[2, 1]    → rows have relative heights 2:1")
    print("Actual sizes calculated automatically to fit figure")

    # ============================================================================
    # SECTION 9: Complex Scientific Figure Example
    # ============================================================================

    """
    Real-world example: Comprehensive data analysis figure
    """

    # Generate synthetic data
    np.random.seed(42)
    time = np.linspace(0, 10, 1000)
    signal1 = np.sin(2 * np.pi * time) + 0.5 * np.random.randn(1000)
    signal2 = np.cos(2 * np.pi * time) + 0.5 * np.random.randn(1000)

    fig = plt.figure(figsize=(16, 10))
    gs = gridspec.GridSpec(3, 4, figure=fig, hspace=0.4, wspace=0.4)

    # Main time series (top, spanning 3 columns)
    ax_main = fig.add_subplot(gs[0, :3])
    ax_main.plot(time, signal1, 'b-', alpha=0.7, label='Signal 1')
    ax_main.plot(time, signal2, 'r-', alpha=0.7, label='Signal 2')
    ax_main.set_xlabel('Time (s)', fontsize=12)
    ax_main.set_ylabel('Amplitude', fontsize=12)
    ax_main.set_title('Time Series Data', fontsize=14, fontweight='bold')
    ax_main.legend(loc='upper right')
    ax_main.grid(True, alpha=0.3)

    # Phase space (top right)
    ax_phase = fig.add_subplot(gs[0, 3])
    ax_phase.scatter(signal1, signal2, alpha=0.3, s=1)
    ax_phase.set_xlabel('Signal 1', fontsize=10)
    ax_phase.set_ylabel('Signal 2', fontsize=10)
    ax_phase.set_title('Phase Space', fontsize=12)
    ax_phase.grid(True, alpha=0.3)

    # Histogram signal 1
    ax_hist1 = fig.add_subplot(gs[1, 0])
    ax_hist1.hist(signal1, bins=50, alpha=0.7, edgecolor='black')
    ax_hist1.set_xlabel('Signal 1', fontsize=10)
    ax_hist1.set_ylabel('Frequency', fontsize=10)
    ax_hist1.set_title('Distribution: Signal 1', fontsize=12)
    ax_hist1.grid(True, alpha=0.3)

    # Histogram signal 2
    ax_hist2 = fig.add_subplot(gs[1, 1])
    ax_hist2.hist(signal2, bins=50, alpha=0.7, edgecolor='black')
    ax_hist2.set_xlabel('Signal 2', fontsize=10)
    ax_hist2.set_ylabel('Frequency', fontsize=10)
    ax_hist2.set_title('Distribution: Signal 2', fontsize=12)
    ax_hist2.grid(True, alpha=0.3)

    # Correlation
    ax_corr = fig.add_subplot(gs[1, 2])
    correlation = np.correlate(signal1 - signal1.mean(), 
                               signal2 - signal2.mean(), 
                               mode='same')
    lags = np.arange(-len(correlation)//2, len(correlation)//2 + 1)
    ax_corr.plot(lags, correlation)
    ax_corr.set_xlabel('Lag', fontsize=10)
    ax_corr.set_ylabel('Correlation', fontsize=10)
    ax_corr.set_title('Cross-Correlation', fontsize=12)
    ax_corr.grid(True, alpha=0.3)

    # Statistics
    ax_stats = fig.add_subplot(gs[1, 3])
    stats_text = f"""Signal 1:
      μ = {np.mean(signal1):.3f}
      σ = {np.std(signal1):.3f}

    Signal 2:
      μ = {np.mean(signal2):.3f}
      σ = {np.std(signal2):.3f}

    Correlation:
      ρ = {np.corrcoef(signal1, signal2)[0,1]:.3f}
    """
    ax_stats.text(0.1, 0.5, stats_text, fontsize=11, family='monospace',
                  transform=ax_stats.transAxes, verticalalignment='center')
    ax_stats.axis('off')

    # Spectrogram or rolling statistics (bottom row, full width)
    ax_bottom = fig.add_subplot(gs[2, :])
    window = 100
    rolling_mean1 = np.convolve(signal1, np.ones(window)/window, mode='same')
    rolling_mean2 = np.convolve(signal2, np.ones(window)/window, mode='same')
    ax_bottom.plot(time, rolling_mean1, 'b-', linewidth=2, label='Signal 1 (smoothed)')
    ax_bottom.plot(time, rolling_mean2, 'r-', linewidth=2, label='Signal 2 (smoothed)')
    ax_bottom.set_xlabel('Time (s)', fontsize=12)
    ax_bottom.set_ylabel('Smoothed Amplitude', fontsize=12)
    ax_bottom.set_title('Rolling Average (window=100)', fontsize=14, fontweight='bold')
    ax_bottom.legend()
    ax_bottom.grid(True, alpha=0.3)

    plt.suptitle('Comprehensive Signal Analysis', fontsize=18, fontweight='bold', y=0.98)
    plt.show()

    # ============================================================================
    # KEY TAKEAWAYS
    # ============================================================================

    """
    1. Basic plt.subplots() creates equal-sized subplots
    2. sharex/sharey links axes across subplots
    3. GridSpec provides flexible layout control:
       - Different subplot sizes
       - Spanning multiple cells
       - Custom spacing (wspace, hspace)
    4. GridSpec indexing: gs[row_start:row_end, col_start:col_end]
    5. Nested GridSpecs allow grids within grids
    6. subplot2grid is simpler syntax for some layouts
    7. width_ratios and height_ratios control relative sizes
    8. Complex layouts require planning:
       - Sketch layout first
       - Choose grid size
       - Decide which cells each subplot occupies

    Common Patterns:
    ---------------
    Dashboard: Large main plot + smaller auxiliary plots
    Comparison: Multiple views of same data
    Multi-scale: Overview + detail views
    Analysis: Data + distributions + statistics

    GridSpec Syntax Comparison:
    ---------------------------
    gs[0, :]       → Row 0, all columns
    gs[:, 0]       → All rows, column 0  
    gs[0:2, 1:3]   → Rows 0-1, columns 1-2
    gs[0, 0]       → Single cell

    Tips:
    ----
    - Plan layout on paper first
    - Start with outer structure, then details
    - Use wspace/hspace for breathing room
    - tight_layout() helps but isn't always perfect
    - For complex figures, manual spacing may be needed
    """