Colorado River Atlas
  • Home
  • Reservoirs
  • State Usage
  • Snow Pack

Overview and Trends at Major Reservoirs

render_date = d3.max(Res_Elv, d => d.Date).toLocaleDateString("en-US", {
  month: "long",
  day: "numeric",
  year: "numeric"
})
html`
<div style="
  background: #5cabfa2f;
  border-radius: 6px;
  padding: 6px 12px;
  font-size: 12px;
  color: #6c757d;
  margin-bottom: 16px;
  text-align: center;
">
  Data current as of <b>${render_date}</b>
</div>
`
// Data
Res_Elv = (await FileAttachment("Data/Reservoir_Elevation.csv").csv()).map(d => ({
  ...d,
  Date: new Date(d.Date + "T00:00:00"), // adjusts for JS messing with time zone
  Elevation: parseFloat((+d.Elevation).toFixed(2)),
  Elv_1yr_Ago: parseFloat((+d.Elv_1yr_Ago).toFixed(2)),
  Elv_Day_Avg_10yr: parseFloat((+d.Elv_Day_Avg_10yr).toFixed(2)),
  Elv_Day_Avg_30yr: parseFloat((+d.Elv_Day_Avg_30yr).toFixed(2))
})).sort((a, b) => a.Date - b.Date)
Res_Stor = (await FileAttachment("Data/Reservoir_Storage.csv").csv()).map(d =>({
  ...d,
  Date: new Date(d.Date + "T00:00:00"),
  Storage_MAF: parseFloat((+d.Storage_MAF).toFixed(2)),
  Stor_1yr_Ago: parseFloat((+d.StorMAF_1yr_Ago).toFixed(2)),
  Stor_Day_Avg_10yr: parseFloat((+d.Stor_Day_Avg_10yr).toFixed(2)),
  Stor_Day_Avg_30yr: parseFloat((+d.Stor_Day_Avg_30yr).toFixed(2)),
  Perc_Full: parseFloat((+d.Percent_Full_MAF).toFixed(2))
})).sort((a, b) => a.Date - b.Date)
viewof reservoir_choice = Inputs.select(
  ["Total", "Lake Mead", "Lake Powell", "Flaming Gorge", "Lake Mohave", "Navajo Reservoir", "Strawberry Reservoir", "Blue Mesa Reservoir", "Lake Havasu", "Granby Reservoir"],
  { label: "Reservoir:", value: "Lake Mead" }
)
  • Elevation
  • Storage
filtered_elv = reservoir_choice === "Total"
  ? []
  : Res_Elv.filter(d => d.Reservoir === reservoir_choice)
// Summary stats from filtered data

