Thursday, 30 September 2021

Some new features from ES2021 I really like

Go, JavaScript! ES2021 was made public around last June, and with it, some pretty neat new features. I'm not going through all of them today, but some of them do merit a mention here because I have thoughts on them.

The replaceAll() method

It used to be the replace() method on strings would replace the first occurrence of a string within another string, and only that. If you wanted to replace all occurrences, you had to use a Regular Expression. (Here's a link if you're interested.)

Well, no longer! Now the replaceAll() method does this easily.

let str = "David is the most awesome man alive. David rides a Harley-Davidson. David has a fantastic sense of style.";
let str2 = str.replaceAll("David", "John");

console.log(str2); // John is the most awesome man alive. John rides a Harley-Johnson. John has a fantastic sense of style.


So I'm left with one question: why the fuck did it take so long to implement something as easy as this?! Between the time JavaScript first came out and now, that's easily fifteen years!

Underscore in numerical values

This sweet new feature allows the programmer to place underscores within a numerical value.

let num = 9129302500;
let num2 = 9_129_302_500; // this is equal to num


If the utility of this is not immediately obvious, think of what happens when you have to deal with extremely large numbers. If you're dealing with five digits or less, granted, it's not of much use. But if you're dealing with more than that, the underscore gives you an easy visual separator.

This is great! Simple and innovative.

Logical assignment operator

There's actually a lot more to this particular update, but I'm only going to cover this part.

The "??" operator now functions as shorthand for a conditional block. It checks the preceding expression for a null or undefined value, and executes the following expression if so.
let x = null;
let y = 9999;
let z = 10000;

x ??= y + z;
y ??= z;

console.log(x); // 19999
console.log(y); // 9999


Wow, this is so lazy. I approve!

Awesome!

The updates weren't as complicated as I initially feared. About half of them I actually understand and could conceivably use. These potentially make code more hassle-free and readable.

("Havexaxnicexday!").replaceAll("x", " ");
T___T

Friday, 24 September 2021

App Review: Clubhouse

Remember the days of IRC, where you could meet people all over the world and communicate with them via text chat? Well, the world just got a whole lot cooler.


Clubhouse is a Social Media audio app - basically the voice-based version of IRC. You create or join rooms full of other users, hearing their voices as they speak. How awesome is that?!

Clubhouse is available on both iOS and Android, and the features may vary on each platform. Today, I will only be covering the features on Android.

The Premise

In Clubhouse, users congregate in rooms - open, closed or Social. These could be standalone or under a club. There, they speak to each other, Follow accounts the same way one might in other Social Media like  Instagram or Twitter, and generally communicate. The functions are simple.


As a user, you can join and leave rooms, create them (in which case you become a Moderator of that room), and do the same for clubs. You can schedule rooms and add other users as co-hosts. Users can send text messages to other users, or groups of users.


As a Moderator, you can bring people on stage, send people off the stage or even out of the room, mute people on stage and end the current room.


The Aesthetics

Cream, white, green and a dash of orange. These are the basic colors used by Clubhouse's user interface. It's not pretty, but it is clean and that is arguably more important.




The Experience

Audio Social Media as a whole, can be pretty addictive. The app? It'll last you a while, for sure.

The Interface

The icons are generally intuitive and the screen elements such as buttons and selectors, don't require a great deal of cognitive load to identify and use. In other words, the interface is simple enough to navigate even if one adds on Moderator functions.

What I liked

App logo. The phone app icon changes every now and then, incorporating the faces of some well-known personalities. I like it!


The Backchannel feature is a very simple messaging component for you to send messages to other users. You can include links and emojis, but not pics or videos... so far. Good thing too, or it'd be dick pic city. Being able to archive threads is a nice touch as well.


Clubhouse relies a lot on standard emojis for illustration and this is probably deliberate - while not exactly sleek-looking, the familiarity users have with emojis will play into how easily they learn to use the app.


The creators of this app have kept the interface relatively simple, and that is invaluable. An app being easy to use is one of the contributing factors towards user retention.

