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.

No comments:

Post a Comment