Wednesday, 22 July 2020

Web Tutorial: D3 Pie Chart (Part 2/4)

So now we have a placeholder to put the pie chart in, and let's waste no time getting into it.

Add properties scale, dataWidth and dataSpacing to the config object. Also add the setData() method.

scale is used to enlarge or shrink your chart as you see fit. It beats having to make changes to the whole of your setup when you can just change one value.

dataWidth specifies the diameter of your pie and width of your legend.

dataSpacing is all the spaces between your elements and data points.

And, of course, setData() renders your pie chart.
var config =
{
    "scale": 12,
    "dataWidth": 20,
    "dataSpacing": 2,
    "setData": function ()
    {

    }
}


In here, get the values of the drop-down lists, assigning the value to the variables year and stat. Those will be used to generate the pie.
"setData": function ()
{
    var year = d3.select("#ddlYear").node().value;
    var stat = d3.select("#ddlStat").node().value;
}


Then create the dataSet object with the arrays labels and stats.
"setData": function ()
{
    var year = d3.select("#ddlYear").node().value;
    var stat = d3.select("#ddlStat").node().value;

    var dataSet =
    {
      "labels": [],
      "stats": []
    };
}


Use a For loop to go through the cols array of the graphData object.
var dataSet =
{
  "labels": [],
  "stats": []
};

for (var i = 0; i < graphData.cols.length; i++)
{

}


Declare the array filtered and use the filter() method on the stats array of each element, returning those that correspond with year. If filtered is not empty...
for (var i = 0; i < graphData.cols.length; i++)
{
    var filtered = graphData.cols[i].stats.filter(function(x) { return x.year == year;})

    if (filtered.length > 0)
    {

    }
}


... and if the value of the required stat is not 0, push in data such as color and values into the appropriate arrays. Why do we have this condition? Well, it's a pie, and zero values aren't supposed to show up, that's why.
for (var i = 0; i < graphData.cols.length; i++)
{
    var filtered = graphData.cols[i].stats.filter(function(x) { return x.year == year;})

    if (filtered.length > 0)
    {
        if (filtered[0][stat] > 0)
        {
            dataSet.labels.push({title: graphData.cols[i].title, color: graphData.cols[i].color});
            dataSet.stats.push({value: filtered[0][stat], color: graphData.cols[i].color});        
        }
    }
}


Now, use the d3 object's select() method to grab the container, wrapper, chart and legend.
for (var i = 0; i < graphData.cols.length; i++)
{
    var filtered = graphData.cols[i].stats.filter(function(x) { return x.year == year;})

    if (filtered.length > 0)
    {
        if (filtered[0][stat] > 0)
        {
            dataSet.labels.push({title: graphData.cols[i].title, color: graphData.cols[i].color});
            dataSet.stats.push({value: filtered[0][stat], color: graphData.cols[i].color});        
        }
    }
}

var container = d3.select(".pieChartContainer");
var wrapper = d3.select(".pieChart");
var chart = d3.select(".pieChartSvg");
var legend = d3.select(".pieLegendSvg");


This next piece of code sets the width of the container. Since the dataWidth property of the config object is supposed to define the diameter of the pie and the width of the legend, and we want to have some spacing on both sides of the chart, the width of the chart should be double the sum of dataSpacing and dataWidth.
var container = d3.select(".pieChartContainer");
var wrapper = d3.select(".pieChart");
var chart = d3.select(".pieChartSvg");
var legend = d3.select(".pieLegendSvg");

container
.style("width", function(d)
{
    return ((config.dataSpacing + config.dataWidth) * 2) + "em";
});


wrapper measures the height of the chart, which is the diameter of the pie plus spacing at the bottom and top. The height of the legend is measured by how many players are being represented in the pie (which is the length property of the labels array of the dataSet object), plus a bit of spacing at the top. Whichever is greater is the returned value for wrapper.
container
.style("width", function(d)
{
    return ((config.dataSpacing + config.dataWidth) * 2) + "em";
});

wrapper
.style("height", function(d)
{
    var legendHeight = (config.dataSpacing * dataSet.labels.length) + 2;
    var chartHeight = config.dataSpacing + (config.dataWidth * 2);

    return (chartHeight > legendHeight ? chartHeight : legendHeight) + "em";
});


