Lesson 15 - Grid Charts

Advanced Grid Layouts

You have created regular subplot grids with plt.subplots(). Now you will learn GridSpec—a powerful tool for creating irregular layouts where subplots can span multiple rows or columns.

By the end of this lesson, you will be able to:

  • Use GridSpec for flexible subplot positioning
  • Create subplots that span multiple rows or columns
  • Build irregular grid layouts
  • Design professional dashboards with mixed plot sizes
  • Control spacing and alignment precisely
  • Combine different visualization types effectively
  • Create publication-quality multi-panel figures

GridSpec gives you complete control over subplot positioning and sizing.


Why GridSpec?

Regular Grid (plt.subplots)

┌────────┬────────┐
│   A    │   B    │
├────────┼────────┤
│   C    │   D    │
└────────┴────────┘

All subplots same size. Simple, but limited.

Irregular Grid (GridSpec)

┌─────────────────┐
│        A        │  ← Wide plot (spans 2 columns)
├────────┬────────┤
│   B    │   C    │  ← Regular plots
├────────┴────────┤
│        D        │  ← Wide plot again
└─────────────────┘

Flexible sizing and positioning!


Basic GridSpec Usage

Creating a GridSpec

import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec

bikes = pd.read_csv('day.csv')
bikes['dteday'] = pd.to_datetime(bikes['dteday'])

# Create figure and GridSpec
fig = plt.figure(figsize=(14, 10))
gs = GridSpec(3, 2, figure=fig)  # 3 rows, 2 columns

# Add subplots using GridSpec
ax1 = fig.add_subplot(gs[0, :])   # Top row, all columns
ax2 = fig.add_subplot(gs[1, 0])   # Middle-left
ax3 = fig.add_subplot(gs[1, 1])   # Middle-right
ax4 = fig.add_subplot(gs[2, :])   # Bottom row, all columns

# Plot on each axes
ax1.plot(bikes['dteday'], bikes['cnt'], color='steelblue', linewidth=1)
ax1.set_title('Daily Rentals Over Time (Full Width)')
ax1.grid(True, alpha=0.3)

ax2.hist(bikes['cnt'], bins=30, color='coral', edgecolor='black', alpha=0.7)
ax2.set_title('Rental Distribution')
ax2.grid(True, alpha=0.3, axis='y')

ax3.scatter(bikes['temp'], bikes['cnt'], alpha=0.5, s=30, color='green')
ax3.set_title('Temperature vs Rentals')
ax3.grid(True, alpha=0.3)

season_avg = bikes.groupby('season')['cnt'].mean()
season_names = ['Spring', 'Summer', 'Fall', 'Winter']
ax4.bar(season_names, season_avg.values, color='orange', edgecolor='black')
ax4.set_title('Seasonal Averages (Full Width)')
ax4.grid(True, alpha=0.3, axis='y')

plt.tight_layout()
plt.show()

Key syntax:

  • gs = GridSpec(nrows, ncols)
  • gs[row, col] → single cell
  • gs[row, :] → entire row
  • gs[:, col] → entire column
  • gs[start:end, :] → multiple rows

Spanning Multiple Cells

Wide and Tall Subplots

import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec

bikes_hour = pd.read_csv('hour.csv')

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

# Wide plot at top (spans all 3 columns)
ax_top = fig.add_subplot(gs[0, :])
hourly_pattern = bikes_hour.groupby('hr')['cnt'].mean()
ax_top.plot(hourly_pattern.index, hourly_pattern.values, linewidth=2, color='darkblue')
ax_top.set_title('Average Hourly Rental Pattern', fontsize=14, fontweight='bold')
ax_top.set_xlabel('Hour of Day')
ax_top.set_ylabel('Average Rentals')
ax_top.grid(True, alpha=0.3)

# Left tall plot (spans 2 rows)
ax_left = fig.add_subplot(gs[1:, 0])
ax_left.scatter(bikes_hour['temp'], bikes_hour['cnt'], alpha=0.2, s=10, color='red')
ax_left.set_title('Temperature\nvs Rentals', fontsize=12)
ax_left.set_xlabel('Temperature')
ax_left.set_ylabel('Hourly Rentals')
ax_left.grid(True, alpha=0.3)

# Top-right
ax_tr = fig.add_subplot(gs[1, 1:])
ax_tr.hist(bikes_hour['cnt'], bins=50, color='skyblue', edgecolor='black', alpha=0.7)
ax_tr.set_title('Rental Distribution', fontsize=12)
ax_tr.set_xlabel('Hourly Rentals')
ax_tr.grid(True, alpha=0.3, axis='y')

