How to Add a Heatmap to Your Covid Tracker Using React

crashdaddy
The Startup
Published in
11 min readJun 18, 2020

--

It’s easier than you might think

Intro:

Have you heard that old joke recently?

Q: “What does a programmer do while in quarantine?”

 6:00AM — wake up 6:30AM — Shower, breakfast, coffee 7:00AM — build the first Covid tracker of the day

I’m in this joke — and I like it!

So today, instead of just reading API data and spitting it out in pretty rows, let’s jazz it up with a little visual enhancement!

Let’s Get Started:

Go ahead and create your react app using npx create-react-app bestappever and get yourself to the App.js file.

Just like normal, we’re going to gut that file and replace it with our own app.

Here’s the render portion of mine:

render() {
return (
<div>
<Navbar />
{/* // the global totals line above the country data */}
{this.state.global ?
<GlobalStats global={this.state.global} lastUpdate={this.state.lastUpdate} />
:
<NoData/>
}
{/* // the container div for the map */}
<div id="mapdiv" className="map"></div>
</div>
);
}
}

You can see it’s just the navigation bar at the top of the screen, then we use a ternary operator to see if any data got returned. If it did, then we display a “GlobalStats” component, but if it didn’t, we display an “error trap” component called “NoData” that tells the user that the API failed.

The people deserve to know what’s going on, even if it’s nothing

And lastly we have an empty div called “mapDiv” (because I’m so creative like that) which will hold the map.

We’ll get to the map in a minute, settle down.

The Data:

Now let’s call up some data to work with.

The only reason I built this version is because my good buddy Hassan called me up and said, “Hey, Crashdaddy, I found another API for Covid data!”

You may remember the fiasco with Bing a few months ago when they were providing a very nice, very granular API which they then abruptly yanked back after a couple of weeks. That’s why we built the “No Data” component.

I was still a bit gun-shy, but Hassan was eager, and had already gotten the data. Here’s his retrieval function:

// our initial API call
fetchData() {
fetch(`https://api.covid19api.com/summary`)
.then((json) => json.json())
.then((data) => {
this.setState({
currentCountry: '',
global: data.Global,
hits: data.Countries,
lastUpdate: data.Date
});
this.createNewChart();
})
.catch((error) => console.log("parsing failed", error));
}

That’s all pretty cut-and-dried. You probably don’t need me to tell you this, but you call this function from componentDidMount:

componentDidMount() {
this.fetchData();
}

Caveat: we never ran into any CORS errors using this API, but if you do, then just exchange the “fetch” command for the Axios “get” command and you’ll be fine.

So, here’s our data. Pretty straightforward. Here’s the documentation of all their different endpoints.

There’s an object called “Global” that holds the cumulative totals, then there’s an array called “Countries” which has the same data broken down by country. Finally there’s the “Date” element that just shows the latest update time.

You can see in the fetchData() function, Hassan has parsed each of these elements into a corresponding element of the “state” object. Nice work, Jim, now go fight that alligator.

Notice the one extra function call there, called this.createNewChart()? Let’s focus on that next.

The Map:

I was just dorking around one day, looking for a good map to use, when I ran into something called amCharts. It’s outstanding; flexible and super easy to use. Let’s use that.

I know what you’re thinking, “Well if all you’re going to do is read the docs to me, then I could do that myself!”

Docs can be daunting. I get that. They’re usually written in a clinical, detached, highly technical manner that’s often inaccessible to many people until they’ve been kind of eased into it.

I gotchu fam.

So first you’re going to want to install the library. We’re going to use the main library and the maps library, so just install these:

npm install @amcharts/amcharts4
npm install @amcharts/amcharts4-geodata

The beauty of amCharts is that they treat a map as just a different type of chart; like a pie chart or a line graph, but it’s a map. Using the geodata library is what allows the map to understand borders and countries without us having to do anything.

