DEV Community

Cover image for Tutorial: How to create a global  vaccinations dashboard using React, Mapbox, and Bootstrap
Saket S
Saket S

Posted on • Updated on

Tutorial: How to create a global vaccinations dashboard using React, Mapbox, and Bootstrap

Hey everyone! In this tutorial, we shall be building a map-cum-dashboard tracking the progress of vaccination against COVID-19 in every country.

Here is a preview of our finished React App. I took inspiration from John Hopkins' COVID-19 Map for this project. I am sure most of you guys have seen their Coronavirus map at some point last year. We shall be building something very similar here but instead of COVID-19 cases, we will be looking at vaccination figures.

πŸ”— Links

Tutorial

I have divided this tutorial into seven sections. I'm linking them here so it becomes easier to follow along and navigate.

  1. Initial Setep
  2. Setting up Mapbox
  3. Styling + Creating a dashboard header
  4. Getting Vaccination figures from the API
  5. Creating markers on the map for every country
  6. Setting variable size markers
  7. Creating styled tooltips

1. Initial setup

We are going to start by creating a blank react app by running the create-react-app command. For this tutorial, I am going to name my app, vaccinations-dashboard but feel free to give it any name you like. Run the following lines in your terminal one at a time started.

npx create-react-app vaccinations-dashboard
cd vaccinations-dashboard
npm install react-map-gl@5.2.11
npm start
Enter fullscreen mode Exit fullscreen mode

React-map-gl is a react wrapper for using Mapbox. It has all the necessary components your react app needs for displaying the map provided by the Mapbox API. Note that we are installing an older version of react-map-gl since the newer releases have some glitches rendering the map in production. You can read about this issue here in case you're interested.

Once you've finished running these lines in your terminal, your React app should be up and running on localhost:3000.

Next, we can remove most of the boilerplate code by clearing everything in App.js, App.css, and index.css.

We shall be writing our own CSS here, and don't worry, it's not going to be too big since we'll be using Bootstrap-5 for styling the navbar and other components. The only bit of CSS we are going to write here is for positioning the map window and styling the tooltip card and circular markers.

2. Setting up Mapbox

Now, head over to Mapbox for creating your account. You are going to need one for using their services.

Once logged in, you should see be able to see your free access token. Copy this string and save it in a new .env file in your root directory. You can give it any name of your choice but make sure it is preceded by REACT_APP. I am going with REACT_APP_MAPBOX for this tutorial. This is going to be referenced by process.env.<<your_variable_name>> from App.js when you create a Mapbox instance.

Your .env file should look like this now.

REACT_APP_MAPBOX = <<your_access_token>>
Enter fullscreen mode Exit fullscreen mode

Next, we are going to create a Mapbox instance in our App.js file. You can design your own map theme or select one from their gallery. The theme that I am using here can be found on this link. You can copy it to your account.

Here is the full code for setting up Mapbox.

App.js

import React, { useState, useEffect } from "react";
import ReactMapGL, { Marker, Popup } from 'react-map-gl';

function App(){
   const [viewport, setViewport] = useState({
    width: "100vw",
    height: "100vh",
    latitude: 0,
    longitude: 0,
    zoom: 2
  });

return(
 <ReactMapGL
    {...viewport}
    mapboxApiAccessToken={process.env.REACT_APP_MAPBOX}
    onViewportChange={nextViewport => setViewport(nextViewport)}     
    mapStyle="<<your_map_style>>"
    >   
 </ReactMapGl>
)
}

export default App;
Enter fullscreen mode Exit fullscreen mode

On refreshing your dev server, you should see a map window on your screen. The viewport also gets adjusted when you zoom in.

3. Styling + Creating a dashboard header

Next up, we copy the Bootstrap-5 CDN file into index.html and import a custom font for our App. I am using Nunito Sans but you can pick any font of your choice from Google Fonts.

copy the following into your index.css file

index.css

@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,600;1,300&display=swap');

/* body style */
body {
  margin: 0;
  font-family: 'Nunito', sans-serif;
}

