Skip to content

Instantly share code, notes, and snippets.

@VictorVelarde
Created June 13, 2019 16:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save VictorVelarde/414fc036469814a27161a3d2aa8c5f08 to your computer and use it in GitHub Desktop.
Save VictorVelarde/414fc036469814a27161a3d2aa8c5f08 to your computer and use it in GitHub Desktop.
[CARTO VL - Cluster-mix]
<!DOCTYPE html>
<html>
<head>
<title>Clusters PoC | CARTO & MGL</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="UTF-8">
<script src="https://libs.cartocdn.com/carto-vl/v1.3/carto-vl.min.js"></script>
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.js"></script>
<link href="https://api.tiles.mapbox.com/mapbox-gl-js/v1.0.0/mapbox-gl.css" rel="stylesheet" />
<link href="https://carto.com/developers/carto-vl/v1.3.0/examples/maps/style.css" rel="stylesheet">
</head>
<body>
<div id="map"></div>
<script>
const map = new mapboxgl.Map({
container: 'map',
style: carto.basemaps.voyager,
center: [7, 25],
zoom: 2,
renderWorldCopies: false
});
const nav = new mapboxgl.NavigationControl();
map.addControl(nav, 'top-left');
//** CARTO VL functionality begins here **//
carto.setDefaultAuth({
user: 'cartovl',
apiKey: 'default_public'
});
// Points layer
const source = new carto.source.Dataset('airbnb_madrid_listings_2018');
// Auxiliary viz, to get the clusters
const viz = new carto.Viz(`
color: opacity(ramp(buckets($room_type, ['Entire home/apt', 'Private room', 'Shared room']), [red, green, blue]), 0.5)
strokeColor: transparent
width: 2
@cc: clusterCount()
@v_features: viewportFeatures($cartodb_id, $room_type)
resolution: 8
`);
const layer = new carto.Layer('points', source, viz);
const interactivity = new carto.Interactivity([layer],
{ autoChangePointer: false }
);
interactivity.on('featureClick', e => {
const f = e.features[0];
if (f) {
const type = f._rawFeature.room_type;
const count = f._rawFeature._cdb_feature_count
console.log(count, type);
}
});
// Client-cluster layer
const entire = ['==', ['get', 'room_type'], 'Entire home/apt'];
const private = ['==', ['get', 'room_type'], 'Private room'];
const shared = ['==', ['get', 'room_type'], 'Shared room'];
layer.on('loaded', () => {
map.addSource('clusters', {
type: 'geojson',
data: null,
cluster: true,
clusterRadius: 64,
clusterProperties: {
'entire': ['+', ['case', entire, ['get', 'room_count'], 0]],
'private': ['+', ['case', private, ['get', 'room_count'], 0]],
'shared': ['+', ['case', shared, ['get', 'room_count'], 0]],
}
});
const clustersSource = map.getSource('clusters');
const layerUpdated = function () {
const features = viz.variables.v_features.value;
const geojsonFeatures = features.map(f => {
const type = f.properties['room_type'];
const count = f._rawFeature._cdb_feature_count;
return {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": f.getRenderedCentroid()
},
"properties": {
"room_type": `${type}`,
"room_count": count,
}
}
});
clustersSource.setData({
type: 'FeatureCollection',
features: geojsonFeatures
});
// console.log('* clusters updated');
};
layer.on('updated', layerUpdated);
map.addLayer({
"id": "clusters_layer",
"type": "circle",
"source": "clusters",
"filter": ["!=", "cluster", true],
"paint": {
"circle-color": ["case",
entire, '#f00',
private, '#0f0',
shared, '#00f', '#444'
],
"circle-opacity": 0.6,
"circle-radius": 12
}
});
// objects for caching and keeping track of HTML marker objects (for performance)
var markers = {};
var markersOnScreen = {};
function updateMarkers() {
var newMarkers = {};
var features = map.querySourceFeatures('clusters');
// for every cluster on the screen, create an HTML marker for it (if we didn't yet),
// and add it to the map if it's not there already
for (var i = 0; i < features.length; i++) {
var coords = features[i].geometry.coordinates;
var props = features[i].properties;
// if (!props.cluster) continue;
var id = props.cluster_id;
var marker = markers[id];
if (!marker) {
var el = createDonutChart(props);
marker = markers[id] = new mapboxgl.Marker({ element: el }).setLngLat(coords);
}
newMarkers[id] = marker;
if (!markersOnScreen[id])
marker.addTo(map);
}
// for every marker we've added previously, remove those that are no longer visible
for (id in markersOnScreen) {
if (!newMarkers[id])
markersOnScreen[id].remove();
}
markersOnScreen = newMarkers;
}
// after the GeoJSON data is loaded, update markers on the screen and do so on every map move/moveend
map.on('data', function (e) {
if (e.sourceId !== 'clusters' || !e.isSourceLoaded) return;
map.on('move', updateMarkers);
map.on('moveend', updateMarkers);
updateMarkers();
});
});
layer.addTo(map);
// Chart
// code for creating an SVG donut chart from feature properties
const colors = ['#f00', '#0f0', '#00f'];
function createDonutChart(props) {
var offsets = [];
var counts = [props.entire, props.private, props.shared];
var total = 0;
for (var i = 0; i < counts.length; i++) {
offsets.push(total);
total += counts[i];
}
var fontSize = total >= 1000 ? 22 : total >= 100 ? 20 : total >= 10 ? 18 : 16;
var r = total >= 1000 ? 50 : total >= 100 ? 32 : total >= 10 ? 24 : 18;
var r0 = Math.round(r * 0.6);
var w = r * 2;
var html = '<svg width="' + w + '" height="' + w + '" viewbox="0 0 ' + w + ' ' + w +
'" text-anchor="middle" style="font: ' + fontSize + 'px sans-serif">';
for (i = 0; i < counts.length; i++) {
html += donutSegment(offsets[i] / total, (offsets[i] + counts[i]) / total, r, r0, colors[i]);
}
html += '<circle cx="' + r + '" cy="' + r + '" r="' + r0 +
'" fill="white" /><text dominant-baseline="central" transform="translate(' +
r + ', ' + r + ')">' + total.toLocaleString() + '</text></svg>';
var el = document.createElement('div');
el.innerHTML = html;
return el.firstChild;
}
function donutSegment(start, end, r, r0, color) {
if (end - start === 1) end -= 0.00001;
var a0 = 2 * Math.PI * (start - 0.25);
var a1 = 2 * Math.PI * (end - 0.25);
var x0 = Math.cos(a0), y0 = Math.sin(a0);
var x1 = Math.cos(a1), y1 = Math.sin(a1);
var largeArc = end - start > 0.5 ? 1 : 0;
return ['<path d="M', r + r0 * x0, r + r0 * y0, 'L', r + r * x0, r + r * y0,
'A', r, r, 0, largeArc, 1, r + r * x1, r + r * y1,
'L', r + r0 * x1, r + r0 * y1, 'A',
r0, r0, 0, largeArc, 0, r + r0 * x0, r + r0 * y0,
'" fill="' + color + '" />'].join(' ');
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment