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.m lists measures; m[0] is "density" and m[1] is "occupancy" on the current family.
  • breakpoints.p lists 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).
  • d is a record keyed by geography type (ST, STCO, TRCT, BG, and whatever future grains the upstream publishes); the entry's derived_grains sub-object lists the grains it carries. Each d[grain] value is an array of per-measure arrays: d[grain][0] is the density breakpoints at ranks p, d[grain][1] is occupancy.
  • zoom_bands maps grains to the zoom ranges where each grain should be styled.

Two rules that prevent rendering bugs:

  1. Do not hard-code the grain key set. breakpoints.d is keyed by the geography type (ST, STCO, TRCT, BG, and whatever future grains the upstream publishes). The tileset's derived_grains sub-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.
  2. Derive your own ramp from the breakpoints you receive. Pick a measure index from m, take d[grain][measureIndex], and pair the values with your color stops at the matching ranks from p. 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:

  • breakpoints is 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 literal version: 2.

Detect which shape you got from the provenance stamps, always present:

FieldDynamic (verified) shapeFallback floor
extra_metadata_verifiedtruefalse
source"bigquery-extra-metadata-kv""extra_metadata.breakpoints payload v2"
breakpoints.d shaperecord of 12-rank arrays per measurefive-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.


Did this page help you?