Visualizing Climate Records using Plotly

10-Year Interactive Drought and Precipitation Analysis for South Dakota, Pine Ridge, and Rosebud

Author

Antigravity Coding Assistant

Published

July 19, 2026

Introduction

In this document, we visualize the weekly drought records for Oglala Lakota County and Todd County alongside the daily precipitation records for South Dakota using Python’s Plotly library, providing interactive and responsive dashboards. These counties correspond to part of the Pine Ridge and Rosebud reservations and are labeled as such in plots.

We will display: 1. Both county drought datasets and the statewide rolling precipitation plotted over time (10-year timeline). 2. Time within the year (standardized month, X-axis) vs. annual cumulative values (Y-axis) for cumulative precipitation and the two counties’ cumulative drought indices. 3. Annual trajectories comparing cumulative precipitation (X-axis) vs. cumulative drought index (Y-axis) faceted side-by-side by county.


Data Processing & Alignment

First, we load the collected datasets, calculate the Drought Severity and Coverage Index (DSCI), compute cumulative precipitation, and merge the tables on matching dates.

  • DSCI Formula: For cumulative USDM statistics, the index is calculated as DSCI = D0 + D1 + D2 + D3 + D4 (range: 0 to 500).
  • Date Matching: We align the daily precipitation and weekly drought records on the weekly ValidStart date of each USDM report.
Code
import pandas as pd
import numpy as np

# Load local CSV files
df_usdm = pd.read_csv("data/oglala_lakota_drought_weekly.csv")
df_todd = pd.read_csv("data/todd_drought_weekly.csv")
df_precip = pd.read_csv("data/south_dakota_precipitation_daily.csv")

# Parse dates
df_usdm["date"] = pd.to_datetime(df_usdm["ValidStart"])
df_todd["date"] = pd.to_datetime(df_todd["ValidStart"])
df_precip["date"] = pd.to_datetime(df_precip["date"])

# --- Pine Ridge Preprocessing ---
df_usdm["dsci"] = df_usdm["D0"] + df_usdm["D1"] + df_usdm["D2"] + df_usdm["D3"] + df_usdm["D4"]
df_usdm["year"] = df_usdm["date"].dt.year
df_usdm = df_usdm.sort_values("date")
df_usdm["cumulative_dsci_year"] = df_usdm.groupby("year")["dsci"].cumsum()

# --- Rosebud Preprocessing ---
df_todd["dsci"] = df_todd["D0"] + df_todd["D1"] + df_todd["D2"] + df_todd["D3"] + df_todd["D4"]
df_todd["year"] = df_todd["date"].dt.year
df_todd = df_todd.sort_values("date")
df_todd["cumulative_dsci_year"] = df_todd.groupby("year")["dsci"].cumsum()

# --- South Dakota Precipitation Preprocessing ---
df_precip = df_precip.sort_values("date").reset_index(drop=True)
df_precip["cumulative_rain"] = df_precip["precipitation_inches"].cumsum()
df_precip["year"] = df_precip["date"].dt.year
df_precip["cumulative_rain_year"] = df_precip.groupby("year")["precipitation_inches"].cumsum()
df_precip["rolling_365d_rain"] = df_precip["precipitation_inches"].rolling(window=365, min_periods=1).sum()

# --- Merges ---
df_merged_oglala = pd.merge(
    df_usdm[["date", "year", "dsci", "cumulative_dsci_year"]], 
    df_precip[["date", "precipitation_inches", "cumulative_rain_year", "rolling_365d_rain"]], 
    on="date", 
    how="inner"
).sort_values("date").reset_index(drop=True)
df_merged_oglala["year_str"] = df_merged_oglala["year"].astype(str)
df_merged_oglala["day_in_year"] = pd.to_datetime(df_merged_oglala["date"].dt.strftime("2020-%m-%d"))

