This project visualizes air quality data overlaid onto a 3D geographical map. The map shows hourly data from thousands of sensors and allows interaction and playback of air quality pattern across various locations. These visualizations particularly highlight the significant impact that the 2020 Bay Area fires had on air quality, but can be used to explore patterns throughout the full year.
Tested platforms:
- iOS (iPhone/iPad): Tested with Firefox, Chrome and Safari.
- macOS/Windows: Tested with Firefox, Chrome and Safari.
- Android: Tested with Firefox and Chrome. (clicking on city label not currently supported)
Use the menu to select a city, radius for the area to show sensors, and date range. Given the potential large amount of sensor data at a specific location/radius, the interface does not dynamically load new data as the map is panned. However, one can click a different city directly to load new data.
Interactive 3D demo (time-series sensor data)
sf_pan.mp4 |
sf_pan_2.mp4 |
| ▶ Pan SF → San Mateo · open demo | ▶ Pan to SF · open demo |
The application allows specification of location in California, start and end dates and other parameters for the visualization. (see URL parameters, below, for full list)
The animated 3D visualizations are based on sensor data from PurpleAir. The current viewer (in viewer/) is built with MapLibre GL JS for satellite imagery and 3D terrain, deck.gl for the GPU-rendered air-quality columns, and Vite as the build tool. Satellite imagery is switchable between seamless Sentinel-2 cloudless (default), high-resolution Esri World Imagery, and MapTiler, with automatic promotion to higher-resolution tiles when zoomed in. Data files were preprocessed with Python and Jupyter Lab.
The original 2020 version was built with procedural-gl.js and p5.js; it was rewritten onto MapLibre + deck.gl after procedural-gl.js became unmaintained. See
viewer/README.mdfor how to run and build the current app.
- 2020 Bay Area fires
- 2020 — 1-year time series
- 3D views
- Input controls
- URL parameters
- Running locally
- Future work
- Acknowledgments
Here is a sample collection of interactive 3D visualization of air quality sensor data within a certain radius from the location, references to 3rd party material, and timelapse videos of the 3D visualizations. The visualizations are specified and configured through URL parameters.
LNU Lightning Complex Fires | Aug 17 to Oct 02
- Locations: Napa, Lake, Sonoma, Yolo, Solano
- References: SF Chronicle Fire Tracker, InciWeb, Wikipedia
- Napa Video
Interactive demo | Napa | Aug 16-Oct 03:
napa.mp4
▶ Open interactive demo — Napa
CZU August Lightning Complex Fires | Aug 17 to Sep 22
- Locations: Santa Cruz, San Mateo
- References: SF Chronicle Fire Tracker, InciWeb, Wikipedia
- San Mateo Video
Interactive demo | Santa Cruz | Aug 16-Sep 23:
santa_cruz.mp4
▶ Open interactive demo — Santa Cruz
SCU August Lightning Complex Fires | Aug 16 to Oct 02
- Locations: Santa Clara, Alameda, Contra Costa, San Joaquin, Stanislaus
- References: SF Chronicle Fire Tracker, InciWeb, Wikipedia
- Santa Clara Video
Interactive demo | Santa Clara | Aug 15-Oct 03:
santa_clara.mp4
▶ Open interactive demo — Santa Clara
These full-year datasets take quite some time to load. Firefox on Desktop seems most stable in loading these.
- Oakland | Timelapse Video (15:29) | 2020 Jan-Dec
- Santa Cruz | Timelapse Video (15:40) | 2020 Jan-Dec
- San Francisco | Timelapse Video (17:11) | 2020 Jan-Dec
sf_pan_to_320.mp4Pan to San Francisco |
sf_rotate_320.mp4Rotating around San Francisco |
east_bay_rotate_320.mp4Rotating around East Bay |
silicon_valley_approach_320.mp4Approaching Silicon Valley |
| key | command | description |
|---|---|---|
| o ENTER | orbit | Start orbiting around location, click with mouse to stop |
| f | focus | Focus on specific location |
| key | command | description |
|---|---|---|
| l | labels | Show/hide location labels |
| g | graph | Show/hide graph |
| key | command | description |
|---|---|---|
| p SPACE | play/pause | Start/stop playback |
| r | rewind | Rewind to beginning |
| x/X | speed | Increase/decrease playback speed |
| { N < | 10 back | Step back 10 frames |
| [ n , | 1 back | Step back 1 frame |
| ] m . | 1 forward | Step forward 1 frame |
| } M > | 10 forward | Step forward 10 frames |
| key | command | description |
|---|---|---|
| h | help | Show/hide instruction hint |
| ESC | abort loading | Abort loading data, useful when loading large datasets |
| action | description |
|---|---|
| Right button + move | Rotate |
| Shift + Left button + move | Rotate |
| Left button + move | Pan |
| Center button + move | Zoom |
| Scroll wheel | Zoom |
| Click on location | Load data for the location |
| Click on data point | Focus on that data point |
| parameter | description | example | default |
|---|---|---|---|
| location | Bay area cities or one of the 3 lightning complex fires (see above). | location=Santa Cruz | |
| longitude | Longitude for location. Not used if location parameter is specified. | longitude=-122.08 | -122.4194 |
| latitude | Latitude for location. Not used if location parameter is specified. | latitude=37.4 | 37.7749 |
| radius | Radius (m) of area from the location to load sensor data for. | radius=10000 | 7500 |
| distance | Canera distance | distance=50000 | 20000 |
| start_date | YYYY-MM-DD for time series start. 2020-01-01 <= start_date <= 2021-01-01. | start_date=2020-10-01 | 2020-08-19 |
| end_date | YYYY-MM-DD for time series end. 2020-01-01 <= end_date <= 2021-01-01. | end_date=2020-11-25 | 2020-09-15 |
Examples:
| description | example |
|---|---|
| San Mateo, Sep 30-Oct 03 | http://olwal.github.io/air/3d?location=San%20Mateo&start_date=2020-09-30&end_date=2020-10-03 |
| Longitude/Latitude (Between Mountain View and Cupertino) with default dates | http://olwal.github.io/air/3d?longitude=-122.08&latitude=37.35 |
| Default location and dates, but 20 km radius | http://olwal.github.io/air/3d?radius=20000 |
| Alameda, Aug 15-Oct 03, 20 km radius, 30 km camera distance | http://olwal.github.io/air/3d?location=Alameda&start_date=2020-08-15&end_date=2020-10-03&radius=20000&distance=30000 |
The current app lives in viewer/ and is built with Vite:
cd viewer
npm install
npm run dev # dev server at http://localhost:5173
npm run build # production build -> viewer/dist/The dev server serves the repo-root data/ directory automatically, so no data copying is needed. See viewer/README.md for imagery-source configuration, keyboard shortcuts, and the full URL-parameter reference.
The original 2020 version had an optional real-time mode (activated with a
?realtime=<seconds> URL parameter) that polled PurpleAir for each sensor's
current reading and animated live air quality, instead of playing back the 2020
time series. It fetched per sensor from the public, keyless endpoint:
https://www.purpleair.com/json?show=<sensorId>
parsed PM2_5Value into an AQI, and refreshed on an interval. (See the legacy
implementation in js/observations_remote.js.)
This no longer works. PurpleAir retired that endpoint — requests to it now redirect to an "over-quota" page and return no sensor data. The current viewer is therefore historical time-series only.
To restore real-time in the new viewer, the fetch layer would need to be rewritten
against PurpleAir's current API at api.purpleair.com, which requires:
- a registered read API key (the old endpoint needed none);
- the v1 request shape — a single sensor (
GET /v1/sensors/<id>?fields=pm2.5,...) or a bounded bulk query (GET /v1/sensors?fields=...&nwlng=...&selng=...) rather than one request per sensor; - mapping the new JSON response fields to the existing AQI/color pipeline in
viewer/src/aqi.js; - keeping the API key out of the client — e.g. a small proxy/serverless function — and respecting PurpleAir's rate limits.
Many thanks to Oskar Rönnberg, Carsten Schwesig, and Sebastian Rinnebach for insightful feedback on the interface and features. Special thanks to Felix Palmer for discussions and for providing the open source procedural-gl.js library.
