Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

This notebook demonstrates visualization techniques for Spire Weather API data.

import os
import requests
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime, timedelta
from dotenv import load_dotenv

load_dotenv()

API_KEY = os.environ.get('SPIRE_API_KEY')
BASE_URL = 'https://api.wx.spire.com'
HEADERS = {'spire-api-key': API_KEY}

# Set matplotlib style
plt.style.use('seaborn-v0_8-whitegrid')

Helper Functions

def get_point_forecast(lat, lon, bundles='basic'):
    """Get point forecast and return as DataFrame."""
    response = requests.get(
        f'{BASE_URL}/forecast/point',
        headers=HEADERS,
        params={'lat': lat, 'lon': lon, 'bundles': bundles}
    )
    response.raise_for_status()
    data = response.json()
    
    records = []
    for item in data['data']:
        record = {
            'valid_time': item['times']['valid_time'],
            **item['values']
        }
        records.append(record)
    
    df = pd.DataFrame(records)
    df['valid_time'] = pd.to_datetime(df['valid_time'])
    return df

def kelvin_to_fahrenheit(k):
    """Convert Kelvin to Fahrenheit."""
    return (k - 273.15) * 9/5 + 32

Time Series Plot

# Get forecast for Denver
df = get_point_forecast(39.7392, -104.9903)
df['temp_f'] = kelvin_to_fahrenheit(df['air_temperature'])

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

ax.plot(df['valid_time'], df['temp_f'], color='#E74C3C', linewidth=2)
ax.fill_between(df['valid_time'], df['temp_f'], alpha=0.3, color='#E74C3C')

ax.set_xlabel('Date/Time', fontsize=12)
ax.set_ylabel('Temperature (°F)', fontsize=12)
ax.set_title('Temperature Forecast - Denver, CO', fontsize=14, fontweight='bold')

# Format x-axis dates
ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %d\n%H:%M'))
ax.xaxis.set_major_locator(mdates.HourLocator(interval=12))

plt.tight_layout()
plt.show()

Multi-Variable Dashboard

# Calculate wind speed
if 'northward_wind' in df.columns and 'eastward_wind' in df.columns:
    df['wind_speed_mph'] = np.sqrt(df['northward_wind']**2 + df['eastward_wind']**2) * 2.237

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

# Temperature
axes[0].plot(df['valid_time'], df['temp_f'], color='#E74C3C', linewidth=2)
axes[0].set_ylabel('Temp (°F)', fontsize=11)
axes[0].axhline(y=32, color='blue', linestyle='--', alpha=0.5, label='Freezing')
axes[0].legend(loc='upper right')

# Wind Speed
if 'wind_speed_mph' in df.columns:
    axes[1].plot(df['valid_time'], df['wind_speed_mph'], color='#3498DB', linewidth=2)
    axes[1].fill_between(df['valid_time'], df['wind_speed_mph'], alpha=0.3, color='#3498DB')
    axes[1].set_ylabel('Wind (mph)', fontsize=11)

# Humidity
if 'relative_humidity' in df.columns:
    axes[2].plot(df['valid_time'], df['relative_humidity'], color='#27AE60', linewidth=2)
    axes[2].fill_between(df['valid_time'], df['relative_humidity'], alpha=0.3, color='#27AE60')
    axes[2].set_ylabel('Humidity (%)', fontsize=11)
    axes[2].set_ylim(0, 100)

# Precipitation
if 'precipitation_amount' in df.columns:
    axes[3].bar(df['valid_time'], df['precipitation_amount'], 
                width=0.2, color='#9B59B6', alpha=0.7)
    axes[3].set_ylabel('Precip (mm)', fontsize=11)
else:
    axes[3].text(0.5, 0.5, 'Precipitation data not available', 
                 ha='center', va='center', transform=axes[3].transAxes)

axes[3].set_xlabel('Date/Time', fontsize=12)
axes[3].xaxis.set_major_formatter(mdates.DateFormatter('%b %d\n%H:%M'))

