Skip to content

Expressions

Anywhere a value should vary per feature, an ezu node takes a raw MapLibre expression on a *-expr field:

"landuse": {
"op": "fill-solid",
"features": "@landuse_f",
"fill-expr": ["match", ["get", "kind"],
"park", "#a6c084",
"forest", "#8fae74",
"#e8e4d8"]
}

The expression is not translated into something ezu-specific. It is evaluated by maplibre-expr, a pure-Rust parser and evaluator that scores 100 % conformance against MapLibre’s official spec fixtures — including the type-checking rules, collator, the geo functions, and legacy filter conversion. Same expression, same result.

Filters. A features node’s filter-expr runs per feature:

"water_f": {
"op": "features", "layer": "water",
"filter-expr": ["all", ["==", ["get", "kind"], "water"], ["has", "name"]]
}

Legacy-form filters (bare field names, !in, !has, none) are converted with MapLibre’s own pre-compile conversion, strict-type semantics included, so an old style keeps working.

Paint values. fill-expr, color-expr, width-expr, opacity-expr, radius-expr, and the text and icon paint properties. ezu translate emits zoom and data functions onto exactly these fields, which is why a translated recipe is zoom-independent — nothing is baked to the zoom you converted at.

Whole-tile scalars. The expr op evaluates one expression per tile, with that tile’s zoom in the context, and emits a Scalar you can wire into any numeric field. That is the idiomatic way to express a MapLibre zoom curve:

"w": { "op": "expr", "expr": ["interpolate", ["linear"], ["zoom"], 10, 0.5, 16, 4] },
"roads": { "op": "stroke", "features": "@roads_f", "width-px": "@w" }

The distinction matters for cost and for what is available:

Evaluated Sees
*-expr on a paint op once per feature the feature’s properties, plus the tile’s zoom
filter-expr once per feature the same
the expr op once per tile the tile’s zoom (no feature)

So ["get", "kind"] belongs on a paint field, and ["zoom"]-only curves are cheaper as an expr node — evaluated once instead of once per feature, and cacheable as a Scalar.

Zoom-dependent numbers can be written two ways, and both are fine:

// MapLibre-native: one expression, one node
"a": { "op": "expr", "expr": ["interpolate", ["linear"], ["zoom"], 10, 0, 16, 1] }
// ezu-native: a small scalar chain
"z": { "op": "zoom" },
"zfrac": { "op": "math", "fn": "div", "a": "@z", "b": 16 },
"a": { "op": "math", "fn": "mul", "a": "$landuse-alpha", "b": "@zfrac" }
`zoom` and `math` feeding a paint field through a `Scalar` port. A `$param` keeps its runtime override all the way down the chain.
zoom and math feeding a paint field through a Scalar port. A $param keeps its runtime override all the way down the chain.

Reach for expr when you are porting a MapLibre curve or want the exact interpolation semantics. Reach for math when the value should be composed from params that a caller overrides at render time — a $param keeps its runtime-override behaviour, while an expression is fixed in the document.

  • The heatmap path bakes a colour expression over heatmap-density into a 256-entry ramp per tile, which is how a GL-JS-shaped colour function becomes a color-ramp.
  • An expression is part of the node’s cache key, so writing an expression instead of a literal costs nothing in cacheability.
  • Errors are reported at build time where the expression can be type-checked statically, and per feature at render time where it cannot.

See expression conformance for what the 100 % figure actually covers.

Map renders on this site are made fromOpenStreetMap data viaProtomaps (© OpenStreetMap contributors), elevation from Re:Earth Terrain,Mapterhorn andEGM2008 (NGA), and aerial imagery from GSI Japan(© 国土地理院). The painterly styles use CC0 brushes byDavid Revoy.