Friday 17 September 2021

Web Tutorial: The COVID-19 Dashboard (Part 4/4)

Welcome to the final part of this web tutorial!

We begin by adding an svg tag in the correct place.
<div class="bottomContainer">
    <div class="leftContainer">
        <svg>

        </svg>
    </div>

    <div class="rightContainer">
        <svg>

        </svg>

    </div>
</div>


And in the drawBarChart() method, we clear the svg of content.
drawBarChart: function(data, sg, mw, imports, deaths)
{
    var svgBarChart = d3.select(".bottomContainer .rightContainer svg");
    svgBarChart.html("");

}


Then we add a line to it, and style it using the CSS class scaleBarChartVertical. It's just a line to visually align the bars that are following.
var svgBarChart = d3.select(".bottomContainer .rightContainer svg");
svgBarChart.html("");

svgBarChart
.append("line")
.attr("class", "scaleBarChartVertical")
.attr("x1", "5px")
.attr("y1", "0px")
.attr("x2", "5px")
.attr("y2", "295px");


And here's the CSS class. Just add scaleBarChartVertical to that line because it is styled the same way.
.scaleLineChartTick, .scaleLineChartVertical, .scaleLineChartHorizontal, .scaleBarChartVertical
{
    stroke: rgba(100, 100, 100, 1);
    stroke-width: 1px;
}


Here's the line. Doesn't look like much now, does it?


Here, declare f. We're going to use it to format decimal strings, exactly like we did in the previous part of this web tutorial.
svgBarChart
.append("line")
.attr("class", "scaleBarChartVertical")
.attr("x1", "5px")
.attr("y1", "0px")
.attr("x2", "5px")
.attr("y2", "295px");

const f = d3.format(".2f");


Now declare maxSG, maxMW, maxImports and maxDeaths. For maxSG, if sg is true, assign the value of totalSG, 0 if not. Do the same for the other statistics.
const f = d3.format(".2f");

var maxSG = (sg ? dashboard.totalSG : 0);
var maxMW = (mw ? dashboard.totalMW : 0);
var maxImports = (imports ? dashboard.totalImports : 0);
var maxDeaths = (deaths ? dashboard.totalDeaths : 0);


Declare maxVal, and take the highest value out of maxSG, maxMW, maxImports and maxDeaths. Declare unitSize. It should be 350 (the width of the chart) divided by maxVal. This will give the width in pixels of each unit.
var maxSG = (sg ? dashboard.totalSG : 0);
var maxMW = (mw ? dashboard.totalMW : 0);
var maxImports = (imports ? dashboard.totalImports : 0);
var maxDeaths = (deaths ? dashboard.totalDeaths : 0);

var maxVal = d3.max([maxSG, maxMW, maxImports, maxDeaths]);
var unitSize = 350 / maxVal;


Then declare four arrays - arrSG, arrMW, arrImports and arrDeaths.
var maxVal = d3.max([maxSG, maxMW, maxImports, maxDeaths]);
var unitSize = 350 / maxVal;

var arrSG = [];
var arrMW = [];
var arrImports = [];
var arrDeaths = [];


Using a For loop, push the appropriate values from the data array, but only if the appropriate conditions are met.
var arrSG = [];
var arrMW = [];
var arrImports = [];
var arrDeaths = [];

for (var i = 0; i < data.length; i++)
{
    if (sg) arrSG.push(parseInt(data[i].sg));
    if (mw) arrMW.push(parseInt(data[i].mw));
    if (imports) arrImports.push(parseInt(data[i].imports));
    if (deaths) arrDeaths.push(parseInt(data[i].deaths));
}


The next bar should be drawn only if sg is true.
for (var i = 0; i < data.length; i++)
{
    if (sg) arrSG.push(parseInt(data[i].sg));
    if (mw) arrMW.push(parseInt(data[i].mw));
    if (imports) arrImports.push(parseInt(data[i].imports));
    if (deaths) arrDeaths.push(parseInt(data[i].deaths));
}

if (sg)
{

}