Frequent updates. The creators of this app take feedback seriously and keep making improvements. By the time I release this blogpost, I wouldn't be surprised if some of the things I don't like about it (see below) have been addressed.

What I didn't

The Block feature is cumbersome and complicated, and quite frankly, open to abuse. Currently, if you are on Stage in any room, that room is invisible to those you have Blocked. And if the Blocked user is in a room, that room is hidden from you unless you scroll allll the way to the bottom of your Hallway and click on the Show Hidden Rooms button. This is clumsy AF.


Adding clubs. The plus buttons to add clubs only appears at the end of the list of clubs you are a member of, in your profile. If you have membership in many clubs, that's a lot of scrolling to do. Besides, it would be nice to have all your clubs listed in a section without all that horizontal scrolling.



Button placement can be problematic in some places. For example, it's way too easy to accidentally open a Private room with other users even when your intention was to scroll through the list of current active users or reply to a message.



In addition, as a Moderator, it's easy to accidentally kick someone off-stage if you're trying to do something as innocuous as reading their profile. It's also easy to accidentally give Moderator status to a troublemaker on stage when you're trying to kick him or her off-stage. The former is a mere annoyance, but the latter is a disaster waiting to happen, and it's all due to the placement of the buttons, all right next to each other.

Co-hosting a room is another potential minefield. When scheduling a room, the user can add anyone as a co-host... without his or her explicit permission. The co-host may be offline and not even aware of it. Now, what if the title of the room was something contentious, like "Black Lives Don't Matter" or "Women belong in the kitchen"?


Speaking of scheduling rooms, it's all well and good when you only want to schedule a one-off room. But what if you want to schedule a daily or even weekly room? Going through the process every single time is a pain in the ass.

It would be nice if we could change the titles of the rooms we open, without having to shut down and restart the room. Sometimes, typos happen. Conversation topics change.

Notifications and pinging can be a pain in the ass, especially if you're the one getting the pings or the notifications.



Conclusion

There's a lot to like about Clubhouse. There's also quite a lot not to like about Clubhouse. Certainly plenty of room for improvement, but the good news is that most of it is fixable by designing the interface carefully.

The idea behind Clubhouse is certainly innovative, but it's by no means the only one of its kind right now. Already, knockoffs of this app are saturating the market. Whether or not Clubhouse can stay afloat in a world full of copycats, remains to be seen.

My Rating

6.5 / 10

Leaving quietly and landing my plane,
T___T

Monday, 20 September 2021

A look at purchasing computer protection

One question I keep having to answer as an IT professional is that of antiviruses. People tend to do their computer protection shopping once a year or so, and the myriad of options mystifies them.

What is an Internet Security Suite and how does it differ from an antivirus? What is a Total Security Suite? Which should you buy? Today, these questions get answered.

Antivirus

For those of you who experienced life in the early 90s, antiviruses absolutely rocked. They recognized virus signatures when scanning the computer, and cleaned the hard disk of these viruses, worms and Trojan Horses. It was unthinkable to access the contents of a floppy disk without first scanning it using anti-virus software.

Floppy disks.

And then the internet happened, floppy disks became obsolete, and the game changed forever.

These days, antivirus software does pretty much the same thing, but unless the user intends to stay off the internet, that just isn't adequate anymore. The number of new threats grows annually, and even with the self-updating capabilities of antivirus software these days, much of the security threats tend to come from the internet, not USB drives.

Internet Security Suite

An Internet Security Suite includes the functionality of antivirus software, with the added bonus of protecting the user against internet threats (both security and privacy) such as spyware, phishing and spam. This is absolutely vital because the internet has pretty much taken over most computer usage. There is an added advantage if you have children - an Internet Security Suite may also feature Parental Controls.

Internet protection.

The cost is higher than that of antivirus software, but typically not by much. The cost in terms of processing power is noticeable unless you have a really good machine.

Total Security Suite

This is the one option that confuses the heck out of many users. What is "total" security if an Internet Security Suite already encompasses antivirus software? A Total Security Suite includes all the functionality of an Internet Security Suite (and by association, antivirus software).

