Mapbox GL JS Change the fill-color's property value based on an expression - mapbox

I have a chloropleth map with a geojson data source backing it. The data set contains data for each country for two different years, with the fill color based on one of the properties in the JSON. Here is my addLayer code currently, which works fine.
map.addLayer({
id: 'emissions',
type: 'fill',
source: {
type: 'geojson',
data: './data.json'
},
paint: {
'fill-color': {
property: 'total_2014',
type: 'exponential',
base: 0.99999,
stops: [
[3, "hsl(114, 66%, 53%)"],
[2806634, "hsl(0, 64%, 51%)"]
]
},
'fill-opacity': 1
}
});
I would like to be able to programatically switch the json property on which the fill color is based, and an expression seems the obvious way to do so, however the following code fails with the error layers.emissions.paint.fill-color.property: string expected, array found.
...
paint: {
'fill-color': {
property: ['get', propName], // propName var is e.g. 'total_2014'
type: 'exponential',
base: 0.99999,
stops: [
[3, "hsl(114, 66%, 53%)"],
[2806634, "hsl(0, 64%, 51%)"]
]
},
'fill-opacity': 1
}
...
Is there a way to achieve what I'm aiming for? I'm very new to using Mapbox GL JS, so apologies if this is a basic question.

Just in case anyone else happens across this requirement, I found a workaround by updating the map's style property directly. This isn't an exact answer since the approach doesn't use expressions, but the performance of mapbox diffing source and applying the changes is very fast and meets my requirements.
function loadDataForYear(year) {
const style = map.getStyle();
style.layers.find(({ id }) => id === "emissions").paint['fill-color']['property'] = 'total_' + year;
map.setStyle(style);
}

Related

Edit a geojson source in mapbox and re-render layer

I have a geojson layer for extruding polygons in mapbox . A filter that at the beginning shows nothing.
map.current.addSource('congeoex', {
type: 'geojson',
data: 'http://localhost:3000/geometries.geojson'
});
map.current.addLayer({
'id': 'congeoex',
'type': 'fill-extrusion',
'source': 'congeoex',
'paint': {
'fill-extrusion-color': 'black',
'fill-extrusion-height': ['+',['to-number', ['get', 'mytop']] , 7],
'fill-extrusion-base': ['+',['to-number', ['get', 'mytop']], 5]
},
//show nothing at first
'filter': ['==', ['get','no'], 0]
});
Then I dynamically get an array of no properties, representing features to be somehow extruded.
After I get the array, I change the filter of the layer . Then I want to edit the geojson source of the layer, based on that array and add a custom attribute to the geojson that is the increasing elevation of the extrution. That custom attribute must be named mytop, to be the same found in 'fill-extrusion-height fill-extrusion-base.
map.current.setFilter('congeoex', ['in',['get', 'no'] , ['literal',array_of_no]] )
const geojsonSource = map.current.getSource('congeoex');
console.log('geojsonSource ', geojsonSource);
//edit geojson to add mytop attribute
//map.current.triggerRepaint();
I cannot actually do that because the map.current.getSource('congeoex'); does not give back the actual geojson data of the layer to be edited.
I tried getting the geojson data and editing, but looks like it changes the data on the front-end locally, not the data in mapbox, because I see no change on the map
fetch('http://localhost:3000/geometries.geojson')
.then((res)=>{
const data = res.json()
return data
})
.then(a => {
let anew = a.features.filter(item => array_of_no.includes(item.properties.no));
let elevHeight = 10;
anew.forEach(e =>{
e.properties.customTop = elevHeight;
elevHeight = elevHeight + 10;
});
})
map.current.setFilter('congeoex', ['in',['get', 'no'] , ['literal',array_of_no]] )
map.current.triggerRepaint();
So, how can I edit the geojson source of a mapbox layer, change layer filter and then re-render the map ?
Thank you
You're so close.
Fetch the GeoJSON file.
Manipulate it however you want.
Call map.getSource('congeoex').setData(...)

Update style of individual feature from single geoJSON source on Mapbox map, when clicked

