With CARTO VL you can make animated maps of points, lines and polygons using the animation
expression. In this guide, you will learn how to make animated maps that tell a story by gaining a better understanding of how to adjust animation parameters based on the type of data you are mapping.
Before getting into the details, it is important to understand the general syntax for animating data in CARTO VL:
filter: animation(input, duration, fade(fadeIn, fadeOut))
We will walk through each part throughout this guide.
Using the syntax above, let’s create a basic animation where points appear and disappear on a map. We will use a date_time
attribute (a time associated with each feature) as our input
, set the duration
to 10
seconds and set both fade
parameters to 1
second.
The result is an animation that cycles through all of the records in the dataset over 10 seconds. Each point will take 1 second to become visible (fade in) and another second to disappear (fade out). You can see it in action in the next map.
1
2
3
const viz = new carto.Viz(`
filter: animation($date_time, 10, fade(1, 1))
`);
Note:
If you are wondering about the points, this is an animation of the journey of three birds, from January to April of 2014. It is using data from movebank.org and the date_time column represents the date of the bird track and its associated timestamp. You can check the code of the previous map to see how to configure this bird_journey as the carto.source.Dataset
for the visualization.
Now that you have a basic map of animated points, you’ll get a deeper understanding of the different pieces that make up the animation expression to fine-tune the visualization.
In the example above, the animation expression is set to the filter
property (for more information see the vizSpec).
The convention in CARTO VL is that:
0
represents the boolean value false
(the absence of or an off state) and1
represents true
(the presence of or an on state).In the context of an animation expression assigned to a filter
, any feature that has a value of 0
will be “filtered-out” and not shown, while any feature that meets the filter is assigned a value of 1
and is shown.
You can think of the animation expression like a clock, cycling through each record of the input. Any time there is a match, the feature is drawn.
With this in mind, you can get a better understanding of the next set of animation parameters, and how they are used to define the property, speed, and transition between animated features.
The first animation parameter (input
), is the attribute that you want to animate. By default, the animation progresses from the attribute minimum to maximum values.
This parameter defines the duration of the animation in seconds. As stated above, during the animation cycle, all possible input values are evaluated. When an input value is matched, animation returns a value of 1
for the feature and it is drawn.
The fade
parameter is used to define two additional (fadeIn, fadeOut
) durations in seconds. These parameters allow for smooth transitions between features during the animation. During the fadeIn
phase, all features with a match will fade-in to the animation, transitioning from 0
(invisible) to 1
(visible). During the fadeOut
phase features will transition from 1
back to 0
and the next set of features will begin to fade-in.
To illustrate these concepts, we’ll keep on working on the animated journey of our three birds.
At the beginning of the guide, we created a simple point animation using the attribute $date_time
for the temporal input, set the duration to 10
seconds and both fade parameters to 1
second:
1
2
3
const viz = new carto.Viz(`
filter: animation($date_time, 10, fade(1, 1))
`);
In the animation above, you will notice that the birds spend the majority of this three month period in West Africa and then eventually migrate to Northern Europe. To see the journey in more detail, try adjusting the duration to 30
seconds. With this adjustment that slows down the animation, you can see that each bird, at a different time, flies north along the coast of Morocco to Eastern Europe.
1
2
3
const viz = new carto.Viz(`
filter: animation($date_time, 30, fade(1, 1))
`);
Since you are visualizing bird migration, the journey the birds took should be shown more clearly through symbology. You can get this added effect by adjusting the fade
parameters. By setting fadeIn
to 0
and fadeOut
to 0.5
, the previous point from the journey is visible for longer in the animation which helps to better visualize the flight path of the migration journey.
filter: animation($date_time, 30, fade(0, 0.5))
Assigning unique colors to each of the three birds helps visualize each one’s journey better. Using their names, you can assign a unique color to each one using buckets
inside of a ramp
. Next, decrease the width
of the points to 4
and finally, remove the strokeWidth
by setting it to 0
.
Note: For a more in-depth discussion of ramps and other styling properties, check out our Data-driven visualizations guide - part 1.
1
2
3
4
5
6
const viz = new carto.Viz(`
filter: animation($date_time, 10, fade(0, 0.5))
color: ramp(buckets($bird_name, ["Sanne", "Eric", "Nico"]), [deeppink, yellow, turquoise])
width: 4
strokeWidth: 0
`);
Congratulations! You have created an animated visualization.
To complete the map, you can add animation controls to play and pause the animation as well as a slider bar to adjust the duration of the animation on-the-fly.
To get started, add two variables inside of the Viz object: @duration
and @animation
. Making the duration of the animation a variable (@duration
) and putting that inside of the animation expression will allow you to dynamically modify the duration of the animation using the controls on the map.
1
2
3
4
5
const viz = new carto.Viz(`
@duration: 30
@animation: animation($date_time, @duration, fade(0, 0.5))
filter: @animation
`);
The variables are now available in the viz
object. If you want to access the animation variable you can do it by typing viz.variables.animation
. By assigning the animation expression to the @animation
variable, you are going to be able to call its public methods as follows:
1
viz.variables.animation.play();
The next step is to add buttons and elements that allow you to control and interact with the animation.
The elements we will add are:
js-progress-range
).js-play-button
).js-pause-button
).js-duration-range
).js-current-time
).To display the controls, you’re going to create a panel. You have to add this just under the map, like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div id="map"></div>
<!-- Animation control elements -->
<aside>
<header>
<h1>Animation controls</h1>
</header>
<section>
<p>Progress: <input type="range" id="js-progress-range" min="0" max="1" step="0.01"></p>
</section>
<section>
<p>Current: <span id="js-current-time" class="open-sans"></span></p>
</section>
<section>
<button id="js-play-button">Play</button>
<button id="js-pause-button">Pause</button>
<input type="range" id="js-duration-range" min="1" max="30" step="1">
</section>
</aside>
As seen above, there is an id
attribute assigned to each element which you can use to access using JavaScript:
1
2
3
4
5
const $progressRange = document.getElementById('js-progress-range');
const $playButton = document.getElementById('js-play-btn');
const $pauseButton = document.getElementById('js-pause-btn');
const $durationRange = document.getElementById('js-duration-range');
const $currentTime = document.getElementById('js-current-time');
Note:
This guide makes use of some conventions. When setting an id
to an element that is going to be accessed via JavaScript, the id
starts with js
. In addition, when assigning the element (const $progressBanner = document.getElementById('js-progress-banner')
), the JavaScript value starts with $
, which indicates that it contains an HTML element.
In this step, you will add listening events tied to the different interaction buttons used to control the animation as described above. For example, you will tell the Pause button that it has to be ready and react when clicked to pause the animation.
1
2
3
$pauseButton.addEventListener('click', () => {
viz.variables.animation.pause();
});
And the same applies to the Play button:
1
2
3
$playButton.addEventListener('click', () => {
viz.variables.animation.play();
});
In the same way, you can update the duration of the animation when the duration range is adjusted in the slider:
1
2
3
$durationRange.addEventListener('change', () => {
viz.variables.duration = parseInt($durationRange.value, 10);
});
Now that you have added the listening events for the different buttons and defined the behavior upon interaction, next you will walk through updating the progress bar and the current time text.
The updateProgress
function will be responsible for:
getProgressPct()
method andgetProgressValue()
method:1
2
3
4
function updateProgress () {
$progressRange.value = viz.variables.animation.getProgressPct();
$currentTime.innerText = viz.variables.animation.getProgressValue();
}
Let’s call this function periodically by using setInterval
:
1
setInterval(updateProgress, 100);
Note:
The setInterval()
is a commond method available at the browser, that calls a function or evaluates an expression at specified intervals, in milliseconds. You can also do this update by listening to carto.layer
events. This topic is covered more in-depth in the Interactivity and Events Guide; once you know how, the second options is preferred.
Well done! In the example below you will find the full code, including the animation controls. You can play with it and make adjustments.
Remember you can style your HTML controls using CSS. These styles are very simple, but at the beginning of this guide you have a complete example that includes beautiful styles. This example is also available in the examples section.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
const map = new mapboxgl.Map({
container: 'map',
style: carto.basemaps.darkmatter,
center: [-0.12796893854942937, 35.1654623242204],
zoom: 2.8,
scrollZoom: false
});
const nav = new mapboxgl.NavigationControl();
map.addControl(nav, 'top-left');
// Autenticate the client
carto.setDefaultAuth({
username: 'cartovl',
apiKey: 'default_public'
});
// Create the source
const source = new carto.source.Dataset('bird_journey');
// Add better styles
const viz = new carto.Viz(`
@duration: 30
@animation: animation($date_time, @duration, fade(0, 0.5))
filter: @animation
width: 4
strokeWidth: 0
color: #1785FB
`);
// Create the layer
const layer = new carto.Layer('layer', source, viz);
// Get animation control elements
const $progressRange = document.getElementById('js-progress-range');
const $playButton = document.getElementById('js-play-button');
const $pauseButton = document.getElementById('js-pause-button');
const $durationRange = document.getElementById('js-duration-range');
const $currentTime = document.getElementById('js-current-time');
// Listen to interaction events
$playButton.addEventListener('click', () => {
viz.variables.animation.play();
});
$pauseButton.addEventListener('click', () => {
viz.variables.animation.pause();
});
$durationRange.addEventListener('change', () => {
viz.variables.duration = parseInt($durationRange.value, 10);
});
// Update progress each 100 milliseconds
function updateProgress () {
$progressRange.value = viz.variables.animation.getProgressPct();
$currentTime.innerText = viz.variables.animation.getProgressValue();
}
setInterval(updateProgress, 100);
// Add the layer to the map
layer.addTo(map);
In this section, we explore other properties that can be used for your animation, including how to animate only a subset of the data and how to symbolize that time range with color.
In the examples above, we are animating the entire timespan of the bird journeys using $date_time
as our input. If we want to only visualize a subset of the timespan, we can change the input range of values using a linear
expression combined with a time
expression:
1
2
3
const viz = new carto.Viz(`
filter: animation(linear($date_time, time('2018-01-01T00:00:00'), time('2018-01-05T00:00:00'))
`);
Note: Values outside of the specified range will not appear in the animation.
This isn’t only limited to timestamp properties. If your input property is a number, for example the month of the year ranging from 1-12
, you can adjust the input parameter to that attribute ($month
) and define the min and max range (2,6
) that you want to visualize:
1
2
3
const viz = new carto.Viz(`
filter: animation(linear($month, 2, 6))
`);
You can also apply the same time range (@timeSteps
) to other properties like color using a ramp
expression. That double-encoding of the same time dimension, within the filter and the color, will reinforce the expressiveness of the visualization:
1
2
3
4
5
6
7
8
9
const viz = new carto.Viz(`
@duration: 10
@animation: animation(@timeSteps, @duration, fade(0, 0.5))
@timeSteps: linear($date_time, time('2014-03-30T20:24:25Z'), time('2014-04-24T23:52:14Z'))
color: ramp(@timeSteps, SunsetDark)
filter: @animation
width: 10
strokeWidth: 0
`);
As you can see in the result, you are able to visualize, via color, where each bird is at the same time range in the data and get some interesting insights about their individual and combined journey.