skip to Main Content

The way I’ve typically seen mapboxgl fill properties work on choropleth maps is something like this:

map.on('load', function () {
      map.addSource('bb', { type: 'geojson', data: data, generateId: true});
      map.addLayer({
        'id': 'berlin',
        'type': 'fill',
        'source': 'bb',
        'paint': {
          'fill-color': {
          'property': some_numeric_val,
          'stops': [[4, '#feebe2'], [8, '#fbb4b9'], [12, '#f768a1'], [16, '#c51b8a'], [20, '#7a0177']]
          },
        'fill-opacity': .65
          }
      });
      map.addLayer({
        'id': 'berlin-stroke',
        'type': 'line',
        'source': 'bb',
        'paint': {
          'line-color': '#000',
          'line-width': [
          'case',
            ['boolean', ['feature-state', 'hover'], false],
            2,
            .5
          ]
        }
      });
    });

i.e. the colors are created based on a property that the user selects. However, it seems like mapboxgl’s default behavior is to interpolate colors. For example, if one of my geographic units has a value is somewhere between the breakpoints, mapboxgl will interpolate the color, resulting in a gradient of colors.

Is there a way to make the colors distinct (non-interpolated)? i.e. if value is 4 or less, the color is #feebe2, if the value is 8 or less, the color is ‘#fbb4b9’, for a total of 5 discrete colors in the example I have here.

I have not been able to find an answer to this anywhere. Thanks.

2

Answers


  1. Instead of stops, you can use match.

    map.on('load', function () {
      map.addSource('bb', { type: 'geojson', data: data, generateId: true });
      map.addLayer({
        'id': 'berlin',
        'type': 'fill',
        'source': 'bb',
        'paint': {
          'fill-color': [
            'match',
            ['get', 'some_numeric_val'],
            4, '#feebe2',
            8, '#fbb4b9',
            12, '#f768a1',
            16, '#c51b8a',
            20, '#7a0177',
            // Add a default color if no values match
            '#000000', // Example - black
          ],
          'fill-opacity': 0.65,
        },
      });
      map.addLayer({
        'id': 'berlin-stroke',
        'type': 'line',
        'source': 'bb',
        'paint': {
          'line-color': '#000',
          'line-width': ['case', ['boolean', ['feature-state', 'hover'], false], 2, 0.5],
        },
      });
    });
    

    In the example provided above, match compares some_numeric_val to each value and returns the color if there’s a match. If there isn’t, it returns a default color that you can set.

    EDIT: To also include a value between the specified range of values you have to preprocess the data first.

    Here’s an example of how:

    map.on('load', function () {
      // Example data
      const data = [
        { id: 1, some_numeric_val: 3 },
        { id: 2, some_numeric_val: 6 },
        { id: 3, some_numeric_val: 10 },
      ];
    
      // Set ranges
      const colorOptions = [
        { value: 4, color: '#feebe2' },
        { value: 8, color: '#fbb4b9' },
        { value: 12, color: '#f768a1' },
        { value: 16, color: '#c51b8a' },
        { value: Infinity, color: '#7a0177' } // values > 16
      ];
    
      // Preprocess the data
      function preprocessData(data, colorOptions) {
        colorOptions.sort((a, b) => a.value - b.value);
    
        const preprocessedData = data.map((feature) => {
          const currentValue = feature.some_numeric_val;
          let color;
          // Find the appropriate color for the current value
          for (let i = 0; i < colorOptions.length; i++) {
            if (currentValue <= colorOptions[i].value) {
              color = colorOptions[i].color;
              break;
            }
          }
          return { ...feature, color };
        });
        return preprocessedData;
      }
    
      const preprocessedData = preprocessData(data, colorOptions);
    
      map.addSource('bb', { type: 'geojson', data: { type: 'FeatureCollection', features: preprocessedData }, generateId: true });
      map.addLayer({
        'id': 'berlin',
        'type': 'fill',
        'source': 'bb',
        'paint': {
          'fill-color': ['get', 'color'],
          'fill-opacity': 0.65
        }
      });
      map.addLayer({
        'id': 'berlin-stroke',
        'type': 'line',
        'source': 'bb',
        'paint': {
          'line-color': '#000',
          'line-width': [
            'case',
            ['boolean', ['feature-state', 'hover'], false],
            2,
            0.5
          ]
        }
      });
    });
    
    Login or Signup to reply.
  2. You can use step expressions.

    Produces discrete, stepped results by evaluating a piecewise-constant function defined by pairs of input and output values ("stops"). The input may be any numeric expression (e.g., ["get", "population"]). Stop inputs must be numeric literals in strictly ascending order. Returns the output value of the stop just less than the input, or the first output if the input is less than the first stop.

    Syntax

    ["step",
        input: number,
        stop_output_0: OutputType,
        stop_input_1: number, stop_output_1: OutputType,
        stop_input_n: number, stop_output_n: OutputType, ...
    ]: OutputType
    

    Reference : Sample example from mapbox which demonstrate similar requirement as mentioned in question.

    You can try updating your code like below.

    map.addLayer({
      'id': 'berlin',
      'type': 'fill',
      'source': 'bb',
      'paint': {
        'fill-color': [
          // Use step expressions (https://docs.mapbox.com/style-spec/reference/expressions/#step)
          'step',
          // Replace some_numeric_val by required property name from which you want to get the value.
          ['get', 'some_numeric_val'],  // input: number,
          // Set color which is expected to fill when value is less than 5, since 5 is first step value (stop_input_1) which is mentioned in next parameter value.
          // stop_output_0: OutputType,
          '#feebe2',
          // Property value & required color to apply from given value till Property value mentioned in next step.
          // stop_input_1: number, stop_output_1: OutputType,
          // For current example #fbb4b9 will for value between 5 to 8.
          5, '#fbb4b9',
          // Property value & required color to apply from given value till Property value mentioned in next step.
          // stop_input_2: number, stop_output_2: OutputType,
          // For current example #f768a1 will for value between 9 to 12.
          9, '#f768a1',
          // Property value & required color to apply from given value till Property value mentioned in next step.
          // stop_input_3: number, stop_output_3: OutputType,
          // For current example #c51b8a will for value between 13 to 16.
          13, '#c51b8a',
          // Property value & required color to apply from given value till Property value mentioned in next step.
          // stop_input_4: number, stop_output_4: OutputType,
          // For current example #7a0177 will for value between 17 to 20.
          17, '#7a0177',
          // Property value & required color to apply from given value till Property value mentioned in next step.
          // stop_input_5: number, stop_output_5: OutputType
          // For current example #7a0177 will for value >= 21
          21, '#7a0177'
        ],
        'fill-opacity': .65
      }
    });
      
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search