df_merged_todd = pd.merge(
    df_todd[["date", "year", "dsci", "cumulative_dsci_year"]], 
    df_precip[["date", "precipitation_inches", "cumulative_rain_year", "rolling_365d_rain"]], 
    on="date", 
    how="inner"
).sort_values("date").reset_index(drop=True)
df_merged_todd["year_str"] = df_merged_todd["year"].astype(str)
df_merged_todd["day_in_year"] = pd.to_datetime(df_merged_todd["date"].dt.strftime("2020-%m-%d"))

df_merged_oglala.head()
date year dsci cumulative_dsci_year precipitation_inches cumulative_rain_year rolling_365d_rain year_str day_in_year
0 2016-07-26 2016 156.07 310.79 0.029943 0.339813 0.339813 2016 2020-07-26
1 2016-08-02 2016 200.00 510.79 0.061321 1.274443 1.274443 2016 2020-08-02
2 2016-08-09 2016 200.00 710.79 0.084065 1.623693 1.623693 2016 2020-08-09
3 2016-08-16 2016 198.53 909.32 0.035364 3.016655 3.016655 2016 2020-08-16
4 2016-08-23 2016 198.53 1107.85 0.000309 3.544805 3.544805 2016 2020-08-23

Interactive Visualizations with Plotly

1. Visualizing Data Over Time (10-Year Timelines)

This faceted timeline displays the 10-year history of the weekly drought index (DSCI) for both counties alongside the daily South Dakota precipitation rolling average.

Code
from plotly.subplots import make_subplots
import plotly.graph_objects as go

fig_time = make_subplots(
    rows=3, cols=1, 
    shared_xaxes=True, 
    vertical_spacing=0.06,
    subplot_titles=(
        "DSCI - Pine Ridge", 
        "DSCI - Rosebud", 
        "365-Day Rolling Precipitation (inches) [South Dakota Mean]"
    )
)

# Pine Ridge
fig_time.add_trace(
    go.Scatter(
        x=df_merged_oglala["date"], 
        y=df_merged_oglala["dsci"], 
        name="Pine Ridge DSCI", 
        line=dict(color="#1f77b4", width=1.5)
    ), 
    row=1, col=1
)

# Rosebud
fig_time.add_trace(
    go.Scatter(
        x=df_merged_todd["date"], 
        y=df_merged_todd["dsci"], 
        name="Rosebud DSCI", 
        line=dict(color="#ff7f0e", width=1.5)
    ), 
    row=2, col=1
)

# South Dakota rolling precipitation
fig_time.add_trace(
    go.Scatter(
        x=df_merged_oglala["date"], 
        y=df_merged_oglala["rolling_365d_rain"], 
        name="365-Day Rolling Precip", 
        line=dict(color="#2ca02c", width=1.5)
    ), 
    row=3, col=1
)

fig_time.update_layout(
    height=650, 
    template="plotly_white", 
    title=dict(
        text="10-Year Climate Metrics Over Time",
        y=0.98,
        x=0.5,
        xanchor="center",
        yanchor="top"
    ),
    margin=dict(t=50),
    showlegend=False,
    updatemenus=[
        dict(
            type="buttons",
            direction="right",
            x=1,
            y=0,
            xanchor="left",
            yanchor="bottom",
            showactive=False,
            buttons=[
                dict(
                    label="Reset View",
                    method="relayout",
                    args=[{
                        "xaxis.autorange": True,
                        "yaxis.autorange": True,
                        "yaxis2.autorange": True,
                        "yaxis3.autorange": True
                    }]
                )
            ]
        )
    ]
)
fig_time.show()

2. Visualizing Annual Cumulative Progression Over the Year (Time vs. Cumulative Values)

Here we plot time within the year (standardized month, X-axis) against cumulative values (Y-axis).

This contains 3 panels: - Cumulative Precipitation (South Dakota Mean) - Cumulative Drought Index (Pine Ridge) - Cumulative Drought Index (Rosebud)

