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

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']
]
}

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.

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'], '%']

Related

Using complex object for grouping in Ag Grid

I am trying to use a complex object to group my ag grid rows. Object of my rowdata looks like this -
const rowData= {
id : '123',
name: 'dummy',
category: 'A',
group : {
name : 'dummyGroup',
id : '456',
category: 'A'
}
}
Now, I am using group object to group the rows. And according to this documentation https://www.ag-grid.com/javascript-data-grid/grouping-complex-objects/ I am using keyCreator as keyCreator: params => params.value.name . My group object is uniquely identified by combination of id and catogory.
The problem that I am facing is, as I am using group.name in the keyCreator, if I have two row data object whose group.names are same but id and category are different, ag grid is grouping those rows together. I understand that this is the behavior from ag grid. So can I get any workaround for it? I need to show name on group row. But to identify the groups differently I need to use id+catogory in keyCreator. How can I achieve this ?
You need to utilise the groupRowInnerRenderer property so you can group by a combination of the id and category fields, while displaying the name as the group.
const gridOptions = {
groupDisplayType: 'groupRows',
groupRowInnerRenderer: function (params) {
return params.node.childrenAfterFilter[0].data.name;
},
columnDefs: [
{ field: 'id' },
{ field: 'name' },
{ field: 'category' },
{
field: 'group',
valueFormatter: groupValueFormatter,
rowGroup: true,
keyCreator: function (params) {
return params.value.id + params.value.category;
},
},
],
};
Demo.

Mapbox js - tileset colors assignment

I am trying to create a colored uk areas map - I need the colors to be driven by local data (say for example sales by postcode), which will always change depending on user selections like report date ranges etc. I have a tileset source which I am assigning to a layer, as in the following (sensitive values blanked out):
this.map.addLayer({
"id": "uk-pc-data",
"type": "fill",
"source-layer": "***",
"source": {
type: 'vector',
url: '***'
}
});
I am then able to style the postcode areas within the tileset by addressing a name property on the tileset features, like so:
"paint": {
"fill-color": [
"match",
["get", "name", [{name: "PR"}, {name: "CH"}, {name: "LN"}]],
[
"PR",
"DD",
"AL",
"PO"
],
"hsla(344, 84%, 29%, 0.37)",
"hsla(131, 94%, 34%, 0)"
]
}
The above will assign one color to the matched areas, and a default to all the other (the non matched ones); What I would like to do, is computing the colour value locally (so based on data constantly changing) based on the feature name, like so
"paint": {
"fill-color": function (feature = {}) {
return localSalesByArea(feature.name)
}
}
This though does not seem to be possible: any help or pointers appreciated; I have been through examples from Mapbox
such as choroplet, heatmaps, expressions but these seem to rely on values delivered via the dataset itself (i.e Population), in my case the values which determine the color scale are separated (they come from an internal reporting api)
If the data on which the colours are determined does not exist in the tileset, you basically have to do a gigantic lookup by name.
You will have a function that generates the fill-color expression, something like:
function makeFillExpression(valuesByName) {
function valueToColor(value) {
return /* insert your choroplething special sauce here */;
}
return ret = [
"match",
["get", "name"],
Object.keys(valuesByName).map(name => valueToColor(valuesByName[name])),
'black'
];
}
Yes, it will be a very large and unwieldy expression.

mapbox gl data driven styles (choropleth maps)

I'm trying to create a choropleth map using mapbox-gl. In the example choropleth maps, it looks like they set the feature's paint fill colour based on the properties of the feature. Is there a way to set the colour by accessing a map?
ie. I have tiles each with a unique id in the feature property called id. I also have a json which maps each id with a value and would like to access those values to set the colour.
Is this possible? or am i limited to only being able to access values in the feature properties?
I'm not entirely sure if I understood your question correctly. But I think what you are trying to achieve can be done with expressions:
const geojson = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {
id: 'foo'
},
geometry: {
/* */
}
}
]
};
const values = {
foo: 'green',
bar: 'red',
baz: 'blue'
};
map.addLayer({
// ...
paint: {
'fill-color': [
[
'get',
// get the id property and use it as a key into "values"
['get', 'id'],
values
]
]
}
});
See the get expression: https://www.mapbox.com/mapbox-gl-js/style-spec#expressions-get
Update from 04.2018:
Since some moment constructions like
[
'get',
// get the id property and use it as a key into "values"
['get', 'id'],
values
]
stopped to work. The following exception occures: 'Bare objects invalid. Use ["literal", {...}]'. Now it is necessary to use type expressions, like this:
[
'get',
['string', ['get', 'id'],
['literal', values]
]
See this and this for reference.

Building a dynamic mongo query for meteor

I'm building an app that has clickable 'filters'; I'm creating a list of objects(?) that I want to pass to a mongo 'find', so that I can pull out listings if selected attributes match a certain score.
My data is structured like this (a snippet):
name: 'Entry One',
location: {
type: 'Point',
coordinates: [-5.654182,50.045414]
},
dogs: {
score: '1',
when: 'seasonal',
desc: 'Dogs allowed from October to April'
},
lifeguard: {
score: '1',
when: 'seasonal',
desc: 'A lifeguard hut is manned between April and October',
times: ''
},
cafe: {
score: '1',
name:'Lovely cafe',
open:'seasonal'
}, ...
My search variable is a list of objects (I think?) that I assign to a session variable. If I output this session var ('searchString') via JSON.stringify, it looks like this:
{"cafe":{"score":"1"},"dogs":{"score":"1"}}
I'd like to pass this to my mongo find so that it only lists entries that match these scores on these attributes, but it's returning zero results. Do I need to somehow make this an $and query?
Currently it looks like this:
Beaches.find(searchString);
Unfortunately as soon as I drop searchString into the find, I get zero results even if it's empty {}. (When it's just a find() the entries list fine, so the data itself is ok)
What am I doing wrong? I'm relatively new to mongo/meteor, so I apologise in advance if it's something stupidly obvious!
Don't stringify the query. Flatten the object instead. Example:
Beaches.find({
"cafe.score": 1,
"dogs.score": 1,
});

How to do geo searches on two properties as opposed to an array of [ lng, lat ]?

All of the examples and implementations of geo search in Mongo, that I've been able to find, use an array property [lng, lat] to perform the search on:
An example record would be:
{
name: 'foo bar',
locations: [
{
name: 'franks house',
geo: [-0.12, 34.51]
},
{
name: 'jennifers house',
geo: [-0.09, 32.17]
}
]
}
And the resulting query:
db.events.find({ 'locations.geo': { $nearSphere: [ -0.12, 34.51 ], $maxDistance: 0.02 }})
This works fine, but the format of the record is not great for users to read or write because it's not self-documenting where lat and lng go in that array. There's room for gotchas. I'd like to make the records more human friendly:
{
name: 'foo bar',
locations: [
{
name: 'franks house',
lat: 34.51,
lng: -0.12
},
{
name: 'jennifers house',
lat: 32.17,
lng: -0.09
}
]
}
What would the resulting mongo query look like for this type of record? I haven't been able to find any examples of this so am wondering if it's even possible.
It's not recommended to use separate fields for latitude and longitude. The 2dsphere index is used to index geospatial data which requires an array of coordinates, see documentation. This is the reason you can't find examples for separate coordinate fields.
You should separate representation from data storage. Just because coordinates are stored in an array, you don't necessarily need to present them as an array to the user. For example you could use a pre call on save to store separate parameters in an array, something like:
var schema = new Schema(..);
schema.pre('save', function (next) {
this.coordinates = [this.longitude, this.latitude]
next();
});