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 cellgs[row, :]→ entire rowgs[:, col]→ entire columngs[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 widthsheight_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) andwspace(width) control gaps - Custom ratios:
width_ratiosandheight_ratiosfor different sizes - Nested grids:
GridSpecFromSubplotSpecfor 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.