Each year is shown as a separate colored curve in a rainbow series, with open circles indicating the end of each month. Click on a year in the legend to toggle its visibility across all subplots.

Code
import plotly.express as px

# Identify colors for each year in a rainbow series (turbo colorscale)
years = sorted(df_merged_oglala["year_str"].unique())
colors = px.colors.sample_colorscale("turbo", [i / (len(years) - 1) for i in range(len(years))])
year_colors = dict(zip(years, colors))

fig_cum_time = make_subplots(
    rows=3, cols=1, 
    shared_xaxes=True, 
    vertical_spacing=0.06,
    subplot_titles=(
        "Cumulative Precipitation (South Dakota Mean)", 
        "Cumulative Drought Index (Pine Ridge)", 
        "Cumulative Drought Index (Rosebud)"
    )
)

# Identify month-end markers
df_month_ends_oglala = df_merged_oglala.groupby(["year", df_merged_oglala["date"].dt.month]).tail(1).copy()
df_month_ends_oglala["day_in_year"] = pd.to_datetime(df_month_ends_oglala["date"].dt.strftime("2020-%m-%d"))

df_month_ends_todd = df_merged_todd.groupby(["year", df_merged_todd["date"].dt.month]).tail(1).copy()
df_month_ends_todd["day_in_year"] = pd.to_datetime(df_month_ends_todd["date"].dt.strftime("2020-%m-%d"))

for year in years:
    # 1. Cumulative Precipitation
    df_y_p = df_merged_oglala[df_merged_oglala["year_str"] == year]
    df_pts_p = df_month_ends_oglala[df_month_ends_oglala["year_str"] == year]
    
    fig_cum_time.add_trace(
        go.Scatter(
            x=df_y_p["day_in_year"], y=df_y_p["cumulative_rain_year"], 
            mode="lines", line=dict(color=year_colors[year]), 
            name=year, legendgroup=year, showlegend=True
        ), 
        row=1, col=1
    )
    fig_cum_time.add_trace(
        go.Scatter(
            x=df_pts_p["day_in_year"], y=df_pts_p["cumulative_rain_year"], 
            mode="markers", marker=dict(symbol="circle-open", size=5, color=year_colors[year], line=dict(width=1)), 
            name=year, legendgroup=year, showlegend=False
        ), 
        row=1, col=1
    )
    
    # 2. Pine Ridge DSCI
    df_y_o = df_merged_oglala[df_merged_oglala["year_str"] == year]
    df_pts_o = df_month_ends_oglala[df_month_ends_oglala["year_str"] == year]
    
    fig_cum_time.add_trace(
        go.Scatter(
            x=df_y_o["day_in_year"], y=df_y_o["cumulative_dsci_year"], 
            mode="lines", line=dict(color=year_colors[year]), 
            name=year, legendgroup=year, showlegend=False
        ), 
        row=2, col=1
    )
    fig_cum_time.add_trace(
        go.Scatter(
            x=df_pts_o["day_in_year"], y=df_pts_o["cumulative_dsci_year"], 
            mode="markers", marker=dict(symbol="circle-open", size=5, color=year_colors[year], line=dict(width=1)), 
            name=year, legendgroup=year, showlegend=False
        ), 
        row=2, col=1
    )
    
    # 3. Rosebud DSCI
    df_y_t = df_merged_todd[df_merged_todd["year_str"] == year]
    df_pts_t = df_month_ends_todd[df_month_ends_todd["year_str"] == year]
    
    fig_cum_time.add_trace(
        go.Scatter(
            x=df_y_t["day_in_year"], y=df_y_t["cumulative_dsci_year"], 
            mode="lines", line=dict(color=year_colors[year]), 
            name=year, legendgroup=year, showlegend=False
        ), 
        row=3, col=1
    )
    fig_cum_time.add_trace(
        go.Scatter(
            x=df_pts_t["day_in_year"], y=df_pts_t["cumulative_dsci_year"], 
            mode="markers", marker=dict(symbol="circle-open", size=5, color=year_colors[year], line=dict(width=1)), 
            name=year, legendgroup=year, showlegend=False
        ), 
        row=3, col=1
    )