Now this next bit may not make sense at first. so bear with me. In chart, append a g tag. g is a group, in which we will append pie slices. And since they will all originate from here, g will be a one pixel element located right in the middle of chart. And that's why we use the transform property to translate the g tag. The x and y values for the translation are the same - the sum of half of dataSpacing (resulting in the radius of the pie) and dataWidth, multiplied by the scale property.

While we're at it, let's also clear the legend and set its height.
wrapper
.style("height", function(d)
{
    var legendHeight = (config.dataSpacing * dataSet.labels.length) + 2;
    var chartHeight = config.dataSpacing + (config.dataWidth * 2);

    return (chartHeight > legendHeight ? chartHeight : legendHeight) + "em";
});

chart
.html("")
.append("g")
.attr("transform", "translate(" + ((config.dataSpacing + (config.dataWidth / 2)) * (config.scale)) + "," + ((config.dataSpacing + (config.dataWidth / 2)) * (config.scale)) + ")");

legend
.html("")
.style("height", function(d) {
    return ((config.dataSpacing * dataSet.labels.length) + 2) + "em";
});


And then we set chart to the freshly created g tag instead. What this means is that every element inserted into this group will automatically be translated.
chart
.html("")
.append("g")
.attr("transform", "translate(" + ((config.dataSpacing + (config.dataWidth / 2)) * (config.scale)) + "," + ((config.dataSpacing + (config.dataWidth / 2)) * (config.scale)) + ")");

chart = d3.select(".pieChartSvg g");

legend
.html("")
.style("height", function(d) {
    return ((config.dataSpacing * dataSet.labels.length) + 2) + "em";
});


Now we run the pie() method of the d3 object! This should work in Version 3 but somehow doesn't, so I'm not gonna sweat it and just use Version 4 instead. At the time of this writing, Version 5 of D3 is out already and as such I see no value in persisting with Version 3.

What does pie() do? This method creates a function which will be used to calculate the angle values for any array of values you pass in later. Do let's set the function to a variable called pie.
legend
.html("")
.style("height", function(d) {
    return ((config.dataSpacing * dataSet.labels.length) + 2) + "em";
});

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


Declare data, and use our newly minted pie() function, passing in the stats array of the dataSet object as an argument. This will convert data into an array of all the start and end angles of each pie slice, based on the stats array!
var pie =
d3.pie()
.value(function(d)
{
    return d.value;
});

var data = pie(dataSet.stats);


Now we declare the variable arc. Set arc to the function returnd by the arc() method of the d3 object. In here. we are going to declare two things by running the innerRadius() and outerRadius() methods. We want a pie, so the inner radius needs to be 0. If we wanted a donut, of course, then we'd have a larger number.

For the outer radius, we use the dataWidth property, halved, which is the intended radius of the pie, multipled by scale.
var data = pie(dataSet.stats);

var arc =
d3.arc()
.innerRadius(0)
.outerRadius((this.dataWidth / 2) * config.scale);


Are you ready? Because the next part is pure D3 magic. We grab chart (which is now the grouping element in the SVG) and append a path tag to it. For data, we'll pass in data which is actually the array of start and end angles generated by the pie() function! Isn't it nice you didn't have to calculate it yourself?
var arc =
d3.arc()
.innerRadius(0)
.outerRadius((this.dataWidth / 2) * config.scale);

chart
.selectAll("path")
.data(data)
.enter()
.append("path");


Obviously, once you append a path tag, you will need its d attribute. d is supplied by arc itself! Now, how did that happen? The thing is, the newly generated arc() function uses the inner and outer radii specified, plus the start and end angle given by data, and generates the d attribute of the pie slice from there.
chart
.selectAll("path")
.data(data)
.enter()
.append("path")
.attr("d", arc);


Next, supply the fill attribute by referencing the color property of the data object inside d.
chart
.selectAll("path")
.data(data)
.enter()
.append("path")
.attr("d", arc)
.attr("fill", function(d) {
    return d.data.color;
});


How did we get that data object inside d anyway?

Remember this? This basically ensured that when we ran the pie() function, it would embed the current value of d (which was the object containing the value and color) into data, along with the generated start and end angles!
var pie =
d3.pie()
.value(function(d)
{
    return d.value;
});

var data = pie(dataSet.stats);


