In part one after a quick SVG primer, we downloaded some open source COVID-19
data from GitHub, transformed it into the data we actually wanted to visualise,
and created the following chart:
3/25/20
While it isn't fully static as you can interact with the chart to pan through
the data, it isn't animated, either - let's look at that now.
Now that we've got a chart displaying data for specified days, let's look at
animating it when the day is changed.
We want to animate the following things:
When the countries change order, their position should be transitioned.
When a new country sees their first case, it should fade in (and vice versa if
going through the data backwards).
When a countries case count increases or decreases, the bar width should
change smoothly.
Vue's
list transitions
can help us with the first two, but it can't really help us with the width
change, as that's a state transition.
Before we start animating the chart, let's move the bar logic into a separate
component which will be passed the value and be responsible for the width - that
means the animation will also happen inside the component.
Let's call our component ChartBar and give it the following API:
<ChartBarv-for="({ country, value }, i) in chartData":transform="`translate(0, ${i * 60})`":country="country":chart-width="chartWidth":max-value="maxValue":value="value"/>
We could also pass in i and have the component be responsible for its own
positioning, but I decided it would be better outside the component. It doesn't
really make much difference!
Let's start by fading in countries when they're added and fading them out again
when they're removed.
Vue has a bunch of helpers built for animating and transitioning content. You
can find the full documentation for it here:
Transitions & Animation.
The list enter and leave transitions in particular are what we will be using
here. In fact, there's an example in the documentation which is almost exactly
what we want to do, just with HTML instead of SVG (which has implications we'll
get to in a bit!)
Using Vue's list transitions is usually pretty straightforward. To start with,
let's wrap the <ChartBar> element with a <transition-group> component:
<transition-groupname="country-list"tag="g"><ChartBarv-for="({ country, value }, i) in chartData"class="country":transform="`translate(0, ${i * 60})`":country="country":max-value="maxValue":value="value":chart-width="chartWidth":key="country"/></transition-group>
Now, when an element enters or leaves the DOM, Vue will add a class that we can
use to apply a CSS transition to the change.
Let's add the following CSS to our main file (not the ChartBar component) to
utilise the classes Vue is adding:
.country-list-enter is added before the element is added to the DOM and
removed immediately afterwards, so the element will fade in, and
.country-list-leave-to is added when an element is going to be removed, so the
element will fade out.
Here's where we're at now:
3/25/20
It's easier to see the affect if you go to day one and click forwards from
there, as that's when most countries start appearing.
Vue contains "list move transition" functionality where it applies
the FLIP technique
to elements being reordered to smoothly transition them to the new position. At
first glance, it looks like it might come in handy here, but it turns out we
don't have to use it in this case as we're positioning groups using transform
in the first place.
All we have to do is add a transition for transform to the existing .country
transition declaration:
Unfortunately, I ran into
what looked like an issue with Vue
here and had to make some changes to work around it. While currently i is the
index of the country in chartData (a sorted array), that caused some strange
unexpected behaviour.
To work around the issue, we have to sort the data in another array and add a
position property to the object for each country instead.
Now the order of chartData isn't changed so the issue won't occur, but we have
to use position instead of i. Let's go ahead and make that change:
<transition-groupname="country-list"><ChartBarv-for="{ country, value, position } in chartData"class="country":transform="`translate(0, ${position * 60})`":country="country":max-value="maxValue":value="value":chart-width="chartWidth"/></transition-group>
You could even just call it i if you wanted!
Here's the updated demo:
3/25/20
We can now see that in addition to elements fading in and out when they're added
and removed, they're also moving up and down smoothly when the countries change
positions.
Transitioning the width of the bar is much more complicated than the previous
transitions we've looked at, as it involves transitioning the data itself, not
just an attribute. If we only changed the width of the bar, the numbers
displayed on the visualisation would be wrong.
This is a large part of the reason we created the ChartBar component—handling
the transition logic for each bar in the component is a lot simpler than
handling the transition logic for every single bar at the same time in the main
file.
So how do we transition state?
There's a few different ways we can approach this, but the one we'll go for is
as follows:
Add a new property of the data object of the ChartBar component called
tweenedValue (I'll explain what tweening is in a couple paragraphs).
Add a watcher watching for when the value prop is changed.
When the value prop is changed, transition tweenedValue from the old value
to the new value.
Use tweenedValue instead of value in the bar width calculation.
We also have to do the same to the maxValue prop to ensure that the bar width
is transitioned smoothly.
To transition a value from one number to another, we could use
requestAnimationFrame and transition the value ourselves, but for the sake of
simplicity we'll use one of a number of available tweening libraries to do it
for us.
Sidenote: if you're interested in how this could work without a library,
check out this codepen where I
tween the values using requestAnimationFrame.
Tweening (short for in-betweening) is the process in animation of generating the
frames between two images, called key frames. For example, if you wanted to
animate an element from one position to another and have it slow down when it's
reaching its destination, a tweening library can help you with that.
In this case, we're not going to use the tweening library to change an element
in the DOM, we're just going to use it to change a value on the data object.
GSAP (GreenSock Animation Platform) is a widely
used animation library that provides pretty much everything you'd need to
animate anything on your website or application. It turns out that its tweening
functionality, in addition to being able to modify DOM elements, can also be
used to smoothly transition values on the Vue data object.
It's important to note at this point that loading all of GSAP just to transition
a number is definitely overkill. There's other smaller libraries that can do a
good job of this, I just wanted to stick with a well known example.
Let's look at a quick example of how this works.
Click this button a couple times to see an example of a tweened number:
this.number is being set directly to a new random number. This simulates the
width prop of the ChartBar component.
We're then calling gsap.to() to tell GSAP to tween tweenedNumber from its
previous value to the new random number. this.$data is the data object of the
current Vue instance - we could also pass it just this, but this.$data makes
it clearer what is going on.
In affect, GSAP then repeatedly increases or decreases
this.$data.tweenedNumber (aka this.tweenedNumber) until it equals the value
specified.
Now let's apply this in watchers for width and maxWidth:
And now, after changing the bar width logic to use tweenedValue instead of
value, this is what we get:
3/25/20
Nice!
The final piece is to display the tweened number on the bar instead of the
actual value, so that it increases as the bar changes width. To do that, we'll
also need to round it.
Here is our complete COVID-19 tracker, animated and interactive:
3/25/20
You can now see that when you change the day, the numbers are animated as well
as the bar widths.
The code on GitHub contains a couple extra features such as configurable
animation time and smarter bar heights—here's the link again:
callumacrae/covid-visualisation.
So what have we learned here?
SVG allows us to draw shapes embedded in our HTML documents, which enables us
to create visualisations.
As SVG can be embedded directly in our HTML documents, we can use Vue
templates to generate SVGs.
<transition-group> works in SVGs as well, and we can use it to fade elements
in an out, just like with HTML.
CSS transitions also work just fine in SVGs, which was useful for animating
positions when elements were being reordered.
Finally, while Vue doesn't provide us with any help here, animating state
doesn't have to be difficult, especially if we use the help of a library like
GSAP.
There's loads of other potential applications for this, and not just in data
visualisation. In a future article I hope write about how you can use d3-shape
directly with Vue to display and visualise data on a world map. If that's
something you'd be interested in, make sure to
follow me on Twitter so that you can see when
I post it.