fig_cum_time.update_layout(
    height = 880,
    template = "plotly_white",
    title = dict(
        text = "Annual Progression of Cumulative Climate Metrics",
        y = 0.95, x = 0.5, xanchor = "center", yanchor = "top"
    ),
    margin = dict(t = 90),
    legend = dict(
        # groupclick="togglegroup",  <-- You can replace or keep this, but the below are safer for multi-subplots
        # itemclick = "toggle3",         # Single-click toggles the entire group
        # itemdoubleclick = "isolate3",  # Double-click isolates the entire group
        groupclick="togglegroup",        # Natively validated to handle groups on double-click
        title_text = "Year"
    ),
    updatemenus=[
        dict(
            type="buttons",
            direction="right",
            x=1,
            y=0,
            xanchor="left",
            yanchor="bottom",
            showactive=False,
            buttons=[
                dict(
                    label="Reset View",
                    method="update",
                    args=[
                        {"visible": [True] * len(fig_cum_time.data)},
                        {
                            "xaxis.autorange": True,
                            "yaxis.autorange": True,
                            "yaxis2.autorange": True,
                            "yaxis3.autorange": True
                        }
                    ]
                )
            ]
        )
    ]
)
# Format the x-axes
fig_cum_time.update_xaxes(tickformat="%b", dtick="M1")
fig_cum_time.show()

3. Visualizing Trajectories of Cumulative Rain vs. Cumulative Drought Index (Faceted by County)

Finally, we plot annual cumulative precipitation (X-axis) against the annual cumulative drought index (DSCI) (Y-axis).

The curves for Pine Ridge and Rosebud are displayed side-by-side as two facets on the same plot surface. Individual years are colored in a rainbow series with open circles marking the end of each month.

How to Interpret this Plot:

  • X-Axis (Precipitation): Represents the total cumulative rain since January 1 of that year.
  • Y-Axis (Drought DSCI): Represents the total cumulative weekly USDM DSCI since January 1 of that year.
  • Watermarks (DRY / WET):
    • Upper-Left Corner (“DRY”): High cumulative drought index but low rainfall. Curves that bend steeply upwards and stay to the left represent persistent, severe drought years.
    • Lower-Right Corner (“WET”): High cumulative rainfall but low/no drought index. Curves that stretch far to the right and remain low represent wet, drought-free years.
    • Month Circles: Progress from left to right chronologically (January to December), allowing you to trace the exact weeks when drought conditions escalated (steep slopes) or recovered (slopes flattening out as rainfall accumulated).
Code
fig_combined_annual = make_subplots(
    rows=1, cols=2, 
    shared_yaxes=True, 
    horizontal_spacing=0.08,
    subplot_titles=("Pine Ridge", "Rosebud")
)

# Identify month-end markers for Rosebud
df_month_ends_todd = df_merged_todd.groupby(["year", df_merged_todd["date"].dt.month]).tail(1).copy()

