Callum Macrae

Building an animated COVID-19 tracker using Vue.js: part one

I've given a few talks with the title "Data Visualisation in Vue.js": this is the first part of a two-part mini-series which goes a bit more in depth with a specific example.

  • Part one contains a brief primer to SVG and builds a static chart using SVG and Vue.
  • Part two shows how to animate it the chart built in part one.

The code for the entire article can be found on GitHub: callumacrae/covid-visualisations.


If you've been on the internet in the last couple months or so, there's a good chance you'll have seen a visualisation that looked something like this:

I've been working with data visualisation using only Vue.js for a while now, so I wanted to see if I could implement it in Vue without using any other libraries.

We're going to generate the chart using SVG.

SVG is an image format that can be used to generate vector graphics, such as 2D shapes and lines. It's commonly used for icons and simple graphics—SVG images are significantly smaller than their PNG and JPEG counterparts. While it's typically generated by a graphics editors such as Illustrator, Sketch or Figma, they're XML-based which means that it's also possible to write them by hand—or programmatically using code.

Common libraries for working with SVGs include D3, Highcharts, and Chartist, but it's actually possible to use Vue.js to generate and manipulate SVGs directly!

#A quick SVG primer

Let's take a brief look at how SVGs work and how you can write them by hand, before we look at how we can produce them using JavaScript. If you're familiar with SVGs, feel free to skip this section.

To write SVG directly into an HTML document, you can use the <svg> tag, like so:

<svg
  width="400" height="250"
  xmlns="http://www.w3.org/2000/svg">
</svg>

When you load your page, you'll see something like this:

As you can see, it's a blank rectangle. I added the border to make it clearer, but usually you wouldn't see that either. This is our blank canvas; our space to draw!

Let's start by adding a rectangle using the <rect> element:

<svg
  width="400" height="250"
  xmlns="http://www.w3.org/2000/svg">
  <rect
    width="200" height="100"
    x="50" y="50"
    fill="hsl(10, 80%, 70%)"
  />
</svg>

We've passed the element five attributes, telling it how big it should be (width and height), where it should display in the SVG element (x and y—positioned from the top left of the SVG viewport), and what it should look like (the fill attribute).

This is the output:

It'll have become clear by now that SVG is a pretty different language to HTML—while you can write it directly in an HTML document, nearly everything about it is different other than the fact that they have similar syntaxes. It has a different set of elements and attributes; while elements in HTML documents are by default displayed one after the other in the order they appear, SVG elements generally have to be told where to display, or they'll appear at the top left, overlapping one another; and the way we style SVG elements are also different—while with HTML we use CSS with properties like background-color and border to style rectangular elements, in SVG we style shapes using attributes like fill, stroke, and stroke-width.

Some of these differences are simply because HTML and SVG are two different languages, but some of the differences are what makes SVG so good for vector-based images compared to HTML.

The other thing we'll need for the visualisation is some text. To add text, we use the appropriately named <text> element:

<svg
  width="400" height="250"
  xmlns="http://www.w3.org/2000/svg">
  <rect
    width="200" height="100"
    x="50" y="50"
    fill="hsl(10, 80%, 70%)"
  />
  <text x="50" y="50">Some text</text>
</svg>

Which outputs:

Some text

There are three things to note here:

  • The <text> element isn't a child of the <rect> element like it would be if it were a <div> and some text in an HTML document. The <rect> element can only be used to output the shape, and cannot be used to position anything inside it. There is an element we can use to do this which we will look at later.
  • The text is displaying above the rectangle, not inside it. This is because by default, text is positioned so that the baseline of the text lies on the point specified by x and y. This is configurable with the dominant-baseline attribute, which again, we will be looking in a bit.
  • While text inside the SVG element inherits font size and font family from the document, it doesn't inherit the colour. This is because the CSS I've applied to the body element of this website is also inherited by the elements inside the SVG, and while we can style SVGs using CSS, it works slightly differently to styling HTML elements.

#Grouping SVG elements

In the example above, the rectangle and text elements are direct children of the SVG element—but what if we have multiple rectangles each with some text associated with them? We could have a rect followed by a text followed by a rect, by a text, etc, but that's not especially nice—especially when we want to start looping through data in Vue.

