Created
June 13, 2019 16:38
-
-
Save VictorVelarde/414fc036469814a27161a3d2aa8c5f08 to your computer and use it in GitHub Desktop.
[CARTO VL - Cluster-mix]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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