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));
{
"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:
- Open ArcGIS Dashboards and create or edit a dashboard.
- 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 - 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') - Add an Indicator element that shows the highest score in your jurisdiction using a statistics expression on
CHS_Day_0. - Add a List element to display ZIP codes currently at score 3 or above.
Use Power BI's web data connector:
- Open Power BI Desktop and click Get Data > Web.
- Enter the full query URL (with your ZIP codes and
f=json). - Power BI will parse the JSON. Expand the
featuresandattributescolumns. - Convert
CHS_Day_0throughCHS_Day_6from text to whole number. - Create a conditional formatting rule on your table or map visual using the 0–4 score range.
- Set up Scheduled Refresh in the Power BI Service to pull fresh data daily.
Use the Grafana JSON or Infinity data source plugin:
- Install the Infinity data source plugin in Grafana.
- Add a new data source pointing to the CalHeatScore query URL.
- Parse the JSON response using JSONPath:
$.features[*].attributes. - Create a Stat panel showing the max score across your jurisdiction.
- Create a Table panel listing all ZIP codes with their 7-day forecast.
- Set up a Grafana Alert rule: trigger when any
CHS_Day_0value 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.