Rendering Anytime tiles in MapLibre GL
Wire a grant's TileJSON into MapLibre GL JS and build color ramps from the grant's per-grain breakpoint metadata.
Fetch the grant's TileJSON and hand it to MapLibre as a vector source. The TileJSON covers every tileset the grant authorizes; renderer metadata for styling lives in its x-mw.extra_metadata block.
Minimal setup
const map = new maplibregl.Map({ /* ... */ });
map.on('load', async () => {
// tileJsonUrl is the capability URL returned when you minted the grant
map.addSource('anytime', { type: 'vector', url: tileJsonUrl });
map.addLayer({
id: 'anytime-fill',
type: 'fill',
source: 'anytime',
'source-layer': '<layer name from the manifest vector_layers>',
paint: { 'fill-color': rampExpression, 'fill-outline-color': 'rgb(18,19,46)' },
});
});Your front-end calls this with no auth header: the grant URL is the credential, pinned by the grant's allowed_origins. See Minting an Anytime tile grant.
Reading x-mw.extra_metadata (the dynamic shape)
Each TileJSON carries x-mw.extra_metadata, keyed by tileset slug. On the verified path (the manifest is fresh), each entry is a faithful pass-through of the upstream publisher's metadata, stamped with three provenance fields. Illustrative shape (long arrays abbreviated, not a parseable payload):
{
"anytime_21d529097c81af04_202501-202512_bg_tiles_v4": {
"breakpoints": {
"d": { "ST": [[...12 numbers...], [...12 numbers...]], "BG": [[...], [...]], "...": "..." },
"m": ["density", "occupancy"],
"p": [0, 1, 5, 25, 33, 50, 66, 75, 90, 95, 99, 100],
"v": 2
},
"breakpoints_spec": {},
"derived_grains": { "TRCT": "block group id less trailing digit; ST_UNION_AGG geometry; SUM occupancy; reconciliation to BG asserted at build time" },
"zoom_bands": {},
"refreshed_at": "2026-08-08T12:00:00Z",
"extra_metadata_verified": true,
"source": "bigquery-extra-metadata-kv"
}
}How to build a ramp from it:
breakpoints.mlists measures;m[0]is"density"andm[1]is"occupancy"on the current family.breakpoints.plists the percentile ranks the arrays are sampled at (12 values on the current family: 0, 1, 5, 25, 33, 50, 66, 75, 90, 95, 99, 100).dis a record keyed by geography type (ST,STCO,TRCT,BG, and whatever future grains the upstream publishes); the entry'sderived_grainssub-object lists the grains it carries. Eachd[grain]value is an array of per-measure arrays:d[grain][0]is the density breakpoints at ranksp,d[grain][1]is occupancy.zoom_bandsmaps grains to the zoom ranges where each grain should be styled.
Two rules that prevent rendering bugs:
- Do not hard-code the grain key set.
breakpoints.dis keyed by the geography type (ST,STCO,TRCT,BG, and whatever future grains the upstream publishes). The tileset'sderived_grainssub-object (a record of grain name to derivation note, exact object shape) lists which grains the entry carries; a renderer that assumes exactly['ST','STCO','TRCT','BG']will silently mis-style a tileset with a fifth grain. - Derive your own ramp from the breakpoints you receive. Pick a measure index from
m, taked[grain][measureIndex], and pair the values with your color stops at the matching ranks fromp. Example (density, one grain):
const meta = tilejson['x-mw'].extra_metadata[slug];
const mi = meta.breakpoints.m.indexOf('density'); // measure index
const stops = meta.breakpoints.d[grain][mi]; // 12 numbers
const ranks = meta.breakpoints.p; // 12 ranks
const colors = ['#12132e', '#1e1f53', '#3d5fa0', '#25c393', '#f3fffb'];
const fill = [
'interpolate', ['linear'], ['get', 'density'],
...stops.flatMap((v, i) => [v, colors[Math.min(i * colors.length / stops.length | 0, colors.length - 1)]])
];The fail-stale floor (fallback shape)
For slugs lacking a verified metadata entry, the TileJSON falls back to a hand-authored v2 shape that is observably different from the dynamic shape:
breakpointsis a flat five-key object (ST,STCO,TRCT,BG), each a five-element array of strictly-ascending numbers[0, ceil(p66), ceil(p90), ceil(p95), ceil(p99)]: one measure (density) only, five stops, not the 12-rank two-measure lattice.- The entry also carries
palette(five Motionworks ramp colors) and a literalversion: 2.
Detect which shape you got from the provenance stamps, always present:
| Field | Dynamic (verified) shape | Fallback floor |
|---|---|---|
extra_metadata_verified | true | false |
source | "bigquery-extra-metadata-kv" | "extra_metadata.breakpoints payload v2" |
breakpoints.d shape | record of 12-rank arrays per measure | five-element arrays per grain key |
A consumer that builds a generic reader should branch on extra_metadata_verified (or on Array.isArray(breakpoints.d)), not on a guess. Neither shape is an error: the floor is the guarantee that the surface never goes empty while upstream metadata is being refreshed.
Token refresh
The tile-token embedded in tiles[] expires 24h after each TileJSON fetch. Re-fetch the TileJSON after x-mw.refresh_after seconds (82,800 = 23h) for a fresh token; MapLibre picks the new tiles[] URL up automatically when you re-set the source.
Updated 3 days ago