Refine climate metrics and data pipeline

This commit is contained in:
2026-07-24 22:55:14 -04:00
parent 016f7386d8
commit 4e2d878e5b
32 changed files with 3557 additions and 3347 deletions
+59 -24
View File
@@ -1,6 +1,6 @@
"use strict";
const APP_ASSET_VERSION = "precip-months-cloudiness-20260601";
const APP_ASSET_VERSION = "solar-resource-clear-sky-ghi-reduction-20260715";
const US_COUNTIES_GEOJSON_URL = `data/geojson-counties-fips.json?v=${APP_ASSET_VERSION}`;
const CLIMATE_DATA_CSV_URL = `data/climate-data.csv?v=${APP_ASSET_VERSION}`;
const NO_DATA_FILL_COLOR = "#d9dde8";
@@ -162,7 +162,6 @@ const METRICS = {
min: 14,
max: 33,
boundsStep: 0.1,
sliderStep: 0.1,
unit: "F",
palette: ["#f2f7f5", "#bcdccf", "#83bba6", "#4b927d", "#1f5f54"],
source: {
@@ -247,12 +246,11 @@ const METRICS = {
}
},
absoluteExtremeDays: {
label: "Absolute Extreme Days / Year",
label: "Annual Extreme Temperature Days",
type: "numeric",
min: 0,
max: 150,
boundsStep: 1,
sliderStep: 1,
unit: "days",
palette: ["#fff9e6", "#ffe49a", "#ffd067", "#e7af20", "#b88409"],
source: {
@@ -267,21 +265,24 @@ const METRICS = {
}
},
humidHeatDays: {
label: "Heat Index",
label: "Annual 90 F+ Heat Index Days",
type: "numeric",
min: 0,
max: 180,
boundsStep: 1,
sliderStep: 0.1,
unit: "days",
palette: ["#fff7ec", "#fdd49e", "#fdbb84", "#ef6548", "#b30000"],
source: {
description:
"Heat Index is estimated from NOAA nClimGrid-Daily county Tmax and gridMET daily minimum relative humidity (rmin), 1991-2020. This metric is the average annual count of days where estimated daily Heat Index is at least 90 F; county-vintage joins and proxies are documented in the humidHeatSourceFips and humidHeatFipsAdjustment fields.",
"Average annual count of days with an estimated Heat Index of at least 90 F, the lower bound of the NWS Extreme Caution category. The estimate pairs NOAA nClimGrid-Daily county Tmax with gridMET daily minimum relative humidity (rmin), 1991-2020; it is a daily-extrema proxy rather than an observed hourly maximum. County-vintage joins and proxies are documented in the humidHeatSourceFips and humidHeatFipsAdjustment fields.",
links: [
{
label: "NOAA National Weather Service Heat Index",
url: "https://www.weather.gov/safety/heat-index"
label: "NWS Heat Index Categories",
url: "https://www.weather.gov/ama/heatindex"
},
{
label: "NWS Heat Index Equation",
url: "https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml"
},
{
label: "NOAA nClimGrid-Daily",
@@ -294,41 +295,39 @@ const METRICS = {
]
}
},
avgSolarGhiKwhM2Day: {
label: "Avg Daily Solar Irradiance (GHI)",
meanDailyGlobalHorizontalRadiationKwhM2Day: {
label: "Mean Daily Global Horizontal Radiation (GHI)",
type: "numeric",
min: 2.5,
max: 6.5,
boundsStep: 0.1,
sliderStep: 0.1,
unit: "kWh/m2/day",
palette: ["#fff8d8", "#f8df83", "#e8b94e", "#c9892b", "#985f1b"],
source: {
description:
"NREL National Solar Radiation Database TMY PSM v4. County polygon archive summaries use area-weighted GHI; representative-point values are kept as fallback where polygon summaries are unavailable.",
"National Renewable Energy Laboratory (NREL) National Solar Radiation Database (NSRDB) TMY PSM v4. County polygon archive summaries use area-weighted GHI; representative-point values are kept as fallback where polygon summaries are unavailable.",
links: [
{
label: "NREL NSRDB",
label: "NREL National Solar Radiation Database",
url: "https://nsrdb.nrel.gov/"
}
]
}
},
cloudinessIndexPct: {
label: "Cloudiness Index",
clearSkyGhiReductionIndex: {
label: "Clear-Sky GHI Reduction Index",
type: "numeric",
min: 0.05,
max: 0.6,
boundsStep: 0.01,
sliderStep: 0.01,
unit: "ratio",
palette: ["#fff7bc", "#d9f0d3", "#a6cee3", "#6baed6", "#4b5563"],
source: {
description:
"NREL National Solar Radiation Database TMY PSM v4 representative-point summary. The index is 1 minus the daylight average of observed GHI divided by modeled Clearsky GHI, clamped to a 0-1 range; higher values indicate more sunlight reduction relative to clear-sky conditions.",
"National Renewable Energy Laboratory (NREL) National Solar Radiation Database (NSRDB) TMY PSM v4. The index is 1 minus the daylight average of observed GHI divided by modeled clear-sky GHI, clamped to a 0-1 range; higher values indicate more solar radiation reduction relative to clear-sky conditions.",
links: [
{
label: "NREL NSRDB",
label: "NREL National Solar Radiation Database",
url: "https://nsrdb.nrel.gov/"
}
]
@@ -340,7 +339,6 @@ const METRICS = {
min: 4,
max: 20,
boundsStep: 0.1,
sliderStep: 0.1,
unit: "g/kg",
palette: ["#f6f0d6", "#d7dfad", "#9cc895", "#58a9a3", "#2e6f8e"],
source: {
@@ -380,9 +378,9 @@ const METRIC_GROUPS = [
metricKeys: ["annualPrecipIn", "seasonalityIndex", "wettestPrecipMonth", "driestPrecipMonth", "avgSummerSpecificHumidityGKg"]
},
{
key: "solarExposure",
label: "Solar Exposure",
metricKeys: ["avgSolarGhiKwhM2Day", "cloudinessIndexPct"]
key: "solarResource",
label: "Solar Resource",
metricKeys: ["meanDailyGlobalHorizontalRadiationKwhM2Day", "clearSkyGhiReductionIndex"]
}
].map((group) => ({
...group,
@@ -965,6 +963,17 @@ function roundMetricBound(value, step, direction) {
return Number(rounded.toFixed(decimalPlaces));
}
// Choose a power-of-ten slider step that keeps three significant digits of range precision.
function getMetricSliderStep(metric) {
const largestMagnitude = Math.max(Math.abs(metric.min), Math.abs(metric.max));
if (!Number.isFinite(largestMagnitude) || largestMagnitude === 0) {
return 1;
}
const magnitudeExponent = Math.floor(Math.log10(largestMagnitude));
return 10 ** (magnitudeExponent - 2);
}
// Collect the category values that are present in loaded county records for a metric.
function getUniqueCategoryValuesInData(metricKey) {
const valueSet = new Set();
@@ -1433,6 +1442,30 @@ function updateRangeLabels() {
maxValue.textContent = formatMetricValue(currentMetricKey, max);
}
// Move a hovered range input by one configured step without scrolling the page.
function adjustRangeOnWheel(event) {
if (event.deltaY === 0 || isCurrentMetricCategorical()) {
return;
}
const input = event.currentTarget;
const previousValue = input.value;
input.focus({ preventScroll: true });
if (event.deltaY < 0) {
input.stepUp();
} else {
input.stepDown();
}
if (input.value === previousValue) {
return;
}
event.preventDefault();
input.dispatchEvent(new Event("input", { bubbles: true }));
}
// Zoom the map to a county without animation.
function focusCounty(layer) {
map.stop();
@@ -1636,7 +1669,7 @@ function configureRangeControls() {
function configureRangeInput(input, metric, value) {
input.min = String(metric.min);
input.max = String(metric.max);
input.step = String(metric.sliderStep ?? 1);
input.step = String(getMetricSliderStep(metric));
input.value = String(value);
}
@@ -1845,6 +1878,8 @@ function configureDomEventListeners() {
metricSelect.addEventListener("change", onMetricChange);
minRange.addEventListener("input", restyleAllCounties);
maxRange.addEventListener("input", restyleAllCounties);
minRange.addEventListener("wheel", adjustRangeOnWheel, { passive: false });
maxRange.addEventListener("wheel", adjustRangeOnWheel, { passive: false });
resetViewButton.addEventListener("click", resetMapView);
clearSelectionButton.addEventListener("click", clearSelection);