Bring static data to life with an animated chart

Bring static data to life with an animated chart

Animated charts can be very effective for telling a data-story in an engaging way, and a neat trick for making static data look more like live data. In this article, we will show you how to create an animated chart from a static data set. When we are done, the chart will work like the one below (click the “Start” button to run the animation):

Let’s have a look at how this was made:

1. Create a basic interactive chart

Let’s start by creating a basic interactive chart. Our data source will be a daily updated dataset on the infections and deaths caused by the COVID-19 virus worldwide.

To create a chart, we simply need to fetch the data then map it to Highcharts’ input format. Here is a simple snippet to do that:

const globalData = [];
let chart;
// Fetch data:
fetch('https://pomber.github.io/covid19/timeseries.json')
  .then(response => response.json())
  .then(data => {
    parseData(data);
    createChart();
  });
function parseData(data) {
  Highcharts.objectEach(
    data,
    // Prepare Highcharts data-format:
    // series: [{
    //   data: [ [x, y], [x, y], ..., [x, y]]
    // }]
    (countryData, country) => globalData.push({
      name: country,
      data: countryData.map(p => [Date.parse(p.date), p.confirmed])
    })
  );
}
function createChart() {
  chart = Highcharts.chart('container', {
    title: {
      text: 'Confirmed cases per country'
    },
    xAxis: {
      type: 'datetime'
    },
    yAxis: {
      title: ''
    },
    series: globalData
  });
}

The static chart looks like the following:

Even though our chart is interactive and displays all the data, it is not yet an optimal visualization:

  • Overloaded with data: showing all 180+ countries at the same time not only could easily decrease your browser’s performance but also leave the audience confused.
  • Readability: the US data is the only one visible on the chart, as the US has the biggest number of cases, whereas all the other countries’ cases are barely visible on the chart.
  • Design: it’s a bit plain, titles are missing, and tooltips are not helpful as they could be.
  • A little boring: does not tell a clear story.

Let’s improve this chart by tweaking the design and use some animation.

Improving the chart’s design

To make a compelling chart it is important to think about how the chart’s elements are designed, such as the styling of the title, the position of the legend, and of course use a reasonable amount of data on the chart to help the audience to get the best insights, there is no rule, just use common sense. The chart below is a design update of the previous chart:

You can tell that this chart is way better than the previous one. First of all, only the 8 top countries with confirmed cases are represented; this will allow the audience to better digest the information and get better insights by comparing 8 countries instead of 180+ countries. The second thing to notice is that the title is on the left side to give more room to the exponential progress of the data on the right side, and to the positioning of the legend. By the way, the legend also helps the audience to associate each country with the right series.
Let’s take a look at the different Highcharts features and options used to achieve this improvement:

  • Use the floating option to move freely your chart.title and chart.subtitle around the chart.
  • Use the following setting legend.layout='proximate' to render legend items as close as possible to the last point in the data set.
  • Set up the tooltip option to true tooltip.split=true to display an individual tooltip for each series, this will help the audience to find out when series-crossing have happened.

Ok, now our chart looks much better. Animation next!

3. Make the chart animated

The third and final step is to make our chart an animated chart. This can be achieved in a few steps:

  1. Adding GUI to allow the audience to start/stop/replay the chart.
  2. Preparing dataset to be updated every x-milliseconds.
  3. Live updating.

1. GUI
Remark
I will not explain HTML & CSS in the demo here; you can find related code in HTML and CSS tabs in the jsFiddle demo below.

First, we need to define the following options in the code:

let duration = 500; // Determines how long the animation between new points should be take
let startIterator = 1; // Determines how many points will be rendered on chart's init
let currentIterator = startIterator;
let maxIterator = 1;

let guiButton = document.getElementById('start');
let guiButtonState = 'Start';
let intervalId;

Then create initEvents() method to manage the GUI:

function initEvents() {
  guiButton.addEventListener('click', function() {
    if (guiButtonState === 'Stop') {
      // User clicked "Stop" -> stop animation and allow to resume
      intervalId = clearInterval(intervalId);
      guiButton.innerText = guiButtonState = 'Resume';
    } else {
      // If animation has finished, recreate chart
      if (guiButtonState === 'Restart') {
        createChart();
      }
      guiButton.innerText = guiButtonState = 'Stop';
      // Start animation:
      redrawChart(iterator += 1);
      intervalId = setInterval(function() {
        // If we reached last available point, stop animation:
        if (iterator === maxIterator) {
          intervalId = clearInterval(intervalId);
          iterator = startIterator;
          guiButton.innerText = guiButtonState = 'Restart';
        } else {
          redrawChart(iterator += 1);
        }
      }, duration);
    }
  });
}