/* header style. position:relative fixes the position of the header wrt the map window */
.header{
  position: relative;
}
Enter fullscreen mode Exit fullscreen mode

Copy the bootstrap-5 CDN into index.html

index.html

<head>
 <!---
 boilerplate meta tags
 --->
<link 
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/boot strap.min.css" rel="stylesheet" integrity="sha384-wEmeIV1mKuiNpC+IOBjI7aAzPcEZeedi5yW5f2yOq55WWLwNGmvvx4Um1vskeMj0" crossorigin="anonymous">
<title>React App</title>
</head>
Enter fullscreen mode Exit fullscreen mode

Now create a header <div> element with the following styles right before the Mapbox instance.

App.js

return(
<>
 <div className="header">
    <div className="navbar navbar-dark bg-dark">
        <div className="container-fluid">
          <h6 className="mx-auto navbar-brand">
            Global Vaccination Tracker
          </h6>
        </div>
      </div>
 </div>
 <ReactMapGL
    {...viewport}
    mapboxApiAccessToken={process.env.REACT_APP_MAPBOX}
    onViewportChange={nextViewport => setViewport(nextViewport)}     
    mapStyle="<<your_map_style>>"
    >   
 </ReactMapGl>
</>
)

Enter fullscreen mode Exit fullscreen mode

You should now see a header component and the new font style should also reflect on the dev server.

4. Getting Vaccination figures from the API

We are going to use two public APIs here for getting country-wise vaccination data. The first API contains general COVID-19 stats along with the ISO code and geo-JSON data for every country. The second one contains vaccination data.

We are going to send chained fetch requests to both APIs and store the combined response data as an object in a single state variable.

App.js


const url_cases = "https://disease.sh/v3/covid-19/countries"
const url_vaccinations = "https://disease.sh/v3/covid-19/vaccine/coverage/countries?lastdays=1&fullData=false"

const [dataCountries, setDataCountries] = useState({})

useEffect(async() => {
    let full_data =  {}

    let res_items = await Promise.all([ fetch(url_cases), fetch(url_vaccinations) ])

    let data_cases = await res_items[0].json()
    data_cases.map((item) => {
      const {country, countryInfo, cases, deaths, population} = item

      full_data[country] = {country, countryInfo, cases, deaths, population}
    })

    let data_vaccinations = await res_items[1].json()
    data_vaccinations.map((item, index) => {
     if(full_data[item.country]){
       full_data[item.country]['total_vaccinations'] = Object.values(item.timeline)[0]
     }
    })

}, [])

Enter fullscreen mode Exit fullscreen mode

To understand this better, take a look at the response data from both APIs.

API-1

[
 {
    "updated": 1620970488191,
    "country": "USA",
    "countryInfo": {
      "_id": 840,
      "iso2": "US",
      "iso3": "USA",
      "lat": 38,
      "long": -97,
      "flag": "https://disease.sh/assets/img/flags/us.png"
    },
    "cases": 33626097,
    "todayCases": 0,
    "deaths": 598540,
    "todayDeaths": 0,
    "recovered": 26667199,
    "todayRecovered": 0,
    "active": 6360358,
    "critical": 8611,
    "casesPerOneMillion": 101076,
    "deathsPerOneMillion": 1799,
    "tests": 462795300,
    "testsPerOneMillion": 1391111,
    "population": 332680263,
    "continent": "North America",
    "oneCasePerPeople": 10,
    "oneDeathPerPeople": 556,
    "oneTestPerPeople": 1,
    "undefined": 19119,
    "activePerOneMillion": 19118.53,
    "recoveredPerOneMillion": 80158.64,
    "criticalPerOneMillion": 25.88
  },
 ...
]
Enter fullscreen mode Exit fullscreen mode

API-2