# Bottom-right
ax_br = fig.add_subplot(gs[2, 1:])
weather_avg = bikes_hour.groupby('weathersit')['cnt'].mean()
weather_names = ['Clear', 'Mist', 'Light Rain']
ax_br.barh(weather_names, weather_avg.values, color='green', edgecolor='black')
ax_br.set_title('Weather Impact', fontsize=12)
ax_br.set_xlabel('Average Hourly Rentals')
ax_br.grid(True, alpha=0.3, axis='x')

plt.tight_layout()
plt.show()

Layout achieved:

┌─────────────────────────────┐
│     Hourly Pattern (wide)   │
├──────┬──────────────────────┤
│ Temp │  Distribution        │
│  vs  ├──────────────────────┤
│Rental│  Weather Impact      │
└──────┴──────────────────────┘

Custom Grid Spacing

Controlling Gaps

import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec

bikes = pd.read_csv('day.csv')
bikes['dteday'] = pd.to_datetime(bikes['dteday'])

# GridSpec with custom spacing
fig = plt.figure(figsize=(14, 10))
gs = GridSpec(2, 2, figure=fig, hspace=0.4, wspace=0.3)

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

# Create plots
ax1.plot(bikes['dteday'], bikes['cnt'], linewidth=1)
ax1.set_title('Daily Rentals')
ax1.grid(True, alpha=0.3)

ax2.hist(bikes['cnt'], bins=30, edgecolor='black', alpha=0.7)
ax2.set_title('Distribution')
ax2.grid(True, alpha=0.3, axis='y')

ax3.scatter(bikes['temp'], bikes['cnt'], alpha=0.5, s=30)
ax3.set_title('Temperature Impact')
ax3.grid(True, alpha=0.3)

ax4.scatter(bikes['hum'], bikes['cnt'], alpha=0.5, s=30, color='blue')
ax4.set_title('Humidity Impact')
ax4.grid(True, alpha=0.3)

fig.suptitle('Bike Rental Analysis', fontsize=16, fontweight='bold')
plt.show()

Parameters:

  • hspace: height spacing between rows (0.0 to 1.0)
  • wspace: width spacing between columns (0.0 to 1.0)
  • Higher values → more space

Complex Dashboard Layout

Multi-Level Grid

import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
import numpy as np

bikes = pd.read_csv('day.csv')
bikes['dteday'] = pd.to_datetime(bikes['dteday'])

# Create complex layout
fig = plt.figure(figsize=(16, 12))
gs = GridSpec(4, 3, figure=fig, hspace=0.3, wspace=0.3)

# ===== Row 1: Full-width time series =====
ax_time = fig.add_subplot(gs[0, :])
ax_time.plot(bikes['dteday'], bikes['cnt'], color='steelblue', linewidth=1.5)
ax_time.fill_between(bikes['dteday'], bikes['cnt'], alpha=0.3, color='steelblue')
ax_time.set_ylabel('Daily Rentals', fontsize=11)
ax_time.set_title('Daily Bike Rentals Over Time (2011-2012)',
                   fontsize=13, fontweight='bold')
ax_time.grid(True, alpha=0.3)

# ===== Row 2: Three distributions =====
ax_dist1 = fig.add_subplot(gs[1, 0])
ax_dist1.hist(bikes['cnt'], bins=30, color='coral', edgecolor='black', alpha=0.7)
ax_dist1.set_xlabel('Daily Rentals', fontsize=10)
ax_dist1.set_ylabel('Frequency', fontsize=10)
ax_dist1.set_title('Total Rental Distribution', fontsize=11)
ax_dist1.grid(True, alpha=0.3, axis='y')

ax_dist2 = fig.add_subplot(gs[1, 1])
ax_dist2.hist(bikes['casual'], bins=30, color='skyblue', edgecolor='black', alpha=0.7)
ax_dist2.set_xlabel('Casual Rentals', fontsize=10)
ax_dist2.set_title('Casual User Distribution', fontsize=11)
ax_dist2.grid(True, alpha=0.3, axis='y')

ax_dist3 = fig.add_subplot(gs[1, 2])
ax_dist3.hist(bikes['registered'], bins=30, color='lightgreen', edgecolor='black', alpha=0.7)
ax_dist3.set_xlabel('Registered Rentals', fontsize=10)
ax_dist3.set_title('Registered User Distribution', fontsize=11)
ax_dist3.grid(True, alpha=0.3, axis='y')

