It's D3 time! Been a while since I did D3, but let's have some fun amid the ongoing COVID-19 pandemic. You see, for the past year and a half now, I've been collecting data about Singapore's COVID-19 cases. And it's now time to put all that lovely, lovely data to good use.
Here's a sample of the data in CSV format. The entire dataset can be found
here.
Day,Imports,MW,SG,Deaths
2020/04/01,20,0,54,0
2020/04/02,8,0,41,1
2020/04/03,0,0,65,1
2020/04/04,6,0,69,1
.
.
.
This is what the data represents:
Day - The date in which the data takes place on. There is one entry for every day of each month.
SG - The number of discovered COVID-19 cases that were Singapore citizens on that day.
MW - The number of discovered COVID-19 cases that were migrant workers on that day.
Imports - The number of discovered COVID-19 cases that were foreigners that traveled to Singapore on that day.
Deaths - The number of people who passed away due to COVID-19 on that day.
Caution
Although this is JavaScript, a server is required because we will be using an asynchronous operation to pull data. We could possibly get around this by turning off security settings, but why take that chance, really?
We begin with some HTML. We will use Verdana for the font and set the
outline property of all divs to
red, for some visibility. We have also included the link to the D3 library, with a script tag in the body.
<!DOCTYPE html>
<html>
<head>
<title>COVID-19 Dashboard</title>
<style>
body
{
font-family: verdana;
}
div
{
outline: 1px solid red;
}
</style>
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
</script>
</body>
</html>
Now, we have a div styled using the CSS class
dashboardContainer. Within it, we have three divs styled using the CSS classes
topContainer,
middleContainer and
bottomContainer respectively.
<body>
<div class="dashboardContainer">
<div class="topContainer">
</div>
<div class="middleContainer">
</div>
<div class="bottomContainer">
</div>
</div>
<script>
</script>
</body>
Here are some styles. We set the height of
dashboardContainer and center it in the screen.
topContainer,
middleContainer and
bottomContainer will each take up full width and float left, with varying heights.
middleContainer and
bottomContainer will have the
margin-top property set.
<style>
body
{
font-family: verdana;
}
div
{
outline: 1px solid red;
}
.dashboardContainer
{
width: 800px;
height: 620px;
margin: 0 auto 0 auto;
}
.topContainer
{
width: 100%;
height: 250px;
float: left;
}
.middleContainer
{
width: 100%;
height: 50px;
margin-top: 10px;
float: left;
}
.bottomContainer
{
width: 100%;
height: 300px;
margin-top: 10px;
float: left;
}
</style>
And immediately, you get a promising-looking layout!
Then in the divs styled by
middleContainer and
bottomContainer, insert two divs each styled by
leftContainer and
rightContainer, respectively.
<div class="dashboardContainer">
<div class="topContainer">
</div>
<div class="middleContainer">
<div class="leftContainer">
</div>
<div class="rightContainer">
</div>
</div>
<div class="bottomContainer">
<div class="leftContainer">
</div>
<div class="rightContainer">
</div>
</div>
</div>
Now we style these. They float left and right respectively, and have roughly half the width of their parents, and all of the height.
.bottomContainer
{
width: 100%;
height: 300px;
margin-top: 10px;
float: left;
}
.leftContainer
{
width: 395px;
height: 100%;
float: left;
}
.rightContainer
{
width: 395px;
height: 100%;
float: right;
}
So now, in this div, add a p tag and the following elements within - a string, two buttons and a drop-down list. Add ids for the buttons and the drop-down list.
<div class="middleContainer">
<div class="leftContainer">
<p>
COVID-19 DASHBOARD
<input type="button" id="btnPrev" value="<<" />
<select id="ddlPeriod"></select>
<input type="button" id="btnNext" value=">>" />
</p>
</div>
<div class="rightContainer">
</div>
</div>
This is a preview of what your main controls on your dashboard will look like.
Add a series of checked checkboxes with strings as labels, in the other div.
<div class="middleContainer">
<div class="leftContainer">
<p>
COVID-19 DASHBOARD
<input type="button" id="btnPrev" value="<<" />
<select id="ddlPeriod"></select>
<input type="button" id="btnNext" value=">>" />
</p>
</div>
<div class="rightContainer">
<input type="checkbox" checked /> Singapore Citizens
<input type="checkbox" checked /> Migrant Workers
<input type="checkbox" checked /> Imports
<input type="checkbox" checked /> Deaths
</div>
</div>
It's a little messy and we'll be cleaning this up.
Each of these checkboxes and their labels should go into a div with the class
statContainer.
<div class="rightContainer">
<div class="statContainer">
<input type="checkbox" checked /> Singapore Citizens
</div>
<div class="statContainer">
<input type="checkbox" checked /> Migrant Workers
</div>
<div class="statContainer">
<input type="checkbox" checked /> Imports
</div>
<div class="statContainer">
<input type="checkbox" checked /> Deaths
</div>
</div>
statContainer will have width and height set, float left and the font will be set as well, but that last part is purely aesthetic.
.rightContainer
{
width: 395px;
height: 100%;
float: right;
}
.statContainer
{
width: 45%;
height: 40%;
float: left;
font-size: 0.8em;
font-weight: bold;
}
And now we have a better layout.
How about we color-code these? Add a class to each of these divs.
<div class="rightContainer">
<div class="statContainer colSG">
<input type="checkbox" checked /> Singapore Citizens
</div>
<div class="statContainer colMW">
<input type="checkbox" checked /> Migrant Workers
</div>
<div class="statContainer colImports">
<input type="checkbox" checked /> Imports
</div>
<div class="statContainer colDeaths">
<input type="checkbox" checked /> Deaths
</div>
</div>
Each of these CSS classes has a different color.
.statContainer
{
width: 45%;
height: 40%;
float: left;
font-size: 0.8em;
font-weight: bold;
}
.colSG { color: #FF0000; }
.colMW { color: #9999FF; }
.colImports { color: #44AA44; }
.colDeaths { color: #999999; }
And here we see that SG data is
red, MW data is
pale blue, Imports data is
green and Deaths data is
grey. This should be consistent throughout the dashboard.
Now for data!
We'll go right into the script tag. First, we create the object dashboard. In it, we have the following properties.
keys - an array that will hold all the possible month-year combinations of the dataset, so as to be able to traverse the array
covidData easily.
currentKey - a value that will be one of the values within keys. It defaults to undefined.
covidData - an array that holds your entire dataset.
totalSG - the pre-calculated sum of all Singapore Citizen cases. The initial value is 0.
totalMW - the pre-calculated sum of all migrant worker cases. The initial value is 0.
totalImports - the pre-calculated sum of all imported cases. The initial value is 0.
totalDeaths - the pre-calculated sum of all deaths. The initial value is 0.
<script>
let dashboard =
{
keys: [],
currentKey: undefined,
covidData: [],
totalSG: 0,
totalMW: 0,
totalImports: 0,
totalDeaths: 0
};
</script>
Here, we use the
csv() method of the
d3 object. Within it, we pass the name of our CSV file, which is
covid19.csv, and a callback.
data is a parameter which represents the CSV data extracted from the file.
data consists of an array of objects. Each object represents a line in the CSV data.
<script>
let dashboard =
{
keys: [],
currentKey: undefined,
covidData: [],
totalSG: 0,
totalMW: 0,
totalImports: 0,
totalDeaths: 0
};
d3.csv("covid19.csv", function(data)
{
});
</script>
Here, we are going to manipulate the data into a form that our dashboard can easily parse. We first use a
For loop to iterate through the contents of
data. We then define the variable,
key, and use the
getKey() method of the
dashboard object, passing in the
Day property of the current object.
d3.csv("covid19.csv", function(data)
{
for (var i = 0; i < data.length; i++)
{
var key = dashboard.getKey(data[i].Day);
}
});
Now we define the
getKey() method. It will accept a string,
x, as a parameter. This is the date string which is the
Day property.
let dashboard =
{
keys: [],
currentKey: undefined,
covidData: [],
totalSG: 0,
totalMW: 0,
totalImports: 0,
totalDeaths: 0,
getKey: function(x)
{
}
};
Each Day string is in the format YYYY/MM/DD. So we first define a variable,
elems, as an array derived from running
x through the
split() method and using "/" as an argument. The variable
year is the first element of
elems. The variable
month is the second element of
elems, minus 1 because we are going to use this value as a pointer to an array. The array is
monthNames, and it contains all the month names of the calendar.
let dashboard =
{
keys: [],
currentKey: undefined,
covidData: [],
totalSG: 0,
totalMW: 0,
totalImports: 0,
totalDeaths: 0,
getKey: function(x)
{
var elems = x.split("/");
var year = elems[0];
var month = parseInt(elems[1]) - 1;
var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
}
};
We return the element of
monthNames pointed to by
month, and
year, in a string. So if we passed in "2020/04/03" as an argument, we would get "Apr 2020".
let dashboard =
{
keys: [],
currentKey: undefined,
covidData: [],
totalSG: 0,
totalMW: 0,
totalImports: 0,
totalDeaths: 0,
getKey: function(x)
{
var elems = x.split("/");
var year = elems[0];
var month = parseInt(elems[1]) - 1;
var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
return monthNames[month] + " " + year;
}
};
Now that we've defined
getKey(), let's move on. We use an
If block to check if the current value of
currentKey is undefined.
d3.csv("covid19.csv", function(data)
{
for (var i = 0; i < data.length; i++)
{
var key = dashboard.getKey(data[i].Day);
if (dashboard.currentKey == undefined)
{
}
else
{
}
}
});
If so, we set
currentKey to the value of
key, and create a new sub-array under
covidData using
key as the key. And then we push
key into
keys.
d3.csv("covid19.csv", function(data)
{
for (var i = 0; i < data.length; i++)
{
var key = dashboard.getKey(data[i].Day);
if (dashboard.currentKey == undefined)
{
dashboard.currentKey = key;
dashboard.covidData[key] = [];
dashboard.keys.push(key);
}
else
{
}
}
});
Otherwise, we first check if
currentKey is equal to
key. If not, we repeat what we did earlier.
d3.csv("covid19.csv", function(data)
{
for (var i = 0; i < data.length; i++)
{
var key = dashboard.getKey(data[i].Day);
if (dashboard.currentKey == undefined)
{
dashboard.currentKey = key;
dashboard.covidData[key] = [];
dashboard.keys.push(key);
}
else
{
if (dashboard.currentKey != key)
{
dashboard.currentKey = key;
dashboard.covidData[key] = [];
dashboard.keys.push(key);
}
}
}
});
Now, we start pushing the entire contents of the current element of
data into that sub-array. What this does is that this will segregate data into sub-arrays delineated by month-year combinations.
d3.csv("covid19.csv", function(data)
{
for (var i = 0; i < data.length; i++)
{
var key = dashboard.getKey(data[i].Day);
if (dashboard.currentKey == undefined)
{
dashboard.currentKey = key;
dashboard.covidData[key] = [];
dashboard.keys.push(key);
}
else
{
if (dashboard.currentKey != key)
{
dashboard.currentKey = key;
dashboard.covidData[key] = [];
dashboard.keys.push(key);
}
}
dashboard.covidData[key].push({"sg": data[i].SG, "imports": data[i].Imports, "mw": data[i].MW, "deaths": data[i].Deaths});
}
});
At this moment, we will take advantage of the
For loop to increment the values of
totalSG,
totalMW,
totalImports and
totalDeaths accordingly. At the end of it, we should get the totals of the entire dataset. These will be useful later!
d3.csv("covid19.csv", function(data)
{
for (var i = 0; i < data.length; i++)
{
var key = dashboard.getKey(data[i].Day);
if (dashboard.currentKey == undefined)
{
dashboard.currentKey = key;
dashboard.covidData[key] = [];
dashboard.keys.push(key);
}
else
{
if (dashboard.currentKey != key)
{
dashboard.currentKey = key;
dashboard.covidData[key] = [];
dashboard.keys.push(key);
}
}
dashboard.covidData[key].push({"sg": data[i].SG, "imports": data[i].Imports, "mw": data[i].MW, "deaths": data[i].Deaths});
dashboard.totalSG = dashboard.totalSG + parseInt(data[i].SG);
dashboard.totalMW = dashboard.totalMW + parseInt(data[i].MW);
dashboard.totalImports = dashboard.totalImports + parseInt(data[i].Imports);
dashboard.totalDeaths = dashboard.totalDeaths + parseInt(data[i].Deaths);
}
});
Outside of the
For loop, define variable
ddlPeriod and use the
select() method of the
d3 object to get the drop-down list
ddlPeriod.
dashboard.totalSG = dashboard.totalSG + parseInt(data[i].SG);
dashboard.totalMW = dashboard.totalMW + parseInt(data[i].MW);
dashboard.totalImports = dashboard.totalImports + parseInt(data[i].Imports);
dashboard.totalDeaths = dashboard.totalDeaths + parseInt(data[i].Deaths);
}
var ddlPeriod = d3.select("#ddlPeriod");
Now we use the keys object (which we should have populated with all the possible month-year combinations in the dataset) to fill the drop-down list with option tags.
var ddlPeriod = d3.select("#ddlPeriod");
ddlPeriod.selectAll("option")
.data(dashboard.keys)
.enter()
.append("option");
We'll use the actual value as the text, but use the element number as the value. And by default, we will select the first value.
ddlPeriod.selectAll("option")
.data(dashboard.keys)
.enter()
.append("option")
.property("selected", function(d, i)
{
return i == 0;
})
.attr("value", function(d, i)
{
return i;
})
.text(function(d)
{
return d;
});
And see now, we've populated the drop-down list!
We follow up by calling the
drawCharts() method of the
dashboard object.
ddlPeriod.selectAll("option")
.data(dashboard.keys)
.enter()
.append("option")
.property("selected", function(d, i)
{
return i == 0;
})
.attr("value", function(d, i)
{
return i;
})
.text(function(d)
{
return d;
});
dashboard.drawCharts();
We are going to define the
drawCharts() method soon. But first, make sure that it fires off whenever the checkboxes are clicked, using the
onclick attribute. We will also add an extra class to each containing div so that it is easily identifiable by the drawCharts() method.
<div class="rightContainer">
<div class="statContainer cb_sg colSG">
<input type="checkbox" onclick="dashboard.drawCharts();" checked/> Singapore Citizens
</div>
<div class="statContainer cb_mw colMW">
<input type="checkbox" onclick="dashboard.drawCharts();" checked/> Migrant Workers
</div>
<div class="statContainer cb_imports colImports">
<input type="checkbox" onclick="dashboard.drawCharts();" checked/> Imports
</div>
<div class="statContainer cb_deaths colDeaths">
<input type="checkbox" onclick="dashboard.drawCharts();" checked/> Deaths
</div>
</div>
Let us define the
drawCharts() method now.
getKey: function(x)
{
var elems = x.split("/");
var year = elems[0];
var month = parseInt(elems[1]) - 1;
var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
return monthNames[month] + " " + year;
},
drawCharts: function()
{
}
Here, we select
ddlPeriod. Then we define
data as a subset of
covidData, using the selected value of
ddlPeriod. It is an integer, but when we use that integer as a pointer to reference
covidData, we get the key! And it's this key that we use to get the data of, say, April 2020.
drawCharts: function()
{
var ddlPeriod = d3.select("#ddlPeriod");
var data = this.covidData[this.keys[ddlPeriod.node().value]];
}
Then we'll define
sg,
mw,
imports and
deaths as Boolean values based on which checkboxes are checked. So if the user only wishes to view Deaths and Imports, he would only check thse checkboxes. And the dashbard will reflect thse choices.
drawCharts: function()
{
var ddlPeriod = d3.select("#ddlPeriod");
var sg = d3.select(".cb_sg input[type=checkbox]").property("checked");
var mw = d3.select(".cb_mw input[type=checkbox]").property("checked");
var imports = d3.select(".cb_imports input[type=checkbox]").property("checked");
var deaths = d3.select(".cb_deaths input[type=checkbox]").property("checked");
var data = this.covidData[this.keys[ddlPeriod.node().value]];
}
Then we will call the
drawLineChart(),
drawDonutChart() and
drawBarChart() methods, passing in the variables we defined, as arguments. Note that
drawDonutChart() does not use deaths.
drawCharts: function()
{
var ddlPeriod = d3.select("#ddlPeriod");
var sg = d3.select(".cb_sg input[type=checkbox]").property("checked");
var mw = d3.select(".cb_mw input[type=checkbox]").property("checked");
var imports = d3.select(".cb_imports input[type=checkbox]").property("checked");
var deaths = d3.select(".cb_deaths input[type=checkbox]").property("checked");
var data = this.covidData[this.keys[ddlPeriod.node().value]];
dashboard.drawLineChart(data, sg, mw, imports, deaths);
dashboard.drawDonutChart(data, sg, mw, imports);
dashboard.drawBarChart(data, sg, mw, imports, deaths);
}
And we will just populate the
dashboard object with these methods.
drawCharts: function()
{
var ddlPeriod = d3.select("#ddlPeriod");
var sg = d3.select(".cb_sg input[type=checkbox]").property("checked");
var mw = d3.select(".cb_mw input[type=checkbox]").property("checked");
var imports = d3.select(".cb_imports input[type=checkbox]").property("checked");
var deaths = d3.select(".cb_deaths input[type=checkbox]").property("checked");
var data = this.covidData[this.keys[ddlPeriod.node().value]];
dashboard.drawLineChart(data, sg, mw, imports, deaths);
dashboard.drawDonutChart(data, sg, mw, imports);
dashboard.drawBarChart(data, sg, mw, imports, deaths);
},
drawLineChart: function(data, sg, mw, imports, deaths)
{
},
drawDonutChart: function(data, sg, mw, imports)
{
},
drawBarChart: function(data, sg, mw, imports, deaths)
{
}
After calling the
drawCharts() method, make sure it runs whenever
ddlPeriod changes in value.
dashboard.drawCharts();
d3.select("#ddlPeriod").on("change", function() { dashboard.drawCharts(); });
When the buttons are clicked, we need to run
prev() and
next() methods before running
drawCharts().
dashboard.drawCharts();
d3.select("#ddlPeriod").on("change", function() { dashboard.drawCharts(); });
d3.select("#btnPrev").on("click", function() { dashboard.prev(); dashboard.drawCharts(); });
d3.select("#btnNext").on("click", function() { dashboard.next(); dashboard.drawCharts(); });
Now let us create the
prev() and
next() methods.
getKey: function(x)
{
var elems = x.split("/");
var year = elems[0];
var month = parseInt(elems[1]) - 1;
var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
return monthNames[month] + " " + year;
},
prev: function()
{
},
next: function()
{
},
drawCharts: function()
{
Now it's just a matter of getting the current value of
ddlPeriod, and set it to
currentVal.
prev: function()
{
var currentVal = parseInt(d3.select("#ddlPeriod").node().value);
},
next: function()
{
},
Since this basically sets the pointer to the previous value and the first value is 0, we only proceed if
currentVal is greater than 0.
prev: function()
{
var currentVal = parseInt(d3.select("#ddlPeriod").node().value);
if (currentVal > 0)
{
}
},
next: function()
{
},
We decrement
currentVal, and set the value of
ddlPeriod to
currentVal.
prev: function()
{
var currentVal = parseInt(d3.select("#ddlPeriod").node().value);
if (currentVal > 0)
{
currentVal = currentVal - 1;
d3.select("#ddlPeriod").property("value", currentVal);
}
},
next: function()
{
},
Now we do pretty much the same for
next(), except that we increment
currentVal, and only if
currentVal is lesser than the maximum value.
prev: function()
{
var currentVal = parseInt(d3.select("#ddlPeriod").node().value);
if (currentVal > 0)
{
currentVal = currentVal - 1;
d3.select("#ddlPeriod").property("value", currentVal);
}
},
next: function()
{
var currentVal = parseInt(d3.select("#ddlPeriod").node().value);
if (currentVal < this.keys.length - 1)
{
currentVal = currentVal + 1;
d3.select("#ddlPeriod").property("value", currentVal);
}
},
Now if you click those buttons, you can see the selected value in the drop-down list change! And once you reach "Apr 2020" or "Aug 2021" (the first and last values respectively), the value no longer changes.
That's all for now. But we have created a good framework for what we're about to do next.
Next
Drawing the line chart.