[
 {
    "country": "USA",
    "timeline": {
      "5/13/21": 264680844
 },
...
]
Enter fullscreen mode Exit fullscreen mode

So we basically merge the two response objects for each country and store this merged data in a state variable.

Our dataCountries state variable should now contain the country-wise response data object from both APIs.

Here is how the dataCountries variable would look like on the react dev-tools window of your browser.

dataCountries
state variable after data is loaded

The screenshot I included contains an additional property called 'size' for every country. This controls the size of the marker depending on the number of doses administered by a country. More on that later!

5. Creating markers on the map for every country

In this step, we are going to use the geographic coordinates of every country to draw markers on the map. You'd need to import the Marker and Popup components from the react-map-gl package. Every Marker takes two properties: the latitude and longitude of a location. We are going to style each marker by giving it a light green background and border radius.

App.js

<ReactMapGL
    {...viewport}
    mapboxApiAccessToken={process.env.REACT_APP_MAPBOX}
    onViewportChange={nextViewport => setViewport(nextViewport)}     
    mapStyle="<<your_map_style>>"
    >   
    {dataCountries && Object.values(dataCountries).map((country, index) => {
          return(
            <Marker key={index} latitude={country.countryInfo.lat} longitude={country.countryInfo.long}>
              <div 
              style={{height: 30, width: 30}}
              className="map-marker" 
              > 
              </div>
            </Marker>
          )
        })}
 </ReactMapGl>
Enter fullscreen mode Exit fullscreen mode

index.css

.map-marker{
  border-radius: 50%;
  cursor: pointer;
  background-color: #1de9b6;
  opacity: 0.5;
}
Enter fullscreen mode Exit fullscreen mode

On refreshing the dev server, you should now be able to see a map with green markers on the coordinates of every country.

Alt Text

6. Setting variable size markers

Remember the "size" property that every country in the object had? We are now going to create a method that decides the size of the marker based on the number of vaccine doses a country has administered till now. Let's go with 5 size choices for every marker: 0, 15, 30, 45, 60. Here's how our method will decide the size:

  • The countries in the top 25% in terms of doses administered get a marker size of 60.
  • Countries lying in the (50-75)% percentile range get a marker size of 45.
  • Countries lying in the (25-50)% percentile range get a marker size of 30.
  • Countries lying in the bottom 25% get a marker size of 15.
  • And finally, countries with no vaccination data or zero doses get a size of 0.

Here's the code for this function. It's named prepareData()

  const prepareData = (data) => {
    let vaccinations = []
    Object.values(data).map((obj) => {
      if(obj.total_vaccinations){
        vaccinations.push(parseInt(obj.total_vaccinations))
      }
    })
    vaccinations.sort((a,b) => a - b)
    let firstq = vaccinations[Math.floor(vaccinations.length/4)]
    let secondq = vaccinations[Math.floor(vaccinations.length/2)]
    let thirdq = vaccinations[Math.floor(vaccinations.length*3/4)]

    Object.values(data).map((obj) => {
      if(!obj.total_vaccinations){
        obj.size = 0
      }
      else if(obj.total_vaccinations > 0 && obj.total_vaccinations <= firstq){
        obj.size = 15
      }
      else if(obj.total_vaccinations > firstq && obj.total_vaccinations <= secondq){
        obj.size = 30
      }
      else if(obj.total_vaccinations > secondq && obj.total_vaccinations <= thirdq){
        obj.size = 45
      }
      else{
        obj.size = 60
      }
    })

    setDataCountries(data)
  }
Enter fullscreen mode Exit fullscreen mode

Now the useEffect hook is modified a bit.

App.js

  useEffect(async() => {
    let full_data =  {}

    let res_items = await Promise.all([ fetch(url_cases), fetch(url_vaccinations) ])

    let data_cases = await res_items[0].json()
    data_cases.map((item) => {
      const {country, countryInfo, cases, deaths, population} = item

      full_data[country] = {country, countryInfo, cases, deaths, population}
    })

    let data_vaccinations = await res_items[1].json()
    data_vaccinations.map((item, index) => {
     if(full_data[item.country]){
       full_data[item.country]['total_vaccinations'] = Object.values(item.timeline)[0]
     }
    })

    prepareData(full_data)
  }, [])

Enter fullscreen mode Exit fullscreen mode

Also, we add this size property to the marker by setting it equal to its height and width.

App.js

<ReactMapGL
    {...viewport}
    mapboxApiAccessToken={process.env.REACT_APP_MAPBOX}
    onViewportChange={nextViewport => setViewport(nextViewport)}     
    mapStyle="<<your_map_style>>"
    >   
    {dataCountries && Object.values(dataCountries).map((country, index) => {
          return(
            <Marker key={index} latitude={country.countryInfo.lat} longitude={country.countryInfo.long}>
              <div 
              style={{height: country.size, width: country.size}}
              className="map-marker" 
              > 
              </div>
            </Marker>
          )
        })}
 </ReactMapGl>
Enter fullscreen mode Exit fullscreen mode

You should now be able to see variable size markers on the map.

7. Creating styled tooltips

Next up, we'll be creating tooltips showing vaccination figures when you hover over or click on a country. Import the Popup component from react-map-gl. Let's start by writing its CSS styles.

index.css

/* tooltip card style */
.tooltip-card{
  background-color: white;
  padding: 2px;
  max-width: 250px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-around;
}

/* style for every row of content inside the tooltip card  */
.content-row{
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: flex-start;
}

/* tooltip header size  */
.tooltip-header{
  font-weight: 600;
  display: flex;
  font-size: 14px;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 0.5rem;
}
Enter fullscreen mode Exit fullscreen mode

Next, we create the tooltip component inside the popup element. It is controlled by a new state variable called tooltipData. Initially, it's null but when the popup element gets triggered, it is set to the data of the hovered country.

App.js

const [tooltipData, setTooltipData] = useState(null)
Enter fullscreen mode Exit fullscreen mode

App.js

 <ReactMapGL
        {...viewport}
        mapboxApiAccessToken={process.env.REACT_APP_MAPBOX}
        onViewportChange={nextViewport => setViewport(nextViewport)}
        mapStyle="mapbox://styles/saket2000/ckolf18ga1lxq17l31rw3lrxk"
      > 
        {dataCountries && Object.values(dataCountries).map((country, index) => {
          return(
            <Marker key={index} latitude={country.countryInfo.lat} longitude={country.countryInfo.long}>
              <div 
              style={{height: country.size, width: country.size}}
              className="map-marker"
              onClick = {() => setTooltipData(country)} 
              > 
              </div>
            </Marker>
          )
        })}
        {tooltipData && <Popup
          latitude={tooltipData.countryInfo.lat}
          longitude={tooltipData.countryInfo.long}
          anchor="bottom"
          closeButton={true}
          onClose={() => setTooltipData(null)}
        >
            <div className="tooltip-card">
              <div className="tooltip-header">
                <img className="tooltip-img" src={tooltipData.countryInfo.flag}></img>
                {tooltipData.country}
              </div>
              <div className="tooltip-content">
                <div className="content-row">
                  <div className="small heading text-secondary me-2">Total doses given</div>
                  <div className="h6 heading">{tooltipData.total_vaccinations.toLocaleString()}</div>
                </div>
                <div className="content-row">
                  <div className="small heading text-secondary me-2">Doses per hundred people</div>
                  <div className="h6 heading">{Math.round((tooltipData.total_vaccinations/tooltipData.population)*100).toLocaleString()}</div>
                </div>
              </div>
            </div>
          </Popup>}
      </ReactMapGL>
Enter fullscreen mode Exit fullscreen mode

You should now be able to see a tooltip showing vaccination figures of any country that is hovered over or clicked.

Complete project checkpoint

So at this point, you have a fully functional interactive map/dashboard that shows vaccination figures of every country. I couldn't add more data points under vaccinations since the APIs didn't host any additional info. It'd be really awesome if someone can find a way to add a % vaccinated section and a first vs second dose breakdown.

And with this, your app is ready to be launched into the world-wide-web!! Congratulations for making it to the end of this long-drawn boring-ass tutorial. I tried to simplify things as much as possible. So hope you folks enjoyed it. Would really appreciate feedback and suggestions from y'all. If you have any questions or doubts, you can write them down in the comments below. I will try to answer them to the best of my ability

If you want to deploy your React app in the easiest/fastest possible way, do check out this blog by Netlify developers. They've outlined the steps in a very easy-to-understand way.

Oldest comments (0)