MapBox Randomizing Building Color - mapbox-gl-js

Is it possible to loop through all of the buildings on the map an assign them different colours, or a different property value that drives a thematic grouping?
I have code to loop through the buildings:
features = map.queryRenderedFeatures({ layers: ["building"], filter: ['==', 'extrude', 'true']});
features.forEach(function(feature){
// how to change feature colour or property in here?
}

There's a couple of ways achieving something like what you want.
Feature state
If your buildings layer has feature IDs, you can use setFeatureState to set a state on each building.
features = map.querySourceFeatures('building', { sourceLayer: 'buildings' }));
features.forEach(function(feature){
if (!map.getFeatureState({ id: feature.id, source: 'building', sourceLayer: 'buildings' }).color) {
map.setFeatureState({ id: feature.id, source: 'building', sourceLayer: 'buildings' }, { color: makeRandomColor() }
}
});
You can then use ['feature-state', 'color'] in an expression.
Quasi random
If there's no feature ID, you might be able to use some other attribute in a way that appears to be random. For instance, if there's some other kind of ID, you can use a mod function to map it to a color, in a way that might look kind of random.

Related

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

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);
}

vue-chartjs: is it possible to dynamically change color based on value

Using vue-chartjs is it possible to change to color of the bar?
This is similar to this question which is based on chart.js
chart.js bar chart color change based on value, and the jsfiddle provided in the answer is exactly what I'm looking for, except within vue-chartjs.
Any help appreciated
So this should be your chart calling (in my case, I named it bar-chart)
<bar-chart
:chart-data="barData"
:options="barChartOptions">
</bar-chart>
:chart-data and :options are 2 props defined when I create my bar component:
Vue.component('bar-chart', {
extends: VueChartJs.Bar,
props: ['chartData', 'options'],
So your barData, should be an object like this:
{datasets: [...], labels: [...]}
Your dataset is an array with the charts you want to show. So if you want to show only 1 data, than your array only has one position. So let's assume that by now. We'll use dataset = dataset[0]
Your dataset accepts some properties, 2 of them are a must:
Data (an array with the data you want to show)
Label (the name of the label when you hover on the bardata. It should display "Label: value"
It also accepts some other properties like:
fill
hoverBackgroundColor
backgroundColor
check more here: https://www.chartjs.org/docs/latest/charts/bar.html
so now, your backgroundColor property is either a color value (e.g. red, #FF0000), or an array.
If it is an array, then this should be true dataset.bgColors.length === dataset.data.length
and each dataset.bgColors array position is the color of the respective value in the dataset.data array.
dataset: {
data: [1,2,-3,-4,2,1]
backgroundColor: ['green', 'green', 'red', 'red', 'green', 'green']
...
}
So now, you can just build a bgColors array with the color you want, based on your data.
------------- UPDATING THE DATA -----------------
To anybody else who is looking for a way to UPDATE your chart data after it was rendered. It's a different question, but to help the community:
When you set your chart component, you can define a watch for the chartData prop, so when the chartData changes, the method is called and you re-render the chart:
Vue.component('bar-chart', {
extends: VueChartJs.Bar,
props: ['chartData', 'options'],
methods: {
renderLineChart () {
this.renderChart(this.chartData, this.options)
}
},
mounted () {
this.renderLineChart()
},
watch: {
chartData: function () {
this.$data._chart.destroy()
this.renderLineChart()
}
}
})
IMPORTANT: Make sure you make a new copy of the new chartData object info because the watch will only check if the object itself changed, not its inner properties.
However, if you really want to change ONLY the dataset.data or dataset.backgroundColor or any other, than the watcher will not know it changed. You can use the property deep: true in the watcher for this, as it will check changes deep inside the chartData object:
watch:
chartData: {
handler: function () {
this.$data._chart.destroy()
this.renderLineChart()
},
deep: true
}
}
I hope the answer was clear for everyone.
Best regards

Get coordinate of selected tileset tile (by expression)

I am able to find and change the properties of a vector tileset using expression, for example:
map.setPaintProperty('my-shp', 'fill-opacity', [
'match',
['get', 'NO'],
13708, 1,
/* else */ 0
]);
I was wondering if there is a way to get the exact coordinate of the tileset item using expression?
Sorry if I am wrong with the terminology (of tileset/tile item), feel free to correct me. Thanks
To clarify, using the above expression, I can change the opacity of the tileset item where NO is 13708. My question is, is there a way to get the lat/long coordinate of the tileset item where NO is 13708?
You could just iterate over all rendered features and test the "NO" property then return the coordinates of this feature. To get all rendered features you need to use queryRenderedFeatures. Features follow the GeoJSON structure.
For example:
let features = map.queryRenderedFeatures({ layers: ['my-shp'] });
let filtered = features.filter(feature => {
return feature.properties.NO === 13708;
});
console.log(filtered);

How to get the results of a filtered Mapbox layer?

I am trying to update an old Mapbox.js map to Mapbox GL. I am generating the map from geojson (and using coffescript).
map.addSource 'my_datasource',
'type': 'geojson'
'data': my_geojson
map.addLayer
'id': 'my_layer'
'type': 'symbol'
'source': 'my_datasource'
I am filtering the layer based on a user input that returns value
map.setFilter('my_layer', ["==", 'my_attribute', value ])
So far, so good. But now I want to zoom and reposition the map to fit the bounds of the filtered symbols.
I thought I would be able to do something like this
bounds = new (mapboxgl.LngLatBounds)
map.queryRenderedFeatures(layers: [ 'my_layer' ]).forEach (feature) ->
bounds.extend feature.geometry.coordinates
return
map.fitBounds bounds
But queryRenderedFeatures appears to be returning all (i.e., un-filtered) symbols.
After much reading around, my understanding is that queryRenderedFeatures should return the filtered symbols that are visible in the viewport (i.e., would be suitable for zooming in but not zooming out).
Is this correct? And if so, why is my function above returning unfiltered symbols? Appreciate any advice to help my transition to MapboxGL!
It's a bit unclear to me from the documentation whether filters in the layer should be applied, but in any case, there is an explicit filter parameter you can, pass, so:
map.queryRenderedFeatures(layers: [ 'my_layer' ], filter:["==", 'my_attribute', value ]).forEach (feature) ->
bounds.extend feature.geometry.coordinates
But I suspect you really want querySourceFeatures, because you don't want to be constrained by what's currently within the viewport:
map.querySourceFeatures(my_source, filter:["==", 'my_attribute', value ]).forEach (feature) ->
bounds.extend feature.geometry.coordinates
or, in native ES2015:
map.querySourceFeatures(my_source, { filter:['==', 'my_attribute', value ]} )
.forEach (feature => bounds.extend(feature.geometry.coordinates))
Try this post, I have added the code which will let you have the features using queryRenderedFeatures() or even using querySourceFeatures():
https://stackoverflow.com/a/66308173/9185662

Layer order changing when turning layer on/off

I have two geoJson layers being loaded - both layers are the same data for testing purposes, but being drawn from two different json files. When I turn the layers on and off in the layer controller, the draw order of the layers change.
Any ideas why this is happening?
I have put my code into a JSFiddle: http://jsfiddle.net/lprashad/ph5y9/10/ and the JS is below:
//styling for watersheds_copy
var Orange = {
"color": "#ff7800",
"weight": 5,
"opacity": 0.65
};
var Water_Orange = L.geoJson(watersheds_copy, {
style: Orange
});
Water_Orange.addData(watersheds_copy);
//these are blue
var Water_blue = L.geoJson(watersheds, {});
Water_blue.addData(watersheds);
//This sets the inital order - last in layer list being on top. Except minimal - tile layer is always on bottom
var map = L.map('map', {
center: [41.609, -74.028],
zoom: 8,
layers: [minimal, Water_Orange, Water_blue]
});
var baseLayers = {
"Minimal": minimal,
"Night View": midnight
};
//This controls the order in the layer switcher. This does not change draw order
var overlays = {
"Water_Orange": Water_Orange,
"Water_blue": Water_blue
};
L.control.layers(baseLayers, overlays).addTo(map);
LP
While searching I happened upon this site that shows some of the Leaflet code:
http://ruby-doc.org/gems/docs/l/leaflet-js-0.7.0.3/lib/leaflet/src/control/Control_Layers_js.html
In it I found this condition for the application of autoZIndex:
if (this.options.autoZIndex && layer.setZIndex) {
this._lastZIndex++;
layer.setZIndex(this._lastZIndex);
}
TileLayer is the only layer type that has a setZIndex function, so apparently autoZIndex only works there.
I'm not sure which annoys me more. This incredible limitation or the fact that Leafet documentation doesn't point it out.
At least on 0.7.2, I had to use bringToFront in the callback of map.on('overlayadd'). autoZIndex: false did not work in my case neither. A comment on this issue may explain the reason.
It's not specific to L.GeoJson layers. As far as I can tell, it's true of all Leaflet layers with layer control. The last layer turned on is simply on top. I don't think this is a bug either. It's predictable behavior which I use and depend on when I'm designing maps with layer control...