We are going to represent, with an outlined bar, the total number of Singapore Citizen cases in the entire dataset. It will be styled using colSG and nofill. The width is maxSG multiplied by unitSize. The x and y properties are fixed. The height is 50 pixels, and that's the standard set for all the bars.
if (sg)
{
    svgBarChart
    .append("rect")
    .attr("class", "colSG nofill")
    .attr("x", "5px")
    .attr("y", "50px")
    .attr("height", "50px")
    .attr("width", (maxSG * unitSize) + "px");

}


In the styles, add a nofill class with white as a background.
.colSG { color: #FF0000; fill: #FF0000; stroke: #FF0000; }
.colMW { color: #9999FF; fill: #9999FF; stroke: #9999FF; }
.colImports { color: #44AA44; fill: #44AA44; stroke: #44AA44; }
.colDeaths { color: #999999; fill: #999999; stroke: #999999; }
.nofill { fill: #FFFFFF; }


And this is the empty bar. The stroke is red, but because of nofill, the fill is white.


Here, we'll just add some animation.
if (sg)
{
    svgBarChart
    .append("rect")
    .attr("class", "colSG nofill")
    .attr("x", "5px")
    .attr("y", "50px")
    .attr("height", "50px")
    .attr("width", "0px")
    .transition()
    .duration(500)

    .attr("width", (maxSG * unitSize) + "px");
}


Now we append another rect tag. They will be styled by colSG, but since we want this to be filled, we omit nofill. We start with fixed values for the x and y properties. The height property is 50 pixels, just like the previous rect tag. The width property, this time, is the sum of the arrSG array multiplied by unitSize.
svgBarChart
.append("rect")
.attr("class", "colSG nofill")
.attr("x", "5px")
.attr("y", "50px")
.attr("height", "50px")
.attr("width", "0px")
.transition()
.duration(500)
.attr("width", (maxSG * unitSize) + "px");

svgBarChart
.append("rect")
.attr("class", "colSG")
.attr("x", "5px")
.attr("y", "50px")
.attr("height", "50px")
.attr("width", (d3.sum(arrSG) * unitSize) + "px");




Again, we add some animation just to be fancy.
svgBarChart
.append("rect")
.attr("class", "colSG")
.attr("x", "5px")
.attr("y", "50px")
.attr("height", "50px")
.attr("width", "0px")
.transition()
.duration(500)

.attr("width", (d3.sum(arrSG) * unitSize) + "px");


Now we are going to add some text. We want to be explicit about what the bars represent. The x and y properties will be fixed, and then we use f (which we defined earlier) to show the percentages. The CSS class used is barChartText.
svgBarChart
.append("rect")
.attr("class", "colSG")
.attr("x", "5px")
.attr("y", "50px")
.attr("height", "50px")
.attr("width", "0px")
.transition()
.duration(500)
.attr("width", (d3.sum(arrSG) * unitSize) + "px");

svgBarChart
.append("text")
.attr("class", "barChartText")
.attr("x", "350px")
.attr("y", "80px")
.text(d3.sum(arrSG) + " of " + maxSG + " (" + f(d3.sum(arrSG) / maxSG * 100) + "%)");


We need to define the CSS class barChartText. The text-anchor property is set to end.
.donutChartText
{
    font: bold 10px verdana;
    fill: rgba(0, 0, 0, 1);
    text-anchor: middle;
}

.barChartText
{
    font: bold 10px verdana;
    fill: rgba(0, 0, 0, 1);
    text-anchor: end;
}


And here's the text!


Now it's just a matter of replicating the code for the rest of the statistics. Note that we have to adjust the fixed values.
if (sg)
{
    svgBarChart
    .append("rect")
    .attr("class", "colSG nofill")
    .attr("x", "5px")
    .attr("y", "50px")
    .attr("height", "50px")
    .attr("width", "0px")
    .transition()
    .duration(500)
    .attr("width", (maxSG * unitSize) + "px");

    svgBarChart
    .append("rect")
    .attr("class", "colSG")
    .attr("x", "5px")
    .attr("y", "50px")
    .attr("height", "50px")
    .attr("width", "0px")
    .transition()
    .duration(500)
    .attr("width", (d3.sum(arrSG) * unitSize) + "px");

    svgBarChart
    .append("text")
    .attr("class", "barChartText")
    .attr("x", "350px")
    .attr("y", "80px")
    .text(d3.sum(arrSG) + " of " + maxSG + " (" + f(d3.sum(arrSG) / maxSG * 100) + "%)");
}

if (mw)
{
    svgBarChart
    .append("rect")
    .attr("class", "colMW nofill")
    .attr("x", "5px")
    .attr("y", "115px")
    .attr("height", "50px")
    .attr("width", "0px")
    .transition()
    .duration(500)
    .attr("width", (maxMW * unitSize) + "px");

    svgBarChart
    .append("rect")
    .attr("class", "colMW")
    .attr("x", "5px")
    .attr("y", "115px")
    .attr("height", "50px")
    .attr("width", "0px")
    .transition()
    .duration(1500)
    .attr("width", (d3.sum(arrMW) * unitSize) + "px");

    svgBarChart
    .append("text")
    .attr("class", "barChartText")
    .attr("x", "350px")
    .attr("y", "145px")
    .text(d3.sum(arrMW) + " of " + maxMW + " (" + f(d3.sum(arrMW) / maxMW * 100) + "%)");
}

if (imports)
{
    svgBarChart
    .append("rect")
    .attr("class", "colImports nofill")
    .attr("x", "5px")
    .attr("y", "180px")
    .attr("height", "50px")
    .attr("width", "0px")
    .transition()
    .duration(500)
    .attr("width", (maxImports * unitSize) + "px");

    svgBarChart
    .append("rect")
    .attr("class", "colImports")
    .attr("x", "5px")
    .attr("y", "180px")
    .attr("height", "50px")
    .attr("width", "0px")
    .transition()
    .duration(1500)
    .attr("width", (d3.sum(arrImports) * unitSize) + "px");

    svgBarChart
    .append("text")
    .attr("class", "barChartText")
    .attr("x", "350px")
    .attr("y", "210px")
    .text(d3.sum(arrImports) + " of " + maxImports + " (" + f(d3.sum(arrImports) / maxImports * 100) + "%)");
}

if (deaths)
{
    svgBarChart
    .append("rect")
    .attr("class", "colDeaths nofill")
    .attr("x", "5px")
    .attr("y", "245px")
    .attr("height", "50px")
    .attr("width", "0px")
    .transition()
    .duration(500)
    .attr("width", (maxDeaths * unitSize) + "px");

    svgBarChart
    .append("rect")
    .attr("class", "colDeaths")
    .attr("x", "5px")
    .attr("y", "245px")
    .attr("height", "50px")
    .attr("width", "0px")
    .transition()
    .duration(1500)
    .attr("width", (d3.sum(arrDeaths) * unitSize) + "px");

    svgBarChart
    .append("text")
    .attr("class", "barChartText")
    .attr("x", "350px")
    .attr("y", "275px")
    .text(d3.sum(arrDeaths) + " of " + maxDeaths + " (" + f(d3.sum(arrDeaths) / maxDeaths * 100) + "%)");
}


Well, damn. This looks great!


Let's just add a chart title. This should be simple.
    svgBarChart
    .append("text")
    .attr("class", "barChartText")
    .attr("x", "350px")
    .attr("y", "275px")
    .text(d3.sum(arrDeaths) + " of " + maxDeaths + " (" + f(d3.sum(arrDeaths) / maxDeaths * 100) + "%)");
}

svgBarChart
.append("text")
.attr("class", "chartTitle")
.attr("x", "10px")
.attr("y", "20px")
.text("This Month's Cases vs Total");



And some cleanup!
div
{
    outline: 0px solid red;
}


And this is your beautiful, beautiful dashboard. Play around with it; click on the controls and see how it responds!


Final thoughts

One of the objectives of this web tutorial is to illustrate extracting data from a CSV file. Hope I did an adequate job there. Other than that, most of the D3 functionality has probably already been covered in previous web tutorials.

The dataset itself is something I have been collecting for over a year, and I'm ecstatic at being able to finally deploy it to some use.

Gotta dash, seeya later!
T___T

No comments:

Post a Comment