SVG has an element for grouping other elements, <g>. It's kind of similar to HTML's <div> element. Here's how we use it:

<svg
  width="400" height="250"
  xmlns="http://www.w3.org/2000/svg">
  <g transform="translate(50, 50)">
    <rect
      width="200" height="100"
      fill="hsl(10, 80%, 70%)"
    />
    <text>Some text</text>
  </g>
</svg>

This outputs exactly the same as the example above that used x and y, but without the duplication of the positions as the transform is also being applied to the elements within the group. Note that the group element doesn't accept x and y attributes: you have to use a transform to translate the group instead.

Now that we've freed up the x and y attributes of the text, let's use it to position the text in the centre of the rectangle:

<svg
  width="400" height="250"
  xmlns="http://www.w3.org/2000/svg">
  <g transform="translate(50, 50)">
    <rect
      width="200" height="100"
      fill="hsl(10, 80%, 70%)"
    />
    <text
      x="100" y="50"
      dominant-baseline="middle"
      text-anchor="middle">
      Some text
    </text>
  </g>
</svg>

Here's the output:

Some text

There's a couple things going on here, including two new attributes. First, note that the x and y positions of the text are set to half the width and height of the rectangle. That means that the text will be positioned from the centre of the rectangle. By default, though, that would result in the text appearing in the upper right of the rectangle, like this:

Some text

The two attributes added in the last example help us here. dominant-baseline says where the text should be positioned vertically by specifying which baseline of the element should be used. text-anchor does a similar thing, but horizontally instead of vertically.

Here's a quick demo:

dominant-baseline: text-anchor:

Some text

Don't ask me what all the possible values for dominant-baseline do - I have no idea! You can find an explanation on the MDN article: dominant-baseline.

#CSS and SVG

As I mentioned before, you can style SVGs using CSS, but it works in quite a different way to styling HTML using CSS.

The selector logic is (mostly) the same, but the declarations themselves are different. color: red won't do anything if you apply it to an SVG element. Instead, the declarations you write in the CSS are the same as the attributes you provide directly to the elements.

It's easier understood with an example:

svg text {
  x: 100;
  y: 50;
  dominant-baseline: middle;
  text-anchor: middle;
}

That's the equivalent of writing x="100" y="50" dominant-baseline="middle" text-anchor="middle" on every text element inside the SVG element. I tend to use this for styling stuff—dominant-baseline and text-anchor—but keep the positioning attributes directly on the SVG elements.

So that's a super quick tour of SVG. For a longer read that goes more into depth into all the features of SVGs, I'm a big fan of the SVG tutorial on MDN.

Let's start looking at our COVID-19 visualisation.

#Getting the data

The first part of any data visualisation project is often also the most challenging—getting the data!

In this case, it's not too tricky as a lot of the existing media coverage cites John Hopkins University as a source, and a quick search finds this GitHub repository by the Center of System Science and Engineering at John Hopkins.

Now that we've found that, it's a case of working what data in there we want to visualise—I'm using time_series_covid19_confirmed_global.csv—and transforming the data into the exact data we want to visualise. In this case, I want to visualise the data by country, not by region, so I've got some logic to add the total up for each country.

You can find my code for getting the data and transforming it in data/get-data.js.

Alternatively, I've committed the generated JSON file too—it's in data/country-cases.json (and is probably a bit out of date).

#Building a static chart

Now that we have our data, let's look at how we can use Vue to connect up what we've learned about SVGs so far and the data we just fetched, to create a static chart.

Let's start by creating a Vue instance with some mock data in, so that we can make sure the chart works before we add the real dataset (which is considerably larger) in.

const data = {
  dates: ['1/22/20', '2/11/20', '3/27/20'],
  countryData: {
    China: [548, 44386, 81897],
    'United Kingdom': [0, 8, 14745],
    'United States': [1, 11, 101657]
  }
};

new Vue({
  data: {
    day: 2
  }
});

This is a tiny subset of the data: three days worth of data in three countries. As the final dataset isn't that big—it's not like we have to wait two hours for the visualisation to render—it doesn't have to be perfect and we don't have to test all cases before giving it all the data. We just have to make sure that something is displayed.

Note that we're storing the data in an object outside of the Vue instance. This means that anything we add consuming that data won't be reactive, but that's okay—that object declaration is eventually going to turn into an import statement to get the JSON file anyway.