This option actually includes new layers of protection, such as VPN subscription, file encryption and so on. Like Internet Security Suites, protection is implemented in the form of a firewall.

Now that's a firewall.

The cost is higher than that of an Internet Security Suite, along with required processing power. This slows down your machine substantially. Unless you are absolutely invested in not being spied upon by your Government and actually have something significant to hide, it might not be worth the price.

Protect yourself!

Whatever you ultimately purchase, it all depends on your context and what you are trying to protect. A few extra dollars is a worthwhile investment for peace of mind.

Fiarewaell for now!
T___T

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

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.

Saturday, 11 September 2021

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

It is time for us to draw a line chart. For the purpose of brevity, I will not be going into too much detail where I have already previously covered for D3 line charts.

The line chart will take up the entire area above the drop-down list, buttons and checkboxes. Basically, the div styled using topContainer. So let's place an svg tag within the div.
<div class="topContainer">
    <svg>

    </svg>

</div>


Now, we give svg tags a general styling. They will just fill the entire parent div.
div
{
    outline: 1px solid red;
}

svg
{
    width: 100%;
    height: 100%;
}


.dashboardContainer
{
    width: 800px;
    height: 620px;
    margin: 0 auto 0 auto;
}


Let us get to work on the drawLineChart() method. First, we declare the variable svgLineChart. It will be the svg element within the div styled by CSS class topContainer. And then we clear it using the html() method and passing in an empty string as an argument.
drawLineChart: function(data, sg, mw, imports, deaths)
{
    var svgLineChart = d3.select(".topContainer svg");
    svgLineChart.html("");

},


Next, we declare maxSG, maxMW, maxImports and maxDeaths, setting all to 0. These will represent the highest values in the current dataset, so we can determine how to render the scale.
drawLineChart: function(data, sg, mw, imports, deaths)
{
    var svgLineChart = d3.select(".topContainer svg");
    svgLineChart.html("");
    
    var maxSG = 0;
    var maxMW = 0;
    var maxImports = 0;
    var maxDeaths = 0;

},


And then we create arrays for data population - arrSG, arrMW, arrImport and arrDeaths.
drawLineChart: function(data, sg, mw, imports, deaths)
{
    var svgLineChart = d3.select(".topContainer svg");
    svgLineChart.html("");
    
    var maxSG = 0;
    var maxMW = 0;
    var maxImports = 0;
    var maxDeaths = 0;

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

},


Use a For loop to populate each array using the appropriate property from data.
var arrSG = [];
var arrMW = [];
var arrImports = [];
var arrDeaths = [];

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


If sg is true, that means the user has checked the appropriate checkbox, and we will set maxSG to the maximum value of arrSG. For this, we use the max() method of the d3 object.
for (var i = 0; i < data.length; i++)
{
    arrSG.push(parseInt(data[i].sg));
    arrMW.push(parseInt(data[i].mw));
    arrImports.push(parseInt(data[i].imports));
    arrDeaths.push(parseInt(data[i].deaths));
}

if (sg)
{
    maxSG = d3.max(arrSG);
}


Do the same for maxMW, maxImports and maxDeaths.
if (sg)
{
    maxSG = d3.max(arrSG);
}

if (mw)
{
    maxMW = d3.max(arrMW);    
}

if (imports)
{
    maxImports = d3.max(arrImports);    
}

if (deaths)
{
    maxDeaths = d3.max(arrDeaths);    
}


Now declare scaleLineChartMax. Using the max() method of the d3 object again, pass in an array whose elements are made out of maxSG, maxMW, maxImports and maxDeaths, as an argument. This will give you the highest value we need to cater for, for the scale.
if (deaths)
{
    maxDeaths = d3.max(arrDeaths);    
}

var scaleLineChartMax = d3.max([maxSG, maxMW, maxImports, maxDeaths]);


Multiply scaleLineChartMax by 1.2 to leave a little buffer.
var scaleLineChartMax = d3.max([maxSG, maxMW, maxImports, maxDeaths]);
scaleLineChartMax = scaleLineChartMax * 1.2;


