Links

Getting Started

Mapbox provides different SDKs for developing web and mobile (iOS and Android) applications. These SDKs include different visualization capabilities and you can learn more about them in the Mapbox Docs website.
In this guide, you will learn the basics of visualizing a CARTO dataset with the Mapbox GL JS library. There are no previous requirements to complete this guide, but a basic knowledge of the Mapbox GL JS library would be helpful.
After completing this guide, you will have your first Mapbox GL JS map with a CARTO dataset!

Basic setup

Beginning with v2.0.0, mapbox-gl-js is no longer under the 3-Clause BSD license. Also, from that same version, a billable map load occurs whenever a Map object is initialized. That leaves 1.13.0 as the latest mapbox-gl-js version with BSD, that can be freely used. For more info about this, you can read our blogpost Our Thoughts as MapboxGL JS v2.0 Goes Proprietary.
In this example we are going to use v2 to showcase new available functionality like the 3D elevated terrain capabilities but, if you want to use a library compatible with Mapbox 1.x and CARTO, community supported, we recommend you to check MapLibre.
We are going to start by adding a map with 3D terrain. Please check this example in the Mapbox docs. It uses exaggeration to exaggerate the height of the terrain and adds a sky layer that is shown when the map is highly pitched.

Adding data from CARTO

In order to visualize a CARTO dataset, you need to fetch data from the CARTO platform. With CARTO Maps API v3 you can add two different types of sources: GeoJSON and tilesets (vector).
  • For adding a source with GeoJSON format you first set your credentials and then you can use the getData function from the CARTO module for deck.gl. Once you have retrieved the data, you can just add a new source to the map with geojson type.
  • For adding a tileset, you can need to provide the TileJSON URL when adding your vector source. This URL needs the connection name and the access token. Please check the tileset example.
About Carto platform versions
In this documentation we use the term “CARTO 3” to refer to the latest version of the CARTO platform launched on October 2021, and “CARTO 2” to refer to the previous version. We provide examples for both versions and we add notes when there are differences in the way you need to work with each of them. Note that each platform version has its own set of account credentials.
deck.carto.setDefaultCredentials({
apiBaseUrl: 'https://gcp-us-east1.api.carto.com',
apiVersion: deck.carto.API_VERSIONS.V3,
accessToken: 'eyJhbGciOiJIUzI1NiJ9.eyJhIjoiYWNfbHFlM3p3Z3UiLCJqdGkiOiI1YjI0OWE2ZCJ9.Y7zB30NJFzq5fPv8W5nkoH5lPXFWQP0uywDtqUg8y8c'
});
const { data } = await deck.carto.fetchLayerData({
type: deck.carto.MAP_TYPES.QUERY,
source: `SELECT geom FROM cartobq.public_account.grca_trans_trail_ln`,
connection: 'bqconn',
format: deck.carto.FORMATS.GEOJSON
});
map.addSource('trails', {
'type': 'geojson',
'data': data
})
In order to achieve optimal performance, we recommend you to retrieve only the fields you want to use client-side, instead of selecting all the fields (SELECT *). If you select all the fields from the dataset, the vector tiles or GeoJSON objects will be bigger than needed and would take more time to encode, download, and decode.
Working with Carto 2
If you are working with the CARTO 2 platform (Maps API v2), the source consists always of vector tiles and the approach is very similar to tilesets with CARTO 3. You just need to provide the TileJSON URL using the Maps API within a source of type vector while you are creating your layer using the addLayer method on the map. We also need to indicate the ID for the layer and the styling properties:
const query = 'SELECT the_geom_webmercator FROM grca_trans_trail_ln';
const tilejsonUrl = `https://maps-api-v2.us.carto.com/user/public/carto/sql?source=${query}&format=tilejson&api_key=default_public&rand=3435334`;
map.addLayer({
'id': 'grca-trail-layer',
'type': 'line',
'source': {
'type': 'vector',
url: tilejsonUrl
},
'source-layer': 'default',
'paint': {
'line-color': 'orange',
'line-width': 3
}
});

All together

You can explore the final step here.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<link href="https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.css" rel="stylesheet">
<script src="https://api.mapbox.com/mapbox-gl-js/v2.1.1/mapbox-gl.js"></script>
</head>
<body style="margin: 0; padding: 0;">
<div id="map" style="position: absolute; top: 0; bottom: 0; width: 100%;"></div>
</body>
<script>
mapboxgl.accessToken = 'pk.eyJ1IjoiY2FydG9kYmluYyIsImEiOiJja202bHN2OXMwcGYzMnFrbmNkMzVwMG5rIn0.Zb3J4JTdJS-oYNXlR3nvnQ';
async function initialize() {
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox-map-design/ckhqrf2tz0dt119ny6azh975y',
center: [-112.125, 36.12],
zoom: 12,
pitch: 70,
bearing: 180
});
map.on('load', async () => {
map.addSource('mapbox-dem', {
'type': 'raster-dem',
'url': 'mapbox://mapbox.mapbox-terrain-dem-v1',
'tileSize': 512,
'maxzoom': 14
});
// add the DEM source as a terrain layer with exaggerated height
map.setTerrain({ 'source': 'mapbox-dem', 'exaggeration': 1.5 });
// add a sky layer that will show when the map is highly pitched
map.addLayer({
'id': 'sky',
'type': 'sky',
'paint': {
'sky-type': 'atmosphere',
'sky-atmosphere-sun': [0.0, 0.0],
'sky-atmosphere-sun-intensity': 15
}
});
// Add CARTO layer
const query = 'SELECT the_geom_webmercator FROM grca_trans_trail_ln';
const tilejsonUrl = `https://maps-api-v2.us.carto.com/user/public/carto/sql?source=${query}&format=tilejson&api_key=default_public&rand=3435334`;
map.addLayer({
'id': 'grca-trail-layer',
'type': 'line',
'source': {
'type': 'vector',
url: tilejsonUrl
},
'source-layer': 'default',
'paint': {
'line-color': 'orange',
'line-width': 3
}
});
});
}
initialize();
function updateCameraPosition(position, altitude, target) {
const camera = map.getFreeCameraOptions();
camera.position = mapboxgl.MercatorCoordinate.fromLngLat(
position,
altitude
);
camera.lookAtPoint(target);
map.setFreeCameraOptions(camera);
}
// linearly interpolate between two positions based on time
function lerp(a, b, t) {
if (Array.isArray(a) && Array.isArray(b)) {
const result = [];
for (let i = 0; i < Math.min(a.length, b.length); i++) {
result[i] = a[i] * (1 - t) + b[i] * t;
}
return result;
} else {
return a * (1 - t) + b * t;
}
}
let animationTime = 0.0;
function animate() {
const animationConfig = {
duration: 1000,
animate: (phase) => {
const start = [-112.125, 36.3];
const end = [-112.125, 36.05];
// interpolate camera position while keeping focus on a target lat/lng
const position = lerp(start, end, phase);
const altitude = 5000;
const target = [-112.065, 36.147];
updateCameraPosition(position, altitude, target);
}
};
let lastTime = 0.0;
let frameReq;
function frame(time) {
if (animationTime < animationConfig.duration) {
animationConfig.animate(animationTime / animationConfig.duration);
}
// allow requestAnimationFrame to control the speed of the animation
animationTime += 100 / (time - lastTime);
lastTime = time;
if (animationTime > animationConfig.duration) {
window.cancelAnimationFrame(frameReq);
return;
}
frameReq = window.requestAnimationFrame(frame);
}
frameReq = window.requestAnimationFrame(frame);
};
</script>
</html>