Tuesday, 14 September 2021

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

For this part, we want to compare the proportions of types of COVID-19 cases against each other - Singapore Citizens, migrant workers and imported cases. Therefore, we will omit death statistics.

Begin by placing an svg tag in the appropriate div.
<div class="bottomContainer">
    <div class="leftContainer">
        <svg>

        </svg>

    </div>

    <div class="rightContainer">

    </div>
</div>


Clear the svg the same way we did for the previous chart.
drawDonutChart: function(data, sg, mw, imports)
{
    var svgDonutChart = d3.select(".bottomContainer .leftContainer svg");
    svgDonutChart
    .html("");


Except that in this case, we follow up by appending a g tag and translating it to the middle of the svg. Then we set svgDonutChart to the g tag instead of the svg tag.
drawDonutChart: function(data, sg, mw, imports)
{
    var svgDonutChart = d3.select(".bottomContainer .leftContainer svg");
    svgDonutChart
    .html("")
    .append("g")
    .attr("transform", "translate(200, 150)")
;

    svgDonutChart = d3.select(".bottomContainer .leftContainer svg g");


Declare arrays arrSG, arrMW and arrImports. Iterate through the data array and push the appropriate property values into the arrays... but only if the respective flags are true. So if any of the flags are false, the array will remain empty.
svgDonutChart = d3.select(".bottomContainer .leftContainer svg g");

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

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));
}


Then we declare the array arrTotals. Push into arrTotals the sum() result of the arrSG, arrMW and arrImports arrays, but only if their flags are 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));
}

var arrTotals = [];
if (sg) arrTotals.push({ value: d3.sum(arrSG), class: "colSG"});
if (mw) arrTotals.push({ value: d3.sum(arrMW), class: "colMW"});
if (imports) arrTotals.push({ value: d3.sum(arrImports), class: "colImports" });


Then declare the variable total as the total cases - the total number of Singapore Citizen cases, migrant worker cases and imported cases for that particular dataset.
var arrTotals = [];
if (sg) arrTotals.push({ value: d3.sum(arrSG), class: "colSG"});
if (mw) arrTotals.push({ value: d3.sum(arrMW), class: "colMW"});
if (imports) arrTotals.push({ value: d3.sum(arrImports), class: "colImports" });

var total = d3.sum([d3.sum(arrSG), d3.sum(arrMW), d3.sum(arrImports)]);


Now declare the variable pie and set it by running the pie() method of the d3 object. We set its value property to return the value property of the array we're going to pass into it, which is arrTotals.
var total = d3.sum([d3.sum(arrSG), d3.sum(arrMW), d3.sum(arrImports)]);

var pie =
d3.pie()
.value(function(d)
{
    return d.value;
});


We follow this up by declaring pieData and passing arrTotals into pie. Also, declare constant f and set it to the format() method of the d3 object, passing in ".2f". This is what we'll do to format numerical data.
var pie =
d3.pie()
.value(function(d)
{
    return d.value;
});

var pieData = pie(arrTotals);
const f = d3.format(".2f");


Declare zeroArc and arc, and obtain their values by using the arc() method of the d3 object. Pass in numeric values. This will be useful for animation later.
var pieData = pie(arrTotals);
const f = d3.format(".2f");

var zeroArc =
d3.arc()
.innerRadius(50)
.outerRadius(150);

var arc =
d3.arc()
.innerRadius(100)
.outerRadius(150);


Now what we need is to append paths within svgDonutChart. The data used is pieData. The d property is set to arc.
var arc =
d3.arc()
.innerRadius(100)
.outerRadius(150);

svgDonutChart
.selectAll("path")
.data(pieData)
.enter()
.append("path")
.attr("d", arc)
.attr("class", function(d, i) {
    return d.data.class;
});


Now you see the donut take shape!


Let's just introduce some animation using zeroArc as the initial state.
svgDonutChart
.selectAll("path")
.data(pieData)
.enter()
.append("path")
.attr("d", zeroArc)
.attr("class", function(d, i) {
    return d.data.class;
})
.transition()
.duration(500)

.attr("d", arc);


Now for the text! We do what we did for paths, only this time we insert text tags. The x properties use the endAngle and startAngle properties of the pieData object to determine a point to insert the text, and then we use the pointRadial() method of the d3 object, and take the first element. For the y property, we want to avoid the different section text tags overlapping each other, so we make it a function of the element number, i, of pieData. Again, we use the pointRadial() method and this time we return the second element. The CSS class used for the text is donutChartText.
svgDonutChart
.selectAll("path")
.data(pieData)
.enter()
.append("path")
.attr("d", zeroArc)
.attr("class", function(d, i) {
    return d.data.class;
})
.transition()
.duration(500)
.attr("d", arc);

svgDonutChart
.selectAll("text.donutChartText")
.data(pieData)
.enter()
.append("text")
.attr("class", "donutChartText")
.attr("x", function(d)
{
    var midpoint = d.startAngle + ((d.endAngle - d.startAngle) / 2);
    return d3.pointRadial(midpoint, 120)[0];
})
.attr("y", function(d, i)
{
    var midpoint = d.startAngle + ((d.endAngle - d.startAngle) / 2);
    return d3.pointRadial(midpoint, 100 + (i * 10))[1];
})
.text(function(d)
{
    return f(d.value / total * 100) + "%";
});


We style the donutChartText CSS class. The text-anchor property should be middle.
.scaleLineChartText
{
    font: 8px verdana;
    fill: rgba(100, 100, 100, 1);
    text-anchor: end;
}

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


And here's what the chart looks like now.



Here we add a title to the chart. Use the CSS class chartTitle. We can have some subtitles under that title, using the CSS class donutChartText. This is where we use total, which we calculated earlier.
svgDonutChart
.selectAll("text.donutChartText")
.data(pieData)
.enter()
.append("text")
.attr("class", "donutChartText")
.attr("x", function(d)
{
    var midpoint = d.startAngle + ((d.endAngle - d.startAngle) / 2);
    return d3.pointRadial(midpoint, 120)[0];
})
.attr("y", function(d, i)
{
    var midpoint = d.startAngle + ((d.endAngle - d.startAngle) / 2);
    return d3.pointRadial(midpoint, 100 + (i * 10))[1];
})
.text(function(d)
{
    return f(d.value / total * 100) + "%";
});

svgDonutChart
.append("text")
.attr("class", "chartTitle")
.attr("x", "-50px")
.attr("y", "0px")
.text("Case Types");

svgDonutChart
.append("text")
.attr("class", "donutChartText")
.attr("x", "-5px")
.attr("y", "20px")
.text(total + " cases");


Beautiful!


Next

The last part of our dashboard looms. It should be simple compared to everything we've been through so far.

No comments:

Post a Comment