Now near the bottom of your code, don't forget to call the setData() method after populating the drop-down lists... and also set the method to run every time the values of the drop-down lists change.
ddlStat.selectAll("option")
.data(graphData.stats)
.enter()
.append("option")
.property("selected", function(d, i)
{
    return i == 0;
})
.attr("value", function(d)
{
    return d;
})
.text(function(d)
{
    return d;
});

config.setData();

d3.select("#ddlYear").on("change", function() { config.setData(); });
d3.select("#ddlStat").on("change", function() { config.setData(); });


Anyway. This is your pie! You should see how it fits in with the area you've defined.


We're not quite done yet...

Remember we still have values to use? Let's use 'em!

Using the D3 method, insert text tags into chart. Remember chart is, at this point, referring to the grouping tag within the SVG. For data(), you pass in data again. The text() method is straightforward, and here you're just supplying the value property.
chart
.selectAll("path")
.data(data)
.enter()
.append("path")
.attr("d", arc)
.attr("fill", function(d) {
    return d.data.color;
});

chart
.selectAll("text")
.data(data)
.enter()
.append("text")
.text(function(d)
{
    return d.value;
});



For the x and y positioning of the text, we need to run the pointRadial() method of the d3 object. What does this do? Well, it takes an angle and a distance as parameters, and returns an array with x and y coordinates. Read more about it here!

To find the appropriate angle, we'll use the startAngle and endAngle properties of d (which were generated earlier using pie(), remember?) and determine the midpoint with some arithmetic. pointRadial() doesn't count the distance in em, so we'll need to pad dataWidth a bit.

Then after running the pointRadial() method, get the first element in the array returned as the x attribute.
chart
.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("x", function(d)
{
    var midpoint = d.startAngle + ((d.endAngle - d.startAngle) / 2);
    return d3.pointRadial(midpoint, config.dataWidth * 3)[0];
})
.text(function(d)
{
    return d.value;
});


For y, do the same but get the second element.
chart
.selectAll("text")
.data(data)
.enter()
.append("text")
.attr("x", function(d)
{
    var midpoint = d.startAngle + ((d.endAngle - d.startAngle) / 2);
    return d3.pointRadial(midpoint, config.dataWidth * 3)[0];
})
.attr("y", function(d)
{
    var midpoint = d.startAngle + ((d.endAngle - d.startAngle) / 2);
    return d3.pointRadial(midpoint, config.dataWidth * 3)[1];
})
.text(function(d)
{
    return d.value;
});


We have numbers nicely positioned! But they need to be styled.


So let's set the color to a nice yellow. Add any other formatting you'd like.
.pieChartSvg
{
    width: 50%;
    height: 100%;
    float: left;
    background-color: rgba(200, 100, 100, 0.5);
}

.pieChartSvg text
{
    fill: rgba(255, 255, 0, 1);
    text-anchor: middle;
    font-weight: bold;
}

.pieLegendSvg
{
    width: 40%;
    height: 100%;
    float: right;
    background-color: rgba(100, 200, 100, 0.5);
}


Nice!


Next

A pie's not complete without a legend. We'll be working on that next. Don't go away!

Monday, 20 July 2020

Web Tutorial: D3 Pie Chart (Part 1/4)

After showing you how to make a bar and line chart in D3, the natural progression from such hallowed acts of achievement would be the pie chart. Sure, back in 2017, I walked you through creating a pie chart using only HTML, CSS and vanilla JavaScript. It was a lot of fun, but the math was just... ugh. There are far easier ways; namely various charting libraries.

And, of course, D3 is one such charting library.

The past two web tutorials used version 3 of D3, but for this one we will use version 4 because that's the version which actually has the functions we need to produce that pie. And unlike in the case of the line chart web tutorial a couple months back, we won't be reusing all much code because the operation is very different.

So let's get to it!

We begin with some boilerplate HTML. Once again, note that the D3 library we're using is version 4. Font size and type has been set.
<!DOCTYPE html>
<html>
    <head>
        <title>D3 Pie Chart</title>

        <style>
            body
            {
                font-size: 12px;
                font-family: arial;
            }
        </style>

        <script src="https://d3js.org/d3.v4.min.js"></script>
    </head>

    <body>
        <script>

        </script>
    </body>
</html>


In the body, insert a div with an class of pieChartContainer. That will contain all the elements of the entire setup. Then insert a div into that, and give it a class of pieDashboard.
<body>
    <div class="pieChartContainer">
        <div class="pieDashboard">

        </div>
    </div>
   
    <script>

    </script>
