A full-stack weather model forecast viewer that fetches real-time gridded forecast data from NOAA NOMADS, decodes GRIB2 with a pure-Python decoder, and renders interactive map overlays with wind arrows, color-coded parameters, contour lines, animation controls, and point analysis tools — inspired by Pivotal Weather and Aguacero.
Live site: https://modelforecastpy.app/
- Leaflet-based map with CartoDB dark/light basemap tiles
- Gap-free canvas overlay rendering gridded forecast data with bilinear edge interpolation
- Contour lines with labeled isolines placed on actual contour midpoints
- Wind arrows — U/V component vectors drawn on the map when wind parameters are selected
- Opacity slider — adjustable overlay transparency (0–100%)
- Color bar legend — dynamic per-parameter color scale with labeled tick marks
- Region presets — CONUS, North America, Global, North Atlantic, West Pacific, and custom saved regions
- Sounding Profile — click map to fetch and render a model sounding plot directly from Model Forecast GRIB profile data
- Meteogram — time-series forecast at a clicked point across all forecast hours
- Cross-Section — vertical cross-section along a user-drawn line
- Ensemble Plume — GFS ensemble spread with percentile bands at a clicked point
Server-side computed derived parameters:
- STP (Sig Tornado Parameter) — approximated from CAPE, CIN, bulk shear
- SCP (Supercell Composite) — CAPE × shear × SRH proxy
- SHIP (Sig Hail Parameter) — CAPE × mixing ratio × lapse rate × shear
- Tornado Composite — multi-ingredient composite with CIN damping
- Effective Bulk Shear — |V500 − Vsfc| scalar approximation
- 1000–500 hPa Thickness — geopotential thickness
- Direct GRIB2 filter downloads from NOMADS — no API keys, no rate limits
- Per-model variable/level overrides — handles model-specific GRIB variable names (e.g., MSLMA for HRRR/RAP pressure, different cloud cover levels for NAM)
- Automatic latest-run detection — finds the most recent model cycle with data available
- Geographic subsetting — downloads only the bounding box needed, not the full grid
- 30-minute response cache with 10-minute latest-run cache
- No C libraries required — works on Windows, Linux, macOS without compiled dependencies
- Grid templates: 3.0 (regular lat/lon) and 3.30 (Lambert Conformal Conic with bilinear regridding)
- Packing templates: 5.0 (simple packing) and 5.40 (JPEG2000)
- Sign-magnitude encoding — correctly decodes GRIB2 scale factors (not two's complement)
| Model | Resolution | Max Forecast | Cycles | Source |
|---|---|---|---|---|
| GFS | 0.25° (~28 km) | 384 hours | 00/06/12/18Z | NOMADS filter_gfs_0p25.pl |
| HRRR | 3 km | 48 hours | Every hour | NOMADS filter_hrrr_2d.pl |
| NAM | 12 km | 84 hours | 00/06/12/18Z | NOMADS filter_nam.pl |
| RAP | 13 km | 51 hours | Every hour | NOMADS filter_rap.pl |
| Category | Parameters |
|---|---|
| Surface & Near-Surface | Temperature (2m), Dewpoint (2m), Wind Speed (10m), Wind Gusts (10m), Surface Pressure (MSLP) |
| Precipitation & Moisture | Accumulated Precipitation, Snowfall, CAPE, Cloud Cover |
| Upper Air | 500 hPa Geopotential Height, 850 hPa Temperature, 250/500/850 hPa Wind Speed, 1000–500 hPa Thickness |
| Severe Weather | CAPE, CIN, Wind Gusts, Visibility, Effective Bulk Shear |
| Severe Composites | STP (Sig Tornado), SCP (Supercell), SHIP (Sig Hail), Tornado Composite |
- PNG export — composited map image with parameter name, model/init-time/valid-time stamp, and color legend overlay
- Shareable URLs — model, parameter, forecast hour, and region encoded in the URL hash
- Play/pause/step through forecast hours with weekday + Zulu date/time display
- Adjustable playback speed (0.5×, 1×, 2×, 4×)
- Frame caching for smooth playback
- Dark/light theme toggle (persisted)
- Colorblind mode (persisted)
- Custom regions — save/delete map bounds to localStorage
- Draggable panels — all point analysis panels can be repositioned and minimized
├── app.py # Flask entry point (CORS, Talisman, SPA catch-all)
├── gunicorn.conf.py # Gunicorn WSGI config (reads PORT from env)
├── requirements.txt # Python dependencies
├── Dockerfile # Multi-stage build (Node + Python)
├── render.yaml # Free Docker web-service blueprint
├── free_hosting_deployment.md
├── forecast/ # Core data package
│ ├── nomads.py # NOMADS GRIB filter client (4 models, 20 variables)
│ ├── grib2.py # Pure-Python GRIB2 decoder (lat/lon + Lambert grids)
│ ├── parameters.py # Parameter definitions, 18 color scales, categories
│ └── open_meteo.py # Open-Meteo client (ensemble plume data)
├── routes/ # Flask API blueprints
│ ├── __init__.py # Blueprint registry
│ ├── forecast_routes.py # /api/forecast, /api/color-scale, sounding, meteogram, cross-section, ensemble, composites
│ ├── meta.py # /api/health, /api/models, /api/parameters
│ └── helpers.py # NaN-safe JSON serializer
└── frontend/ # React 18 + Vite 6
├── src/
│ ├── App.jsx # Main app, state management, frame cache, export
│ ├── api.js # API client (10 endpoints)
│ └── components/
│ ├── Sidebar.jsx # Model/parameter/region selection
│ ├── Header.jsx # Top bar, theme toggle, opacity slider
│ ├── ForecastMap.jsx # Leaflet map container
│ ├── CanvasOverlay.jsx # Canvas grid renderer + wind arrows + contour lines
│ ├── ColorBar.jsx # Color scale legend
│ ├── AnimationControls.jsx # Play/pause/step/speed + Zulu time display
│ ├── ParameterPicker.jsx # Grouped parameter selector
│ ├── SoundingProfile.jsx # Skew-T sounding viewer (via SA proxy)
│ ├── Meteogram.jsx # Point time-series chart
│ ├── CrossSection.jsx # Vertical cross-section viewer
│ └── EnsemblePlume.jsx # Ensemble spread chart
├── public/
│ └── manifest.json # PWA manifest
├── package.json
└── vite.config.js
pip install -r requirements.txt
python app.py
# → http://localhost:5001cd frontend
npm install
npm run dev
# → http://localhost:3002The Vite dev server proxies /api requests to the backend at localhost:5001.
| Component | Platform | URL |
|---|---|---|
| App | Docker web service | https://modelforecastpy.app/ |
The app is a single Dockerized Flask service that builds and serves the React frontend.
# Free Docker host blueprint
# Render reads render.yaml from the repo root.See free_hosting_deployment.md for the current non-Google deployment path.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/health |
Health check |
GET |
/api/models |
List available models with metadata |
GET |
/api/parameters?model={m} |
Parameters grouped by category |
GET |
/api/forecast?model={m}&variable={v}&fhour={h} |
Gridded forecast data (lats, lons, values, wind U/V) |
GET |
/api/color-scale?cmap={name} |
Color scale stops for a parameter's colormap |
GET |
/api/meteogram?model={m}&lat={lat}&lon={lon} |
Full time-series at a single point |
GET |
/api/sounding?model={m}&lat={lat}&lon={lon}&fhour={h} |
Vertical profile data at a point |
GET |
/api/sounding-plot?model={m}&lat={lat}&lon={lon}&fhour={h} |
Skew-T plot image (proxied from Sounding Analysis) |
GET |
/api/cross-section?model={m}&variable={v}&fhour={h}&lat1=... |
Vertical cross-section along a line |
GET |
/api/ensemble?lat={lat}&lon={lon}&variable={v} |
Ensemble plume data from Open-Meteo |
- HTTPS: forced in production via Flask-Talisman (HSTS 2-year preload)
- Content Security Policy: restrictive CSP allowing only required external resources (Carto, OSM, Google Fonts, NOMADS)
- CORS: production origin (
shianmike.github.io) + localhost for development - Application rate limiting: disabled for forecast traffic
- Upstream limits: third-party weather providers may still return
429responses - Security headers: X-Content-Type-Options, X-Frame-Options DENY, COOP, CORP, Permissions-Policy, Referrer-Policy
- Input validation: path-traversal blocking, 16 MB request-size limit
| Package | Purpose |
|---|---|
| Flask 3.1 | Web framework |
| flask-cors 4.0 | CORS headers |
| flask-talisman 1.1 | Security headers |
| gunicorn 22.0 | WSGI server |
| requests 2.31 | NOMADS HTTP client |
| numpy 1.26 | Array operations |
| Pillow 10.0 | Image generation for color scales |
| Package | Purpose |
|---|---|
| React 18.3 | UI framework |
| Vite 6 | Build tooling |
| Leaflet + React-Leaflet | Interactive maps |
| D3 7.9 | Data visualization |
| Recharts 3.7 | Charts |
| html2canvas-pro | PNG export |
| Lucide React | SVG iconography |
This project is for educational and research purposes.