I'm working with Mapbox GL JS to plot geoJSON data on a map using their external geoJSON example as a starting point. The geoJSON file contains lots of features which are plotted as individual markers on the same layer. I would like to highlight the clicked marker by changing its colour from red to blue. I have adapted the example to show a pop-up with the point id when clicked (just as a proof of concept that the markers can fire events when clicked), however, I'm struggling to find a way to change the styling of the individual clicked marker.
The code is currently as follows:
mapboxgl.accessToken = 'pk.eyJ1IjoiZGFuYnJhbWFsbCIsImEiOiJjbDB3ODFveHYxOG5rM2pubWpwZ2R1Y2xuIn0.yatzJHqBTjQ6F3DHASlriw';
const map = new mapboxgl.Map({
container: 'map', // container ID
style: 'mapbox://styles/mapbox/satellite-v9', // style URL
zoom: 7, // starting zoom
center: [138.043, 35.201] // starting center
});
map.on('load', () => {
map.addSource('earthquakes', {
type: 'geojson',
data: 'https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson'
});
map.addLayer({
'id': 'earthquakes-layer',
'type': 'circle',
'source': 'earthquakes',
'paint': {
'circle-radius': 8,
'circle-stroke-width': 2,
'circle-color': 'red',
'circle-stroke-color': 'white'
}
});
});
map.on('click', 'earthquakes-layer', (e) => {
new mapboxgl.Popup()
.setLngLat(e.lngLat)
.setHTML('Id: ' + e.features[0].properties.id)
.addTo(map);
});
Here is a codepen: https://codepen.io/danb1/pen/BaYjOyx
Is it the case that it's actually not possible to use this approach, and instead each feature from the geoJSON file needs to be plotted as a separate layer? I'm struggling to find any examples of this and am not able to modify the geoJSON source — it has to come from one single file (rather than loading multiple geoJSON files separately on separate layers).
This is possible using feature-state. The first thing to do is to ensure the layer data contains ids for each feature (in the example the source data doesn't so we need to add generateId: true to the map.addSource method).
We then need to add mousemove and mouseleave events to the map to store the moused-over feature id (if there is one, i.e. if the mouse is hovering over a feature):
let hoveredEarthquakeId = null;
map.on('mousemove', 'earthquakes-layer', (e) => {
map.getCanvas().style.cursor = 'pointer';
if (e.features.length > 0) {
map.setFeatureState(
{ source: 'earthquakes', id: e.features[0].id },
{ hover: true }
);
hoveredEarthquakeId = e.features[0].id;
}
});
map.on('mouseleave', 'earthquakes-layer', () => {
map.getCanvas().style.cursor = '';
if (hoveredEarthquakeId !== null) {
map.setFeatureState(
{ source: 'earthquakes', id: hoveredEarthquakeId },
{ hover: false }
);
}
hoveredEarthquakeId = null;
});
Finally, in the layer properties, the colour setting of the circle needs to be updated to reflect the hover value stored against the feature:
'circle-color': [
'case',
['boolean', ['feature-state', 'hover'], false],
'#00f',
'#f00'
],
The final thing can be seen in the modified pen. There is also a MapBox tutorial covering this kind of thing in a slightly more complicated way, which I hadn't come across until now: https://docs.mapbox.com/help/tutorials/create-interactive-hover-effects-with-mapbox-gl-js/.

Check if a GeoJSON source is present in mapbox viewport

I have a map with several layers of GeoJSON each with their own unique layer name:
var map = new mapboxgl.Map({
container: 'map',
center: [-97.5651505, 37.89549,],
zoom: 4
});
var sources = {
'ord': 'chicago',
'pit': 'pittsburgh',
'atl': 'atlanta'
};
map.on('load', function () {
for (var s in sources) {
map.addSource(s, { type: 'geojson', data: `/geojson/${s}.json` });
map.addLayer({
'id': sources[s],
'type': 'fill',
'source': s,
'layout': {
'visibility': 'visible'
},
'paint': {
'fill-color': '#088',
'fill-opacity': 0.5
}
});
}
});
I would like to check if a user has zoomed in past zoom level 13 evaluate if any of these three layers is in the viewport. If it is I'll take action to add a button to the overlay. However, I'm having issues finding any documentation other than leaflet on how to check if a layer is inside the viewport. I've found some mention of markers that that doesn't seem to apply.
You can achieve this with queryRenderedFeatures which returns an array of features rendered within a given bounding box. However, if you omit the bounding box argument, queryRenderedFeatures will query within the entire viewport. You can also use the options.layers argument to limit your query to specific layers to avoid getting a bunch of features that are in the underlying style (for example, streets and lakes). You can do this query in a zoomend event listener to achieve your desired outcome. Putting it all together would look something like this:
map.on('zoomend', () => {
if (map.getZoom() > 13) {
const visibleFeatures = map.queryRenderedFeatures(null, {layers: ['ord', 'pit', 'atl']});
// if none of the layers are visible, visibleFeatures will be an empty array
if (visibleFeatures.length) {
// figure out which layers are showing and add your button
}
}
});

How to use clusterProperties in mapbox-gl

I built a mapbox-gl js (v0.52) map where points are getting aggregated into clusters; much like in the clusters example from mapbox page.
The cluster color needs to be a function of an aggregation of individual points properties: For simplicity, say each point has a status property, which determine its color, and the color of a cluster should just be the color corresponding to the max of each of its points' status values.
Example geojson data would look like:
const geoData = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {
id: 'fakeid11',
status: 20,
},
geometry: {
type: 'Point',
coordinates: [-151.5129, 63.1016, 0]
}
},
{
type: 'Feature',
properties: {
id: 'fakeid22',
status: 10,
},
geometry: {
type: 'Point',
coordinates: [-150.4048, 63.1224, 105.5]
}
}
]
};
I am trying to use clusterProperties to compute the aggregation as described in the api docs, similar to this example from the source code, to create this layer:
map.addSource('earthquakes', {
type: 'geojson',
data: geoData,
cluster: true,
clusterMaxZoom: 14, // Max zoom to cluster points on
clusterRadius: 50, // Radius of each cluster when clustering points (defaults to 50)
clusterProperties: {
status: ['max', ['get', 'status']]
}
});
This snippet is exactly like the clusters example from mapbox page, just replacing the data by my static 2-elements copy, and adding clusterProperties.
However this throws a validation error (a bit mangled from the minified mapbox-gl version):
Error: sources.earthquakes: unknown property "clusterProperties"
at Object.Jr [as emitValidationErrors] (modules.js?hash=34588b9e7240c1a9b3fd2f8685e299d9dbbb40d9:504146)
at De (modules.js?hash=34588b9e7240c1a9b3fd2f8685e299d9dbbb40d9:504150)
at i._validate (modules.js?hash=34588b9e7240c1a9b3fd2f8685e299d9dbbb40d9:504150)
What is wrong with this clusterProperties usage? It would seem it is simply refusing to validate this property. Note that the map works ok (without the status aggregated property of course) if I comment the last 3 lines that set it.
I found the answer days later: The version of react-map-gl we were using had dependency on mapbox-gl ~0.52.0, which did not yet support clusterProperties. Support for these aggregated properties comes in mapbox-gl 0.53. (And since the react wrapper uses undocumented features of mapbox-gl, they depend on exact versions at patch level). This was confirmed by the react library developers.
Now react-map-gl 4.0.14 is released and it works ok with clusterProperties, using internally mapbox-gl 0.53.0.

