Skip to content

Example 2: Emergency Response Department

How a city or county emergency response department could integrate daily CalHeatScore data into their internal emergency dashboard.

Integrating CalHeatScores into Your Emergency Dashboard

Imagine you're analyst within a county-level Office of Emergency Services. Your team already monitors wildfire risk, air quality, and power grid status on an internal dashboard that helps you decide when and where to issue public alerts, distribute resources, and open community resilience centers.

You are interested in adding CalHeatScore forecasts via the API to your internal dashboard. The API is free, public, and requires no credentials, which means you can integrate it into your existing dashboard without procurement, API key management, or vendor contracts. The data refreshes daily, returns a 7-day forecast for every California ZIP code, and supports JSON and GeoJSON formats that plug directly into common dashboard platforms like Grafana, Power BI, ArcGIS Dashboards, or custom web applications. You can automate a daily pull, filter to your jurisdiction's ZIP codes, and display the scores alongside your other emergency indicators.

The workflow below walks through how to connect CalHeatScore to your emergency dashboard, from querying scores for your jurisdiction to setting up automated alerts when risk levels cross activation thresholds.

Step-by-Step: Connect CalHeatScore to Your Dashboard

Step 1: Define your jurisdiction's ZIP codes

Compile the ZIP codes within your city or county boundaries. For example, a Riverside County department might track these key ZIP codes across the county's geographical areas:

Area ZIP Codes
Riverside metro 92501, 92503, 92505, 92507
Coachella Valley 92234, 92260, 92262, 92274
Hemet / San Jacinto 92543, 92544, 92583
Banning / Beaumont 92220, 92223

Step 2: Query today's scores for your jurisdiction

Pull the current 7-day forecast for all of your ZIP codes in a single request:

curl "https://services1.arcgis.com/PCHfdHz4GlDNAhBb/arcgis/rest/services/CalHeatScore_Live_Data_for_API_Use/FeatureServer/0/query?where=ZIP_CODE+IN+('92501','92503','92505','92507','92234','92260','92262','92274','92543','92544','92583','92220','92223')&outFields=ZIP_CODE,DATE,CHS_Day_0,CHS_Day_1,CHS_Day_2,CHS_Day_3,CHS_Day_4,CHS_Day_5,CHS_Day_6&returnGeometry=false&f=json"
import requests

BASE_URL = (
    "https://services1.arcgis.com/PCHfdHz4GlDNAhBb/arcgis/rest/services/"
    "CalHeatScore_Live_Data_for_API_Use/FeatureServer/0/query"
)

# Riverside County ZIP codes by area
zip_codes = {
    "Riverside metro": ["92501", "92503", "92505", "92507"],
    "Coachella Valley": ["92234", "92260", "92262", "92274"],
    "Hemet / San Jacinto": ["92543", "92544", "92583"],
    "Banning / Beaumont": ["92220", "92223"],
}

all_zips = [z for group in zip_codes.values() for z in group]
where_clause = "ZIP_CODE IN (" + ",".join(f"'{z}'" for z in all_zips) + ")"

params = {
    "where": where_clause,
    "outFields": "ZIP_CODE,DATE,CHS_Day_0,CHS_Day_1,CHS_Day_2,CHS_Day_3,CHS_Day_4,CHS_Day_5,CHS_Day_6",
    "returnGeometry": "false",
    "f": "json",
}

response = requests.get(BASE_URL, params=params)
data = response.json()

if "error" in data:
    print(f"Error: {data['error']['message']}")
else:
    for feature in data["features"]:
        attrs = feature["attributes"]
        scores = [int(attrs[f"CHS_Day_{d}"]) for d in range(7)]
        peak = max(scores)
        print(f"ZIP {attrs['ZIP_CODE']}: today={scores[0]}, peak={peak} (Day {scores.index(peak)})")
const baseUrl =
  "https://services1.arcgis.com/PCHfdHz4GlDNAhBb/arcgis/rest/services/" +
  "CalHeatScore_Live_Data_for_API_Use/FeatureServer/0/query";

// Riverside County ZIP codes
const zipCodes = [
  "92501", "92503", "92505", "92507",
  "92234", "92260", "92262", "92274",
  "92543", "92544", "92583",
  "92220", "92223",
];
const whereClause = `ZIP_CODE IN (${zipCodes.map((z) => `'${z}'`).join(",")})`;