We're aiming to display the number of cases in each country on a given day, so we want to transform the data to something like this:

[
  { "country": "United States", "value": 101657 },
  { "country": "China", "value": 81897 },
  { "country": "United Kingdom", "value": 14745 }
]

Let's add a chartData computed property that looks at the day and countryData and returns something like that object:

computed: {
  chartData() {
    return Object.entries(data.countryData)
      .map(([country, dataArray]) => {
        return {
          country,
          value: dataArray[this.day]
        };
      })
      .filter(({ value }) => value)
      .sort((a, b) => b.value - a.value);
  }
}

When we get to animating the chart, Vue's reactivity means that if we modify this.day, the data returned by this.chartData will be updated as well.

For now though, we've got enough to move to the template and start displaying some stuff on screen.


In the template, let's loop through chartData, outputting a rectangle for each country:

<svg
  width="760" height="170"
  xmlns="http://www.w3.org/2000/svg">
  <rect
    v-for="({ country, value }, i) in chartData"
    :width="760 / 101657 * value" height="50"
    x="0" :y="i * 60"
    fill="hsl(10, 80%, 70%)"
  />
</svg>

This uses a v-for directive to loop through the data, destructuring each object to get country and value, and also getting i to set the position with.

Then, we calculate the width as a percentage of the maximum value (101657) multiplied by the width of the SVG, so that the widest bar is 100% of the width and the other bars are scaled proportional to that.

Finally, we're using i to set the y position of each rectangle, so that they display one below each other.

This outputs:

There's a few numbers and calculations that are hardcoded that probably shouldn't be, so let's move them out into the Vue instance:

new Vue({
  data: {
    day: 2,
    chartWidth: 760
  },
  computed: {
    chartData() { /* ... */ },
    maxValue() {
      return this.chartData.reduce((max, { value }) => Math.max(value, max), 0);
    }
  },
  methods: {
    barWidth(value) {
      return (this.chartWidth / this.maxValue) * value;
    }
  }
});

And the new template:

<svg
  :width="chartWidth" height="170"
  xmlns="http://www.w3.org/2000/svg">
  <rect
    v-for="({ country, value }, i) in chartData"
    :width="barWidth(value)" height="50"
    x="0" :y="i * 60"
  />
</svg>