There are many built in features available to us when using a map. For the most part, they’re all listed on the Anatomy of a Map Chart section.

We start by instantiating our map to the “mapDiv” element we made first:

var map = am4core.create("mapDiv", am4maps.MapChart);

I put that in the createNewChart() function that we called after we got our data back from the API.

Here’s the whole function and then we’ll go over what’s happening

createNewChart = () => {// setup the chart in its container
let map = am4core.create("mapdiv", am4maps.MapChart);
// we're going to use the low-res world map
map.geodata = am4geodata_worldLow;
// the type of map we're using
// here's the others: https://www.amcharts.com/docs/v4/chart-types/map/
map.projection = new am4maps.projections.Miller();// this is going to define all the countries on the map
var polygonSeries = new am4maps.MapPolygonSeries();
// and handle it automatically! ooh, yeah...
polygonSeries.useGeodata = true;
// get rid of Antarctica (remove the next line to bring it back)
polygonSeries.exclude = ["AQ"];
// this is the template to interface our own configurations
// to the map countries (polygonseries)
var polygonTemplate = polygonSeries.mapPolygons.template;
polygonSeries.calculateVisualCenter = true;
polygonSeries.tooltip.label.interactionsEnabled = true;
polygonSeries.tooltip.keepTargetHover = true;
polygonSeries.mapPolygons.template.tooltipPosition = "fixed";
// when we hover over a country show its name and number of casespolygonTemplate.tooltipHTML = `
<img style="float:left;vertical-align:middle;margin-right:4px;" src="https://www.countryflags.io/{id}/shiny/24.png"/>
<strong>{name}</strong><br/>
<hr>
Confirmed Cases: {value} <br/>
New Cases: {newConfirmed} <br/>
Total Deaths: {deaths}<br/>
Recent Deaths: {newDeaths}<br/>
Recovered: {recovered}<br/>
<a href="/country/{slug}/{name}" style="text-decoration:none;font-size:small">View Progression</a>
`;
// Create hover state and set alternative fill color
let hs = polygonTemplate.states.create("hover");
hs.properties.fill = am4core.color("#a09d9a");
// when we click on a country zoom in on that country
polygonTemplate.events.on("hit", function(ev) {
ev.target.series.chart.zoomToMapObject(ev.target)
});
// set up the scale and values for the heatmappolygonSeries.heatRules.push({
"target": polygonSeries.mapPolygons.template,
"property": "fill",
"min": am4core.color("#c2918c"),
"max": am4core.color("#ff0000"),
"dataField": "value",
"logarithmic": true
});
// check if the API data has filled the state object yetlet mapData = this.state.hits || [];// go through all the API data and add each country's id, name and // TotalConfirmed to the map// all these values can be changed to whatever we want to display on our mapmapData.forEach(newData => {polygonSeries.data.push({"id": newData.CountryCode,
"slug":newData.Slug,
"name": newData.Country,
"value": newData.TotalConfirmed,
"newConfirmed":newData.NewConfirmed,
"newDeaths":newData.NewDeaths,
"deaths": newData.TotalDeaths,
"recovered": newData.TotalRecovered})
});
// tell it what property to use for the heatmap
polygonTemplate.propertyFields.fill = "fill";
// commit our settings
map.series.push(polygonSeries);
// establish our map. Ease to the pease.
this.map = map;
}

Wow! That’s a lot. Let’s see what’s going on.

Since we won’t be zooming our map in finely enough to need totally accurate borders between countries we can use the low-res map version.

// we're going to use the low-res world map
map.geodata = am4geodata_worldLow;
// the type of map we're using
// here's the others: https://www.amcharts.com/docs/v4/chart-types/map/
map.projection = new am4maps.projections.Miller();

And we set our “projection” to Miller. On the Anatomy of a Map Chart page you’ll find 11 different types of projection you can use. I chose Miller because it’s a wide-angle view of the whole globe at once.

But if you’ve run it already, then you’ve notice that lousy Antarctica is hogging up half the real estate for the map! :mad_smiley:

Here’s how we solved that problem

// this is going to define all the countries on the map
var polygonSeries = new am4maps.MapPolygonSeries();
// and handle it automatically! ooh, yeah...
polygonSeries.useGeodata = true;
// get rid of Antarctica (remove the next line to bring it back)
polygonSeries.exclude = ["AQ"];

The “polygonSeries” is basically the geodata reference for all the countries, so the map knows where each one starts and ends. We tell it to “useGeodata” because if we wanted to we could define our own country boundaries and borders, like “Narnia,” “Hyboria,” “Ohio” or any other fictional places.

Since we’re using geoData, each country has a unique country code based on the ISO 2 standard. And Antarctica is “AQ”, so that last line just tells the map not to even show Antarctica at all.

One thing that can happen as a programmer is that we can become desensitized to the data we’re working with and forget that these numbers represent actual people. So if Antarctica ends up becoming some kind of Covid-19 hot spot, we can just go back and take that line out of our code and it’ll pop right back into place.

Next we’re going to build a template so that this standard map will become our own specific map, with its own attributes and methods:

// this is the template to interface our own configurations
// to the map countries (polygonseries)
var polygonTemplate = polygonSeries.mapPolygons.template;
polygonSeries.calculateVisualCenter = true;
polygonSeries.tooltip.label.interactionsEnabled = true;
polygonSeries.tooltip.keepTargetHover = true;
polygonSeries.mapPolygons.template.tooltipPosition = "fixed";

We’re just telling it to build a template in which when we hover over a country our default offset is the center of the country, not just where the mouse is pointing.

We’re doing that because we don’t want to output our data in lists anymore (that’s sooo 2019) but as an interactive component of the map instead.

We tell it to enable interactive tooltips. This way we can add links, buttons, even forms on the tooltip that pops up when the user hovers over a country.

“keepTargetHover” and “toolTipPosition” just tell the map that we don’t want the tooltip to just disappear, or worse, move around when we try to mouse over it.

Let’s look at our map again and see what we’re talking about:

Notice you can use HTML in the tooltip

Here’s what we’re going to display on the tooltip

// when we hover over a country show its name and number of casespolygonTemplate.tooltipHTML = `
<img style="float:left;vertical-align:middle;margin-right:4px;" src="https://www.countryflags.io/{id}/shiny/24.png"/>
<strong>{name}</strong><br/>
<hr>
Confirmed Cases: {value} <br/>
New Cases: {newConfirmed} <br/>
Total Deaths: {deaths}<br/>
Recent Deaths: {newDeaths}<br/>
Recovered: {recovered}<br/>
<a href="/country/{slug}/{name}" style="text-decoration:none;font-size:small">View Progression</a>
`;

Yep, it’s just regular good ‘ol HTML. Not like the old days, that’s for sure.

First, we’re using an API called CountryFlags API to display a little tiny flag for each country over which we’re hovering.

Then we show the name of the country, then a horizontal line followed by the actual data from the API.

Another Caveat: That last line is from my own version of this app where if the user clicks that link they go to another page that shows a detailed chart of the infection progression for that country over time.

Don’t worry, I’m going to give you a link to the repo so you can see it for yourself.

But how do we get the data from the API to the tooltip?! We’ll get there in a minute. First there’s a couple more things we want to do

// Create hover state and set alternative fill color
let hs = polygonTemplate.states.create("hover");
hs.properties.fill = am4core.color("#a09d9a");
// when we click on a country zoom in on that country
polygonTemplate.events.on("hit", function(ev) {
ev.target.series.chart.zoomToMapObject(ev.target)
});

We want to set the background color for a country when we hover over it. That’s a shade of grey I used because the natural color for the countries is going to be a shade of red based on the number of confirmed cases that country has.

The next line just tells it that when the user actually clicks on a country we want to zoom the map to focus on that country.

Now let’s setup the rules for our heatmap, so all the countries’ background colors will be based on how many confirmed cases they have!

// set up the scale and values for the heatmappolygonSeries.heatRules.push({
"target": polygonSeries.mapPolygons.template,
"property": "fill",
"min": am4core.color("#c2918c"),
"max": am4core.color("#ff0000"),
"dataField": "value",
"logarithmic": true
});

Pretty simple. We’re just telling it these rules are for the template we’ve just built. We’re telling it we want each color to be solid.

We set the minimum color and maximum color to be a light and a dark shade of red. The “logarithmic” algorithm will decide what variation of color to use based on the data it finds.

Did I mention how easy this is?

Now here’s where we start interacting with the data from the API, when we set the “datafield” to “value”. That’ll make more sense in a minute, because now we’re going to hook it up to the data!

// check if the API data has filled the state object yetlet mapData = this.state.hits || [];// go through all the API data and add each country's id, name and // TotalConfirmed to the map// all these values can be changed to whatever we want to display on our mapmapData.forEach(newData => {polygonSeries.data.push({"id": newData.CountryCode,
"slug":newData.Slug,
"name": newData.Country,
"value": newData.TotalConfirmed,
"newConfirmed":newData.NewConfirmed,
"newDeaths":newData.NewDeaths,
"deaths": newData.TotalDeaths,
"recovered": newData.TotalRecovered})
});

First we check if there even is any data yet, and if there isn’t we just make mapData be an empty array, which does nothing. But if there is data, then we assign it from the state.hits variable we filled in our original fetchData() function.

Then we map all the data into an object that we push onto the “polygonSeries.data” array. We can put whatever info we want in this object so we can customize our map in different ways.

I’m pushing the country ID just so we can display the country flag on the tooltip. Remember this?

<img style="float:left;vertical-align:middle;margin-right:4px;" src="https://www.countryflags.io/{id}/shiny/24.png"/>

The next thing, “slug,” is a shortened version of the country’s name that I use in the link at the bottom of the tooltip. The “name” key, of course, is the name of the country.

Here’s where that “value” was that we were using to base our heatmap colors on. It’s specifically set to “TotalConfirmed,” but you could just as easily set it to “TotalDeaths” and make your heatmap display shades of black instead.

Then we tell it what property to use for the heatmap

// tell it what property to use for the heatmap
polygonTemplate.propertyFields.fill = "fill";

Remember in our heatmapRules we told it:

"property": "fill",

This is what connects our heatmap rules to the background color of each country.

And then we’re done. Just commit our settings and create the map!

// commit our settings
map.series.push(polygonSeries);
// establish our map. Ease to the pease.
this.map = map;

And then when you’re done with it, just put this line in to dispose of it:

componentWillUnmount() {
if (this.map) {
this.map.dispose();
}
}

Top Hats and Limousines

Wasn’t that the easiest thing ever? Now you’ve called an API, retrieved the data, and output it onto a map in (way) less than 100 lines of code. Good for you!

Notice I left out the GlobalStats and NoData components? That’s because we’re just here to talk about the map today. I will tell you that the text in the GlobalStats containers is in SVG format so it will automatically scale to fit the container in which it lives.

You can see it in the repo. It’s pretty cool.

Which here’s the repo (it’s Hassan’s but contains all this goodness): https://github.com/hbrashid/covid-react

And here’s a live version of the app: https://covidupdater.herokuapp.com/

(remember, it’s a herokuapp, so the first time you go here it takes a crazy long time to fire up)

That’s all. Have fun and Happy Mapping!

All this badassery make us thirsty.

--

--

crashdaddy
The Startup

4a 75 73 74 20 61 6e 6f 74 68 65 72 20 63 6f 6d 70 75 74 65 72 20 6e 65 72 64 20 77 69 74 68 20 61 20 62 6c 6f 67