Skip to content

OneBusAway/usage-map-visualization

Repository files navigation

OBA Usage Map Visualization

Interactive heatmap of OneBusAway transit stop usage in the Puget Sound region. Combines Google Analytics usage data with GTFS transit data.

Screenshot of the tool

See it Live, Right Now

You can view the visualization with the latest batch of data at opentransitsoftwarefoundation.org/onebusaway/visualize.

View the Visualization

Generate a standalone HTML file using the standalone script:

npm install
npm run generate -- --credentials ./path/to/google-service-account.json
open heatmap.html

See Standalone Generation for full options.

Features

  • Heatmap view shows transit stop usage intensity with a plasma color scale
  • Toggle between Total Riders and Times Viewed metrics
  • Zoom in to see individual stops as colored circles
  • Hover over stops at high zoom to see name and statistics
  • Embeddable - works in iframes as a standalone page

Quick Start Options

There are two ways to generate heatmap data:

Method Best For Requirements
Cloudflare Worker Automated daily updates, public hosting Cloudflare account, R2 bucket
Standalone Script One-off generation, sharing via email, local development Node.js 18+, Google Cloud service account JSON

Google Analytics Setup

Before using either method, you need Google Cloud credentials to access the GA4 Data API.

1. Create a Google Cloud Project

  1. Go to Google Cloud Console
  2. Click Select a projectNew Project
  3. Name it (e.g., "OBA Analytics") and click Create
  4. Select your new project from the project dropdown

2. Enable the Google Analytics Data API

  1. Go to APIs & ServicesLibrary
  2. Search for "Google Analytics Data API"
  3. Click on it and click Enable

3. Create a Service Account

  1. Go to APIs & ServicesCredentials
  2. Click Create CredentialsService account
  3. Fill in:
    • Service account name: oba-analytics (or similar)
    • Service account ID: auto-generated
  4. Click Create and Continue
  5. Skip the optional steps (no roles needed) → Done

4. Download the Credentials JSON

  1. Click on your newly created service account
  2. Go to the Keys tab
  3. Click Add KeyCreate new key
  4. Select JSON format
  5. Click Create - the file will download automatically
  6. Store this file securely (it contains private credentials)

The JSON file will look like:

{
  "type": "service_account",
  "project_id": "your-project-id",
  "private_key_id": "...",
  "private_key": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n",
  "client_email": "oba-analytics@your-project.iam.gserviceaccount.com",
  "client_id": "...",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  ...
}