(to avoid repetition, I've also moved the fill out into CSS now)


The next step we'll take is to add some text. Eventually we want to display both the country names and numeric values, but let's just add the country names for now. Remember when we used groups in the first section of this article and I said it would come in handy again? Now is that time!

Let's wrap the <rect> element in a <g> tag, move the positioning to the group (remember, we have to use translate instead of x and y attributes), and add the new <text> element:

<svg
  :width="chartWidth" height="170"
  xmlns="http://www.w3.org/2000/svg">
  <g
    v-for="({ country, value }, i) in chartData"
    :transform="`translate(0, ${i * 60})`">
    <rect :width="barWidth(value)" height="50" />
    <text
      x="10" y="25"
      dominant-baseline="middle">
      {{ country }}
    </text>
  </g>
</svg>

United States China United Kingdom

Great. I think we're ready to start using the real data now.


To use the real data, we're going to replace the data declaration with an import statement, getting the JSON file we created earlier.

import data from './data/country-cases.json';

Now, after changing day to 63, the chart looks like this:

Afghanistan Albania Algeria Andorra Angola Antigua and Barbuda Argentina Armenia Australia Austria Azerbaijan Bahamas Bahrain Bangladesh Barbados Belarus Belgium Benin Bhutan Bolivia Bosnia and Herzegovina Brazil Brunei Bulgaria Burkina Faso Cabo Verde Cambodia Cameroon Canada Central African Republic Chad Chile China Colombia Congo (Brazzaville) Congo (Kinshasa) Costa Rica Cote d'Ivoire Croatia Diamond Princess Cuba Cyprus Czechia Denmark Djibouti Dominican Republic Ecuador Egypt El Salvador Equatorial Guinea Eritrea Estonia Eswatini Ethiopia Fiji Finland France Gabon Gambia Georgia Germany Ghana Greece Guatemala Guinea Guyana Haiti Holy See Honduras Hungary Iceland India Indonesia Iran Iraq Ireland Israel Italy Jamaica Japan Jordan Kazakhstan Kenya Korea, South Kuwait Kyrgyzstan Latvia Lebanon Liberia Liechtenstein Lithuania Luxembourg Madagascar Malaysia Maldives Malta Mauritania Mauritius Mexico Moldova Monaco Mongolia Montenegro Morocco Namibia Nepal Netherlands New Zealand Nicaragua Niger Nigeria North Macedonia Norway Oman Pakistan Panama Papua New Guinea Paraguay Peru Philippines Poland Portugal Qatar Romania Russia Rwanda Saint Lucia Saint Vincent and the Grenadines San Marino Saudi Arabia Senegal Serbia Seychelles Singapore Slovakia Slovenia Somalia South Africa Spain Sri Lanka Sudan Suriname Sweden Switzerland Taiwan* Tanzania Thailand Togo Trinidad and Tobago Tunisia Turkey Uganda Ukraine United Kingdom Uruguay Uzbekistan Venezuela Vietnam Zambia Zimbabwe Dominica Grenada Mozambique Syria Timor-Leste Belize Laos Libya West Bank and Gaza Guinea-Bissau Mali Saint Kitts and Nevis UAE United States

You'll notice if you inspect the SVG in the DOM that there are many more elements being output to the DOM than are actually displaying on screen—174 to be precise—but that's okay. It'll actually come in handy later when we start animating the chart.


Now that we've got our data displaying on screen, let's add some way of moving through the data. To do that, we'll need some kind of input.

Range inputs are great for moving quickly through data, but can make looking at a specific day a bit tricky. Text inputs are good for looking at specific days, but again, are less optimised for moving through data. In this case, we care more about moving through the data, so I've added a component which uses vue-slider-component for a prettier range slider (with a hidden <input type="range"> element as fallback for screen readers—from a quick test it seems like vue-slider-component isn't accessible by itself) and a play button to automatically go to the next day of data after a short interval.

Afghanistan Albania Algeria Andorra Angola Antigua and Barbuda Argentina Armenia Australia Austria Azerbaijan Bahamas Bahrain Bangladesh Barbados Belarus Belgium Benin Bhutan Bolivia Bosnia and Herzegovina Brazil Brunei Bulgaria Burkina Faso Cabo Verde Cambodia Cameroon Canada Central African Republic Chad Chile China Colombia Congo (Brazzaville) Congo (Kinshasa) Costa Rica Cote d'Ivoire Croatia Diamond Princess Cuba Cyprus Czechia Denmark Djibouti Dominican Republic Ecuador Egypt El Salvador Equatorial Guinea Eritrea Estonia Eswatini Ethiopia Fiji Finland France Gabon Gambia Georgia Germany Ghana Greece Guatemala Guinea Guyana Haiti Holy See Honduras Hungary Iceland India Indonesia Iran Iraq Ireland Israel Italy Jamaica Japan Jordan Kazakhstan Kenya Korea, South Kuwait Kyrgyzstan Latvia Lebanon Liberia Liechtenstein Lithuania Luxembourg Madagascar Malaysia Maldives Malta Mauritania Mauritius Mexico Moldova Monaco Mongolia Montenegro Morocco Namibia Nepal Netherlands New Zealand Nicaragua Niger Nigeria North Macedonia Norway Oman Pakistan Panama Papua New Guinea Paraguay Peru Philippines Poland Portugal Qatar Romania Russia Rwanda Saint Lucia Saint Vincent and the Grenadines San Marino Saudi Arabia Senegal Serbia Seychelles Singapore Slovakia Slovenia Somalia South Africa Spain Sri Lanka Sudan Suriname Sweden Switzerland Taiwan* Tanzania Thailand Togo Trinidad and Tobago Tunisia Turkey Uganda Ukraine United Kingdom Uruguay Uzbekistan Venezuela Vietnam Zambia Zimbabwe Dominica Grenada Mozambique Syria Timor-Leste Belize Laos Libya West Bank and Gaza Guinea-Bissau Mali Saint Kitts and Nevis UAE United States

You can find the source for this component in src/DayInput.vue on the repository.


It's a bit difficult to know the scale of the data as it has no numerical information on it, so let's add something to help with that.

