@dclimate/zarr-map
v0.1.5
Published
Interactive MapLibre visualizations for dClimate-compatible Zarr and Jaxray gridded climate datasets.
Readme
@dclimate/zarr-map
@dclimate/zarr-map is a TypeScript library for rendering
dClimate-compatible Zarr datasets on interactive maps. The package exists so a
website can pass a dataset request, variable, optional map config, time range,
spatial bounds, and visual options, then get a working MapLibre visualization
without solving Zarr metadata, IPFS/gateway access, selector normalization, or
renderer compatibility itself.
The package should feel like a light map-rendering wrapper over
@dclimate/dclimate-client-js, Jaxray-compatible dataset metadata/stores,
MapLibre, and the selected Zarr renderer.
Product Direction
The primary use case is no-hassle dClimate map rendering:
<DClimateZarrMap
dataset={{
collection: "ecmwf_era5",
dataset: "temperature_2m",
variant: "finalized"
}}
gatewayUrl="https://ipfs-gateway.dclimate.net"
variable="2m_temperature"
bounds={[-12, 35, 16, 60]}
timeRange={{
start: "2024-01-01T00:00:00Z",
end: "2024-01-07T23:00:00Z"
}}
colorScale={{
palette: "viridis",
domain: [260, 320],
unit: "K",
displayUnit: "C",
quantity: "temperature"
}}
/>The package should support two usage modes:
dClimate mode
- Accept a dClimate dataset query/config.
- Use
@dclimate/dclimate-client-jsto resolve and load datasets, including spatial bounds and time-window selection when provided. - Render selected variables on a MapLibre map with light controls such as a legend, time selector, loading/error status, and inspect callback.
- Available datasets: dClimate STAC Browser
Generic grid-source mode
- Accept a normalized
GridDataSourceobject. - Support Jaxray-compatible datasets through an adapter.
- Keep the renderer reusable outside dClimate datasets.
- Accept a normalized
The preferred architecture is a generic core with first-class dClimate and Jaxray adapters, but package features should stay close to rendering and request-scoped data loading.
Product Boundaries
Include functionality that removes friction from map rendering:
- dClimate dataset resolution and bounded/time-window source loading.
- Normalized metadata contracts for Zarr/Jaxray sources.
- Selector normalization for time, band, and other non-spatial dimensions.
- MapLibre layer creation and updates.
- Renderer loading/error state.
- Light visual customization: palette/gradient, color domain, opacity, display unit, legend, time selector, and inspect callbacks.
- Request preflight checks that estimate cells and bytes before oversized map requests begin expensive bounded loading.
Avoid functionality that turns this package into an application:
- Marketplace purchase flow, wallet/order state, or decryption UX.
- CSV/JSON export.
- 2D chart rendering or chart aggregation.
- Dashboard layouts, modal workflows, generic form builders, or rich explorer pages.
- Heavy map drawing/editing tools as core behavior.
If a feature is mostly about discovering, resolving, slicing, or decrypting
dClimate data rather than rendering it, prefer implementing it upstream in
dclimate-client-js or another dClimate data package and consuming it here.
Initial Technical Direction
- Base map: MapLibre GL JS with a packaged Mapbox-dark-inspired OpenFreeMap default.
- Optional compatibility: allow users to provide existing MapLibre or Mapbox map instances where practical.
- First renderer candidate:
@carbonplan/zarr-layer. - Avoid a custom WebGL raster renderer unless the research spike proves existing renderers cannot support target datasets.
- Keep
@dclimate/dclimate-client-jsoptional for users who only need generic grid-source mode. - Keep React UI primitives small. Prefer prop-driven rendering over bundled explorer workflows.
Target Package Shape
@dclimate/zarr-map
@dclimate/zarr-map/core
@dclimate/zarr-map/react
@dclimate/zarr-map/dclimatePlanned responsibilities:
src/core: framework-agnostic contracts, selectors, validation, color scales, and query helpers.src/renderers: renderer bridges, starting with MapLibre plus@carbonplan/zarr-layer.src/dclimate: dClimate client adapter and dataset discovery/loading helpers.src/react: React components such asZarrMap,DClimateZarrMap,TimeSlider, andLegend.docs: architecture notes and decision records.tests: future unit and integration tests.fixtures: small fixture datasets or mocks for tests.todo: implementation tickets with acceptance criteria and dependencies.
MVP API Target
import { DClimateZarrMap } from "@dclimate/zarr-map/react";
export function App() {
return (
<DClimateZarrMap
dataset={{
collection: "ecmwf_era5",
dataset: "temperature_2m",
variant: "finalized"
}}
gatewayUrl="https://ipfs-gateway.dclimate.net"
variable="2m_temperature"
timeDimension="time"
initialSelector={{ time: "2024-01-01T00:00:00Z" }}
bounds={[-12, 35, 16, 60]}
timeRange={{
start: "2024-01-01T00:00:00Z",
end: "2024-01-07T23:00:00Z"
}}
colorScale={{
palette: "viridis",
domain: [260, 320],
unit: "K"
}}
controls={{
timeSlider: true,
legend: true,
inspect: true
}}
/>
);
}Generic mode should use the same renderer through a normalized source:
import { createJaxraySource } from "@dclimate/zarr-map/core";
import { ZarrMap } from "@dclimate/zarr-map/react";
const source = createJaxraySource(dataset);
<ZarrMap
source={source}
variable="precipitation"
selector={{ time: 0 }}
controls={{ timeSlider: true, legend: true }}
/>;Install
npm install @dclimate/zarr-map maplibre-gl @carbonplan/zarr-layerReact users also install react and react-dom. dClimate mode additionally
uses the optional peers @dclimate/dclimate-client-js and @dclimate/jaxray.
Demo
- Demo URL after Vercel deployment: https://zarr-map-demo.vercel.app
- Demo source: https://github.com/dClimate/zarr-map-demo
The standalone Vite demo app consumes this package like an external user. For
local development before the first NPM publish, the demo can depend on the
package with a local file:../zarr-map-visualizer link.
Package Entrypoints
import { createJaxraySource, queryPoint } from "@dclimate/zarr-map/core";
import {
createDefaultMapLibreMapConfig,
createMapLibreGridLayer
} from "@dclimate/zarr-map/renderer";
import { createDClimateSource } from "@dclimate/zarr-map/dclimate";
import { DClimateZarrMap, ZarrMap } from "@dclimate/zarr-map/react";Generic Grid Mode
import { createJaxraySource } from "@dclimate/zarr-map/core";
import { ZarrMap } from "@dclimate/zarr-map/react";
const source = createJaxraySource(dataset, {
bounds: [-10, 35, 5, 45],
spatialDimensions: { x: "lon", y: "lat" }
});
<ZarrMap
colorScale={{ palette: "viridis", domain: [0, 50], unit: "mm" }}
controls={{ legend: true, timeSlider: true, inspect: true }}
source={source}
variable="precipitation"
/>;dClimate Mode
import { DClimateZarrMap } from "@dclimate/zarr-map/react";
<DClimateZarrMap
colorScale={{
palette: "viridis",
domain: [260, 320],
unit: "K",
displayUnit: "C",
quantity: "temperature"
}}
dataset={{
collection: "ecmwf_era5",
dataset: "temperature_2m",
variant: "finalized"
}}
gatewayUrl="https://ipfs-gateway.dclimate.net"
initialSelector={{ time: "2024-01-01T00:00:00.000Z" }}
bounds={[-12, 35, 16, 60]}
timeRange={{
start: "2024-01-01T00:00:00Z",
end: "2024-01-07T23:00:00Z"
}}
variable="2m_temperature"
/>;bounds and timeRange are convenience aliases for
sourceOptions.selection.bounds and sourceOptions.selection.timeRange. If a
caller needs lower-level dClimate selection options, it can pass
sourceOptions.selection directly. The aliases normalize into that same
selection object and do not create a separate selection model.
map is optional for React components. When omitted, the package uses a
Mapbox-dark-inspired OpenFreeMap basemap centered at [10, 50], zoom 2.25,
with MapLibre globe projection enabled in the style object. map.bounds and
map.maxBounds are forwarded to MapLibre for viewport fitting and pan limits
when callers provide a custom map config.
Default Basemap
ZarrMap and DClimateZarrMap use the packaged default basemap when the
map prop is omitted:
<ZarrMap
colorScale={{ palette: "viridis", domain: [0, 50], unit: "mm" }}
controls={{ legend: true, timeSlider: true, inspect: true }}
source={source}
variable="precipitation"
/>Callers that need an explicit MapLibre config can reuse the same default:
import { createDefaultMapLibreMapConfig } from "@dclimate/zarr-map/renderer";
const portugalMap = createDefaultMapLibreMapConfig({
attributionPosition: "top-left",
center: [-8.6, 39.5],
projection: "globe",
zoom: 5
});The helper returns a fresh map config and style object on every call. The
default style uses OpenFreeMap/OpenMapTiles vector data with a
Mapbox-dark-inspired palette, and keeps projection in the style JSON so globe
mode works when the package owns the MapLibre instance.
The default map config shows expanded attribution for OpenFreeMap,
OpenMapTiles, OpenStreetMap, and MapLibre in the bottom-right corner. Use
attributionPosition to move it to "top-left", "top-right",
"bottom-left", or "bottom-right".
Map Position And Style
The map prop accepts the MapLibre options this package owns, without
container. Use it to set the initial camera or replace the basemap style:
import { createDefaultMapLibreMapConfig } from "@dclimate/zarr-map/renderer";
<ZarrMap
map={createDefaultMapLibreMapConfig({
center: [-8.6, 39.5],
zoom: 5
})}
source={source}
variable="precipitation"
/>;center is [longitude, latitude], not [latitude, longitude]. For example,
Portugal is near [-8.6, 39.5]. zoom uses MapLibre's normal scale: lower
numbers show more of the world, while higher numbers move closer to the data.
For a known region, prefer map.bounds when you want MapLibre to fit the
initial camera to an extent instead of hand-picking a center and zoom:
import { createDefaultMapLibreMapConfig } from "@dclimate/zarr-map/renderer";
<DClimateZarrMap
bounds={[-12, 35, 16, 60]}
dataset={{
collection: "ecmwf_era5",
dataset: "temperature_2m",
variant: "finalized"
}}
map={{
...createDefaultMapLibreMapConfig(),
bounds: [
[-12, 35],
[16, 60]
],
maxBounds: [
[-20, 30],
[25, 65]
]
}}
timeRange={{
start: "2024-01-01T00:00:00Z",
end: "2024-01-07T23:00:00Z"
}}
variable="2m_temperature"
/>;bounds on DClimateZarrMap selects the data to load. map.bounds controls
the initial MapLibre viewport, and map.maxBounds limits how far the user can
pan. dClimate selection bounds use [west, south, east, north]; MapLibre camera
bounds use [[west, south], [east, north]]. When map.bounds is present,
MapLibre fits the initial camera to those bounds, so center and zoom are
usually unnecessary.
To replace the basemap entirely, pass any MapLibre style URL or style object:
<ZarrMap
map={{
center: [0, 20],
zoom: 1.5,
style: "https://demotiles.maplibre.org/style.json"
}}
source={source}
variable="precipitation"
/>When you provide a custom style object and want globe mode, include
projection: { type: "globe" } in that style object.
Globe Projection
The package default uses MapLibre's globe projection. To force a flat map while
keeping the packaged basemap, pass projection: "mercator":
import { createDefaultMapLibreMapConfig } from "@dclimate/zarr-map/renderer";
<ZarrMap
map={createDefaultMapLibreMapConfig({ projection: "mercator" })}
source={source}
variable="precipitation"
/>;For custom styles, put projection in the MapLibre style object:
<ZarrMap
colorScale={{ palette: "viridis", domain: [0, 50], unit: "mm" }}
controls={{ legend: true, timeSlider: true, inspect: true }}
map={{
center: [0, 20],
zoom: 1.5,
style: {
version: 8,
projection: { type: "globe" },
sources: {
"base-raster": {
type: "raster",
tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
tileSize: 256
}
},
layers: [
{
id: "base-raster",
type: "raster",
source: "base-raster"
}
]
}
}}
source={source}
variable="precipitation"
/>If a custom basemap is loaded from a style URL, create the map yourself with
renderer.mapFactory and switch the projection after MapLibre loads the style:
<ZarrMap
map={{
center: [0, 20],
zoom: 1.5,
style: "https://demotiles.maplibre.org/style.json"
}}
renderer={{
mapFactory: async (config) => {
const maplibre = await import("maplibre-gl");
const map = new maplibre.Map(config as ConstructorParameters<typeof maplibre.Map>[0]);
map.on("style.load", () => map.setProjection({ type: "globe" }));
return map;
}
}}
source={source}
variable="precipitation"
/>Globe mode is provided by MapLibre and the selected Zarr renderer. dClimate dataset selection, bounds, time controls, legends, opacity, and inspect callbacks continue to use the same package API.
Visual Customization
colorScale.palette accepts a built-in palette name or a custom color-stop
array such as ["#1d4ed8", "#f8fafc", "#b91c1c"]. Renderer domains stay in
the source unit. Built-in names include viridis, inferno, magma,
plasma, cividis, turbo, greys, temperature, precipitation,
humidity, vegetation, and wind. The magma, precipitation, and
vegetation palettes start transparent at the lower bound for overlaying low,
dry, or no-vegetation cells on top of the basemap. To show a converted display
unit in legends and inspect results, include unit, displayUnit, and
quantity:
colorScale={{
palette: ["#1d4ed8", "#f8fafc", "#b91c1c"],
domain: [260, 320],
unit: "K",
displayUnit: "C",
quantity: "temperature"
}}Supported display conversions are temperature (K, C, F) and precipitation
(m, mm, in).
To override the variable name or unit text shown in the legend, including when
using DClimateZarrMap with a dataset request, pass a legend control object:
<DClimateZarrMap
dataset={{ collection: "ecmwf_era5", dataset: "temperature_2m", variant: "finalized" }}
variable="2m_temperature"
colorScale={{ palette: "viridis", domain: [260, 320], unit: "K" }}
controls={{ legend: { variableName: "Air temperature", unit: "custom units" } }}
/>These fields change the legend text only. Use displayUnit and quantity when
values should be converted before display.
Inspect controls default to click-to-inspect with controls={{ inspect: true }}.
Use hover mode for a pointer-following tooltip:
<ZarrMap
...
controls={{ inspect: { mode: "hover", debounceMs: 75 } }}
/>Request Preflight
dClimate mode can estimate request size before bounded raster chunks are materialized:
<DClimateZarrMap
...
preflight={{ limits: { maxCells: 100_000, maxBytes: 256 * 1024 * 1024 } }}
onPreflight={(result) => {
console.log(result.cells, result.bytes, result.warnings);
}}
/>The framework-independent helper is also exported from core as
preflightGridRequest.
The dClimate gateway is not generally down. On June 9, 2026,
../dclimate-client-js-autoresearch still opened the current STAC ERA5 dataset
through @dclimate/dclimate-client-js plus @dclimate/jaxray, and its smoke
benchmark succeeded against https://ipfs-gateway.dclimate.net.
Current validated fixture:
- collection:
ecmwf_era5 - dataset:
temperature_2m - variant:
finalized - variable:
2m_temperature - CID:
bafyr4ifr5jtjeommsulg7srirdkskybj5pay3n4qyehecyzsesas5k2kd4
The earlier blocked static CIDs should be treated as stale or unretrievable
fixtures, not evidence that the gateway or Jaxray are broken. dClimate loading
should call the real DClimateClient.loadDataset({ request, options }) API and
unwrap the returned [Dataset, metadata] tuple.
Query Helpers
import { queryGrid, queryPoint } from "@dclimate/zarr-map/core";
const clicked = await queryPoint(source, "temperature", [-8.6, 39.5], {
time: "2024-01-01T00:00:00.000Z"
});
const bounded = await queryGrid({
source,
variable: "temperature",
geometry: { type: "BoundingBox", bounds: [-9, 38, -8, 40] },
maxCells: 5000
});Development
npm run typecheck
npm run format:check
npm run lint
npm run test
npm run buildCI runs the same package checks on pushes and pull requests. The demo app has
its own build and deployment workflow in the separate zarr-map-demo
repository. Release expectations and known renderer limitations are documented
in docs/release-checklist.md.
