DeLac
DeLac

Reputation: 1122

Cluster markers in MapBox, how to "accumulate" just distinct properties?

I want to show markers on a map, where each marker is a UserLocation. A User can have multiple UserLocation. When I cluster markers, I'd like to show the list of the Users of those clustered markers, without duplicates.

For instance, let's take these 3 near markers:

      { // Marker1
        type: 'Feature',
        properties: {user_id : "Daniele"},
        geometry: { type        : 'Point',
                    coordinates : [lng0, lat0]
                  }
      },
      {// Marker2
        type: 'Feature',
        properties: {user_id : "Daniele"},
        geometry: { type        : 'Point',
                    coordinates : [lng1, lat1]
                  }
      },
      {// Marker3
        type: 'Feature',
        properties: {user_id : "Roberto"},
        geometry: { type        : 'Point',
                    coordinates : [lng2, lat2]
                  }
      }

When I cluster them, clicking the clustered circle, I want to see "Daniele, Roberto". How can I do that?

Moreover, I'd like to set the size of circle, according to the distinct number of different users clustered (in the example above, should be 2).


**UPDATE 2


JSFIDDLE <--

An idea could be build an array of distinct names, and then use the length expression to size the circle.

Anyway, there should be a kind of syntax error...

 clusterProperties: {
  distinctNames : 
                ['case', 
                   /*cond  */ ["!", ['in',['get', 'user_id'], ['accumulated']]], 
                   /*result*/ ['concat', ['concat', ['get', 'user_id'], ',']],
                                                         
                   /*default*/ ['accumulated']
                ]
}

Upvotes: 1

Views: 2173

Answers (2)

Florian Zdrada
Florian Zdrada

Reputation: 161

The documentation is not really clear, but here is how I achieved this "distinct accumulate".

From the clusterProperties definition:

A custom reduce expression that references a special ["accumulated"] value, e.g.:

{"sum": [["+", ["accumulated"], ["get", "sum"]], ["get", "scalerank"]]}

Which results the same as: {"sum": ["+", ["get", "scalerank"]]}

In your case, you want to accumulate the user_id property from your markers without duplicates.

The logic is to add the user_id only if it has not already been added in the accumulated value.

clusterProperties: {
  distinctUsers: [
    // ['accumulated'] is the current value iterated during the reduce (the property is defined at [1])
    // ['get', 'distinctCountries'] is the accumulated / concatenated string
    [
      // Concat accumulated value + current value if not present in accumulated
      'concat',
      ['get', 'distinctUsers'],
      [
        'case',
        ['in', ['accumulated'], ['get', 'distinctUsers']],  // If accumulated (user_id) has already been added
        '',  // Add EMPTY string
        ['concat', ', ', ['accumulated']],  // Add the user_id (concatenated with a comma in your case)
      ],
    ],
    ['get', 'user_id'], // [1]: source marker property iterated in the custom reduce function
  ]
}

As Steve said in his answer, you could also wrap the user_id in some unique character so you don't accidentally find user "rob" within another user "robin" for instance.

The source property ['get', 'user_id'] defined at [1] would become :

['concat', '%', ['get', 'user_id'], '%']

Upvotes: 3

Steve Bennett
Steve Bennett

Reputation: 126295

According to the documentation you want to do something like this:

map.addSource(userData, {
   id: 'user-locations',
   type: 'geojson',
   data: 'myuserdata.geojson',
   cluster: true,
   clusterProperties: {
      names: ['concat', ['concat', ['get', 'user_id'], ',']]
   }
}

Clustered points in your source will now have a property, names which will contain the comma-separated (and comma-terminated) string of names.

Moreover, I'd like to set the size of circle, according to the distinct number of different users clustered (in the example above, should be 2)

That sounds...challenging. One way I can think of doing that would be writing a custom accumulator function along these lines:

  • Make the function return an array of two values, [distinctNames, allNames] where the first is an integer, and the second is a string.
  • If allNames contains our current name, just return the array.
  • Otherwise, return an array which is [distinctNames + 1, allNames + thisName].

Manipulating arrays like this in Mapbox GL expressions is possible, but pretty fiddly. You need to use ['literal', ...] and ['at', ...]

The code would look something like this:

   clusterProperties: {
      names: ['concat', ['concat', ['get', 'user_id'], ',']],
      distinctNames: [
          ['case', ['in', ['get', 'distinctNames'], ['at', ['accumulated'], 1]
            ['accumulated'],
            ['literal', ['+', ['at', ['accumulated'], 0], 1], ['concat', ['at', ['accumulated'], 1], ['get', 'distinctNames']]]
          ],
          ['concat', '%', ['get', 'user_id'], '%'] // wrap user ID in some unique character so we don't accidentally find user "rob" within another user "robin" for instance.
      ]
   }

It's unclear from the documentation exactly how the accumulator function works, or how you access the current value. Their example implies that it would be ['get', <name of cluster property>] although that seems a bit weird.

Upvotes: 3

Related Questions