Refine climate metrics and data pipeline
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user