We could either add an axis, or we could display a numerical value on each bar, but I think we'll go with displaying a value for each bar for this project—not just for the sake of simplicity, but it's also a bit more precise and won't clutter the chart too much.

We're also going to move the bar slightly to the right so that the country text isn't on top of it. This will involve changing the maths slightly as the bar widths will be slightly narrower.

Afghanistan 84 Albania 146 Algeria 302 Andorra 188 Angola 3 Antigua and Barbuda 3 Argentina 387 Armenia 265 Australia 2364 Austria 5588 Azerbaijan 93 Bahamas 5 Bahrain 419 Bangladesh 39 Barbados 18 Belarus 86 Belgium 4937 Benin 6 Bhutan 2 Bolivia 32 Bosnia and Herzegovina 176 Brazil 2554 Brunei 109 Bulgaria 242 Burkina Faso 146 Cabo Verde 4 Cambodia 96 Cameroon 75 Canada 3251 Central African Republic 3 Chad 3 Chile 1142 China 81661 Colombia 470 Congo (Brazzaville) 4 Congo (Kinshasa) 48 Costa Rica 201 Cote d'Ivoire 80 Croatia 442 Diamond Princess 712 Cuba 57 Cyprus 132 Czechia 1654 Denmark 1862 Djibouti 11 Dominican Republic 392 Ecuador 1173 Egypt 456 El Salvador 9 Equatorial Guinea 9 Eritrea 4 Estonia 404 Eswatini 4 Ethiopia 12 Fiji 5 Finland 880 France 25600 Gabon 6 Gambia 3 Georgia 75 Germany 37323 Ghana 93 Greece 821 Guatemala 24 Guinea 4 Guyana 5 Haiti 8 Holy See 4 Honduras 36 Hungary 226 Iceland 737 India 657 Indonesia 790 Iran 27017 Iraq 346 Ireland 1564 Israel 2369 Italy 74386 Jamaica 26 Japan 1307 Jordan 172 Kazakhstan 81 Kenya 28 Korea, South 9137 Kuwait 195 Kyrgyzstan 44 Latvia 221 Lebanon 333 Liberia 3 Liechtenstein 51 Lithuania 274 Luxembourg 1333 Madagascar 19 Malaysia 1796 Maldives 13 Malta 129 Mauritania 2 Mauritius 48 Mexico 475 Moldova 149 Monaco 31 Mongolia 10 Montenegro 52 Morocco 225 Namibia 7 Nepal 3 Netherlands 6438 New Zealand 205 Nicaragua 2 Niger 7 Nigeria 51 North Macedonia 177 Norway 3084 Oman 99 Pakistan 1063 Panama 443 Papua New Guinea 1 Paraguay 37 Peru 480 Philippines 636 Poland 1051 Portugal 2995 Qatar 537 Romania 906 Russia 658 Rwanda 41 Saint Lucia 3 Saint Vincent and the Grenadines 1 San Marino 208 Saudi Arabia 900 Senegal 99 Serbia 384 Seychelles 7 Singapore 631 Slovakia 216 Slovenia 528 Somalia 1 South Africa 709 Spain 49515 Sri Lanka 102 Sudan 3 Suriname 8 Sweden 2526 Switzerland 10897 Taiwan* 235 Tanzania 12 Thailand 934 Togo 23 Trinidad and Tobago 60 Tunisia 173 Turkey 2433 Uganda 14 Ukraine 145 United Kingdom 9640 Uruguay 217 Uzbekistan 60 Venezuela 91 Vietnam 141 Zambia 12 Zimbabwe 3 Dominica 7 Grenada 1 Mozambique 5 Syria 5 Timor-Leste 1 Belize 2 Laos 3 Libya 1 West Bank and Gaza 59 Guinea-Bissau 2 Mali 2 Saint Kitts and Nevis 2 UAE 333 United States 65778

I hope you're starting to see why SVG is such a powerful tool. Achieving something like the above graph is possible in HTML, but wouldn't be especially fun. And we've barely scraped the surface of what SVG can do!


That's it for part one of this article. In part two, we'll look at how the chart can be animated—make sure you follow me on Twitter to hear when it's published.

Big thanks to Darek Gusto and Bartosz Trzos for proof reading this article!

« Return to home