elv_stats = {
  const target      = filtered_elv.find(d => 
    d.Date.toISOString().slice(0, 10) === "2000-02-06"
  ) 
  const elv_2000    = target?.Elevation ?? "N/A"
  const Date = filtered_elv[filtered_elv.length - 1]?.Date
    ? filtered_elv[filtered_elv.length - 1].Date.toLocaleDateString("en-US", { month: "long", day: "numeric" }): "N/A"
  const last        = filtered_elv[filtered_elv.length - 1]
  const current     = last?.Elevation          ?? "N/A"
  const dif_2000    = current !== "N/A" && elv_2000 !== "N/A"
    ? parseFloat((current - elv_2000).toFixed(2)) : "N/A"
  const perc_2000   = dif_2000 !== "N/A" && elv_2000 !== "N/A"
    ? parseFloat(((dif_2000 / elv_2000)*100).toFixed(2)) : "N/A"
  const dif_2000_color   = dif_2000 !== "N/A"
    ? (dif_2000 >= 0 ? "#198754" : "#dc3545") : "#6c757d"
  const prior_yr    = last?.Elv_1yr_Ago        ?? "N/A"
  const avg_10yr    = last?.Elv_Day_Avg_10yr   ?? "N/A"
  const avg_30yr    = last?.Elv_Day_Avg_30yr   ?? "N/A"
  const dif_1yr     = current !== "N/A" && prior_yr !== "N/A"
    ? parseFloat((current - prior_yr).toFixed(2)) : "N/A"
  const dif_1yr_color   = dif_1yr !== "N/A"
    ? (dif_1yr >= 0 ? "#198754" : "#dc3545") : "#6c757d"
  const perc_1yr    = dif_1yr !== "N/A" && prior_yr !== "N/A"
    ? parseFloat(((dif_1yr / prior_yr)*100).toFixed(2)) : "N/A"
  const dif_10yr    = current !== "N/A" && avg_10yr !== "N/A"
    ? parseFloat((current - avg_10yr).toFixed(2)) : "N/A"
  const dif_10yr_color   = dif_10yr !== "N/A"
    ? (dif_10yr >= 0 ? "#198754" : "#dc3545") : "#6c757d"
  const perc_10yr   = dif_10yr !== "N/A" && avg_10yr !== "N/A"
    ? parseFloat(((dif_10yr / avg_10yr)*100).toFixed(2)) : "N/A"
  const dif_30yr    = current !== "N/A" && avg_30yr !== "N/A"
    ? parseFloat((current - avg_30yr).toFixed(2)) : "N/A"
  const dif_30yr_color   = dif_30yr !== "N/A"
    ? (dif_30yr >= 0 ? "#198754" : "#dc3545") : "#6c757d"
  const perc_30yr   = dif_30yr !== "N/A" && avg_30yr !== "N/A"
    ? parseFloat(((dif_30yr / avg_30yr)*100).toFixed(2)) : "N/A"
  
  return { perc_2000, dif_2000, dif_2000_color, Date, current, prior_yr, avg_10yr, avg_30yr, dif_1yr, dif_1yr_color, perc_1yr, dif_10yr, dif_10yr_color, perc_10yr, dif_30yr, dif_30yr_color, perc_30yr }
}
// Summary stats cards
html`
<div style="
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
  margin-bottom: 24px;
">

  <div style="
    background: #f8f9fa;
    border: 1px solid #dee2e6;
    border-left: 4px solid #0d6efd;
    border-radius: 6px;
    padding: 16px;
  ">
    <div style="font-size: 12px; color: #6c757d; text-transform: uppercase; letter-spacing: 0.05em;">Current Elevation</div>
    <div style="font-size: 28px; font-weight: 700; margin-top: 4px;">${elv_stats.current} <span style="font-size:14px; font-weight:400;">ft</span></div>
    <div style="font-size: 12px; color: ${elv_stats.dif_2000_color}; margin-top: 4px;">${elv_stats.dif_2000}ft | ${elv_stats.perc_2000}% Change Since 2000</div>
  </div>

  <div style="
    background: #f8f9fa;
    border: 1px solid #dee2e6;
    border-left: 4px solid #198754;
    border-radius: 6px;
    padding: 16px;
  ">
    <div style="font-size: 12px; color: #6c757d; text-transform: uppercase; letter-spacing: 0.05em;">Elevation ${elv_stats.Date} Last Year</div>
    <div style="font-size: 28px; font-weight: 700; margin-top: 4px;">${elv_stats.prior_yr} <span style="font-size:14px; font-weight:400;">ft</span></div>
    <div style="font-size: 12px; color: ${elv_stats.dif_1yr_color}; margin-top: 4px;">${elv_stats.dif_1yr}ft | ${elv_stats.perc_1yr}% Change</div>
  </div>

  <div style="
    background: #f8f9fa;
    border: 1px solid #dee2e6;
    border-left: 4px solid #ffc107;
    border-radius: 6px;
    padding: 16px;
  ">
    <div style="font-size: 12px; color: #6c757d; text-transform: uppercase; letter-spacing: 0.05em;">${elv_stats.Date} Average Past 10yrs</div>
    <div style="font-size: 28px; font-weight: 700; margin-top: 4px;">${elv_stats.avg_10yr} <span style="font-size:14px; font-weight:400;">ft</span></div>
    <div style="font-size: 12px; color: ${elv_stats.dif_10yr_color}; margin-top: 4px;">${elv_stats.dif_10yr}ft | ${elv_stats.perc_10yr}% Change</div>
  </div>

  <div style="
    background: #f8f9fa;
    border: 1px solid #dee2e6;
    border-left: 4px solid #dc3545;
    border-radius: 6px;
    padding: 16px;
  ">
    <div style="font-size: 12px; color: #6c757d; text-transform: uppercase; letter-spacing: 0.05em;">${elv_stats.Date} Average Past 30yrs</div>
    <div style="font-size: 28px; font-weight: 700; margin-top: 4px;">${elv_stats.avg_30yr} <span style="font-size:14px; font-weight:400;">ft</span></div>
    <div style="font-size: 12px; color: ${elv_stats.dif_30yr_color}; margin-top: 4px;">${elv_stats.dif_30yr}ft | ${elv_stats.perc_30yr}% Change</div>
  </div>

</div>
`
{
  // Show message if Total is selected
  if (reservoir_choice === "Total") {
    return html`
      <div style="
        width: 100%;
        height: 500px;
        display: flex;
        align-items: center;
        justify-content: center;
        background: #f8f9fa;
        border-radius: 6px;
        font-family: sans-serif;
        color: #6c757d;
      ">
        <div style="text-align: center;">
          <div style="font-size: 32px; margin-bottom: 12px;">⚠</div>
          <div style="font-size: 16px; font-weight: 600; margin-bottom: 6px;">
            Elevation Total Not Available
          </div>
          <div style="font-size: 13px;">
            Select an individual reservoir to view elevation records.
          </div>
        </div>
      </div>
    `
  }


  // Using date slider to dynamically adjust y axis range
  const elv_slider_filtered = filtered_elv.filter(d => 
  d.Date >= DateSlider[0] && d.Date <= DateSlider[1]
  )
  const elv_min = d3.min(elv_slider_filtered, d => d.Elevation)
  const elv_max = d3.max(elv_slider_filtered, d => d.Elevation)
  const padding = (elv_max - elv_min) * 0.05

  // Y axis domain
  const elv_y_domain = reservoir_choice === "Lake Powell" ? [3360, elv_max + padding] 
  : reservoir_choice === "Lake Mead" ? [890, elv_max + padding]
  : [elv_min - padding, elv_max + padding]

  // Define reservoir-specific horizontal lines
  const refLines = reservoir_choice === "Lake Mead" ? [
    { y: 1075, label: "Tier 1 Shortage - 1075ft", color: "orange" },
    { y: 1050, label: "Tier 2 Shortage - 1050ft", color: "red" },
    { y: 1025, label: "Tier 3 Shortage - 1025ft", color: "darkred" },
    { y: 895,  label: "Dead Pool - 895ft", color: "black" }
  ] : reservoir_choice === "Lake Powell" ? [
    { y: 3490, label: "Minimum Power Pool - 3490ft", color: "orange" },
    { y: 3370, label: "Dead Pool - 3370ft", color: "black" }
  ] : []  // empty array for "Total" or reservoirs with no reference lines

  // Build marks array dynamically
  const marks = [
    Plot.lineY(filtered_elv, { x: "Date", y: "Elevation", stroke: "blue" }),
    //Plot.crosshair(filtered_elv, {x: "Date", y: "Elevation", color: "red", opacity: 0.5}),

    // Add a ruleY and text annotation for each reference line
    ...refLines.map(ref => Plot.ruleY([ref.y], {
      stroke: ref.color,
      strokeWidth: 1.5,
      strokeDasharray: "4,4"
    })),
    ...refLines.map(ref => Plot.text([ref.y], {
      y: ref.y,
      text: [ref.label],
      frameAnchor: "middle",
      dy: -8,
      fill: ref.color,
      fontSize: 11
    })),
    Plot.tip(filtered_elv, Plot.pointerX({ x: "Date", y: "Elevation" }))
  ]

  const Elevation_Chart = Plot.plot({
    title: "Reservoir Elevation",
    width: width,
    height: 500,
    y: { grid: true, label: "Reservoir Elevation (ft)", 
         domain: elv_y_domain },
    x: { label: "Date", domain: DateSlider },
    marks: marks
  })

  return Elevation_Chart
}
{
  const Elevation_Chart_Reference = Plot.plot({
    width: width,
    height: 100,
    marks: [
      Plot.lineY(filtered_elv, { x: "Date", y: "Elevation", stroke: "blue" }),
      Plot.rectX([{}], {
        x1: new Date(DateSlider[0]),   // left edge
        x2: new Date(DateSlider[1]),   // right edge
        fill: "#9c9c9c",
        fillOpacity: 0.15
      })
    ]
  })

  return Elevation_Chart_Reference
}
import { timeRange } from "@jwolondon/time-range-input"
min_date = filtered_elv.length > 0
  ? d3.min(filtered_elv, d => d.Date)
  : d3.min(Res_Elv, d => d.Date)   // fall back to full dataset