const params = new URLSearchParams({
  where: whereClause,
  outFields: "ZIP_CODE,DATE,CHS_Day_0,CHS_Day_1,CHS_Day_2,CHS_Day_3,CHS_Day_4,CHS_Day_5,CHS_Day_6",
  returnGeometry: "false",
  f: "json",
});

fetch(`${baseUrl}?${params}`)
  .then((res) => res.json())
  .then((data) => {
    if (data.error) {
      console.error(`Error: ${data.error.message}`);
      return;
    }
    data.features.forEach((feature) => {
      const a = feature.attributes;
      const scores = Array.from({ length: 7 }, (_, d) => Number(a[`CHS_Day_${d}`]));
      const peak = Math.max(...scores);
      console.log(`ZIP ${a.ZIP_CODE}: today=${scores[0]}, peak=${peak} (Day ${scores.indexOf(peak)})`);
    });
  })
  .catch((err) => console.error("Request failed:", err));
Example response (during a July heat wave)
{
  "features": [
    {
      "attributes": {
        "ZIP_CODE": "92234",
        "DATE": "2026-07-18",
        "CHS_Day_0": "4",
        "CHS_Day_1": "4",
        "CHS_Day_2": "4",
        "CHS_Day_3": "3",
        "CHS_Day_4": "3",
        "CHS_Day_5": "2",
        "CHS_Day_6": "1"
      }
    },
    {
      "attributes": {
        "ZIP_CODE": "92501",
        "DATE": "2026-07-18",
        "CHS_Day_0": "3",
        "CHS_Day_1": "3",
        "CHS_Day_2": "4",
        "CHS_Day_3": "3",
        "CHS_Day_4": "2",
        "CHS_Day_5": "1",
        "CHS_Day_6": "1"
      }
    },
    {
      "attributes": {
        "ZIP_CODE": "92543",
        "DATE": "2026-07-18",
        "CHS_Day_0": "2",
        "CHS_Day_1": "3",
        "CHS_Day_2": "3",
        "CHS_Day_3": "2",
        "CHS_Day_4": "1",
        "CHS_Day_5": "1",
        "CHS_Day_6": "0"
      }
    }
  ]
}

Step 3: Define activation thresholds

Map CalHeatScore levels to your department's emergency response actions.

For example, a CalHeatScore value of 3 could correspond with Response Actions A and B, and a CalHeatScore value of 4 could correspond with Response Actions C and D.

Step 4: Automate daily ingestion and threshold alerts

Set up a scheduled script that runs each morning, pulls fresh scores, and sends an alert when any ZIP code in your jurisdiction crosses your activation threshold:

import requests
import json
from datetime import datetime

BASE_URL = (
    "https://services1.arcgis.com/PCHfdHz4GlDNAhBb/arcgis/rest/services/"
    "CalHeatScore_Live_Data_for_API_Use/FeatureServer/0/query"
)

# Your jurisdiction's ZIP codes
zip_codes = ["92501", "92503", "92505", "92507", "92234", "92260",
             "92262", "92274", "92543", "92544", "92583", "92220", "92223"]

ACTIVATION_THRESHOLD = 3  # Alert when any ZIP reaches this score

where_clause = "ZIP_CODE IN (" + ",".join(f"'{z}'" for z in zip_codes) + ")"

params = {
    "where": where_clause,
    "outFields": "ZIP_CODE,DATE,CHS_Day_0,CHS_Day_1,CHS_Day_2,CHS_Day_3,CHS_Day_4,CHS_Day_5,CHS_Day_6",
    "returnGeometry": "false",
    "f": "json",
}

response = requests.get(BASE_URL, params=params)
data = response.json()

if "error" in data:
    print(f"Error: {data['error']['message']}")
    exit(1)

# Check each ZIP code across the 7-day forecast
alerts = []
for feature in data["features"]:
    attrs = feature["attributes"]
    for day in range(7):
        score = int(attrs[f"CHS_Day_{day}"])
        if score >= ACTIVATION_THRESHOLD:
            alerts.append({
                "zip": attrs["ZIP_CODE"],
                "day": day,
                "score": score,
                "level": "SEVERE" if score == 4 else "HIGH",
            })

