time series API¶
There is a family of /timeseries
endpoints in the titiler.cmr
API that can be used to generate time-aware responses.
The Timeseries Extension provides endpoints for requesting results for all points or intervals
along a timeseries. The /timeseries family of endpoints works by converting
the provided timeseries parameters (datetime
, step
, and temporal_mode
) into a set of
datetime
query parameters for the corresponding lower-level endpoint, running asynchronous
requests to the lower-level endpoint, then collecting the results and formatting them in a coherent
format for the user.
The timeseries structure is defined by the datetime
, step
, and temporal_mode
parameters.
The temporal_mode
mode parameter controls whether or not CMR is queried for a particular
point-in-time (temporal_mode=point
) or over an entire interval (temporal_mode=interval
).
In general, it is best to use temporal_mode=point
for datasets where granules overlap completely
in space (e.g. daily sea surface temperature predictions) because the /timeseries endpoints will
create a mosaic of all assets returned by the query and the first asset to cover a pixel will
be used. For datasets where it requires granules from multiple timestamps to fully cover an AOI,
temporal_mode=interval
is appropriate. For example, you can get weekly composites of satellite
imagery for visualization purposes with step=P1W & temporal_mode=interval
.
from IPython.display import IFrame
# if running titiler-cmr in the docker network
# titiler_endpoint = "http://localhost:8081"
# titiler-cmr-staging deployment
titiler_endpoint = "https://dev-titiler-cmr.delta-backend.com"
IFrame(f"{titiler_endpoint}/api.html#Timeseries", 900, 500)
import json
from datetime import datetime
import httpx
import matplotlib.pyplot as plt
import numpy as np
from folium import LayerControl, Map, TileLayer
from geojson_pydantic import Feature, Polygon
from IPython.display import Image, display
timeseries API¶
The timeseries API makes it possible to return results for many points along a timeseries with a single request. The available parameters are:
datetime
(str): Either a date-time, an interval, or a comma-separated list of date-times or intervals. Date and time expressions adhere to rfc3339 ('2020-06-01T09:00:00Z') format.step
(str): width of individual timesteps expressed as a IS8601 durationtemporal_mode
(str): if"point"
, queries will be made for the individual timestamps along the timeseries. If"interval"
, queries will be made for the periods between each timestamp along the timeseries.
There are many ways to combine the parameters to produce a timeseries.
- Exact points in time from a start to and end datetime:
- provide
datetime={start_datetime}/{end_datetime}
,step={step_width}
, andtemporal_mode=point
wherestep_width
is something likeP1D
for daily orP2W
for bi-weekly. - provide
datetime={start_datetime}/{end_datetime}
, andtemporal_mode=point
withoutstep
to get a point for every unique timestamp in the granules betweenstart_datetime
andend_datetime
.
- Fixed-width intervals between a start and end datetime:
- provide
datetime={start_datetime}/{end_datetime}
,step
, andtemporal_mode=interval
- Specific datetimes
- provide
datetime=2024-10-01T00:00:01Z,2024-10-02T00:00:01Z
- Specific datetime intervals
- provide
datetime=2024-10-01T00:00:01Z/2024-10-01T23:59:59Z,2024-10-05T00:00:01Z/2024-10-05T23:59:59Z
How to use the timeseries API with titiler.cmr
¶
The /timeseries
endpoints work by interpreting the timeseries parameters (e.g. datetime
and step
) and parameterizing a set of lower-level requests to the related endpoint. For example, a request to /timeseries/statistics
for a set of four timepoints each one week apart will fire off four requests to the /statistics
endpoint with a particular value in the datetime
parameter. The results are collected and returned in a coherent format that can be consumed in a table or a chart.
Every /timeseries
request in titiler.cmr
will require both a concept_id
and a set of timeseries parameters. The GHRSST Level 4 GAMSSA_28km Global Foundation Sea Surface Temperature Analysis v1.0 dataset (GDS2) is a useful dataset for demo purposes because the granule assets are small (~1MB each).
concept_id = "C2036881735-POCLOUD"
The /timeseries
GET
endpoint is useful for demonstrating how the timeseries family of endpoints constructs sub-requests. It returns the list of titiler.cmr
query parameters (datetime
and concept_id
) that will be used to generate the timeseries results.
Timeseries for all granules between a start/end datetime¶
For some datasets that have granules that are regularly spaced in time (e.g. daily), it is useful to be able to quickly specify a summary of all timepoints between a start and end datetime. You can do that by simply providing the start_datetime
and end_datetime
parameters. The application will query CMR and produce a list of unique datetime
values from the results of the granule search. If a granule represents a datetime range, it will return the midpoint between the start and end for a single granule.
response = httpx.get(
f"{titiler_endpoint}/timeseries",
params={
"concept_id": concept_id,
"datetime": "2024-10-01T00:00:01Z/2024-10-05T00:00:01Z",
},
timeout=None,
).json()
print(json.dumps(response, indent=2))
[ { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-01T12:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-02T12:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-03T12:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-04T12:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-05T12:00:00+00:00" } ]
Weekly timeseries¶
Sometimes you might be interested in a report with lower temporal resolution than the full dataset timeseries. By setting step="P1W"
and temporal_mode="point"
, you can get a weekly series.
response = httpx.get(
f"{titiler_endpoint}/timeseries",
params={
"concept_id": concept_id,
"datetime": "2024-10-01T00:00:01Z/2024-10-30T00:00:01Z",
"step": "P1W",
"temporal_mode": "point",
}
).json()
print(json.dumps(response, indent=2))
[ { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-01T00:00:01+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-08T00:00:01+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-15T00:00:01+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-22T00:00:01+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-29T00:00:01+00:00" } ]
Periodic timeseries¶
Some datasets (like satellite imagery) may consist of granules that do not fully cover an arbitrary area of interest. In this case it is useful to construct a timeseries from a set of datetime ranges so that granules can be mosaiced to ensure each step has full coverage.
To create a set of non-overlapping week-long datetime ranges, you can modify the query to use temporal_mode="interval"
which will create ranges that start on the weekly values returned in the previous query and extend up to the second before the next value in the series.
response = httpx.get(
f"{titiler_endpoint}/timeseries",
params={
"concept_id": concept_id,
"datetime": "2024-10-01T00:00:01Z/2024-10-30T00:00:01Z",
"step": "P1W",
"temporal_mode": "interval",
}
).json()
print(json.dumps(response, indent=2))
[ { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-01T00:00:01+00:00/2024-10-08T00:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-08T00:00:01+00:00/2024-10-15T00:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-15T00:00:01+00:00/2024-10-22T00:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-22T00:00:01+00:00/2024-10-29T00:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-29T00:00:01+00:00/2024-10-30T00:00:01+00:00" } ]
Custom timeseries¶
If you want to specify the exact datetime values for a timeseries and you either cannot do not want to use the timeseries parameters, you can supply a set of comma-separated datetimes and/or datetime ranges to the datetime
parameter.
response = httpx.get(
f"{titiler_endpoint}/timeseries",
params={
"concept_id": concept_id,
"datetime": ",".join(
["2024-10-01T00:00:01Z", "2024-10-07T00:00:01Z/2024-10-09T23:59:59Z"]
),
}
).json()
print(json.dumps(response, indent=2))
[ { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-01T00:00:01+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-07T12:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-08T12:00:00+00:00" }, { "concept_id": "C2036881735-POCLOUD", "datetime": "2024-10-09T12:00:00+00:00" } ]
Example: sea surface temperature GIF¶
The /timeseries/bbox
endpoint can be used to produce a GIF that shows a visualization of granules over time.
The example below shows biweekly sea surface temperature estimates from the GAMSSA dataset for the period from November 2023 through October 2024.
minx, miny, maxx, maxy = -180, -90, 180, 90
request = httpx.get(
f"{titiler_endpoint}/timeseries/bbox/{minx},{miny},{maxx},{maxy}.gif",
params={
"concept_id": concept_id,
"datetime": "2023-11-01T00:00:01Z/2024-10-30T23:59:59Z",
"step": "P2W",
"temporal_mode": "point",
"variable": "analysed_sst",
"backend": "xarray",
"colormap_name": "thermal",
"rescale": [[273, 315]],
},
timeout=None,
)
display(Image(request.content))
Example: HLSL30 GIF¶
The example below shows a weekly mosaic of imagery from the Harmonized Landsat Sentinel L30 (HLSL30) collection for the period from January to November 2024.
minx, miny, maxx, maxy = -91.464,47.353,-90.466,47.974
request = httpx.get(
f"{titiler_endpoint}/timeseries/bbox/{minx},{miny},{maxx},{maxy}/512x512.gif",
params={
"concept_id": "C2021957657-LPCLOUD",
"datetime": "2024-01-01T00:00:00Z/2024-11-30T00:00:00Z",
"step": "P1W",
"temporal_mode": "interval",
"backend": "rasterio",
"bands_regex": "B[0-9][0-9]",
"bands": ["B04", "B03", "B02"],
"color_formula": "Gamma RGB 3.5 Saturation 1.7 Sigmoidal RGB 15 0.35",
"fps": 5,
},
timeout=None,
)
display(Image(request.content))
Example: sea surface temperature statistics¶
The /timeseries/statistics
endpoint will produce summary statistics for an AOI for all points along a timeseries.
The example below shows biweekly sea surface temperature summary statistics for the Gulf of Mexico from the GAMSSA dataset for the period from November 2023 through October 2024.
%%time
minx, miny, maxx, maxy = -98.676, 18.857, -81.623, 31.097
geojson = Feature(
type="Feature",
geometry=Polygon.from_bounds(minx, miny, maxx, maxy),
properties={},
)
request = httpx.post(
f"{titiler_endpoint}/timeseries/statistics",
params={
"concept_id": concept_id,
"datetime": "2022-02-01T00:00:01Z/2024-10-30T23:59:59Z",
"step": "P1D",
"temporal_mode": "point",
"variable": "analysed_sst",
"backend": "xarray",
},
json=geojson.model_dump(exclude_none=True),
timeout=None,
)
request.raise_for_status()
response = request.json()
CPU times: user 52.8 ms, sys: 12.3 ms, total: 65.1 ms Wall time: 20.2 s
%%time
minx, miny, maxx, maxy = -98.676, 18.857, -81.623, 31.097
geojson = Feature(
type="Feature",
geometry=Polygon.from_bounds(minx, miny, maxx, maxy),
properties={},
)
request = httpx.post(
f"{titiler_endpoint}/timeseries/statistics",
params={
"concept_id": "C2036881735-POCLOUD",
"datetime": "2022-02-01T00:00:01Z/2024-10-30T23:59:59Z",
"step": "P1D",
"temporal_mode": "point",
"variable": "analysed_sst",
"backend": "xarray",
},
json=geojson.model_dump(exclude_none=True),
timeout=None,
)
request.raise_for_status()
response = request.json()
CPU times: user 53.8 ms, sys: 5.75 ms, total: 59.6 ms Wall time: 24.9 s
The /timeseries/statistics
endpoint returns the GeoJSON with statistics for each step in the timeseries embedded in the properties.
stats = response["properties"]["statistics"]
print(len(stats))
stats_preview = {timestamp: sst_stats for i, (timestamp, sst_stats) in enumerate(stats.items()) if i < 2}
print(json.dumps(stats_preview, indent=2))
1001 { "2022-02-01T00:00:01+00:00": { "analysed_sst": { "min": 285.27000000000004, "max": 300.34000000000003, "mean": 296.3800266967469, "count": 2337.9599609375, "sum": 692924.6356385816, "std": 2.701563618833078, "median": 296.83000000000004, "majority": 300.16, "minority": 285.27000000000004, "unique": 819.0, "histogram": [ [ 14, 31, 40, 62, 88, 154, 321, 853, 378, 422 ], [ 285.27000000000004, 286.77700000000004, 288.28400000000005, 289.79100000000005, 291.29800000000006, 292.80500000000006, 294.312, 295.819, 297.326, 298.833, 300.34000000000003 ] ], "valid_percent": 68.49, "masked_pixels": 1087.0, "valid_pixels": 2363.0, "percentile_2": 288.46000000000004, "percentile_98": 300.20000000000005 } }, "2022-02-02T00:00:01+00:00": { "analysed_sst": { "min": 285.45000000000005, "max": 300.36, "mean": 296.3582956145494, "count": 2337.9599609375, "sum": 692873.8292384959, "std": 2.658495800828904, "median": 296.79, "majority": 296.59000000000003, "minority": 285.45000000000005, "unique": 827.0, "histogram": [ [ 14, 27, 51, 56, 90, 157, 332, 899, 329, 408 ], [ 285.45000000000005, 286.94100000000003, 288.432, 289.92300000000006, 291.41400000000004, 292.90500000000003, 294.396, 295.887, 297.37800000000004, 298.869, 300.36 ] ], "valid_percent": 68.49, "masked_pixels": 1087.0, "valid_pixels": 2363.0, "percentile_2": 288.69000000000005, "percentile_98": 300.15000000000003 } } }
The statistics output can be used to generate plots like this:
data = response['properties']['statistics']
dates = []
means = []
stds = []
for date_str, values in data.items():
dates.append(datetime.fromisoformat(date_str))
means.append(values["analysed_sst"]["mean"])
stds.append(values["analysed_sst"]["std"])
plt.figure(figsize=(10, 6))
plt.plot(dates, means, "b-", label="Mean")
plt.fill_between(
dates,
np.array(means) - np.array(stds),
np.array(means) + np.array(stds),
alpha=0.2,
color="b",
label="Standard Deviation",
)
plt.xlabel("Date")
plt.ylabel("Temperature (K)")
plt.title("Mean sea surface temperature in the Gulf of Mexico")
plt.legend()
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
Example: Timeseries raster tiles¶
It could be useful to allow users to select a timestep in an interactive map. You can use the /timeseries/tilejson
endpoint for that purpose. The following example shows how you could use it to provide timeseries capability to an interactive map of sea ice cover.
minx, miny, maxx, maxy = -180, -90, 180, 90
request = httpx.get(
f"{titiler_endpoint}/timeseries/WebMercatorQuad/tilejson.json",
params={
"concept_id": concept_id,
"datetime": "2023-11-01T00:00:01Z/2024-10-30T23:59:59Z",
"step": "P1M",
"temporal_mode": "point",
"variable": "sea_ice_fraction",
"backend": "xarray",
"colormap_name": "blues_r",
"rescale": [[0, 1]],
},
timeout=None,
)
tilejsons = request.json()
tilejson_preview = {timestamp: tilejson for i, (timestamp, tilejson) in enumerate(tilejsons.items()) if i < 2}
print(json.dumps(tilejson_preview, indent=2))
{ "2023-11-01T00:00:01+00:00": { "tilejson": "2.2.0", "version": "1.0.0", "scheme": "xyz", "tiles": [ "https://9ox7r6pi8c.execute-api.us-west-2.amazonaws.com/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?concept_id=C2036881735-POCLOUD&variable=sea_ice_fraction&backend=xarray&colormap_name=blues_r&rescale=%5B0%2C+1%5D&datetime=2023-11-01T00%3A00%3A01%2B00%3A00" ], "minzoom": 0, "maxzoom": 24, "bounds": [ -180.0, -90.0, 180.0, 90.0 ], "center": [ 0.0, 0.0, 0 ] }, "2023-12-01T00:00:01+00:00": { "tilejson": "2.2.0", "version": "1.0.0", "scheme": "xyz", "tiles": [ "https://9ox7r6pi8c.execute-api.us-west-2.amazonaws.com/tiles/WebMercatorQuad/{z}/{x}/{y}@1x?concept_id=C2036881735-POCLOUD&variable=sea_ice_fraction&backend=xarray&colormap_name=blues_r&rescale=%5B0%2C+1%5D&datetime=2023-12-01T00%3A00%3A01%2B00%3A00" ], "minzoom": 0, "maxzoom": 24, "bounds": [ -180.0, -90.0, 180.0, 90.0 ], "center": [ 0.0, 0.0, 0 ] } }
m = Map(location=[0, 0], zoom_start=3, min_zoom=3)
for datetime_, tilejson in tilejsons.items():
label = datetime.fromisoformat(datetime_).strftime("%Y-%m")
TileLayer(
tiles=tilejson["tiles"][0],
attr="GAMSSA SST",
overlay=True,
name=label,
show=False,
).add_to(m)
LayerControl(collapsed=False).add_to(m)
m