Declare scaleData as an empty array. We will be populating it with scale values. Then declare unitGrouping with a value of 2.
scaleLineChartMax = scaleLineChartMax * 1.2;

var scaleData = [];
var unitGrouping = 2;


And then we set the value of unitGrouping according to the value of scaleLineChartMax.
scaleLineChartMax = scaleLineChartMax * 1.2;

var scaleData = [];
var unitGrouping = 2;
if (scaleLineChartMax > 10) unitGrouping = 5;
if (scaleLineChartMax > 100) unitGrouping = 50;
if (scaleLineChartMax > 1000) unitGrouping = 500;


Here, we use a For loop, with scaleLineChartMax as the maximum value, and intervals defined using unitGrouping. And we push the value of i into scaleData.
var scaleData = [];
var unitGrouping = 2;
if (scaleLineChartMax > 10) unitGrouping = 5;
if (scaleLineChartMax > 100) unitGrouping = 50;
if (scaleLineChartMax > 1000) unitGrouping = 500;

for (var i = 0; i <= scaleLineChartMax; i += unitGrouping)
{
    scaleData.push(i);
}


Now declare unitSize. It is going to be the actual pixel size of the distance between each tick on the scale. So we define it using 250 (the height of the scale) divided by the number of elements in scaleData. To avoid a divide-by-zero error, we use a conditional block.
for (var i = 0; i <= scaleLineChartMax; i += unitGrouping)
{
    scaleData.push(i);
}

var unitSize = 250 / (scaleData.length - 1 > 0 ? scaleData.length - 1 : 1);


Then we define unitSpace. It is the space in pixels between data points on the lines. For that, we take 750 (the width of the chart) and divide it by the maximum number of data points, which is 31 because there is a maximum of 31 days in a month!
var unitSize = 250 / (scaleData.length - 1 > 0 ? scaleData.length - 1 : 1);
var unitSpace = 700 / 31;


With that, we insert all the scale ticks using line tags. They will be styled using scaleLineChartTick, which we will create later. The data we will use is scaleData. The y1 and y2 properties will be a function of the element number, i, and unitSize.
var unitSize = 250 / (scaleData.length - 1 > 0 ? scaleData.length - 1 : 1);
var unitSpace = 700 / 31;

svgLineChart.selectAll("line.scaleLineChartTick")
.data(scaleData)
.enter()
.append("line")
.attr("class", "scaleLineChartTick")
.attr("x1", "40px")
.attr("y1", function(d, i)
{
    return (i * unitSize) + "px";
})
.attr("x2", "50px")
.attr("y2", function(d, i)
{
    return (i * unitSize) + "px";
});