# Save full dataset for dashboard ingestion
filename = f"calheatscore_{datetime.now().strftime('%Y-%m-%d')}.json"
with open(filename, "w") as f:
    json.dump(data, f, indent=2)
print(f"Saved {len(data['features'])} ZIP codes to {filename}")

# Report alerts
if alerts:
    print(f"\n⚠️  ACTIVATION ALERT: {len(alerts)} threshold exceedances detected")
    for a in alerts:
        print(f"  ZIP {a['zip']}: {a['level']} (score {a['score']}) on Day {a['day']}")
else:
    print("\n✅ All ZIP codes below activation threshold")
const fs = require("fs");

const baseUrl =
  "https://services1.arcgis.com/PCHfdHz4GlDNAhBb/arcgis/rest/services/" +
  "CalHeatScore_Live_Data_for_API_Use/FeatureServer/0/query";

// Your jurisdiction's ZIP codes
const zipCodes = [
  "92501", "92503", "92505", "92507", "92234", "92260",
  "92262", "92274", "92543", "92544", "92583", "92220", "92223",
];

const ACTIVATION_THRESHOLD = 3; // Alert when any ZIP reaches this score

const whereClause = `ZIP_CODE IN (${zipCodes.map((z) => `'${z}'`).join(",")})`;

const params = new URLSearchParams({
  where: whereClause,
  outFields: "ZIP_CODE,DATE,CHS_Day_0,CHS_Day_1,CHS_Day_2,CHS_Day_3,CHS_Day_4,CHS_Day_5,CHS_Day_6",
  returnGeometry: "false",
  f: "json",
});

fetch(`${baseUrl}?${params}`)
  .then((res) => res.json())
  .then((data) => {
    if (data.error) {
      console.error(`Error: ${data.error.message}`);
      process.exit(1);
    }

    // Check each ZIP code across the 7-day forecast
    const alerts = [];
    data.features.forEach((feature) => {
      const a = feature.attributes;
      for (let day = 0; day <= 6; day++) {
        const score = Number(a[`CHS_Day_${day}`]);
        if (score >= ACTIVATION_THRESHOLD) {
          alerts.push({
            zip: a.ZIP_CODE,
            day,
            score,
            level: score === 4 ? "SEVERE" : "HIGH",
          });
        }
      }
    });

    // Save full dataset for dashboard ingestion
    const today = new Date().toISOString().split("T")[0];
    const filename = `calheatscore_${today}.json`;
    fs.writeFileSync(filename, JSON.stringify(data, null, 2));
    console.log(`Saved ${data.features.length} ZIP codes to ${filename}`);

    // Report alerts
    if (alerts.length > 0) {
      console.log(`\n⚠️  ACTIVATION ALERT: ${alerts.length} threshold exceedances detected`);
      alerts.forEach((a) => {
        console.log(`  ZIP ${a.zip}: ${a.level} (score ${a.score}) on Day ${a.day}`);
      });
    } else {
      console.log("\n✅ All ZIP codes below activation threshold");
    }
  })
  .catch((err) => console.error("Request failed:", err));

Schedule this script to run each morning using cron (Linux/Mac), Task Scheduler (Windows), or your department's automation platform. Output the JSON file to a location your dashboard can read, such as a shared drive, an S3 bucket, or a database that your dashboard queries.

Step 5: Display scores on your dashboard

How you visualize the data depends on your dashboard platform:

Add the CalHeatScore feature layer directly — no script needed:

  1. Open ArcGIS Dashboards and create or edit a dashboard.
  2. Add a Map element and add the CalHeatScore layer using this URL:
    https://services1.arcgis.com/PCHfdHz4GlDNAhBb/arcgis/rest/services/CalHeatScore_Live_Data_for_API_Use/FeatureServer/0
    
  3. Apply a definition expression to filter to your jurisdiction's ZIP codes: ZIP_CODE IN ('92501','92503','92505','92507','92234','92260','92262','92274','92543','92544','92583','92220','92223')
  4. Add an Indicator element that shows the highest score in your jurisdiction using a statistics expression on CHS_Day_0.
  5. Add a List element to display ZIP codes currently at score 3 or above.

