I am working on an application using Mapbox GL JS, in which I am showing a heatmap layer, with a data-driven ‘heatmap-weight’ property (based on a custom numerical attribute in my GeoJSON data – ‘detections’).
Because many of the points in my data are very near or even overlapping each other, their values of ‘detections’ are often counting the same detections and thus making the heatmap coloring heavier than it should, therefore I am trying to cluster them and adding a new ‘average’ (average) property, inside the ‘clusterProperties’ object, and use that instead to interpolate heatmap-weight of clustered points.
I have been digging through mapbox documentation and examples on using expressions and it seems rather straightforward to implement properties (like ‘sum’ in this example: https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/#geojson-clusterProperties) , but I have not been able to come up with a working expression to calculate the ‘average’ I need.
Basically I am trying to get a ‘sum’ of my ‘detections’ property, and divide it by the ‘point_count’ property of a cluster, so I first tried:
map.addSource(detections_heatmap_src, { type: "geojson", data: heatmapCloud_value.recordings, cluster: true, clusterRadius: 10, // Radius of each cluster when clustering points (defaults to 50) clusterProperties: { clusterTotal: ["+", ["get", "detections"]], //used for debug output text layer average: [ "let", "total", ["+", ["to-number", ["get", "detections"]]], ["/", ["number", ["var", "total"], 0], ["number", ["get", "point_count"], 1]], ] }, });
But this approach always throws the following error, which I have not been able to understand / fix:
Error: sources.detections_heatmap_src.average.reduce: Expected at least 3 arguments, but found 2 instead. at Object.ai [as emitValidationErrors] (mapbox-gl.js:31) at Oe (mapbox-gl.js:35) at je._validate (mapbox-gl.js:35) at je.addSource (mapbox-gl.js:35) at Map.addSource (mapbox-gl.js:35) at addHeatmapLayer (Map.svelte:516)
I also tried another relatively simpler approach, like so:
(...) clusterProperties: { (...) average: [ "/", ["number", ["+", ["to-number", ["get", "detections"]]]], ["number", ["get", "point_count"], 1], ], }
And with this I did not get any errors, and in some cases it even seemed to calculate the correct values (for instance 9/9 = 1), but for most other cases it is calculating completely wrong values, like 155 / 92 = 0.004408…, which should be 1.6847… or 154 / 106 = 0.46875 instead of 1.4528… .
I am checking / debugging these values by adding a text layer to output them on the map (example screenshot attached), like so:
map.addLayer({ id: detections_heatmap_clusterCount, type: "symbol", source: detections_heatmap_src, filter: ["has", "point_count"], layout: { "text-field": [ "concat", ["get", "clusterTotal"], "/", ["get", "point_count"], " = ", ["get", "average"], ], "text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"], "text-size": 12, "text-allow-overlap": true, }, paint: { "text-color": "#EE4B2B", }, });
It really seems like calculating an average should be feasible with these expressions, but I am not being able to understand what exactly is wrong with either of the expressions I have tried, so I’m hoping someone here might be able to help me with this.
UPDATE:
Following @steve-bennet’s suggestion / accepted answer, I am adding only ‘clusterTotal’ (sum) as a cluster property, like so:
map.addSource(detections_heatmap_src, { type: "geojson", data: heatmapCloud_value.recordings, cluster: true, clusterRadius: 10, // Radius of each cluster when clustering points (defaults to 50) clusterProperties: { clusterTotal: ["+", ["get", "detections"]], }, });
And then computing the average (sum/count) where I actually need to use it, in my case, for the heatmap-weight property, it meant going from this:
"heatmap-weight": [ "interpolate", ["linear"], ["get", "detections"], 0, 0, 6, 1, 18, 5 ],
To this:
"heatmap-weight": [ "case", ["has", "point_count"], [ "interpolate", ["linear"], ["/", ["number", ["get", "clusterTotal"]], ["number", ["get", "point_count"]]], 0, 0, 6, 1, 18, 5 ], ["interpolate", ["linear"], ["get", "detections"], 0, 0, 6, 1, 18, 5] ],
Advertisement
Answer
The Mapbox documentation is very terse here.
An object defining custom properties on the generated clusters if clustering is enabled, aggregating values from clustered points. Has the form
{"property_name": [operator, map_expression]}
. operator is any expression function that accepts at least 2 operands (e.g. “+” or “max”) — it accumulates the property value from clusters/points the cluster contains; map_expression produces the value of a single point.
Example:
{"sum": ["+", ["get", "scalerank"]]}
.
Your first problem is that you need to write your expression in the MapReduce paradigm – that’s the map_expression
they’re referring to. The operator is actually the reduce expression: the thing that combines the results of applying map_expression
to two different input values.
The second problem is that it’s actually not that easy to write an average function as a map/reduce expression. A sum is easy: just keep adding the next value. But an average function needs to keep track of the total number of things, and the running sum.
My suggestion would probably be to instead create two separate cluster properties, one for the sum, and one for the count, and compute the average (sum/count) at the time you use the expression, not in the clustering.
Since point_count
is provided for you already, and the example of sum is given for you there, you should have everything you need.