And now attach events:

// Fetch data:
fetch('https://pomber.github.io/covid19/timeseries.json')
  .then(response => response.json())
  .then(data => {
    parseData(data);
    createChart();
    initEvents(); // Init events
  });

2. Prepare the data
Just two, minor changes are needed:

  1. Don’t set full data set on chart init, only subset of it:
    function createChart() {
      ...
      series: globalData.map(series => {
          return {
            name: series.name,
            data: series.data.slice(0, startIterator)
          }
        })
        ...
    }
  2. Calculate maxIterator so we will know when the animation should end:
    function parseData(data) {
      ...
      maxIterator = Math.max.apply(null, globalData.map(series => series.data.length - 1));
    }

Live update
You may have observed that in initEvents() there is a call to chartRedraw(integer) – it’s time to implement it. To get nice and smooth animation, we use series.addPoint() which adds a point to one series. Here is code:

function redrawChart(index) {
  // Set new subtitle on every redraw
  chart.setTitle(null, {
    text: Highcharts.dateFormat('%d-%m-%Y', globalData[0].data[index][0])
  }, false);

  // To each series, add a point:
  chart.series.forEach(
    (series, seriesIndex) =>
    series.addPoint(
      globalData[seriesIndex].data[index],
      false,
      false,
      false
    )
  );

  // Now, once everything is updated, redraw chart:
  chart.redraw({
    duration
  });
}

The result of the changes is presented in the following chart:

Even though the chart is animated, there are still issues to address:

  1. Initial steps in the animation are too aggressive; it’s a good idea to set options like yAxis.softMax and xAxis.minRange to prevent this issue.
  2. Each point can have individually set option like marker and dataLabels that means we can limit noise in the chart caused by markers using these options in addPoint() method.

The chart looks like the following after adding the changes to the code:

What’s next?

That’s not all. Our chart looks great: it’s quite easy to read which series is where, at any point, the audience can stop the animation to check the data on the chart and can easily customize it; for example, change the duration of the one-step in animation, set how many points are rendered on chart init, etc.

There’s however one more thing we can change in our dataset. Right now all series start at the same point in time and it’s easy to compare values on a chart at a specific point in time. But we cannot compare trends between countries based on the start of the outbreak in each country. In order to do that, let’s set the common origin, called Day #0, which is the first day with a confirmed case in each country.
To do that we need to:

  • Resign from date-time axis and use basic linear axis with labels.format.
  • Remove from the dataset the points that have value=0.

And voilà, here is the animated chart after the latest updates:

You can find the changes in the source code. I will not explain them here as this is basically related to the data, not the Highcharts code.

We could analyze further lines in the chart, but instead, let’s modify our data even more. Instead of showing confirmed cases in a country, we will try to display the number of confirmed cases per 1000 inhabitants indicator. All we need is an estimated population per country. We can get that from Wikipedia. Again, we start with our main chart and modify it.
In parseData() function we add the following:

Highcharts.objectEach(
  data,
  // Prepare Highcharts data-format:
  // series: [{
  //   data: [ [x, y], [x, y], ..., [x, y]]
  // }]
  (countryData, country) => globalData.push({
    name: country,
    data: countryData.map(p => [Date.parse(p.date), p.confirmed / getCountryPopulation(country) * 1000])
  })
);

Where getCountryPopulation(country) method returns population value.
To improve readability, let’s create a method which can be used in tooltip.pointFormatter and dataLabels.formatter:

function format(y) {
  return y < 0.01 ? '<0.01' : '~' + y.toFixed(2);
}

Here is the result:

The dataset provides us also with other information such as the number of deaths and recovery. Here you can find demos with these numbers demo1 and demo2.

Now you have an idea about how you can create an animated chart. Let us know what else are you able to read from the charts above? Feel free to come up with your own animated charts and share with us your experience in the comment section below.

Remember, these numbers are actual people’s lives, not a ranking.