max_date = filtered_elv.length > 0
  ? d3.max(filtered_elv, d => d.Date)
  : d3.max(Res_Elv, d => d.Date)
viewof DateSlider = {
  // Convert dates to timestamps for numeric slider math
  const min_ts = min_date.getTime()
  const max_ts = max_date.getTime()

  // Initial values
  let start_ts = min_ts
  let end_ts   = max_ts

  // Helper to format timestamp as display date
  const fmt = ts => new Date(ts).toLocaleDateString("en-US", {
    month: "short", day: "numeric", year: "numeric", timeZone: "UTC"
  })

  // Helper to convert timestamp to percentage position
  const to_pct = ts => ((ts - min_ts) / (max_ts - min_ts)) * 100

  const container = html`
    <div style="
      font-family: sans-serif;
      font-size: 12px;
      padding: 8px 0 4px 0;
      width: 100%;
      user-select: none;
    ">
      <!-- Date labels -->
      <div style="display: flex; justify-content: space-between; margin-bottom: 6px; color: #6c757d;">
        <span id="start-label">${fmt(start_ts)}</span>
        <span id="end-label">${fmt(end_ts)}</span>
      </div>

      <!-- Slider track -->
      <div id="track" style="
        position: relative;
        height: 6px;
        background: #dee2e6;
        border-radius: 3px;
        margin: 10px 20px 10px 40px;
        cursor: pointer;
      ">
        <!-- Selected range highlight -->
        <div id="range-fill" style="
          position: absolute;
          height: 100%;
          background: steelblue;
          border-radius: 3px;
          left: 0%;
          right: 0%;
        "></div>

        <!-- Start handle -->
        <div id="handle-start" style="
          position: absolute;
          width: 16px;
          height: 16px;
          background: white;
          border: 2px solid steelblue;
          border-radius: 50%;
          top: 50%;
          transform: translate(-50%, -50%);
          cursor: ew-resize;
          left: 0%;
          box-shadow: 0 1px 4px rgba(0,0,0,0.2);
          z-index: 2;
        "></div>

        <!-- End handle -->
        <div id="handle-end" style="
          position: absolute;
          width: 16px;
          height: 16px;
          background: white;
          border: 2px solid steelblue;
          border-radius: 50%;
          top: 50%;
          transform: translate(-50%, -50%);
          cursor: ew-resize;
          left: 100%;
          box-shadow: 0 1px 4px rgba(0,0,0,0.2);
          z-index: 2;
        "></div>
      </div>

      <!-- Min/Max labels -->
      <div style="display: flex; justify-content: space-between; color: #aaaaaa; font-size: 11px;">
        <span>${fmt(min_ts)}</span>
        <span>${fmt(max_ts)}</span>
      </div>
    </div>
  `

  const track        = container.querySelector("#track")
  const handle_start = container.querySelector("#handle-start")
  const handle_end   = container.querySelector("#handle-end")
  const range_fill   = container.querySelector("#range-fill")
  const start_label  = container.querySelector("#start-label")
  const end_label    = container.querySelector("#end-label")

  // Update visual positions and fill
  const update_visuals = () => {
    const s_pct = to_pct(start_ts)
    const e_pct = to_pct(end_ts)
    handle_start.style.left = s_pct + "%"
    handle_end.style.left   = e_pct + "%"
    range_fill.style.left   = s_pct + "%"
    range_fill.style.right  = (100 - e_pct) + "%"
    start_label.textContent = fmt(start_ts)
    end_label.textContent   = fmt(end_ts)
  }

  // Convert mouse x position to timestamp
  const mouse_to_ts = (e) => {
    const rect = track.getBoundingClientRect()
    const pct  = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
    return min_ts + pct * (max_ts - min_ts)
  }

  // Snap timestamp to nearest day
  const snap_to_day = ts => {
    const d = new Date(ts)
    d.setUTCHours(0, 0, 0, 0)
    return d.getTime()
  }

  // Drag logic
  const make_draggable = (handle, is_start) => {
    handle.addEventListener("mousedown", e => {
      e.preventDefault()

      const on_move = e => {
        const ts = snap_to_day(mouse_to_ts(e))
        if (is_start) {
          start_ts = Math.min(ts, end_ts - 86400000)   // keep at least 1 day gap
        } else {
          end_ts = Math.max(ts, start_ts + 86400000)
        }
        update_visuals()
        // Dispatch input event to notify OJS of value change
        container.value = [new Date(start_ts), new Date(end_ts)]
        container.dispatchEvent(new Event("input"))
      }

      const on_up = () => {
        window.removeEventListener("mousemove", on_move)
        window.removeEventListener("mouseup", on_up)
      }

      window.addEventListener("mousemove", on_move)
      window.addEventListener("mouseup", on_up)
    })
  }

  make_draggable(handle_start, true)
  make_draggable(handle_end,   false)

  // Set initial value
  update_visuals()
  container.value = [new Date(start_ts), new Date(end_ts)]

  return container
}
{
  // Return nothing if Total is selected
  if (reservoir_choice === "Total") return html`<div></div>`

  const This_Year = filtered_elv
    .filter(d => d.Date >= new Date("2026-01-01T00:00:00"))
    .map(d => ({
      ...d,
      Normalized_Date: new Date(2000, d.Date.getMonth(), d.Date.getDate()),
      Month_Day: d.Date.toLocaleString("en-US", { month: "short", day: "2-digit", timeZone: "UTC" })
    })).sort((a, b) => a.Normalized_Date - b.Normalized_Date)

  const Previous_Year = filtered_elv
    .filter(d => d.Date >= new Date("2025-01-01T00:00:00") && 
                 d.Date <= new Date("2025-12-31T00:00:00"))
    .map(d => ({
      ...d,
      Normalized_Date: new Date(2000, d.Date.getMonth(), d.Date.getDate()),
      Month_Day: d.Date.toLocaleString("en-US", { month: "short", day: "2-digit", timeZone: "UTC" })
    })).sort((a, b) => a.Normalized_Date - b.Normalized_Date)

  const marks = [
  Plot.lineY(This_Year, { 
    x: "Normalized_Date", 
    y: "Elevation", 
    stroke: "blue",
    channels: {
        "Day":      { value: d => d.Month_Day },
        "Elevation (ft)": { value: d => d.Elevation }
      },
    tip: {
      pointer: "xy",
      maxRadius: 8,
      format: {
        x: false,
        y: false,
        y1: false,
        y2: false,
        fill: false,
        "Day": true,
        "Elevation (ft)": true
      }
    }
   }),
  Plot.lineY(Previous_Year, { 
    x: "Normalized_Date", 
    y: "Elevation", 
    stroke: "green",
    channels: {
        "Day":      { value: d => d.Month_Day },
        "Elevation (ft)": { value: d => d.Elv_1yr_Ago }
      },
    tip: {
      pointer: "xy",
      maxRadius: 8,
      format: {
        x: false,
        y: false,
        y1: false,
        y2: false,
        fill: false,
        "Day": true,
        "Elevation (ft)": true
      }
    }
    }),
  Plot.lineY(Previous_Year, { 
    x: "Normalized_Date", 
    y: "Elv_Day_Avg_10yr", 
    stroke: "#ffc107",
    channels: {
        "Day":      { value: d => d.Month_Day },
        "Elevation (ft)": { value: d => d.Elv_Day_Avg_10yr }
      },
    tip: {
      pointer: "xy",
      maxRadius: 8,
      format: {
        x: false,
        y: false,
        y1: false,
        y2: false,
        fill: false,
        "Day": true,
        "Elevation (ft)": true
      }
    } 
    }),
  Plot.lineY(Previous_Year, { 
    x: "Normalized_Date", 
    y: "Elv_Day_Avg_30yr", 
    stroke: "red",
    channels: {
        "Day":      { value: d => d.Month_Day },
        "Elevation (ft)": { value: d => d.Elv_Day_Avg_30yr }
      },
    tip: {
      pointer: "xy",
      maxRadius: 8,
      format: {
        x: false,
        y: false,
        y1: false,
        y2: false,
        fill: false,
        "Day": true,
        "Elevation (ft)": true
      }
    }  
    }),
  ]

  return Plot.plot({
    title: "Reservoir Elevation Annual Trends",
    width: width,
    height: 500,
    color: {
      legend: true,
      domain: ["This Year", "Last Year", "10yr Average", "30yr Average"],
      range:  ["blue", "green", "#ffc107", "red"]
    },
    y: { grid: true, label: "Reservoir Elevation (ft)" },
    x: { 
      label: "Date",
      ticks: d3.utcMonth.every(1),
      tickFormat: "%b" },
    marks: marks
  })
}
filtered_stor = Res_Stor.filter(d => d.Reservoir === reservoir_choice)
// Summary stats from filtered data