I'm attempting to get zip codes to display within zip code borders

Several moments are happening correctly in this effort, the map is displaying as expected, the zip code boundaries are showing as expected, but I'm not able to figure how to get 5-digit zip codes to be the label within each zip code boundary. Any help (with example code if possible) would be greatly appreciated!
Here's some code:
<html>
<div id='mapdiv'></div>
...
mapboxgl.accessToken='<token>';
var mapobj = new mapboxgl.Map({
container: 'mapdiv',
style: 'mapbox://styles/mapbox/streets-v9',
minZoom: 3,
maxZoom: 20,
zoom: 10,
center: [-105.1613858,39.6791558]
});
<script src="https://api.mapbox.com/mapbox-gl-js/v0.39.1/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v0.39.1/mapbox-gl.css" rel="stylesheet" />
...
mapobj.on('load', function() {
// Add ZipCode source to map
mapobj.addSource('zipSource', {
type: 'vector',
url: 'mapbox://mapbox.enterprise-boundaries-p2-v1'
});
mapobj.showTileBoundaries = true;
// Add hot ZipCode layer to map
mapobj.addLayer({
id: 'hotZipLayer',
type: 'fill',
source: 'zipSource',
'source-layer': 'boundaries_postal_2',
paint: {
'fill-outline-color': 'rgba(0,0,0,1)',
'fill-color': 'rgba(0,0,0,0.01)'
}
});
// Add Zip numbers symbol layer to map
mapobj.addLayer({
id: 'zipCodeLabels',
type: 'symbol',
source: 'zipSource',
'source-layer': 'points_postal_2',
layout: {
'text-field': '{id}',
'text-justify': 'center',
'text-size' : 10
},
paint: {
'text-color': 'black',
'text-halo-color': 'white',
'text-halo-width': 25
}
});
});
And an example data entry:
[
{
"geometry":
{
"type":"Point","coordinates":[-105.0908374786377,39.707747919880916]
},
"type":"Feature",
"properties":
{
"id":"USP280226"
},
"id":2,
"layer":
{
"id":"zipCodeLabels",
"type":"symbol",
"source":"zipSource",
"source-layer":"points_postal_2",
"layout":
{
"text-field":"{id}",
"text-justify":"center",
"text-size":10
},
"paint":
{
"text-color":"black",
"text-halo-color":"white",
"text-halo-width":25
}
}
},...]
So in this case the value that would show up within the zip code boundary is "USP280226", what I would like to appear is "80226", so I would like to call substring(4) on that id value, but I don't see an easy way to do that for each displayed zip code on the map.
I would imagine MapBox has a way to do this properly, but I haven't been able to find it in the docs or examples.
Thanks in advance for any help.
The currently released version of Mapbox-GL-JS doesn't support any kind of functions on data. You will have to process the data offline so that it contains the labels you want to display.
(I think a forthcoming version may support this kind of function, but I'm not certain.)
EDIT The "expression" functionality is now released. Unfortunately I don't think it helps you. There's a concat function but no way to split strings that I can see.