Use Power BI's web data connector:

  1. Open Power BI Desktop and click Get Data > Web.
  2. Enter the full query URL (with your ZIP codes and f=json).
  3. Power BI will parse the JSON. Expand the features and attributes columns.
  4. Convert CHS_Day_0 through CHS_Day_6 from text to whole number.
  5. Create a conditional formatting rule on your table or map visual using the 0–4 score range.
  6. Set up Scheduled Refresh in the Power BI Service to pull fresh data daily.

Use the Grafana JSON or Infinity data source plugin:

  1. Install the Infinity data source plugin in Grafana.
  2. Add a new data source pointing to the CalHeatScore query URL.
  3. Parse the JSON response using JSONPath: $.features[*].attributes.
  4. Create a Stat panel showing the max score across your jurisdiction.
  5. Create a Table panel listing all ZIP codes with their 7-day forecast.
  6. Set up a Grafana Alert rule: trigger when any CHS_Day_0 value is ≥ 3.

Step 6: Set up a GeoJSON map view for situational awareness

For a map-based view that shows which parts of your jurisdiction are at elevated risk, request GeoJSON and color ZIP codes by score:

curl "https://services1.arcgis.com/PCHfdHz4GlDNAhBb/arcgis/rest/services/CalHeatScore_Live_Data_for_API_Use/FeatureServer/0/query?where=ZIP_CODE+IN+('92501','92503','92505','92507','92234','92260','92262','92274','92543','92544','92583','92220','92223')&outFields=ZIP_CODE,CHS_Day_0,CHS_Day_1,CHS_Day_2&outSR=4326&f=geojson"
import requests
import json

BASE_URL = (
    "https://services1.arcgis.com/PCHfdHz4GlDNAhBb/arcgis/rest/services/"
    "CalHeatScore_Live_Data_for_API_Use/FeatureServer/0/query"
)

zip_codes = ["92501", "92503", "92505", "92507", "92234", "92260",
             "92262", "92274", "92543", "92544", "92583", "92220", "92223"]

where_clause = "ZIP_CODE IN (" + ",".join(f"'{z}'" for z in zip_codes) + ")"

params = {
    "where": where_clause,
    "outFields": "ZIP_CODE,CHS_Day_0,CHS_Day_1,CHS_Day_2",
    "outSR": "4326",
    "f": "geojson",
}

response = requests.get(BASE_URL, params=params)
geojson = response.json()

if "error" in geojson:
    print(f"Error: {geojson['error']['message']}")
else:
    # Save GeoJSON for map visualization
    with open("jurisdiction_heat_map.geojson", "w") as f:
        json.dump(geojson, f)
    print(f"Saved {len(geojson['features'])} ZIP code polygons")
const fs = require("fs");

const baseUrl =
  "https://services1.arcgis.com/PCHfdHz4GlDNAhBb/arcgis/rest/services/" +
  "CalHeatScore_Live_Data_for_API_Use/FeatureServer/0/query";

const zipCodes = [
  "92501", "92503", "92505", "92507", "92234", "92260",
  "92262", "92274", "92543", "92544", "92583", "92220", "92223",
];
const whereClause = `ZIP_CODE IN (${zipCodes.map((z) => `'${z}'`).join(",")})`;

const params = new URLSearchParams({
  where: whereClause,
  outFields: "ZIP_CODE,CHS_Day_0,CHS_Day_1,CHS_Day_2",
  outSR: "4326",
  f: "geojson",
});

fetch(`${baseUrl}?${params}`)
  .then((res) => res.json())
  .then((geojson) => {
    if (geojson.error) {
      console.error(`Error: ${geojson.error.message}`);
      return;
    }
    fs.writeFileSync("jurisdiction_heat_map.geojson", JSON.stringify(geojson, null, 2));
    console.log(`Saved ${geojson.features.length} ZIP code polygons`);
  })
  .catch((err) => console.error("Request failed:", err));

Load the resulting .geojson file into your dashboard's map component and apply color coding:

CalHeatScore HEX Color Code Map Legend Label
0 #DEDEDE No elevated risk
1 #FAE0C8 Low risk
2 #EA8753 Moderate risk
3 #C23A00 High risk — activation threshold
4 #700006 Severe risk — full activation

Tip for operational use: Focus your dashboard's primary indicator on the highest score across your jurisdiction today and the number of ZIP codes at or above your activation threshold. These two numbers give incident commanders a quick read on whether to escalate. Reserve the detailed per-ZIP breakdown for a secondary view that operations staff can drill into.