Matt R
Matt R

Reputation: 105

How to write a Mapbox paint expression that accounts for zoom, feature-state, and data-driven styling?

I have a layer that renders point features in a geojson source as circles. Here's an example of one of the features:

{
  type: 'Feature',
  properties: {
    color: 'red',
    red: true,
    green: false
  },
  geometry: {
    type: 'Point',
    coordinates: [-77.038659, 38.931567]
  }
};

I want circle-opacity to be a product of 3 factors (some properties on the features, map zoom, and a boolean in feature-state for whether that feature should be hidden). I can't figure out a way to write an expression that accounts for all three. Restrictions around zoom rules seem to be the issue.

Here's the logic I am trying to write:

if (feature-state.hidden) {
    opacity = 0;
} else if (properties.red) {
    opacity = 1;
} else if (properties.green and zoom >= 10) {
    opacity = 1;
} else if (zoom >= 15) {
    opacity = 1;
} else {
    opacity = 0;
}

I tried writing an opacity expression like this:

'circle-opacity': [
  'case',
  ['to-boolean', ['feature-state', 'hidden']], 0,
  ['to-boolean', ['get', 'red']], 1,
  ['to-boolean', ['get', 'green']], ['case', ['>=', ['zoom'], 10], 1, 0], // I could also write a permutation of this using an ['all'] rule
  ['>=', ['zoom'], 15], 1,
  0,
],

That gets rejected with this message: "Error: layers.places.paint.circle-opacity: "zoom" expression may only be used as input to a top-level "step" or "interpolate" expression."

So then I tried something like this:

'circle-opacity': [
  'step',
  ['zoom'],
  0,
  [
    'case',
    ['to-boolean', ['get', 'red'], false], 1,
    ['to-boolean', ['get', 'green'], false], 10,
    15
  ],
  1
]

That gets rejected with this message: "Error: layers.places.paint.circle-opacity[3]: Input/output pairs for "step" expressions must be defined using literal numeric values (not computed expressions) for the input values."

As you can see, I hadn't even gotten around to adding the feature-state checks yet.

Here's a JSFiddle to play with: https://jsfiddle.net/rognstad/px4c8tbe/17/

I think I can make it work by breaking each color out into its own layer with a minZoom property and then only using an opacity expression for feature-state, but that feels really clunky and will result in worse performance.

It seems like there has to be a better way. Do you have any better suggestions for how to achieve this? Thanks!

Upvotes: 4

Views: 3358

Answers (1)

Steve Bennett
Steve Bennett

Reputation: 126045

You're really close. You're getting this error:

Error: layers.places.paint.circle-opacity[3]: Input/output pairs for "step" expressions must be defined using literal numeric values (not computed expressions) for the input values.

Because you've got the order of arguments in your ['step'] expression slightly wrong. It needs to go:

['step', ['zoom'], <value at zoom 0>, <X>, <value at zoom X>, <Y>, <value at zoom Y>

So you don't want that first 0 there.

If we refactor your pseudo code a bit you should be able to get there:

step
  zoom
  // zoom 0 to 10
  if (!feature-state.hidden && properties.red) {
    1
  } else {
    0
  }
  10 // zoom level 10+
  if (!feature-state.hidden && (properties.red || properties.green) {
    1
  } else {
    0
  }
  15 // zoom level 15+
  if (!feature-state.hidden) {
    1
  } else {
    0
  }

The expression code will look something like this. You'll have to add appropriate type conversions probably. (I haven't tested it).

['step', ['zoom'],
['case', ['all', ['!', ['feature-state', 'hidden']], ['get', 'red'], 1, 0],
10,
['case', ['all', ['!', ['feature-state', 'hidden']], ['any', ['get', 'red'], ['get', 'green']], 1, 0],
15,
['case', [['!', ['feature-state', 'hidden']], 1, 0]],

Upvotes: 6

Related Questions