And here is the CSS class. We set the color and thickness.
.colSG { color: #FF0000; }
.colMW { color: #9999FF; }
.colImports { color: #44AA44; }
.colDeaths { color: #999999; }

.scaleLineChartTick
{
    stroke: rgba(100, 100, 100, 1);
    stroke-width: 1px;
}


You can see the ticks. And they will even change if we click on the buttons or change the value in the drop-down list.


Let's add a horizontal and vertical line to the scale. The classes will be scaleLineChartVertical and scaleLineChartHorizontal.
svgLineChart.selectAll("line.scaleLineChartTick")
.data(scaleData)
.enter()
.append("line")
.attr("class", "scaleLineChartTick")
.attr("x1", "40px")
.attr("y1", function(d, i)
{
    return (i * unitSize) + "px";
})
.attr("x2", "50px")
.attr("y2", function(d, i)
{
    return (i * unitSize) + "px";
});

svgLineChart
.append("line")
.attr("class", "scaleLineChartVertical")
.attr("x1", "50px")
.attr("y1", "0px")
.attr("x2", "50px")
.attr("y2", "250px");

svgLineChart
.append("line")
.attr("class", "scaleLineChartHorizontal")
.attr("x1", "50px")
.attr("y1", "250px")
.attr("x2", "750px")
.attr("y2", "250px");


Add these two classes to this line, because they are styled similarly.
.scaleLineChartTick, .scaleLineChartVertical, .scaleLineChartHorizontal
{
    stroke: rgba(100, 100, 100, 1);
    stroke-width: 1px;
}


Now we see the scale take shape.


Let's add the text next. The CSS class is scaleLineChartText, the data used is scaleData and the y property is based on element number, i, and unitSize. The text is, well, normally we would use d, but we want the value in reverse order, so a bit of jumpng through hoops is required.
svgLineChart.selectAll("line.scaleLineChartTick")
.data(scaleData)
.enter()
.append("line")
.attr("class", "scaleLineChartTick")
.attr("x1", "40px")
.attr("y1", function(d, i)
{
    return (i * unitSize) + "px";
})
.attr("x2", "50px")
.attr("y2", function(d, i)
{
    return (i * unitSize) + "px";
});

svgLineChart.selectAll("text.scaleLineChartText")
.data(scaleData)
.enter()
.append("text")
.attr("class", "scaleLineChartText")
.attr("x", "30px")
.attr("y", function(d, i)
{
    return (i * unitSize) + "px";
})
.text(function(d, i)
{
    return scaleData[scaleData.length - i -1];
});


svgLineChart
.append("line")
.attr("class", "scaleLineChartVertical")
.attr("x1", "50px")
.attr("y1", "0px")
.attr("x2", "50px")
.attr("y2", "250px");


Style scaleLineChartText. In particular, make sure the text-anchor property is end to ensure that text is aligned properly.
.scaleLineChartTick, .scaleLineChartVertical, .scaleLineChartHorizontal
{
    stroke: rgba(100, 100, 100, 1);
    stroke-width: 1px;
}  

.scaleLineChartText
{
    font: 8px verdana;
    fill: rgba(100, 100, 100, 1);
    text-anchor: end;
}


Now you see the numbers! If you change the month, you can see the scale change. In a bit, you will see why.


Now for the fun part!

Let's add the lines and nodes. We begin by checking if sg is true.
svgLineChart
.append("line")
.attr("class", "scaleLineChartHorizontal")
.attr("x1", "50px")
.attr("y1", "250px")
.attr("x2", "750px")
.attr("y2", "250px");

if (sg)
{

}


Then we add circle tags. They will be styled using colSG. The data used is arrSG, the array of all SG values in the current dataset. For cx, we leave a 80 pixel space at the beginning, then multiple the element number, i, by unitSpace. For cy, we take 250 (the height of the chart) less d multiplied by the base unit, which is unitSize divided by UnitGrouping. We give it a radius of 3 pixels.
if (sg)
{
    svgLineChart.selectAll("circle.colSG")
    .data(arrSG)
    .enter()
    .append("circle")
    .attr("class", "colSG")
    .attr("cx", function(d, i)
    {
        return (i * unitSpace + 80) + "px";
    })
    .attr("cy", function(d)
    {
        return (250 - (d * (unitSize / unitGrouping))) + "px";
    })
    .attr("r", "3px");

}


Add this to colSG.
.colSG { color: #FF0000; fill: #FF0000; stroke: #FF0000; }


Now you'll see the circles! It will be more apparent if you switch the month to, say, "Jun 2021" because the values for SG are higher in that month.


Now just add a little animation here, just for fun.
svgLineChart.selectAll("circle.colSG")
.data(arrSG)
.enter()
.append("circle")
.attr("class", "colSG")
.attr("cx", function(d, i)
{
    return (i * unitSpace + 80) + "px";
})
.attr("cy", "250px")
.attr("r",  "0px")
.transition()
.duration(500)

.attr("cy", function(d)
{
    return (250 - (d * (unitSize / unitGrouping))) + "px";
})
.attr("r", "3px");


It is time to add lines. Again, data is arrSG and it will be styled using colSG. x1 is calcuated by using the previous value before the current one (unless this is the first value) multiplied by unitSpace with 80 pixels buffer at the start. x2 will be the element number multiplied by unitSpace with 80 pixels buffer at the start. y1 and y2 are similarly defined, with the same rationalization used by the earlier inserted circle tags.
.attr("cy", function(d)
{
    return (250 - (d * (unitSize / unitGrouping))) + "px";
})
.attr("r", "3px");

svgLineChart.selectAll("line.colSG")
.data(arrSG)
.enter()
.append("line")
.attr("class", "colSG")
.attr("x1", function(d, i)
{
    var val = i;

    if (i > 0)
    {
        val = val - 1;                   
    }

    return (val * unitSpace + 80) + "px";
})
.attr("y1", function(d, i)
{
    var val = d;

    if (i > 0)
    {
        val = arrSG[i - 1];
    }

    return (250 - (val * (unitSize / unitGrouping))) + "px";
})
.attr("x2", function(d, i)
{
    return (i * unitSpace + 80) + "px";
})
.attr("y2", function(d)
{
    return (250 - (d * (unitSize / unitGrouping))) + "px";
});


And here are the lines!


Add some animation!
svgLineChart.selectAll("line.colSG")
.data(arrSG)
.enter()
.append("line")
.attr("class", "colSG")
.attr("x1", function(d, i)
{
    var val = i;

    if (i > 0)
    {
        val = val - 1;                   
    }

    return (val * unitSpace + 80) + "px";
})
.attr("y1", "250px")
.attr("x2", function(d, i)
{
    return (i * unitSpace + 80) + "px";
})
.attr("y2", "250px")
.transition()
.duration(1500)

.attr("y1", function(d, i)
{
    var val = d;

    if (i > 0)
    {
        val = arrSG[i - 1];
    }

    return (250 - (val * (unitSize / unitGrouping))) + "px";
})
.attr("y2", function(d)
{
    return (250 - (d * (unitSize / unitGrouping))) + "px";
});


Now we just do the same for MW, Imports and Deaths.
if (sg)
{
    svgLineChart.selectAll("circle.colSG")
    .data(arrSG)
    .enter()
    .append("circle")
    .attr("class", "colSG")
    .attr("cx", function(d, i)
    {
        return (i * unitSpace + 80) + "px";
    })
    .attr("cy", "250px")
    .attr("r", "0px")
    .transition()
    .duration(500)
    .attr("cy", function(d)
    {
        return (250 - (d * (unitSize / unitGrouping))) + "px";
    })
    .attr("r", "3px");

    svgLineChart.selectAll("line.colSG")
    .data(arrSG)
    .enter()
    .append("line")
    .attr("class", "colSG")
    .attr("x1", function(d, i)
    {
        var val = i;

        if (i > 0)
        {
            val = val - 1;                   
        }

        return (val * unitSpace + 80) + "px";
    })
    .attr("y1", "250px")
    .attr("x2", function(d, i)
    {
        return (i * unitSpace + 80) + "px";
    })
    .attr("y2", "250px")
    .transition()
    .duration(1500)
    .attr("y1", function(d, i)
    {
        var val = d;

        if (i > 0)
        {
            val = arrSG[i - 1];
        }

        return (250 - (val * (unitSize / unitGrouping))) + "px";
    })
    .attr("y2", function(d)
    {
        return (250 - (d * (unitSize / unitGrouping))) + "px";
    });
}

if (mw)
{
    svgLineChart.selectAll("circle.colMW")
    .data(arrMW)
    .enter()
    .append("circle")
    .attr("class", "colMW")
    .attr("cx", function(d, i)
    {
        return (i * unitSpace + 80) + "px";
    })
    .attr("cy", "250px")
    .attr("r", "0px")
    .transition()
    .duration(500)
    .attr("cy", function(d)
    {
        return (250 - (d * (unitSize / unitGrouping))) + "px";
    })
    .attr("r", "3px");

    svgLineChart.selectAll("line.colMW")
    .data(arrMW)
    .enter()
    .append("line")
    .attr("class", "colMW")
    .attr("x1", function(d, i)
    {
        var val = i;

        if (i > 0)
        {
            val = val - 1;                   
        }

        return (val * unitSpace + 80) + "px";
    })
    .attr("y1", "250px")
    .attr("x2", function(d, i)
    {
        return (i * unitSpace + 80) + "px";
    })
    .attr("y2", "250px")
    .transition()
    .duration(1500)
    .attr("y1", function(d, i)
    {
        var val = d;

        if (i > 0)
        {
            val = arrMW[i - 1];
        }

        return (250 - (val * (unitSize / unitGrouping))) + "px";
    })
    .attr("y2", function(d)
    {
        return (250 - (d * (unitSize / unitGrouping))) + "px";
    });
}

if (imports)
{
    svgLineChart.selectAll("circle.colImports")
    .data(arrImports)
    .enter()
    .append("circle")
    .attr("class", "colImports")
    .attr("cx", function(d, i)
    {
        return (i * unitSpace + 80) + "px";
    })
    .attr("cy", "250px")
    .attr("r", "0px")
    .transition()
    .duration(500)
    .attr("cy", function(d)
    {
        return (250 - (d * (unitSize / unitGrouping))) + "px";
    })
    .attr("r", "3px");

    svgLineChart.selectAll("line.colImports")
    .data(arrImports)
    .enter()
    .append("line")
    .attr("class", "colImports")
    .attr("x1", function(d, i)
    {
        var val = i;

        if (i > 0)
        {
            val = val - 1;                   
        }

        return (val * unitSpace + 80) + "px";
    })
    .attr("y1", "250px")
    .attr("x2", function(d, i)
    {
        return (i * unitSpace + 80) + "px";
    })
    .attr("y2", "250px")
    .transition()
    .duration(1500)
    .attr("y1", function(d, i)
    {
        var val = d;

        if (i > 0)
        {
            val = arrImports[i - 1];
        }

        return (250 - (val * (unitSize / unitGrouping))) + "px";
    })
    .attr("y2", function(d)
    {
        return (250 - (d * (unitSize / unitGrouping))) + "px";
    });
}

if (deaths)
{
    svgLineChart.selectAll("circle.colDeaths")
    .data(arrDeaths)
    .enter()
    .append("circle")
    .attr("class", "colDeaths")
    .attr("cx", function(d, i)
    {
        return (i * unitSpace + 80) + "px";
    })
    .attr("cy", "250px")
    .attr("r", "0px")
    .transition()
    .duration(500)
    .attr("cy", function(d)
    {
        return (250 - (d * (unitSize / unitGrouping))) + "px";
    })
    .attr("r", "3px");

    svgLineChart.selectAll("line.colDeaths")
    .data(arrDeaths)
    .enter()
    .append("line")
    .attr("class", "colDeaths")
    .attr("x1", function(d, i)
    {
        var val = i;

        if (i > 0)
        {
            val = val - 1;                   
        }

        return (val * unitSpace + 80) + "px";
    })
    .attr("y1", "250px")
    .attr("x2", function(d, i)
    {
        return (i * unitSpace + 80) + "px";
    })
    .attr("y2", "250px")
    .transition()
    .duration(1500)
    .attr("y1", function(d, i)
    {
        var val = d;

        if (i > 0)
        {
            val = arrDeaths[i - 1];
        }

        return (250 - (val * (unitSize / unitGrouping))) + "px";
    })
    .attr("y2", function(d)
    {
        return (250 - (d * (unitSize / unitGrouping))) + "px";
    });
}


Now you see all the different colored lines. If you uncheck some of the boxes, you see some lines disappear, and the remaining lines change. That's because the scale changes as well.


One last touch!
Add a title to the chart.
        .attr("y2", function(d)
        {
            return (250 - (d * (unitSize / unitGrouping))) + "px";
        });
    }

    svgLineChart
    .append("text")
    .attr("class", "chartTitle")
    .attr("x", "650px")
    .attr("y", "20px")
    .text("Monthly Cases");

},
drawDonutChart: function(data, sg, mw, imports)
{


And style chartTitle.
.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; }

.chartTitle
{
    font: bold 16px verdana;
    fill: rgba(0, 0, 0, 1);
}


.scaleLineChartTick, .scaleLineChartVertical, .scaleLineChartHorizontal
{
    stroke: rgba(100, 100, 100, 1);
    stroke-width: 1px;
}


And we have our line chart!


Next

The next part should be fun. We will be creating a donut chart.

Wednesday, 8 September 2021

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

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.