stor_stats = {
  const target      = filtered_stor.find(d => 
    d.Date.toISOString().slice(0, 10) === "2000-02-06"
  ) 
  const stor_2000   = target?.Storage_MAF ?? "N/A"
  const Date = filtered_stor[filtered_stor.length - 1]?.Date
    ? filtered_stor[filtered_stor.length - 1].Date.toLocaleDateString("en-US", { month: "long", day: "numeric" }): "N/A"
  const last        = filtered_stor[filtered_stor.length - 1]
  const current     = last?.Storage_MAF          ?? "N/A"
  const perc_full   = last?.Perc_Full            ?? "N/A"
  const dif_2000    = current !== "N/A" && stor_2000 !== "N/A"
    ? parseFloat((current - stor_2000).toFixed(2)) : "N/A"
  const perc_2000   = dif_2000 !== "N/A" && stor_2000 !== "N/A"
    ? parseFloat(((dif_2000 / stor_2000)*100).toFixed(2)) : "N/A"
  const dif_2000_color   = dif_2000 !== "N/A"
    ? (dif_2000 >= 0 ? "#198754" : "#dc3545") : "#6c757d"
  const prior_yr    = last?.Stor_1yr_Ago        ?? "N/A"
  const avg_10yr    = last?.Stor_Day_Avg_10yr   ?? "N/A"
  const avg_30yr    = last?.Stor_Day_Avg_30yr   ?? "N/A"
  const dif_1yr     = current !== "N/A" && prior_yr !== "N/A"
    ? parseFloat((current - prior_yr).toFixed(2)) : "N/A"
  const dif_1yr_color   = dif_1yr !== "N/A"
    ? (dif_1yr >= 0 ? "#198754" : "#dc3545") : "#6c757d"
  const perc_1yr    = dif_1yr !== "N/A" && prior_yr !== "N/A"
    ? parseFloat(((dif_1yr / prior_yr)*100).toFixed(2)) : "N/A"
  const dif_10yr    = current !== "N/A" && avg_10yr !== "N/A"
    ? parseFloat((current - avg_10yr).toFixed(2)) : "N/A"
  const dif_10yr_color   = dif_10yr !== "N/A"
    ? (dif_10yr >= 0 ? "#198754" : "#dc3545") : "#6c757d"
  const perc_10yr   = dif_10yr !== "N/A" && avg_10yr !== "N/A"
    ? parseFloat(((dif_10yr / avg_10yr)*100).toFixed(2)) : "N/A"
  const dif_30yr    = current !== "N/A" && avg_30yr !== "N/A"
    ? parseFloat((current - avg_30yr).toFixed(2)) : "N/A"
  const dif_30yr_color   = dif_30yr !== "N/A"
    ? (dif_30yr >= 0 ? "#198754" : "#dc3545") : "#6c757d"
  const perc_30yr   = dif_30yr !== "N/A" && avg_30yr !== "N/A"
    ? parseFloat(((dif_30yr / avg_30yr)*100).toFixed(2)) : "N/A"
  
  return { perc_2000, perc_full, dif_2000, dif_2000_color, Date, current, prior_yr, avg_10yr, avg_30yr, dif_1yr, dif_1yr_color, perc_1yr, dif_10yr, dif_10yr_color, perc_10yr, dif_30yr, dif_30yr_color, perc_30yr }
}
// Summary stats cards
html`
<div style="
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 16px;
  margin-bottom: 24px;
">

  <div style="
    background: #f8f9fa;
    border: 1px solid #dee2e6;
    border-left: 4px solid #0d6efd;
    border-radius: 6px;
    padding: 16px;
  ">
    <div style="font-size: 12px; color: #6c757d; text-transform: uppercase; letter-spacing: 0.05em;">Current Storage</div>
    <div style="font-size: 28px; font-weight: 700; margin-top: 4px;">${stor_stats.current} <span style="font-size:14px; font-weight:400;">MAF</span> <span style="font-size:28px; font-weight: 700;">  | ${stor_stats.perc_full}%</span></div>
    <div style="font-size: 12px; color: ${stor_stats.dif_2000_color}; margin-top: 4px;">${stor_stats.dif_2000}MAF | ${stor_stats.perc_2000}% Change Since 2000</div>
  </div>

  <div style="
    background: #f8f9fa;
    border: 1px solid #dee2e6;
    border-left: 4px solid #198754;
    border-radius: 6px;
    padding: 16px;
  ">
    <div style="font-size: 12px; color: #6c757d; text-transform: uppercase; letter-spacing: 0.05em;">Elevation ${stor_stats.Date} Last Year</div>
    <div style="font-size: 28px; font-weight: 700; margin-top: 4px;">${stor_stats.prior_yr} <span style="font-size:14px; font-weight:400;">MAF</span></div>
    <div style="font-size: 12px; color: ${stor_stats.dif_1yr_color}; margin-top: 4px;">${stor_stats.dif_1yr}MAF | ${stor_stats.perc_1yr}% Change</div>
  </div>

  <div style="
    background: #f8f9fa;
    border: 1px solid #dee2e6;
    border-left: 4px solid #ffc107;
    border-radius: 6px;
    padding: 16px;
  ">
    <div style="font-size: 12px; color: #6c757d; text-transform: uppercase; letter-spacing: 0.05em;">${stor_stats.Date} Average Past 10yrs</div>
    <div style="font-size: 28px; font-weight: 700; margin-top: 4px;">${stor_stats.avg_10yr} <span style="font-size:14px; font-weight:400;">MAF</span></div>
    <div style="font-size: 12px; color: ${stor_stats.dif_10yr_color}; margin-top: 4px;">${stor_stats.dif_10yr}MAF | ${stor_stats.perc_10yr}% Change</div>
  </div>

  <div style="
    background: #f8f9fa;
    border: 1px solid #dee2e6;
    border-left: 4px solid #dc3545;
    border-radius: 6px;
    padding: 16px;
  ">
    <div style="font-size: 12px; color: #6c757d; text-transform: uppercase; letter-spacing: 0.05em;">${stor_stats.Date} Average Past 30yrs</div>
    <div style="font-size: 28px; font-weight: 700; margin-top: 4px;">${stor_stats.avg_30yr} <span style="font-size:14px; font-weight:400;">MAF</span></div>
    <div style="font-size: 12px; color: ${stor_stats.dif_30yr_color}; margin-top: 4px;">${stor_stats.dif_30yr}MAF | ${stor_stats.perc_30yr}% Change</div>
  </div>

</div>
`
{
  // Using date slider to dynamically adjust y axis range
  const stor_slider_filtered = filtered_stor.filter(d => 
  d.Date >= StorDateSlider[0] && d.Date <= StorDateSlider[1]
  )
  const stor_min = d3.min(stor_slider_filtered, d => d.Storage_MAF)
  const stor_max = d3.max(stor_slider_filtered, d => d.Storage_MAF)
  const stor_padding = (stor_max - stor_min) * 0.05

  // Y axis domain
  const stor_y_domain = [0, stor_max + stor_padding]

  // Define reservoir-specific horizontal lines
  const Stor_refLines = reservoir_choice === "Lake Mead" ? [
    { y: 9.601, label: "Tier 1 Shortage - 9.6maf", color: "orange" },
    { y: 7.683, label: "Tier 2 Shortage - 7.68maf", color: "red" },
    { y: 5.981, label: "Tier 3 Shortage - 5.98maf", color: "darkred" },
    { y: 0,  label: "Dead Pool - 0maf active storage", color: "black" }
  ] : reservoir_choice === "Lake Powell" ? [
    { y: 4, label: "Minimum Power Pool - ~4maf", color: "orange" },
    { y: 0, label: "Dead Pool - 0maf active storage", color: "black" }
  ] : []  // empty array for "Total" or reservoirs with no reference lines

  const Storage_Chart = Plot.plot({
    title: "Reservoir Storage",
    width: width,
    height: 500,
    y: { grid: true, label: "Storage (Million Acre-Feet)",
        domain: stor_y_domain
     },
    x: { label: "Date", domain: StorDateSlider },
    marks: [
      Plot.lineY(filtered_stor, { x: "Date", y: "Storage_MAF", stroke: "blue" }),
         // Add a ruleY and text annotation for each reference line
      ...Stor_refLines.map(ref => Plot.ruleY([ref.y], {
        stroke: ref.color,
        strokeWidth: 1.5,
        strokeDasharray: "4,4"
      })),
      ...Stor_refLines.map(ref => Plot.text([ref.y], {
        y: ref.y,
        text: [ref.label],
        frameAnchor: "middle",
        dy: -8,
        fill: ref.color,
        fontSize: 11
      })),
      Plot.tip(filtered_stor, Plot.pointerX({ x: "Date", y: "Storage_MAF" }))
    ]
  })
  return Storage_Chart
}
{
  const Storage_Chart_Reference = Plot.plot({
    width: width,
    height: 100,
    marks: [
      Plot.lineY(filtered_stor, { x: "Date", y: "Storage_MAF", stroke: "blue" }),
      Plot.rectX([{}], {
        x1: new Date(StorDateSlider[0]),   // left edge
        x2: new Date(StorDateSlider[1]),   // right edge
        fill: "#9c9c9c",
        fillOpacity: 0.15
      })
    ]
  })

  return Storage_Chart_Reference
}
stor_min_date = d3.min(filtered_stor, d => d.Date)
stor_max_date = d3.max(filtered_stor, d => d.Date)
viewof StorDateSlider = {
  // Convert dates to timestamps for numeric slider math
  const min_ts = stor_min_date.getTime()
  const max_ts = stor_max_date.getTime()

  // Initial values
  let start_ts = min_ts
  let end_ts   = max_ts

  // Helper to format timestamp as display date
  const fmt = ts => new Date(ts).toLocaleDateString("en-US", {
    month: "short", day: "numeric", year: "numeric", timeZone: "UTC"
  })

  // Helper to convert timestamp to percentage position
  const to_pct = ts => ((ts - min_ts) / (max_ts - min_ts)) * 100

  const container = html`
    <div style="
      font-family: sans-serif;
      font-size: 12px;
      padding: 8px 0 4px 0;
      width: 100%;
      user-select: none;
    ">
      <!-- Date labels -->
      <div style="display: flex; justify-content: space-between; margin-bottom: 6px; color: #6c757d;">
        <span id="start-label">${fmt(start_ts)}</span>
        <span id="end-label">${fmt(end_ts)}</span>
      </div>

      <!-- Slider track -->
      <div id="track" style="
        position: relative;
        height: 6px;
        background: #dee2e6;
        border-radius: 3px;
        margin: 10px 20px 10px 40px;
        cursor: pointer;
      ">
        <!-- Selected range highlight -->
        <div id="range-fill" style="
          position: absolute;
          height: 100%;
          background: steelblue;
          border-radius: 3px;
          left: 0%;
          right: 0%;
        "></div>

        <!-- Start handle -->
        <div id="handle-start" style="
          position: absolute;
          width: 16px;
          height: 16px;
          background: white;
          border: 2px solid steelblue;
          border-radius: 50%;
          top: 50%;
          transform: translate(-50%, -50%);
          cursor: ew-resize;
          left: 0%;
          box-shadow: 0 1px 4px rgba(0,0,0,0.2);
          z-index: 2;
        "></div>

        <!-- End handle -->
        <div id="handle-end" style="
          position: absolute;
          width: 16px;
          height: 16px;
          background: white;
          border: 2px solid steelblue;
          border-radius: 50%;
          top: 50%;
          transform: translate(-50%, -50%);
          cursor: ew-resize;
          left: 100%;
          box-shadow: 0 1px 4px rgba(0,0,0,0.2);
          z-index: 2;
        "></div>
      </div>

      <!-- Min/Max labels -->
      <div style="display: flex; justify-content: space-between; color: #aaaaaa; font-size: 11px;">
        <span>${fmt(min_ts)}</span>
        <span>${fmt(max_ts)}</span>
      </div>
    </div>
  `

  const track        = container.querySelector("#track")
  const handle_start = container.querySelector("#handle-start")
  const handle_end   = container.querySelector("#handle-end")
  const range_fill   = container.querySelector("#range-fill")
  const start_label  = container.querySelector("#start-label")
  const end_label    = container.querySelector("#end-label")

  // Update visual positions and fill
  const update_visuals = () => {
    const s_pct = to_pct(start_ts)
    const e_pct = to_pct(end_ts)
    handle_start.style.left = s_pct + "%"
    handle_end.style.left   = e_pct + "%"
    range_fill.style.left   = s_pct + "%"
    range_fill.style.right  = (100 - e_pct) + "%"
    start_label.textContent = fmt(start_ts)
    end_label.textContent   = fmt(end_ts)
  }

  // Convert mouse x position to timestamp
  const mouse_to_ts = (e) => {
    const rect = track.getBoundingClientRect()
    const pct  = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width))
    return min_ts + pct * (max_ts - min_ts)
  }

  // Snap timestamp to nearest day
  const snap_to_day = ts => {
    const d = new Date(ts)
    d.setUTCHours(0, 0, 0, 0)
    return d.getTime()
  }

  // Drag logic
  const make_draggable = (handle, is_start) => {
    handle.addEventListener("mousedown", e => {
      e.preventDefault()

      const on_move = e => {
        const ts = snap_to_day(mouse_to_ts(e))
        if (is_start) {
          start_ts = Math.min(ts, end_ts - 86400000)   // keep at least 1 day gap
        } else {
          end_ts = Math.max(ts, start_ts + 86400000)
        }
        update_visuals()
        // Dispatch input event to notify OJS of value change
        container.value = [new Date(start_ts), new Date(end_ts)]
        container.dispatchEvent(new Event("input"))
      }

      const on_up = () => {
        window.removeEventListener("mousemove", on_move)
        window.removeEventListener("mouseup", on_up)
      }

      window.addEventListener("mousemove", on_move)
      window.addEventListener("mouseup", on_up)
    })
  }

  make_draggable(handle_start, true)
  make_draggable(handle_end,   false)

  // Set initial value
  update_visuals()
  container.value = [new Date(start_ts), new Date(end_ts)]

  return container
}
{
  const This_Year = filtered_stor
    .filter(d => d.Date >= new Date("2026-01-01T00:00:00"))
    .map(d => ({
      ...d,
      Normalized_Date: new Date(2000, d.Date.getMonth(), d.Date.getDate()),
      Month_Day: d.Date.toLocaleString("en-US", { month: "short", day: "2-digit", timeZone: "UTC" })
    })).sort((a, b) => a.Normalized_Date - b.Normalized_Date)

  const Previous_Year = filtered_stor
    .filter(d => d.Date >= new Date("2025-01-01T00:00:00") && 
                 d.Date < new Date("2026-01-01T00:00:00"))
    .map(d => ({
      ...d,
      Normalized_Date: new Date(2000, d.Date.getMonth(), d.Date.getDate()),
      Month_Day: d.Date.toLocaleString("en-US", { month: "short", day: "2-digit", timeZone: "UTC" })
    })).sort((a, b) => a.Normalized_Date - b.Normalized_Date)

  const marks = [
  Plot.lineY(This_Year, { 
    x: "Normalized_Date", 
    y: "Storage_MAF",       
    stroke: "blue",
    channels: {
        "Day":      { value: d => d.Month_Day },
        "Storage (MAF)": { value: d => d.Storage_MAF }
      },
    tip: {
      pointer: "xy",
      maxRadius: 8,
      format: {
        x: false,
        y: false,
        y1: false,
        y2: false,
        fill: false,
        "Day": true,
        "Storage (MAF)": true
      }
    } 
   }),
  Plot.lineY(Previous_Year,  { x: "Normalized_Date", y: "Storage_MAF",       stroke: "green",
    channels: {
        "Day":      { value: d => d.Month_Day },
        "Storage (MAF)": { value: d => d.Storage_MAF }
      },
    tip: {
      pointer: "xy",
      maxRadius: 8,
      format: {
        x: false,
        y: false,
        y1: false,
        y2: false,
        fill: false,
        "Day": true,
        "Storage (MAF)": true
      }
    } 
   }),
  Plot.lineY(Previous_Year,  { x: "Normalized_Date", y: "Stor_Day_Avg_10yr", stroke: "#ffc107",
    channels: {
        "Day":      { value: d => d.Month_Day },
        "Storage (MAF)": { value: d => d.Stor_Day_Avg_10yr }
      },
    tip: {
      pointer: "xy",
      maxRadius: 8,
      format: {
        x: false,
        y: false,
        y1: false,
        y2: false,
        fill: false,
        "Day": true,
        "Storage (MAF)": true
      }
    } 
   }),
  Plot.lineY(Previous_Year,  { x: "Normalized_Date", y: "Stor_Day_Avg_30yr", stroke: "red",
    channels: {
        "Day":      { value: d => d.Month_Day },
        "Storage (MAF)": { value: d => d.Stor_Day_Avg_30yr }
      },
    tip: {
      pointer: "xy",
      maxRadius: 8,
      format: {
        x: false,
        y: false,
        y1: false,
        y2: false,
        fill: false,
        "Day": true,
        "Storage (MAF)": true
      }
    } 
   }),
  ]

  return Plot.plot({
    title: "Reservoir Storage Annual Trends",
    width: width,
    height: 500,
    color: {
      legend: true,
      domain: ["This Year", "Last Year", "10yr Average", "30yr Average"],
      range:  ["blue", "green", "#ffc107", "red"]
    },
    y: { grid: true, label: "Reservoir Storage (MAF)" },
    x: { 
      label: "Date",
      ticks: d3.utcMonth.every(1),
      tickFormat: "%b %d" 
      },
    marks: marks
  })
}