fig.suptitle('Weather Forecast Dashboard - Denver, CO', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

City Comparison Heatmap

cities = {
    'Seattle': (47.6062, -122.3321),
    'San Francisco': (37.7749, -122.4194),
    'Phoenix': (33.4484, -112.0740),
    'Denver': (39.7392, -104.9903),
    'Dallas': (32.7767, -96.7970),
    'Chicago': (41.8781, -87.6298),
    'Miami': (25.7617, -80.1918),
    'New York': (40.7128, -74.0060),
}

# Get forecasts for all cities
city_data = {}
for city, (lat, lon) in cities.items():
    try:
        city_df = get_point_forecast(lat, lon)
        city_df['temp_f'] = kelvin_to_fahrenheit(city_df['air_temperature'])
        city_data[city] = city_df
    except Exception as e:
        print(f"Error fetching {city}: {e}")

print(f"Retrieved data for {len(city_data)} cities")
# Create temperature matrix
if city_data:
    # Get common time points (first 24 hours)
    sample_df = list(city_data.values())[0]
    time_points = sample_df['valid_time'][:24]
    
    temp_matrix = []
    city_names = []
    
    for city, df in city_data.items():
        temps = df['temp_f'][:24].values
        if len(temps) == 24:
            temp_matrix.append(temps)
            city_names.append(city)
    
    temp_matrix = np.array(temp_matrix)
    
    # Create heatmap
    fig, ax = plt.subplots(figsize=(16, 8))
    
    im = ax.imshow(temp_matrix, aspect='auto', cmap='RdYlBu_r')
    
    # Labels
    ax.set_yticks(range(len(city_names)))
    ax.set_yticklabels(city_names)
    ax.set_xlabel('Forecast Hour', fontsize=12)
    ax.set_title('24-Hour Temperature Forecast Comparison (°F)', fontsize=14, fontweight='bold')
    
    # Colorbar
    cbar = plt.colorbar(im, ax=ax)
    cbar.set_label('Temperature (°F)', fontsize=11)
    
    plt.tight_layout()
    plt.show()

Wind Rose Diagram

# Calculate wind direction for Denver
df_denver = city_data.get('Denver')
if df_denver is not None and 'northward_wind' in df_denver.columns:
    df_denver['wind_dir'] = (270 - np.degrees(
        np.arctan2(df_denver['northward_wind'], df_denver['eastward_wind'])
    )) % 360
    df_denver['wind_speed'] = np.sqrt(
        df_denver['northward_wind']**2 + df_denver['eastward_wind']**2
    ) * 2.237  # m/s to mph
    
    # Create polar plot
    fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(projection='polar'))
    
    # Convert to radians and rotate so 0 is North
    theta = np.radians(90 - df_denver['wind_dir'])  # Convert to standard polar
    r = df_denver['wind_speed']
    
    # Scatter plot of wind vectors
    scatter = ax.scatter(theta, r, c=df_denver.index, cmap='viridis', 
                         alpha=0.6, s=50)
    
    ax.set_theta_zero_location('N')
    ax.set_theta_direction(-1)
    ax.set_title('Wind Direction and Speed - Denver\n(Color = Time)', 
                 fontsize=12, fontweight='bold', pad=20)
    
    # Add colorbar
    cbar = plt.colorbar(scatter, ax=ax, pad=0.1)
    cbar.set_label('Forecast Hour')
    
    plt.show()
else:
    print("Wind data not available")

Diurnal Cycle Analysis

if city_data:
    fig, ax = plt.subplots(figsize=(10, 6))
    
    colors = plt.cm.tab10(np.linspace(0, 1, len(city_data)))
    
    for (city, df), color in zip(city_data.items(), colors):
        # Group by hour of day
        df_copy = df.copy()
        df_copy['hour'] = df_copy['valid_time'].dt.hour
        hourly_mean = df_copy.groupby('hour')['temp_f'].mean()
        
        ax.plot(hourly_mean.index, hourly_mean.values, 
                label=city, linewidth=2, color=color)
    
    ax.set_xlabel('Hour of Day (UTC)', fontsize=12)
    ax.set_ylabel('Temperature (°F)', fontsize=12)
    ax.set_title('Average Diurnal Temperature Cycle by City', fontsize=14, fontweight='bold')
    ax.legend(loc='upper left', bbox_to_anchor=(1.02, 1))
    ax.set_xticks(range(0, 24, 3))
    ax.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.show()

GRIB2 Visualization

If you have downloaded GRIB2 files:

try:
    import xarray as xr
    import cartopy.crs as ccrs
    import cartopy.feature as cfeature
    
    # Check for downloaded GRIB2 files
    data_dir = 'downloaded_data'
    if os.path.exists(data_dir):
        grib_files = [f for f in os.listdir(data_dir) if f.endswith('.grib2')]
        
        if grib_files:
            filepath = os.path.join(data_dir, grib_files[0])
            print(f"Loading: {filepath}")
            
            ds = xr.open_dataset(filepath, engine='cfgrib')
            print("Dataset variables:", list(ds.data_vars))
            
            # Plot first available variable
            var_name = list(ds.data_vars)[0]
            data = ds[var_name]
            
            fig, ax = plt.subplots(
                figsize=(14, 8),
                subplot_kw={'projection': ccrs.PlateCarree()}
            )
            
            data.plot(ax=ax, transform=ccrs.PlateCarree(), 
                      cmap='viridis', add_colorbar=True)
            
            ax.add_feature(cfeature.COASTLINE)
            ax.add_feature(cfeature.BORDERS, linestyle=':')
            ax.set_title(f'{var_name}', fontsize=14)
            
            plt.show()
        else:
            print("No GRIB2 files found. Run the file download notebook first.")
    else:
        print("No downloaded_data directory. Run the file download notebook first.")
        
except ImportError as e:
    print(f"Required library not installed: {e}")
    print("Install with: conda install -c conda-forge xarray cfgrib cartopy")

Summary

This notebook demonstrated:

  1. Time series plots - Temperature forecasts over time

  2. Multi-variable dashboards - Multiple weather parameters

  3. City comparison heatmaps - Compare forecasts across locations

  4. Wind rose diagrams - Wind direction and speed visualization

  5. Diurnal cycle analysis - Daily temperature patterns

  6. GRIB2 map visualization - Gridded data on maps

Additional Resources