# ===== Row 3: Weather correlations (2 wide plots) =====
ax_temp = fig.add_subplot(gs[2, :2])
ax_temp.scatter(bikes['temp'], bikes['cnt'], alpha=0.5, s=30, color='red')
corr_temp = bikes['temp'].corr(bikes['cnt'])
ax_temp.set_xlabel('Normalized Temperature', fontsize=10)
ax_temp.set_ylabel('Daily Rentals', fontsize=10)
ax_temp.set_title(f'Temperature vs Rentals (r = {corr_temp:.3f})', fontsize=11)
ax_temp.grid(True, alpha=0.3)

ax_hum = fig.add_subplot(gs[2, 2])
ax_hum.scatter(bikes['hum'], bikes['cnt'], alpha=0.5, s=30, color='blue')
corr_hum = bikes['hum'].corr(bikes['cnt'])
ax_hum.set_xlabel('Humidity', fontsize=10)
ax_hum.set_ylabel('Daily Rentals', fontsize=10)
ax_hum.set_title(f'Humidity vs Rentals\n(r = {corr_hum:.3f})', fontsize=11)
ax_hum.grid(True, alpha=0.3)

# ===== Row 4: Categorical comparisons =====
ax_season = fig.add_subplot(gs[3, :2])
season_avg = bikes.groupby('season')['cnt'].mean()
season_names = ['Spring', 'Summer', 'Fall', 'Winter']
bars = ax_season.bar(season_names, season_avg.values, color='orange', edgecolor='black')
ax_season.set_ylabel('Average Daily Rentals', fontsize=10)
ax_season.set_title('Average Rentals by Season', fontsize=11)
ax_season.grid(True, alpha=0.3, axis='y')

# Add value labels on bars
for bar in bars:
    height = bar.get_height()
    ax_season.text(bar.get_x() + bar.get_width()/2., height,
                   f'{int(height):,}', ha='center', va='bottom', fontsize=9)

ax_weather = fig.add_subplot(gs[3, 2])
weather_avg = bikes.groupby('weathersit')['cnt'].mean()
weather_names = ['Clear', 'Mist', 'Light\nRain']
ax_weather.barh(weather_names, weather_avg.values, color='green', edgecolor='black')
ax_weather.set_xlabel('Avg Daily Rentals', fontsize=10)
ax_weather.set_title('Weather Impact', fontsize=11)
ax_weather.grid(True, alpha=0.3, axis='x')

# Overall title
fig.suptitle('Comprehensive Bike Rental Analysis Dashboard',
             fontsize=16, fontweight='bold', y=0.995)

plt.show()

Dashboard structure:

Row 1: ┌─────────────────────────────┐
       │  Full-width time series     │
       └─────────────────────────────┘

Row 2: ┌────────┬────────┬────────┐
       │ Dist 1 │ Dist 2 │ Dist 3 │
       └────────┴────────┴────────┘

Row 3: ┌─────────────────┬────────┐
       │  Temperature    │ Humid  │
       └─────────────────┴────────┘

Row 4: ┌─────────────────┬────────┐
       │  Season bars    │Weather │
       └─────────────────┴────────┘

Nested GridSpec

Grid Within Grid

import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec, GridSpecFromSubplotSpec

bikes = pd.read_csv('day.csv')
bikes['dteday'] = pd.to_datetime(bikes['dteday'])

# Main grid: 2 rows, 1 column
fig = plt.figure(figsize=(14, 10))
gs_main = GridSpec(2, 1, figure=fig, hspace=0.3)

# Top: Full-width plot
ax_top = fig.add_subplot(gs_main[0, :])
ax_top.plot(bikes['dteday'], bikes['cnt'], linewidth=1.5, color='steelblue')
ax_top.set_title('Daily Bike Rentals', fontsize=14, fontweight='bold')
ax_top.set_ylabel('Rentals')
ax_top.grid(True, alpha=0.3)

# Bottom: Nested 2x2 grid
gs_bottom = GridSpecFromSubplotSpec(2, 2, subplot_spec=gs_main[1], hspace=0.3, wspace=0.3)

ax_bl1 = fig.add_subplot(gs_bottom[0, 0])
ax_bl1.hist(bikes['cnt'], bins=30, edgecolor='black', alpha=0.7, color='coral')
ax_bl1.set_title('Distribution')
ax_bl1.grid(True, alpha=0.3, axis='y')

