SNOTEL = (await FileAttachment("Data/Snow Water Equivalent.csv").csv()).map(d => {
const date = new Date(d.Snow_Date + "T00:00:00")
const month = date.getUTCMonth()
return {
...d,
Date: date,
Normalized_Date: new Date(Date.UTC(
month >= 9 ? 2000 : 2001,
month,
date.getUTCDate()
)),
Calendar_Year: d.Calendar_Year,
Snow_Year: d.Snow_Year,
HUC6: d.HUC6,
Minimum: +d.Min,
Maximum: +d.Max,
Median_91_20: +d.Median_91_20,
Average_SWE: +d.Average_SWE
}
}).sort((a, b) => a.Normalized_Date - b.Normalized_Date)Daily Snow Water Equivalent Trends
{
const current_wy = "2026"
const previous_wy = "2025"
const Current_Year = SNOTEL_Filtered.filter(d => d.Snow_Year === current_wy)
// Use previous year to get a full year's worth of records for min, max and median
const Previous_Year = SNOTEL_Filtered.filter(d => d.Snow_Year === previous_wy)
const Historical = SNOTEL_Filtered.filter(d => d.Snow_Year !== current_wy)
return Plot.plot({
//title: "Basin-Wide Average Snow Water Equivalent Trends",
width: width,
height: 500,
color: { // For some reason adding the legend is causing the multi-year lines to disappear
legend: true,
domain: ["Current Water Year", "Median 1991-2020", "Minimum", "Maximum", "Previous Years"],
range: ["black", "#00c01a", "#f54242", "#3765fa", "#aaaaaa"]
},
x: {
domain: [
new Date(Date.UTC(2000, 9, 1)), // October 1 2000
new Date(Date.UTC(2001, 8, 30)) // September 30 2001
],
tickFormat: "%b",
line: true,
label: "Month"
},
y: { nice: true, grid: true, zero: true, label: "Snow Water Equivalent (in)" },
marks: [
Plot.line(Historical, {
x: "Normalized_Date",
y: "Average_SWE",
stroke: "#aaaaaa",
z: ({ Date }) => Date.getUTCFullYear(), // z separates the lines
strokeOpacity: 0.3
}),
Plot.line(Previous_Year, {
x: "Normalized_Date",
y: "Median_91_20",
stroke: "#00c01a",
strokeWidth: 2,
tip: {
pointer: "xy",
maxRadius: 8,
format: {
x: d => d3.utcFormat("%b %d")(d)
}
}
}),
Plot.line(Previous_Year, {
x: "Normalized_Date",
y: "Minimum",
stroke: "#f54242",
strokeWidth: 3,
tip: {
pointer: "xy",
maxRadius: 8,
format: {
x: d => d3.utcFormat("%b %d")(d)
}
}
}),
Plot.line(Previous_Year, {
x: "Normalized_Date",
y: "Maximum",
stroke: "#3765fa",
strokeWidth: 3,
tip: {
pointer: "xy",
maxRadius: 8,
format: {
x: d => d3.utcFormat("%b %d")(d)
}
}
}),
Plot.line(Current_Year, {
x: "Normalized_Date",
y: "Average_SWE",
stroke: "#000000",
strokeWidth: 3,
tip: {
pointer: "xy",
maxRadius: 8,
format: {
x: d => d3.utcFormat("%b %d")(d)
}
}
})
]
})
}Current SWE Map
{
const selected = Basin_Select
return Plot.plot({
width: width,
height: 500,
marginLeft: 25,
projection: {
type: "albers",
domain: HUC6_geo // auto-fits projection to data extent
},
color: {
legend: true,
marginLeft: 25,
label: "Avg SWE (in)",
domain: [0, 30],
interpolate: d3.interpolateRdYlGn
},
marks: [
// Filled polygons colored by SWE value
Plot.geo(HUC6_geo, {
fill: d => d.properties.Average_SWE,
fillOpacity: d => selected === "Full Basin"
? 0.7
: d.properties.name === selected ? 0.9 : 0.15,
stroke: d => d.properties.name === selected ? "#000000" : "#504f4f",
strokeWidth: d => d.properties.name === selected ? 2 : 0.75
}),
Plot.geo(Basin_States, {
fillOpacity: 0,
stroke: "#0000007c",
strokeWidth: 2,
tip: false
}),
Plot.tip(
HUC6_geo.features,
Plot.pointer(Plot.centroid({
title: d => `${d.properties.name}\n${d.properties.Average_SWE} in`,
geometry: d => d.geometry
}))
),
Plot.frame()
]
})
}{
const div = html`<div style="width:100%; height:700px;"></div>`
// Allow div to render before initializing map
yield div
const map = L.map(div, {
center: [36.5, -112],
zoom: 6
})
// Basemap tile layer
const tiles = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap contributors",
maxZoom: 18
}).addTo(map)
// Color scale based on Average SWE
const color_scale = d3.scaleSequential()
.domain([ 0, 30 // Setting top end of color gradient to 30inches to extend just beyond maximum values observed in past
])
.interpolator(d3.interpolateRdYlGn)
// HUC6 layer
const average_swe_layer = L.geoJSON(HUC6_geo, {
style: feature => ({
color: "#000000",
weight: 2,
fillColor: color_scale(feature.properties.Average_SWE),
fillOpacity: 0.4
}),
onEachFeature: (feature, layer) => {
layer.bindTooltip(
`${feature.properties.name}: ${feature.properties.Average_SWE}in`,
{ sticky: true}
)
}
}).addTo(map)
const legend = L.control({ position: "bottomleft" })
legend.onAdd = function (map) {
var div = L.DomUtil.create('div', 'info legend');
// White background
div.style.cssText = `
background: white; padding: 10px 14px;
border-radius: 6px; border: 1px solid #dee2e6;
font-size: 12px; font-family: sans-serif;
`
// Use CSS to create a smooth gradient bar
div.innerHTML = '<div style="background: linear-gradient(to left, green, yellow, red); height: 20px; width: 200px;"></div>' +
'<div style="display: flex; justify-content: space-between;"><span>0in</span><span>30in</span></div>';
return div;
};
legend.addTo(map);
// Force Leaflet to recalculate map size after render
setTimeout(() => map.invalidateSize(), 100)
}Annual Peak Average Snow Water Equivalent
// Get the max average SWE for each HUC each year
yearly_max = {
const rolled = d3.rollups(
SNOTEL_Filtered,
v => d3.max(v, d => d.Average_SWE),
d => d.Snow_Year
)
return rolled.map(([year, max_avg]) => ({
Year: year,
Date: new Date(year, 1, 1),
Average_SWE: max_avg
})).sort((a, b) => a.Date - b.Date).filter(d => d.Date >= new Date("1979-01-01"))
}{
const valid = SNOTEL_Filtered.filter(d => d.Average_SWE != null && !isNaN(d.Average_SWE))
const Date_Domain = [d3.min(valid, d => d.Date), d3.max(valid, d => d.Date)]
return Plot.plot({
//title: "Basin-Wide Average Snow Water Equivalent Trends",
width: width,
height: 500,
color: {
legend: true,
domain: ["Annual Peak Avg SWE", "Peak Avg SWE 10yr-Avg", "Trend Line"],
range: ["#0049b8", "steelblue", "black"]
},
x: { label: "Water Year", domain: Date_Domain },
y: { nice: true, grid: true, zero: true, label: "Snow Water Equivalent (in)" },
marks: [
Plot.line(yearly_max, {
x: "Date",
y: "Average_SWE",
stroke: "#0049b8",
strokeOpacity: 1,
channels: {
"Year": { value: d => d.Year },
"SWE (in)": { value: d => d.Average_SWE }
},
tip: {
pointer: "xy",
maxRadius: 8,
format: {
x: false,
y: false,
y1: false,
y2: false,
fill: false,
"Year": true,
"SWE (in)": true
}
}
}),
Plot.line(yearly_max, Plot.windowY(
{ k: 10, reduce: "mean", anchor: "end" },
{ x: "Date", y: "Average_SWE", stroke: "steelblue", strokeDasharray: "4,4" }
)),
Plot.linearRegressionY(yearly_max, {x: "Date", y: "Average_SWE"})
]
})
}Days to Peak Average SWE Each Year
{
const Days2Peak = Days2Peak_Data.filter(d => d.HUC6 === Basin_Select)
const valid = SNOTEL_Filtered.filter(d => d.Average_SWE != null && !isNaN(d.Average_SWE))
const Date_Domain = [d3.min(valid, d => d.Date), d3.max(valid, d => d.Date)]
return Plot.plot({
//title: "Basin-Wide Average Snow Water Equivalent Trends",
width: width,
height: 500,
color: {
legend: true,
domain: ["Days to Peak Avg SWE", "Days to Peak 10yr-Avg", "Trend Line"],
range: ["#0049b8", "steelblue", "black"]
},
x: { label: "Water Year", domain: Date_Domain },
y: { grid: true, label: "Number of Days",
domain: [d3.min(Days2Peak, d => d.Days2Peak) * 0.9, d3.max(Days2Peak, d => d.Days2Peak) * 1.1]
},
marks: [
Plot.line(Days2Peak, {
x: "Date",
y: "Days2Peak",
stroke: "#0049b8",
strokeOpacity: 1,
channels: {
"Year": { value: d => d.Snow_Year },
"Days to Peak SWE": { value: d => d.Days2Peak }
},
tip: {
pointer: "xy",
maxRadius: 8,
format: {
x: false,
y: false,
y1: false,
y2: false,
fill: false,
"Year": true,
"Days to Peak SWE": true
}
}
}),
Plot.line(Days2Peak, Plot.windowY(
{ k: 10, reduce: "mean", anchor: "end" },
{ x: "Date", y: "Days2Peak", stroke: "steelblue", strokeDasharray: "4,4" }
)),
Plot.linearRegressionY(Days2Peak, {x: "Date", y: "Days2Peak"})
]
})
}