</body>


In that div, you'll want two drop-down lists. They're ddlYear and ddlStat.
<body>
    <div class="pieChartContainer">
        <div class="pieDashboard">
            <select id="ddlYear">

            </select>

            <select id="ddlStat">

            </select>
        </div>
    </div>
   
    <script>

    </script>
</body>


After this, insert another div into the div styled using pieChartContainer. This one has the pieChart class. It will hold the chart.
<body>
    <div class="pieChartContainer">
        <div class="pieDashboard">
            <select id="ddlYear">

            </select>

            <select id="ddlStat">

            </select>
        </div>

        <div class="pieChart">

        </div>
    </div>
   
    <script>

    </script>
</body>


Within this div, insert two svg tags. The first has a class of pieChartSvg, and the second pieLegendSvg. No prizes for guessing what they'll be doing, eh?
<body>
    <div class="pieChartContainer">
        <div class="pieDashboard">
            <select id="ddlYear">

            </select>

            <select id="ddlStat">

            </select>
        </div>

        <div class="pieChart">
            <svg class="pieChartSvg">

            </svg>

            <svg class="pieLegendSvg">

            </svg>
        </div>
    </div>
   
    <script>

    </script>
</body>


Let's do some styling. First, ensure that all divs have a red outline so you can see what you're doing.
<style>
    body
    {
        font-size: 12px;
        font-family: arial;
    }   

    div {outline: 1px solid #FF0000;}
</style>


Then add the class for pieChartContainer. We're going to specify width and height here using em, and set it middle of the page.
<style>
    body
    {
        font-size: 12px;
        font-family: arial;
    }   

    div {outline: 1px solid #FF0000;}
   
    .pieChartContainer
    {
        width: 100em;
        height: 20em;
        margin: 0 auto 0 auto;
    }
</style>


Next is pieDashboard. It has a small height and all of its parent's width. The text-align property will push the drop-down lists to the middle, since they're inline elements.
div {outline: 1px solid #FF0000;}

.pieDashboard
{
    height: 3em;
    width: 100%;
    text-align: center;
}

.pieChartContainer
{
    width: 100em;
    height: 20em;
    margin: 0 auto 0 auto;
}


And finally, pieChart. width and height are 100%.
div {outline: 1px solid #FF0000;}

.pieDashboard
{
    height: 3em;
    width: 100%;
    text-align: center;
}

.pieChart
{
    width: 100%;
    height: 100%;
}

.pieChartContainer
{
    width: 100em;
    height: 20em;
    margin: 0 auto 0 auto;
}


This is what you should see. You'll notice the div styled using pieChart has been pushed down because the dashboard occupies 3em height. No worries.


Now float pieChartSvg left, and pieLegendSvg right. Specify their widths using percentages (ensure that the combined total width doesn't exceed 100), and give both heights of 100%. Give them a random background color so we can see what's going on.
.pieChartContainer
{
    width: 100em;
    height: 20em;
    margin: 0 auto 0 auto;
}

.pieChartSvg
{
    width: 50%;
    height: 100%;
    float: left;
    background-color: rgba(200, 100, 100, 0.5);
}

.pieLegendSvg
{
    width: 40%;
    height: 100%;
    float: right;
    background-color: rgba(100, 200, 100, 0.5);
}


Here you'll see the shape of things.


Now for some data! We'll just reuse the graphData object from earlier web tutorials.
<script>
var graphData =
{
    "cols":
    [
        {
            "title": "Adam Lallana",
            "stats":
            [
                {"year": 2015, "goals": 7, "appearances": 49},
                {"year": 2016, "goals": 8, "appearances": 35},
                {"year": 2017, "goals": 0, "appearances": 15},
                {"year": 2018, "goals": 0, "appearances": 16}
            ]
        },
        {
            "title": "Sadio Mané",
            "stats":
            [
                {"year": 2016, "goals": 13, "appearances": 29},
                {"year": 2017, "goals": 20, "appearances": 44},
                {"year": 2018, "goals": 26, "appearances": 50}
            ]
        },
        {
            "title": "Roberto Firminho",
            "stats":
            [
                {"year": 2015, "goals": 11, "appearances": 49},
                {"year": 2016, "goals": 12, "appearances": 41},
                {"year": 2017, "goals": 27, "appearances": 54},
                {"year": 2018, "goals": 16, "appearances": 48}
            ]
        },
        {
            "title": "Divock Origi",
            "stats":
            [
                {"year": 2015, "goals": 10, "appearances": 33},
                {"year": 2016, "goals": 11, "appearances": 43},
                {"year": 2017, "goals": 0, "appearances": 1},
                {"year": 2018, "goals": 7, "appearances": 21}
            ]
        },
        {
            "title": "Daniel Sturridge",
            "stats":
            [
                {"year": 2015, "goals": 13, "appearances": 25},
                {"year": 2016, "goals": 7, "appearances": 27},
                {"year": 2017, "goals": 3, "appearances": 14},
                {"year": 2018, "goals": 4, "appearances": 27}
            ]
        },
        {
            "title": "James Milner",
            "stats":
            [
                {"year": 2015, "goals": 7, "appearances": 45},
                {"year": 2016, "goals": 7, "appearances": 40},
                {"year": 2017, "goals": 1, "appearances": 47},
                {"year": 2018, "goals": 7, "appearances": 45}
            ]
        },
    ],
    "rows": [2015, 2016, 2017, 2018],
    "stats": ["goals", "appearances"]
};
</script>


For every element in the cols array, let's add a property, color. Assign each player a different color.
<script>
var graphData =
{
    "cols":
    [
        {
            "title": "Adam Lallana",
            "color": "rgb(100, 50, 0)",
            "stats":
            [
                {"year": 2015, "goals": 7, "appearances": 49},
                {"year": 2016, "goals": 8, "appearances": 35},
                {"year": 2017, "goals": 0, "appearances": 15},
                {"year": 2018, "goals": 0, "appearances": 16}
            ]
        },
        {
            "title": "Sadio Mané",
            "color": "rgb(255, 0, 0)",
            "stats":
            [
                {"year": 2016, "goals": 13, "appearances": 29},
                {"year": 2017, "goals": 20, "appearances": 44},
                {"year": 2018, "goals": 26, "appearances": 50}
            ]
        },
        {
            "title": "Roberto Firminho",
            "color": "rgb(255, 50, 50)",
            "stats":
            [
                {"year": 2015, "goals": 11, "appearances": 49},
                {"year": 2016, "goals": 12, "appearances": 41},
                {"year": 2017, "goals": 27, "appearances": 54},
                {"year": 2018, "goals": 16, "appearances": 48}
            ]
        },
        {
            "title": "Divock Origi",
            "color": "rgb(255, 100, 0)",
            "stats":
            [
                {"year": 2015, "goals": 10, "appearances": 33},
                {"year": 2016, "goals": 11, "appearances": 43},
                {"year": 2017, "goals": 0, "appearances": 1},
                {"year": 2018, "goals": 7, "appearances": 21}
            ]
        },
        {
            "title": "Daniel Sturridge",
            "color": "rgb(0, 0, 0)",
            "stats":
            [
                {"year": 2015, "goals": 13, "appearances": 25},
                {"year": 2016, "goals": 7, "appearances": 27},
                {"year": 2017, "goals": 3, "appearances": 14},
                {"year": 2018, "goals": 4, "appearances": 27}
            ]
        },
        {
            "title": "James Milner",
            "color": "rgb(100, 0, 50)",
            "stats":
            [
                {"year": 2015, "goals": 7, "appearances": 45},
                {"year": 2016, "goals": 7, "appearances": 40},
                {"year": 2017, "goals": 1, "appearances": 47},
                {"year": 2018, "goals": 7, "appearances": 45}
            ]
        },
    ],
    "rows": [2015, 2016, 2017, 2018],
    "stats": ["goals", "appearances"]
};
</script>


While you're at it, add the config object. We won't use it just yet.
var graphData =
{
    "cols":
    [
        {
            "title": "Adam Lallana",
            "color": "rgb(100, 50, 0)",
            "stats":
            [
                {"year": 2015, "goals": 7, "appearances": 49},
                {"year": 2016, "goals": 8, "appearances": 35},
                {"year": 2017, "goals": 0, "appearances": 15},
                {"year": 2018, "goals": 0, "appearances": 16}
            ]
        },
        {
            "title": "Sadio Mané",
            "color": "rgb(255, 0, 0)",
            "stats":
            [
                {"year": 2016, "goals": 13, "appearances": 29},
                {"year": 2017, "goals": 20, "appearances": 44},
                {"year": 2018, "goals": 26, "appearances": 50}
            ]
        },
        {
            "title": "Roberto Firminho",
            "color": "rgb(255, 50, 50)",
            "stats":
            [
                {"year": 2015, "goals": 11, "appearances": 49},
                {"year": 2016, "goals": 12, "appearances": 41},
                {"year": 2017, "goals": 27, "appearances": 54},
                {"year": 2018, "goals": 16, "appearances": 48}
            ]
        },
        {
            "title": "Divock Origi",
            "color": "rgb(255, 100, 0)",
            "stats":
            [
                {"year": 2015, "goals": 10, "appearances": 33},
                {"year": 2016, "goals": 11, "appearances": 43},
                {"year": 2017, "goals": 0, "appearances": 1},
                {"year": 2018, "goals": 7, "appearances": 21}
            ]
        },
        {
            "title": "Daniel Sturridge",
            "color": "rgb(0, 0, 0)",
            "stats":
            [
                {"year": 2015, "goals": 13, "appearances": 25},
                {"year": 2016, "goals": 7, "appearances": 27},
                {"year": 2017, "goals": 3, "appearances": 14},
                {"year": 2018, "goals": 4, "appearances": 27}
            ]
        },
        {
            "title": "James Milner",
            "color": "rgb(100, 0, 50)",
            "stats":
            [
                {"year": 2015, "goals": 7, "appearances": 45},
                {"year": 2016, "goals": 7, "appearances": 40},
                {"year": 2017, "goals": 1, "appearances": 47},
                {"year": 2018, "goals": 7, "appearances": 45}
            ]
        },
    ],
    "rows": [2015, 2016, 2017, 2018],
    "stats": ["goals", "appearances"]
};

var config =
{
   
}


The next thing we will do, is populate the drop-down lists. If you've gone through any of the other web tutorials, I've probably repeated the instructions ad nauseam. It's basically the same as what we did for the bar chart.
var config =
{
   
}

var ddlYear = d3.select("#ddlYear");

ddlYear.selectAll("option")
.data(graphData.rows)
.enter()
.append("option")
.property("selected", function(d, i)
{
    return i == 0;
})
.attr("value", function(d)
{
    return d;
})
.text(function(d)
{
    return d;
});

var ddlStat = d3.select("#ddlStat");

ddlStat.selectAll("option")
.data(graphData.stats)
.enter()
.append("option")
.property("selected", function(d, i)
{
    return i == 0;
})
.attr("value", function(d)
{
    return d;
})
.text(function(d)
{
    return d;
});


So there you go - your drop-down lists!


Next

Things will get a bit complicated. We will dive right into creating your pie chart.

Thursday, 16 July 2020

The Day The Penny Dropped

One of the tasks that got thrown to me in the past few years, by virtue of my not being particularly fussy about what kind of tasks I was assigned, was automated DevOps.

I'm not afraid to say I struggled at the task. Shitty Team Foundation Server (TFS) interface aside, I really wasn't getting the entire build/artifact/configuration/working directory relationship. My Team Lead at the time was dismayed at my apparent inability to understand the process, but since nobody else in my team was willing to shoulder the burden, it fell to me. Note that I wasn't particularly enthusiastic about the task. I just wasn't picky.

So I kept at it, trying to commit everything to memory. But the thing about learning something is, often, simply memorizing it isn't the key. The key is understanding, and the pieces just weren't falling into place.

The final piece.

My Team Lead kept yammering on about artifacts and builds, but this wasn't helping much.

Until one day, I was thinking about Gary Mitchell's Ten Principles of Adult Learning. In particular, the fourth one - People learn easiest what is familiar to them.

That was when the lightbulb flickered. I was going about it all wrong.

What I was doing, was approaching automated DevOps like it was a completely new thing. What was the end result of deployment? A working ASP.NET MVC site. Hadn't I dealt with ASP.NET MVC back in 2012? I had deployed back then, as well, except it had been the manual way. Now what I was trying to do was automate this process.

That train of thought led me further along the path. What had I done back then? The views and assets had been copied directly to the server. I'd had to build, and then copied the resultant DLL file. What were the steps of the automated DevOps, but configured instructions in TFS to do pretty much the same thing?

From there, my understanding of the process grew at an astronomical rate, and within a week, the inevitable happened - a successful series of tests.

About my Team Lead

Not saying he couldn't have done better. Obviously, he could have. But while he was a better tech than I am, unlike me, he wasn't trained to transfer knowledge. He didn't have an ACTA.

On my part, when I set out on my quest for that ACTA in 2017, I had envisioned using it to better transfer knowledge. I certainly hadn't anticipated using the principles learned in order to teach myself.

Life throws you surprises every now and then. This was a pleasant one.

Yours (l)earnestly,
T___T

Wednesday, 8 July 2020

The Electoral Choice, explained in JavaScript

While I don't use Facebook nearly as much as I used to, I like to keep certain types of people on my Friends list.

There are the pro-Government types, in particular, the ever-enthusiastic fans of the incumbent, the People's Action Party (PAP). They post glowing gushing paragraphs about the PAP, extolling their virtues and taking their side in just about every matter, and they crap on the other political parties every opportunity they get.

And then there are the anti-establishment types. They'll oppose the People's Action Party on principle, and always have something negative to say in response. And they will support any political party that isn't the People's Action Party. Well, almost any; some political parties are such no-hopers that anyone in their right mind would be hard-pressed to support them.

And every five years, during the Singapore General Elections, something beautiful happens. Everyone goes full retard. At the same time.

Going full retard.

The pro-Government types will fiercely defend the PAP and vilify the other political parties. The anti-establishment types will vilify the PAP and take the side of the other political parties. In other words, everyone picks a side and praises their own side for doing something, while shitting on the other side just as passionately for doing pretty much the same thing.

The pro-Government types will predictably only vote PAP come what may. And the anti-establishment types will vote just about any political party that isn't the PAP. This is generally why I think political rallies are useless. Everyone has pretty much already made up their minds whom they will vote for. Few people are going to change their vote over some pretty and persuasive words during a passionate rally, and I wouldn't expect them to.

Whom do I support? Let's just say people like me are on the fence and probably equally despised by everybody. People think I'm "neutral" just so I can say I'm smarter than the extremists in both camps. But honestly, that would be more of a bragging right if you guys in your political camps weren't so goddamn foolish.

People despise fence-sitters because they want us to pick a side (preferably theirs), take a stand. But I am taking a stand. My stand is that of the voter.

Let's explain this in programming terms!

Let's say your vote is represented by a JavaScript variable.
var vote;

During an election, various parties would be working hard to change your vote to this...
var vote;

vote = "PAP";

...or this!
var vote;

vote = "WP";

That's good, and perfectly normal. But unfortunately, what I see is this...
const voteForPAP = true;

...or this!
const voteForPAP = false;


In programming, constants can't be changed. They can only be taken reference from. Variable values can be changed, and arguably, much of programming involves changing the values of variables.

What's the difference between people who will always vote for the PAP, or those who will never vote for the PAP? No difference. You no longer matter. If your vote is unchangeable, congratulations, you've pretty much made your vote (and by extension, yourself) irrelevant. No one is interested in working for your vote because your vote can't be earned, or changed.

Working to earn a vote that will never be given is nothing but a massive waste of time. Working to earn a vote that will always be given is also a colossal time-waster. That's right, Junior, you ain't worth the effort. And you have no one but your obstinate self to blame for that.

Final thoughts

People whose votes are already fixed, aren't the goal. They're the goalposts!

Be the goal, not the goalpost.

In football, players don't aim for the goalpost. They aim for the goal. Even if they do aim for the goalpost, ultimately, the intention is for the ball to rebound off the goalpost into goal.

Don't be binary. The only person it hurts is yourself.

vote = "whomever";
T___T

Wednesday, 1 July 2020

Code Cruft in the 2020 General Elections

That time is upon us. The time where earnest political debates break out on social spaces, Singaporeans divide themselves into the camps that they support, and eagerly follow every speech by politicians they follow. They may even ruminate (though from experience, I seriously doubt it) long and hard about the vote they are about to cast.

The 2020 Singapore General Elections, ladies and gentlemen. It's an important time in Singapore, no matter how some trivialize it. And your vote, no matter how insignificant it is by itself, plays a part in Nation-building. Because taking part in a free and fair election is an exercise in democracy, one of the cornerstones of our National Pledge.

This is a tech blog, however, and I wouldn't be talking about the Singapore General Elections if there wasn't a tech angle involved. And today, that angle is Code Cruft.

What in the ever-loving heck is "Code Cruft"?

You can look up the definition online, but in my own words, Code Cruft is superfluous code that adds no value to the code base. It is technically correct and will not throw an exception or cause the compiler to highlight a syntax error, but as mentioned, it basically takes up space without being useful. It could be code that once served a genuine purpose but did not scale well as the code base grew, or code that was poorly-written in the first place.

What does Code Cruft have to do with the General Elections?

Hold your horses, young bucks. I'm getting there.

Here's something I keep hearing whenever the General Elections come around. Sometimes I may even hear it when we're not having an election. It's become something like an urban legend.

Pssst... you're not gonna believe this!

It's usually from someone who will tell you that one of his or her friends have a friend who works in the Civil Service and voted against the incumbent, the People's Action Party, in the last election. And somehow or rather, their impending promotion got curtailed, or they were transferred abruptly to a career dead zone. Something along those lines. The point being that the ruling party keeps track of your votes, and if you know what's good for you, you'll make them happy with your vote.

In other words, the assertion is that the Singapore General Elections are neither free nor fair, and your vote is not secret.

I've heard this from so many people, and all these people all seem to know the same person who got unfairly bullied by the ruling party for not playing ball. Although strangely enough, when pressed, they'll never actually name that person.

Is this story true?

Well, if you're asking if this is technically feasible, probably. It may even be true.

But here's the thing - whether it's true or not, doesn't matter. That's not the question you should be asking. This story is like Code Cruft - useless to just about everybody. Let's think for a moment and ask a few more pertinent questions. What could be gained from spreading this story around? Who stands to gain?

Peoples' Action Party. If you're a fan of the ruling party and think spreading this story around will help induce people to vote for the ruling party out of fear, I have news for you. Firstly, this is a low-class tactic unworthy of a country of Singapore's stature and you should be embarrassed that thought even crossed your mind. What are you, some reject from the 60s? Secondly, the world is watching and the PAP needs the public to see this as a democratic corruption-free election. They have spared no effort to convince Singaporeans that your vote is secret. Tactics such as spreading a story like that, aren't appreciated by them in the least. 

Worker's Party. Surely undermining public faith in the PAP works in favor of their strongest opponents? Not so. See, the WP needs the public to vote without fear. They need the public to believe in the democratic process. Accusing the Government of vote-tampering is a serious matter. In fact, vote-tampering is illegal, and if the ruling party is found to have done so in any shape or form, the legitimacy of their rule will be called into question. Singapore's Government will have lost its standing internationally, and this would be a huge loss for Singapore no matter which side of the political fence you happen to support.



Other parties. You mean the losers who never seem to make it into Parliament? Who cares what they think? My left nut is more relevant to this discussion than they are.

But to answer the question, no, spreading this story around isn't in their best interests either. On the extremely remote chance that they actually manage to gain some ground and get elected into Parliament, loss of voter confidence in the electoral process does not help them at all. Therefore, they too, need the public to believe that their votes are secret; otherwise their already slim chances just got even slimmer.

Citizens. Singaporeans have a stake in this country. We participate in elections because doing so gives us the opportunity to help chart Singapore's course. And, as mentioned, democracy is a cornerstone of our National Pledge. Do you imagine the average Singaporean needs to be told that his or her vote is not secret and that Singapore's democracy is a lie? Who does that help, really?

Saboteurs, perhaps? There are people who aren't happy about Singapore's international standing today. Perhaps they are bitter ex-citizens, or merely cynical political observers. At worst, they could be agents positioned by foreign forces to plant seeds of doubt into the minds of Singaporeans. But look - such a method is slow, laborious and inefficient. The majority of Singaporeans would need to be deeply unhappy with their Government in the first place, for that to even have a prayer of working.

Does the story help the teller of that story, then? Maybe it helps him or her look knowledgeable? Well, no, it really doesn't. I've heard this story so many times that every time it's repeated to me, by the time the teller has finished telling it, I've already dismissed him or her as a hopeless know-nothing; at best, a useful idiot for foreign provocateurs.

Conclusion

This story is analogous to Code Cruft. It's Political Cruft. Just as Code Cruft could be syntactically correct, this story could be true, but it's still worthless garbage. It helps absolutely no one.

Stop spreading this nonsense around. Telling this story does not make you look clever. If something has to come out of your mouth, please at least let it be useful.

You need to be a little more sophisticated than this. Don't be that dude.

Your crufty devil,
T___T