RSS Amplifier

Climate Engine Team · Jul 6, 2026

Exploring Drought Patterns: A Step-by-Step Stacked Bar Chart Tutorial with the Climate Engine API

0
Sign in to vote or save

Climate Engine Team · Climate Engine Team

The Climate Engine API lets you bin a dataset and count the number of pixels falling into each bin within your area of interest, making it easy to track how the spatial distribution of values has changed over time.

Example output of the notebook

In this example, we walk through a Google Colab notebook that builds stacked bar charts for three drought-relevant datasets:

  • US Drought Monitor — An impact-based dataset that classifies conditions across six categories: neutral or wet, abnormally dry, moderate drought, severe drought, extreme drought, and exceptional drought.

  • GRACE Drought Groundwater Storage Indicator — A percentile product (0–100) derived from terrestrial water storage observations collected by the GRACE-FO satellite.

  • Vegetation Drought Response Index (VegDRI) — A dataset that quantifies drought stress on vegetation.

To use the API, you will need an API key. You can request a quota-limited key following these directions, or connect your earth engine account to Climate Engine to create your own non-quota limited key following these directions.

In this step, install and import the required libraries by running this cell. The one package that isn’t pre-installed in Colab (geopandas) is fetched automatically via pip. The remaining imports cover data manipulation (pandas, numpy), HTTP requests (requests), plotting (matplotlib), date handling (datetime). The final line suppresses SSL warnings from the requests library to keep output clean.

# Import/Install Packages
import datetime
import requests
import matplotlib.patches as mpatches

!pip install --quiet geopandas
import pandas as pd
import geopandas as gpd
import matplotlib.pyplot as plt
plt.style.use(’classic’)
%matplotlib inline
import numpy as np
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)

In this step, you will set your API key as a Colab Secret (see directions here). Retrieve your Climate Engine API key from Colab’s secret manager using userdata.get(’key’), then define the base URL for all API requests and store your key in a headers dictionary. This headers object will be passed with every API call to authenticate your requests.

# Authorize
from google.colab import userdata
key = userdata.get(’key’)

# Set root URL for API requests
root_url = ‘https://api.climateengine.org/’
# Authentication info for the API (do not share this widely)
headers = {’Authorization’: key}

In this step, you will load your shapefile files into the runtime files. Note: This example is for a single polygon, with a limited number of vertices. When running for your polygon, complex polygons might need to be passed as POST requests.

Example of runtime File manager

Then using geopandas and the .shp path, the data will be read into the notebook as a GeoDataFrame. Then you will verify that its coordinate reference system (CRS) is WGS84 (EPSG:4326), reprojecting it automatically if not. A function then extracts the boundary coordinates from the geometry, handling both Polygon and MultiPolygon shapes by converting them into nested lists of coordinate pairs (a format the /coordinates endpoints require). The values are printed so you can confirm they look correct before passing them to the API.

# Read in the shapefile
shapefile_path = “/content/Walker_HUC8_SHP_simplfied.shp”
gdf = gpd.read_file(shapefile_path)

# Ensure the CRS is WGS84 (EPSG:4326)
if gdf.crs != “EPSG:4326”:
gdf = gdf.to_crs(”EPSG:4326”)

# Define function to extract coordinates
def extract_coordinates(geometry):
if geometry is None:
return None
if geometry.geom_type == “Polygon”:
# Add an outer list around the coordinates
return [[list(coord) for coord in geometry.exterior.coords]]
elif geometry.geom_type == “MultiPolygon”:
# Add an outer list around each polygon’s coordinates
return [[[list(coord) for coord in polygon.exterior.coords]] for polygon in geometry.geoms]
else:
return None

# Extract coordinates from the single geometry
coords = extract_coordinates(gdf.iloc[0].geometry)

print(coords)

In this step, you will define the API endpoint for pixel count timeseries and set up your request parameters as a dictionary, which specify the area of interest (your coordinates from Step 3), the dataset (USDM_DROUGHT_MONITOR), the variable, the bins, and the start_ and end_ dates. The bins [-1, 0, 1, 2, 3, 4] correspond to the six US Drought Monitor categories: neutral/wet, abnormally dry, moderate, severe, extreme, and exceptional drought. The date range pulls the five years between 2020 and 2025. The GET request is then sent to the API with your headers from Step 2, and the JSON response is printed so you can inspect the raw data before processing it.

# Define Endpoint
endpoint = ‘zonal_stats/pixel_count/timeseries/coordinates’

# Set up parameters for API call
params = {
“coordinates”: f’{coords}’,
“dataset”: “USDM_DROUGHT_MONITOR”,
“variable”: “usdm”,
“bins”: “[-1,0,1,2,3,4]”,
“start_date”: “2020-01-01”,
“end_date”: “2025-12-31”,
}
# Send request to the API
r = requests.get(root_url + endpoint, params=params, headers=headers, verify=False)
response = r.json()