5. Add Service Account to GA4 Property

  1. Go to Google Analytics
  2. Select your property
  3. Click Admin (gear icon) → Property Access Management
  4. Click + to add user
  5. Enter the service account email (from your JSON file's client_email field)
  6. Grant Viewer role
  7. Uncheck "Notify new users by email" (service accounts can't receive email)
  8. Click Add

6. GA4 Custom Dimensions (Required)

The worker expects these custom dimensions to be configured in GA4:

Dimension Name Scope Description
RegionName User Transit region name (e.g., "Puget Sound")
item_id Event Stop ID in format {agency}_{stopId}

These should already be configured if you're using OneBusAway's standard analytics setup.


Standalone Generation (No Cloudflare Required)

Generate a self-contained HTML file that can be opened locally, shared via email, or hosted on any static file server.

Prerequisites

  • Node.js 18+ (required for Web Crypto API)
  • Google Cloud service account JSON file (see Google Analytics Setup above)

Installation

npm install

Usage

# Generate with defaults (Puget Sound region)
npm run generate -- --credentials ./service-account.json

# Custom output file
npm run generate -- --credentials ./service-account.json --output my-heatmap.html

# Different GA4 property and GTFS source (for other regions)
npm run generate -- \
  --credentials ./service-account.json \
  --property-id 123456789 \
  --gtfs-url https://example.com/gtfs.zip

CLI Options

Option Short Required Default Description
--credentials -c Yes - Path to service account JSON file
--output -o No heatmap.html Output HTML file path
--property-id - No 179560067 GA4 Property ID
--gtfs-url - No Puget Sound URL GTFS zip URL
--help -h No - Show help message

Output

The script generates a single HTML file (~50KB + data size) that:

  • Contains all CSS, JavaScript, and data inline
  • Requires no server - open directly in a browser
  • Uses CARTO basemap tiles (requires internet for map tiles)
  • Can be shared via email or hosted on any static server

Cloudflare Worker Setup

For automated daily updates and public hosting, deploy the Cloudflare Worker.

Prerequisites

  • Cloudflare account
  • Node.js 18+
  • Google Cloud service account JSON (see Google Analytics Setup above)

1. Install Dependencies

npm install

2. Install Wrangler CLI

npm install -g wrangler
wrangler login

3. Create R2 Bucket

wrangler r2 bucket create analytics-reports

Or create via the Cloudflare dashboard under R2 Object Storage.

If using a different bucket name, update bucket_name in wrangler.toml:

[[r2_buckets]]
binding = "analytics_reports"
bucket_name = "your-bucket-name"

4. Configure Secrets

# Paste the entire contents of your service account JSON when prompted
wrangler secret put GOOGLE_SERVICE_ACCOUNT_JSON

5. Deploy

npm run deploy

Worker Endpoints

Endpoint Description
/health Health check - returns "OK"
/generate Manual trigger - generates and stores heatmap.json
/view Live visualization - generates data and renders HTML
/json Returns the stored heatmap.json from R2

Cron Schedule

The worker automatically runs daily at 6 AM UTC. Edit wrangler.toml to change:

[triggers]
crons = ["0 6 * * *"]

Local Development

# Start local development server
npm run dev

# Test endpoints
curl http://localhost:8787/health
curl http://localhost:8787/generate
curl http://localhost:8787/view

Serving heatmap.json Publicly

To serve the generated heatmap.json from R2:

Option 1: R2 Custom Domain (Recommended)

  1. Go to Cloudflare dashboard → R2 → your bucket
  2. Click SettingsPublic access
  3. Connect a custom domain (e.g., data.example.com)

Option 2: R2 Public URL

  1. Enable public access in R2 bucket settings
  2. Use the provided *.r2.dev URL

Option 3: Worker Proxy The /json endpoint serves the stored data through the worker.


Configuration

wrangler.toml Settings

[vars]
GA_PROPERTY_ID = "179560067"
GTFS_URL = "https://gtfs.sound.obaweb.org/prod/gtfs_puget_sound_consolidated.zip"

Environment Variables

Variable Description
GA_PROPERTY_ID Google Analytics 4 property ID
GTFS_URL URL to GTFS zip file containing stops.txt
GOOGLE_SERVICE_ACCOUNT_JSON Service account credentials (secret)

Data Format

heatmap.json contains a timeframe and array of data points:

{
  "timeframe": "Nov 22 2025 - Jan 22 2026",
  "generatedAt": "2025-01-22T06:00:00.000Z",
  "dataPoints": [
    {
      "stop_id": "1-575",
      "stop_name": "3rd Ave & Pike St",
      "stop_lat": 47.609642,
      "stop_lon": -122.337585,
      "active_users": 5863,
      "event_count": 29214
    }
  ]
}

Field Descriptions

Field Description
timeframe Human-readable date range (rolling 90 days)
generatedAt ISO 8601 timestamp of generation
stop_id GTFS stop ID (format: {agency}-{id})
stop_name Human-readable stop name
stop_lat Latitude coordinate
stop_lon Longitude coordinate
active_users Unique users who viewed this stop
event_count Total view events for this stop

Troubleshooting

"Failed to get access token"

  • Verify your service account JSON is valid and complete
  • Check that the Google Analytics Data API is enabled in Google Cloud Console
  • Ensure the JSON file contains client_email, private_key, and token_uri

"GA4 API error: 403"

  • Verify the service account has Viewer access to the GA4 property
  • Check that you're using the correct Property ID (not Measurement ID)
  • Property ID is numeric (e.g., 179560067), not G-XXXXXXXX

"GA4 API error" with dimension errors

  • Verify the custom dimension names match your GA4 configuration
  • Check Admin → Custom definitions in GA4 for exact names

No data returned

  • Check that the date range (rolling 90 days) has data
  • Verify the region filter matches exactly ("Puget Sound")
  • Try querying GA4 directly in the Google Analytics UI

Node.js errors running standalone script

  • Ensure you're using Node.js 18 or later: node --version
  • The Web Crypto API is required for JWT signing

GTFS parsing errors

  • Verify the GTFS URL is accessible and returns a valid ZIP file
  • Check that the ZIP contains stops.txt

Data Sources

  • Google Analytics: GA4 Data API (rolling 90-day window)
  • GTFS: Auto-downloaded from configured URL
  • Map Tiles: CARTO Dark Matter (no API key required)

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        Data Pipeline                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   GA4 API ──┐                                                   │
│             ├──► Worker/Script ──► heatmap.json ──► index.html  │
│   GTFS URL ─┘                                                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Worker endpoints:
  /generate  → triggers pipeline, stores to R2
  /view      → generates on-the-fly, returns HTML
  /json      → returns stored heatmap.json

Standalone script:
  generate-standalone.ts → reads credentials file, outputs HTML

License

Apache 2.0 - see LICENSE for details.

About

A usage map of OBA in Puget Sound

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published