chronozarr
chronozarr turns a raster time series into static files that you can put in a storage bucket. Anyone with the link can then open the series in a web browser and play or scrub through it like a video. Clicking a pixel shows its values over time. The same files open in Python with the exact values you wrote.
It is for data that is hard to share today: years of monthly satellite composites, model output, or any stack of georeferenced rasters on one grid. The usual choices are to send the files, which can run to many gigabytes, or to run a tile server. A tile server has to be kept running, and it usually sends the browser images. chronozarr needs no server, and the browser receives the values themselves.
Live demo: 117 monthly Sentinel-2 composites (2015 to 2026) of the Ucayali River in Peru, read directly from a bucket.
How it works
A chronozarr store is a Zarr v3 group arranged so that a browser can read it over plain HTTP.
The map is divided into square cells, 512 by 512 pixels by default. Each chunk holds one cell at one timestep, with all of its bands. To show a view, the browser downloads only the chunks for the visible cells at the current timestep.
The store also holds a pyramid. Level 0 holds your values unchanged. Each coarser level is the mean of 2 by 2 blocks of the level below it, at half the resolution. When you zoom out, the viewer switches to a coarser level, so a view of the whole area still needs only a few chunks.
my_store/
zarr.json root metadata
0/ level 0, the original resolution
data/ (time, band, y, x)
time/ band/ y/ x/ coordinates
mask/ coverage/ optional
1/ 2/ ... coarser levels
The chunks hold the stored numbers in their original type: uint8, uint16, int16 or float32. A scale and offset per band give physical units. An optional mask marks invalid pixels. An optional coverage plane counts the valid observations behind each pixel. The viewer draws these numbers on the GPU and computes products such as true color or NDVI there. It also downloads the timesteps around the current one in the background, so stepping through time usually needs no new download.
The layout follows the Zarr conventions for pyramids (multiscales), coordinate systems (proj) and georeferencing (spatial), all at v0.1. This is why other tools can read a store without chronozarr installed: xarray, GDAL 3.13, and CarbonPlan's zarr-layer for MapLibre.
To publish a store, upload it to a static host that sends CORS headers. A sharded store also needs byte-range requests. The demo store is on Cloudflare R2, and there is no server code to run.
Quickstart
Install the package with GeoTIFF support. It needs Python 3.11 or later.
pip install "chronozarr[geo]"
Without data
-
Save this script as
quickstart.py. It writes a store of three synthetic timesteps and reads one back. The pixels are 10 m in UTM zone 31N.import numpy as np import xarray as xr import chronozarr values = np.arange(3 * 64 * 64, dtype=np.uint16).reshape(3, 1, 64, 64) da = xr.DataArray( values, dims=("time", "band", "y", "x"), coords={ "time": np.array(["2024-01-01", "2024-02-01", "2024-03-01"], dtype="datetime64[ns]"), "band": ["example"], "y": 5000000 - (np.arange(64) + 0.5) * 10, "x": 500000 + (np.arange(64) + 0.5) * 10, }, ) chronozarr.encode(da, "synthetic_store", crs="EPSG:32631", nodata=None) store = chronozarr.open_store("synthetic_store") print((store.read(t=1) == values[1]).all())
-
Run the script. It prints
True: level 0 returns the values that you wrote.python quickstart.py -
Check the store.
chronozarr validate synthetic_store chronozarr info synthetic_store
With your data
-
Write a store from your rasters.
convertis the command for files on disk or in S3. Point it at a directory, a quoted glob or ans3://prefix of GeoTIFFs with the date in each file name (20240131,2024-01-31or2024-01):chronozarr convert "scenes/*.tif" my_store --dry-run # list files and dates, check every file, write nothing chronozarr convert "scenes/*.tif" my_store
The dry run prints each file with its date, then the grid, bands, validity and size of the store. A directory or
s3://prefix is not searched recursively; use a glob such asscenes/**/*.tiffor subdirectories. Listing an S3 prefix needspip install "chronozarr[s3]"and uses your AWS credentials.A date is read from a name only when the name holds exactly one. A name with no date, with several (
20240215_2024-03), or two files with the same date are reported, never guessed.--date-patternsays where the date is, for example--date-pattern "ndvi_%Y%m%d", and--write-manifest found.csvsaves the files and dates as a manifest thatconvertreads back.Before it reads any pixels in bulk,
convertchecks every file for its date, grid and CRS, bands, dtype, scale, offset, units and nodata. It reports all problems at once, grouped by file, each with a suggested fix, and writes nothing:2 problem(s) in 2 of 24 source file(s); nothing was written: scenes/ndvi_2024-03.tif - has scales [0.5, 1.0]; scenes/ndvi_2024-01.tif has [1.0, 1.0]. A store holds one value per band for every timestep, so the sources must agree fix: if the metadata is wrong, correct it (gdal_edit.py -scale -offset -units); ... scenes/readme_copy.tif - no date in the file name fix: name the date YYYYMMDD, YYYY-MM-DD or YYYY-MM, or pass --date-pattern ...Nothing is resampled or rescaled unless you ask. Files on another grid fail with a suggestion to pass
--resampling.The store takes each band's description (its name), scale, offset and units from the files, so a Sentinel-2 file with a scale of 0.0001 gives reflectance. These must be identical in every file. It also takes the nodata value that the files declare; files that declare none give a store with no nodata, so a stored 0 is data.
chronozarr convert --helplists the manifest, Zarr and NetCDF sources and the validity rules.chronozarr encode "scenes/*.tif" my_storeis the in-memory route for GeoTIFFs on one grid. It reads uint8, uint16, int16 and float32 files by the same rules, holds the whole stack in memory and never resamples. It points you toconvertfor directories, S3 prefixes and manifests. -
Check the store against the spec.
chronozarr validate my_store
-
Read the values in Python.
import chronozarr store = chronozarr.open_store("my_store") store.read(t=42) # (band, y, x), exact stored values da = store.to_xarray(lod=0) # xarray DataArray, loaded into memory
-
Look at the store on your machine before you upload it.
chronozarr preview my_store
The command serves the store on
127.0.0.1, opens the viewer in your browser and stops on Ctrl-C. See docs/python.md. -
Upload
my_storeto a static host. docs/hosting.md has recipes for S3 with CloudFront, Cloudflare R2, Google Cloud Storage and Source Cooperative. -
Check the host.
chronozarr doctor https://your-host/my_store
-
Open the store in the hosted viewer:
https://chronozarr.org/demo/?store=https://your-host/my_store
The input must be on an EPSG grid with north up. To write a store from an xarray DataArray, call chronozarr.encode as in the script above.
Read a store without chronozarr
xarray reads each level as a plain Zarr group:
import xarray as xr
ds = xr.open_zarr("my_store", group="0", zarr_format=3, chunks=None)
GDAL 3.13 reads the CRS, the georeferencing and the values. It attaches the coarser levels as overviews when you open the data array as ZARR:"my_store":/0/data. A single time-slice subdataset shows no overviews. To get Cloud Optimized GeoTIFFs for older GDAL or QGIS, run chronozarr export-cog my_store out_dir.
Commands
Run chronozarr <command> --help for every option.
| Command | What it does |
|---|---|
convert SOURCE OUT |
The command for existing raster files. Writes a store one timestep at a time from a directory, quoted glob or s3:// prefix of dated GeoTIFFs, a manifest of COGs or PNG frames, a Zarr store or a NetCDF file. --dry-run checks every file and reports all problems per file. Also converts a v0.2 store to v0.3 |
encode INPUT OUT |
Writes a store in memory from a Zarr store, a NetCDF file or a quoted glob of GeoTIFFs. Points to convert for anything else |
append STORE INPUT |
Adds timesteps at the end of a store. See docs/append.md |
validate STORE |
Checks a store against the spec. Exits with status 1 on failure |
info STORE |
Prints the times, bands and levels of a store |
bands STORE |
Lists the bands, the role each plays and the viewer products they allow. --band-role B04=red sets a band's common_name. encode and convert take the same flag. See docs/python.md |
link STORE_URL |
Prints a viewer URL with an initial product, band, display limits and timestep, checked against the hosted store |
| preview STORE | Serves a local store on 127.0.0.1 and opens it in the viewer. Ctrl-C stops it |
| share STORE | Starts a disposable Cloudflare quick tunnel for a local store, doctor-checks the public route, and prints a viewer link. Needs cloudflared; Ctrl-C stops both processes |
| doctor TARGET | Checks a hosted URL or a local store. See docs/hosting.md |
| publish STORE --destination s3://BUCKET/PREFIX | Uploads a store to S3 or R2, checks the hosted store and prints a viewer link. --update publishes appended timesteps to the same prefix and link. Needs the publish extra. See docs/hosting.md |
| export-cog STORE OUT_DIR | Writes true-value COGs for GDAL and QGIS. Needs the geo extra |
| stac STORE --out DIR | Writes a static STAC Collection and Item. Needs the geo extra |
Other extras: notebook adds chronozarr.view(store), which shows a local store in Jupyter. netcdf and dask add those inputs. s3 adds listing of an s3:// prefix for convert.
JavaScript
npm install chronozarr
The package has four parts:
chronozarris a DOM-free reader. It returns one cell of one timestep as a typed array.chronozarr/maplibreis a MapLibre custom layer that draws a store.chronozarr/geolibreis a GeoLibre plugin that adds a store as a MapLibre layer.chronozarr-viewercopies the full viewer into a folder that you can host next to your store. See docs/viewer-distribution.md.
There is no build step and no runtime dependency. Examples are in js/README.md.
Documentation
| Topic | Document |
|---|---|
| Format rules | spec/CHRONOZARR.md |
| Python functions | docs/python.md |
| Your own data, end to end | examples/bring_your_data |
| Georeferenced PNG frames | docs/png-frames.md |
| Hosting and the doctor checklist | docs/hosting.md |
| Adding timesteps | docs/append.md |
| Embedding the viewer in a page | docs/embedding.md |
| Drawing a store on a MapLibre map | js/maplibre/README.md |
| More examples: Sentinel-2 ingest, water masks, notebooks, SWOT, NISAR | examples |
| Comparison with PMTiles, Mapbox raster-array and zarr-layer | docs/format-comparison.md |
| Measurements and tested reader versions | docs/evidence.md |
Status
The spec is v0.3.0, a draft. Python and npm package release numbers advance separately from the store format. Readers open only v0.3 stores. To use a v0.2 store, convert it:
chronozarr convert OLD_STORE NEW_STORE
License
See LICENSE.
Metadata
Release files for chronozarr 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| chronozarr-0.4.0.tar.gz | 316.4 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| chronozarr-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 497.3 kB
Release files / chronozarr-0.4.0.tar.gz
| Download URL | chronozarr-0.4.0.tar.gz |
|---|---|
| Size | 316.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4f83c8246bc51df472b02c504f063b60614a07354a20b73906dd8363d4a19d32
|
|
BLAKE2b-256 checksum How to use checksums |
0c4eaf92f41296b0326cd25ecf6931ff84553a25e1f9fa4c7b4b47bc5227f589
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.
Transparency logRelease files / chronozarr-0.4.0-py3-none-any.whl
| Download URL | chronozarr-0.4.0-py3-none-any.whl |
|---|---|
| Size | 180.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a674890eb68d0faeb3b2914b26a5c2a1f5e294a301f69edd8710c53d4957844c
|
|
BLAKE2b-256 checksum How to use checksums |
df7ff7c42fb49be5034a43a701e43dd45297f22a465bce664df70749a8977af1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.
Transparency logSecurity for chronozarr 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for chronozarr-0.4.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.