print(response)

In this step, loop through each entry in the API response, extracting the date and flattening the nested bin counts, where each bin range (e.g., -1_to_0, 0_to_1) becomes its own column before appending it to a list. That list is then converted into a pandas DataFrame and the Date column is cast from a string to a proper datetime type for accurate time-series plotting in later steps.

# Create a list to store values in
records = []

# Populate the list with API request values
for entry in response[’Data’]:
row = {’Date’: entry[’Date’]}
# Flatten each bin range into its own column
for bin_range, count in entry[’usdm’].items():
row[bin_range] = count
records.append(row)

# Convert list to dataframe
df = pd.DataFrame(records)

# Parse date string to datetime value
df[’Date’] = pd.to_datetime(df[’Date’])
df

In this step, define the desired bin order from most severe to least severe drought, then filter it down to only the bins that actually exist as columns in your DataFrame. A clean copy of the DataFrame is created with just the Date column and the present bins, with any missing values filled with 0. Each bin’s pixel count is then divided by the total pixel count for that row and multiplied by 100, converting raw counts into percentages so that each date’s values sum to 100.

# Set bin order
bin_order = [’[4, inf)’, ‘[3, 4)’, ‘[2, 3)’, ‘[1, 2)’, ‘[0, 1)’, ‘[-1, 0)’]

# Only keep bins that exist in the dataframe
present_bins = [b for b in bin_order if b in df.columns]

# Create a copy of the dataframe for plotting
plot_df = df[[’Date’] + present_bins].fillna(0).copy()

# Convert to %s
row_totals = plot_df[present_bins].sum(axis=1)
plot_df[present_bins] = plot_df[present_bins].div(row_totals, axis=0) * 100

plot_df

In this final step, define labels and official USDM colors for each bin. Then build the stacked bar chart by looping through the bins in order (most severe to least severe) and layering each as its own bar segment by using a bottoms array that accumulates height so each new layer stacks on top of the last. The x-axis tick positions are set to the first week of each calendar year rather than every data point, keeping the labels readable across the five-year timeseries. The legend is placed outside the plot area to avoid covering the bars, and plt.tight_layout() adjusts spacing to prevent any labels from being clipped.

# Define labels for bins
labels = {
‘[-1, 0)’: ‘No Drought’,
‘[0, 1)’: ‘D0 - Abnormally Dry’,
‘[1, 2)’: ‘D1 - Moderate Drought’,
‘[2, 3)’: ‘D2 - Severe Drought’,
‘[3, 4)’: ‘D3 - Extreme Drought’,
‘[4, inf)’: ‘D4 - Exceptional Drought’,
}

# Define colors for bins
colors = {
‘[-1, 0)’: ‘#FFFFFF’,
‘[0, 1)’: ‘#FFFF00’,
‘[1, 2)’: ‘#FFD37F’,
‘[2, 3)’: ‘#FFAA00’,
‘[3, 4)’: ‘#E60000’,
‘[4, inf)’: ‘#730000’,
}

# Define figure to add data to
fig, ax = plt.subplots(figsize=(16, 6))

x = np.arange(len(plot_df))
bar_width = 1.0
bottoms = np.zeros(len(plot_df))

# Add bars to chart
for bin_col in present_bins:
values = plot_df[bin_col].values
ax.bar(x, values, bar_width,
bottom=bottoms,
color=colors[bin_col],
label=labels[bin_col],
edgecolor=’none’,
linewidth=0.3)
bottoms += values

# Update x-axis to label the years based on when year starts
year_starts = plot_df.groupby(plot_df[’Date’].dt.year).head(1).index
tick_positions = x[year_starts]
tick_labels = plot_df[’Date’].dt.strftime(’%Y’).iloc[year_starts]
ax.set_xticks(tick_positions)
ax.set_xticklabels(tick_labels, rotation=45, ha=’right’, fontsize=8)
ax.set_xlim(-0.5, len(plot_df) - 0.5)

# Update y-axis labels + ticks
ax.set_ylabel(’% of Area’)
ax.set_ylim(0, 100)

# Add title
ax.set_title(’USDM Drought - Walker Basin’)

# Move legend outside the plot so it doesn’t cover bars
ax.legend(handles=[mpatches.Patch(color=colors[b], label=labels[b])
for b in present_bins],
loc=’upper left’, bbox_to_anchor=(1, 1),
title=’USDM Category’, frameon=True)

plt.tight_layout()
plt.show()

Demo output for the US Drought Monitor

To complete this workflow for the other datasets, continue through the notebooks cells. Bins, labels, etc. will be updated to match the dataset. Note: If you run this workflow for more years, etc. you may need to update the plotting code to make the chart nicely formatted. This chart is specific to this 5-year window for this example AOI.

No posts

Read the original on climateengine.substack.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.