ax_bl2 = fig.add_subplot(gs_bottom[0, 1])
ax_bl2.scatter(bikes['temp'], bikes['cnt'], alpha=0.5, s=30, color='red')
ax_bl2.set_title('Temperature')
ax_bl2.grid(True, alpha=0.3)

ax_bl3 = fig.add_subplot(gs_bottom[1, 0])
season_avg = bikes.groupby('season')['cnt'].mean()
ax_bl3.bar(['Spr', 'Sum', 'Fall', 'Win'], season_avg.values,
           color='orange', edgecolor='black')
ax_bl3.set_title('Seasons')
ax_bl3.grid(True, alpha=0.3, axis='y')

ax_bl4 = fig.add_subplot(gs_bottom[1, 1])
weather_avg = bikes.groupby('weathersit')['cnt'].mean()
ax_bl4.barh(['Clear', 'Mist', 'Rain'], weather_avg.values,
            color='green', edgecolor='black')
ax_bl4.set_title('Weather')
ax_bl4.grid(True, alpha=0.3, axis='x')

fig.suptitle('Hierarchical Dashboard Layout', fontsize=16, fontweight='bold')
plt.show()

Use case: Group related subplots visually while maintaining main structure.


Practical Application: Traffic Analysis

Hourly Traffic Dashboard

import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
import numpy as np

bikes_hour = pd.read_csv('hour.csv')

# Create dashboard
fig = plt.figure(figsize=(16, 12))
gs = GridSpec(3, 4, figure=fig, hspace=0.35, wspace=0.4)

# ===== Top row: Hourly pattern (full width) =====
ax_hourly = fig.add_subplot(gs[0, :])
hourly_pattern = bikes_hour.groupby('hr')['cnt'].mean()
ax_hourly.plot(hourly_pattern.index, hourly_pattern.values,
               linewidth=3, color='darkblue', marker='o', markersize=5)
ax_hourly.fill_between(hourly_pattern.index, hourly_pattern.values, alpha=0.3)
ax_hourly.set_xlabel('Hour of Day', fontsize=11)
ax_hourly.set_ylabel('Average Hourly Rentals', fontsize=11)
ax_hourly.set_title('Average Hourly Rental Pattern', fontsize=13, fontweight='bold')
ax_hourly.set_xticks(range(0, 24, 2))
ax_hourly.grid(True, alpha=0.3)

# Add peak markers
peak_hour = hourly_pattern.idxmax()
peak_value = hourly_pattern.max()
ax_hourly.scatter([peak_hour], [peak_value], color='red', s=150, zorder=5)
ax_hourly.text(peak_hour, peak_value + 20, f'Peak: {peak_hour}:00\n({int(peak_value)} rentals)',
               ha='center', fontsize=10, bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))

# ===== Middle row: 4 distributions =====
# Working day vs Weekend
ax_wd = fig.add_subplot(gs[1, 0])
workday_cnt = bikes_hour[bikes_hour['workingday'] == 1]['cnt']
ax_wd.hist(workday_cnt, bins=40, color='lightgreen', edgecolor='black', alpha=0.7)
ax_wd.set_xlabel('Hourly Rentals', fontsize=9)
ax_wd.set_ylabel('Frequency', fontsize=9)
ax_wd.set_title('Workday Distribution', fontsize=10)
ax_wd.grid(True, alpha=0.3, axis='y')

ax_we = fig.add_subplot(gs[1, 1])
weekend_cnt = bikes_hour[bikes_hour['workingday'] == 0]['cnt']
ax_we.hist(weekend_cnt, bins=40, color='lightblue', edgecolor='black', alpha=0.7)
ax_we.set_xlabel('Hourly Rentals', fontsize=9)
ax_we.set_title('Weekend Distribution', fontsize=10)
ax_we.grid(True, alpha=0.3, axis='y')

# Casual vs Registered
ax_cas = fig.add_subplot(gs[1, 2])
ax_cas.hist(bikes_hour['casual'], bins=40, color='coral', edgecolor='black', alpha=0.7)
ax_cas.set_xlabel('Casual Rentals', fontsize=9)
ax_cas.set_title('Casual Distribution', fontsize=10)
ax_cas.grid(True, alpha=0.3, axis='y')