for year in years:
    # 1. Pine Ridge
    df_y_o = df_merged_oglala[df_merged_oglala["year_str"] == year]
    df_pts_o = df_month_ends_oglala[df_month_ends_oglala["year_str"] == year]
    
    fig_combined_annual.add_trace(
        go.Scatter(
            x=df_y_o["cumulative_rain_year"], y=df_y_o["cumulative_dsci_year"], 
            mode="lines", line=dict(color=year_colors[year]), 
            name=year, legendgroup=year, showlegend=True
        ), 
        row=1, col=1
    )
    fig_combined_annual.add_trace(
        go.Scatter(
            x=df_pts_o["cumulative_rain_year"], y=df_pts_o["cumulative_dsci_year"], 
            mode="markers", marker=dict(symbol="circle-open", size=5, color=year_colors[year], line=dict(width=1)), 
            name=year, legendgroup=year, showlegend=False
        ), 
        row=1, col=1
    )

    # 2. Rosebud
    df_y_t = df_merged_todd[df_merged_todd["year_str"] == year]
    df_pts_t = df_month_ends_todd[df_month_ends_todd["year_str"] == year]
    
    fig_combined_annual.add_trace(
        go.Scatter(
            x=df_y_t["cumulative_rain_year"], y=df_y_t["cumulative_dsci_year"], 
            mode="lines", line=dict(color=year_colors[year]), 
            name=year, legendgroup=year, showlegend=False
        ), 
        row=1, col=2
    )
    fig_combined_annual.add_trace(
        go.Scatter(
            x=df_pts_t["cumulative_rain_year"], y=df_pts_t["cumulative_dsci_year"], 
            mode="markers", marker=dict(symbol="circle-open", size=5, color=year_colors[year], line=dict(width=1)), 
            name=year, legendgroup=year, showlegend=False
        ), 
        row=1, col=2
    )

# Add background watermark text annotations for DRY and WET zones
# Subplot 1 (Pine Ridge)
fig_combined_annual.add_annotation(
    xref="x domain", yref="y domain",
    x=0.15, y=0.85,
    text="<b>DRY</b>",
    showarrow=False,
    font=dict(size=40, color="rgba(150, 150, 150, 0.2)"),
    textangle=-30
)
fig_combined_annual.add_annotation(
    xref="x domain", yref="y domain",
    x=0.85, y=0.15,
    text="<b>WET</b>",
    showarrow=False,
    font=dict(size=40, color="rgba(150, 150, 150, 0.2)"),
    textangle=-30
)

# Subplot 2 (Rosebud)
fig_combined_annual.add_annotation(
    xref="x2 domain", yref="y2 domain",
    x=0.15, y=0.85,
    text="<b>DRY</b>",
    showarrow=False,
    font=dict(size=40, color="rgba(150, 150, 150, 0.2)"),
    textangle=-30
)
fig_combined_annual.add_annotation(
    xref="x2 domain", yref="y2 domain",
    x=0.85, y=0.15,
    text="<b>WET</b>",
    showarrow=False,
    font=dict(size=40, color="rgba(150, 150, 150, 0.2)"),
    textangle=-30
)

fig_combined_annual.update_layout(
    height=480, 
    template="plotly_white", 
    title=dict(
        text="Annual Cumulative Rain vs. Cumulative Drought Index (DSCI) Trajectories",
        y=0.94,
        x=0.5,
        xanchor="center",
        yanchor="top"
    ),
    margin=dict(t=65),
    legend = dict(
        # groupclick="togglegroup",  <-- You can replace or keep this, but the below are safer for multi-subplots
        # itemclick = "toggle3",         # Single-click toggles the entire group
        # itemdoubleclick = "isolate3",  # Double-click isolates the entire group
        groupclick="togglegroup",        # Natively validated to handle groups on double-click
        title_text = "Year"
    ),
    updatemenus=[
        dict(
            type="buttons",
            direction="right",
            x=1,
            y=0,
            xanchor="left",
            yanchor="bottom",
            showactive=False,
            buttons=[
                dict(
                    label="Reset View",
                    method="update",
                    args=[
                        {"visible": [True] * len(fig_combined_annual.data)},
                        {
                            "xaxis.autorange": True,
                            "yaxis.autorange": True,
                            "xaxis2.autorange": True,
                            "yaxis2.autorange": True
                        }
                    ]
                )
            ]
        )
    ]
)
# Format the x-axes
fig_combined_annual.update_xaxes(title_text="Cumulative Precipitation (inches)")
fig_combined_annual.update_yaxes(title_text="Cumulative DSCI", row=1, col=1)
fig_combined_annual.show()