ax_reg = fig.add_subplot(gs[1, 3])
ax_reg.hist(bikes_hour['registered'], bins=40, color='steelblue', edgecolor='black', alpha=0.7)
ax_reg.set_xlabel('Registered Rentals', fontsize=9)
ax_reg.set_title('Registered Distribution', fontsize=10)
ax_reg.grid(True, alpha=0.3, axis='y')

# ===== Bottom row: Weather correlations (2+2 layout) =====
ax_temp = fig.add_subplot(gs[2, :2])
ax_temp.scatter(bikes_hour['temp'], bikes_hour['cnt'], alpha=0.2, s=10, color='red')
corr_temp = bikes_hour['temp'].corr(bikes_hour['cnt'])
ax_temp.set_xlabel('Normalized Temperature', fontsize=10)
ax_temp.set_ylabel('Hourly Rentals', fontsize=10)
ax_temp.set_title(f'Temperature vs Rentals (r = {corr_temp:.3f})', fontsize=11)
ax_temp.grid(True, alpha=0.3)

ax_wind = fig.add_subplot(gs[2, 2:])
ax_wind.scatter(bikes_hour['windspeed'], bikes_hour['cnt'], alpha=0.2, s=10, color='green')
corr_wind = bikes_hour['windspeed'].corr(bikes_hour['cnt'])
ax_wind.set_xlabel('Normalized Wind Speed', fontsize=10)
ax_wind.set_ylabel('Hourly Rentals', fontsize=10)
ax_wind.set_title(f'Wind Speed vs Rentals (r = {corr_wind:.3f})', fontsize=11)
ax_wind.grid(True, alpha=0.3)

# Overall title
fig.suptitle('Hourly Bike Rental Analysis Dashboard', fontsize=16, fontweight='bold')

plt.show()

Dashboard features:

  • Top: Full-width hourly pattern with peak annotation
  • Middle: Four distribution comparisons
  • Bottom: Two wide correlation scatter plots
  • Layout: 3 rows × 4 columns with flexible spanning

Width and Height Ratios

Custom Subplot Sizes

import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec

bikes = pd.read_csv('day.csv')

# Create grid with custom ratios
fig = plt.figure(figsize=(14, 8))
gs = GridSpec(2, 3, figure=fig,
              width_ratios=[2, 1, 1],   # First column 2x wider
              height_ratios=[1, 2])      # Bottom row 2x taller

ax1 = fig.add_subplot(gs[0, 0])
ax2 = fig.add_subplot(gs[0, 1])
ax3 = fig.add_subplot(gs[0, 2])
ax4 = fig.add_subplot(gs[1, :])  # Bottom spans all columns

# Main plot (wide)
ax1.plot(bikes['dteday'], bikes['cnt'], linewidth=1.5)
ax1.set_title('Main Time Series (2x width)')
ax1.grid(True, alpha=0.3)

# Small plot 1
ax2.hist(bikes['casual'], bins=20, edgecolor='black', alpha=0.7, color='coral')
ax2.set_title('Casual')

# Small plot 2
ax3.hist(bikes['registered'], bins=20, edgecolor='black', alpha=0.7, color='steelblue')
ax3.set_title('Registered')

# Bottom plot (tall, full width)
ax4.scatter(bikes['temp'], bikes['cnt'], alpha=0.5, s=30, color='green')
ax4.set_xlabel('Temperature')
ax4.set_ylabel('Daily Rentals')
ax4.set_title('Temperature Analysis (2x height, full width)')
ax4.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

Parameters:

  • width_ratios: list of relative widths
  • height_ratios: list of relative heights
  • Values are proportions, not absolute sizes

Summary

You learned advanced grid layouts with GridSpec:

  • GridSpec provides flexible subplot positioning beyond regular grids
  • Syntax: gs = GridSpec(nrows, ncols, figure=fig)
  • Spanning: gs[row, col_start:col_end] for multi-column, gs[row_start:row_end, col] for multi-row
  • Spacing: hspace (height) and wspace (width) control gaps
  • Custom ratios: width_ratios and height_ratios for different sizes
  • Nested grids: GridSpecFromSubplotSpec for hierarchical layouts
  • Use cases: Dashboards, complex reports, mixed-size visualizations
  • Best practice: Plan layout on paper first, then implement with GridSpec

Next Steps: In the final project lesson, you will apply all visualization techniques to analyze I-94 highway traffic data.

Practice: Create a dashboard with (1) full-width time series at top, (2) three equal scatter plots in middle row, (3) two bar charts (one 2x wider) in bottom row.