Monday, 23 June 2025

What Swimming Taught Me About Brooks' Law

Swimming is a great activity. It's good cardio, facilities are inexpensive and its ability to help me solve problems cannot be overestimated. Why's that, you may ask? Well, for the simple fact that when you're underwater slugging it out, there's no getting distracted by Social Media or the wife (bless her heart) snuggling up. No, your brain gets a regular supply of oxygen as your body moves, and that's when I do my best thinking.

Swimming is
good for you.

It was during one of these swim sessions where I realized something else about swimming - as an activity, it's startlingly similar to team activities like, you guessed it, software development.

But let's start at the beginning, shall we?

How it all started...

I was in the pool doing laps, on an idyllic weekend afternoon. I remember it being blazing hot. As I languidly knifed through the water with my Breast Stroke, I overtook a guy doing The Crawl. Quite handily, I might add. This is tremendous because The Crawl is considered the significantly more powerful technique. Plus, the guy was maybe ten years younger than I was. I was feeling pretty smug about the situation, overall.

Until we both got overtaken by this old geezer who just blew past us like we were a couple of chumps. Talk about a reality check right there.

Later on, I lounged by poolside and watched as this old man hobbled up the ladder. He seemed to be walking funny. Then I stared. This guy's entire right foot had been sawed off around the ankle. I had been outraced in the pool by a goddamn amputee.

Missing one foot.

Sometimes, I noticed something similar about other swimmers. They might be taller or more athletic-looking. They moved more furiously. But still, I often overtook them in the chillest way possible.

It occured to me, that, just like with the old man and his amputated foot, the way our bodies were built, just wasn't affecting our speed. After all, everyone's body has mostly the same parts but with infinite variances. Some have bigger feet. Some have different weight distributions across their bodies. Some have longer legs, more streamlined hips, things like that. These things may give one the edge in terms of speed, in addition to just putting in more effort.

But all things being equal, it's more a matter of understanding how to coordinate ones body for optimal results.

What does this mean?

Having been swimming regularly for years, I had taken the time to get comfortable with my own body and maximize my performance in the pool, with minimal effort. The old man, similarly, had probably taken decades to be able to swim this fast, this gracefully, with only one foot.

And that's what I think the crux of the matter is - coordination.

Proper limb positioning.

With the Breast Stroke, for example, the legs are often the means of propulsion while the arms are the stabilizers.

Some people put in more work than me in their Breast Stroke. They complete, say, two strokes per second while I go at a pace of one stroke every three seconds. How, then, do I overtake them? Actually, I don't. They hold themselves back.

You see, these vigorous movers are doing more work, but often, when their legs are propelling them forward, their arms are in the wrong position. The arms are spread out, adding underwater resistance and inhibiting the forward propulsion. When my legs are kicking out for the forward propulsion, my arms are straight ahead, fingertips pointed forward, for minimum underwater resistance. When my legs are curled and preparing to kick, my arms are similarly curled and preparing to straighten. There is no other position they would be in; this has been drilled repeatedly into muscle memory: when my legs curl, so do my arms. When they straighten, so do my arms.

And that's just limb positioning. There's more to be said for correct breathing - when to inhale and exhale. For example, a very uncontroversial statement is that inhaling with your head under water is a horrendous idea; I wouldn't recommend it.

All I outlined above, is coordination. And thus, I work less hard underwater, but move faster.

How Brooks' Law applies

There is a well-known saying in software development.
"Adding manpower to a late software project makes it later." - Brooks' Law.


And this is because, when more developers are added to a project, one also needs to factor in the time it takes to bring them up to speed, find their place and get to a point where they start contributing positively. Thus, more people does not necessarily mean greater speed. This was the point that Fred Brooks, the man who coined this phrase, was trying to make.

The more the merrier?

Similarly, in a hypothetical scenario where I grew a third leg (scrub your filthy minds, please) that extra limb would not necessarily speed me up in a swim. Until I took the time to acclimatize to it and use it productively, it might even slow me down.

The Watery Takeaways

More is not always merrier. Additional elements that get in the way of existing elements, is a sign of the need for better coordination, and not more elements

Also, more work does not necessarily lead to faster results. Though if you didn't know that by now, you probably haven't been in the workforce long enough.

And finally, coordination has a real time cost, and needs to be accounted for.

Just some swimple truth!
T___T

Thursday, 19 June 2025

Failure is an eventuality, not a possibility

Some years ago, I recounted a story I'd heard to a friend.

In ancient Japan, the Lord of a territory had two sons. The first son was an impressive physical specimen - in archery, swordsmanship, wrestling, and he never lost. The second son was competent, but compared to his brother, he often came up short. It was expected that the Lord would pass on the leadership of the clan to his first son.

To the first son's consternation, however, when the time came for the Lord to retire, it was the second son who inherited the territory. The first son demanded to know why, and his father explained.

Inheriting leadership.

While the first son had gained a reputation for excellence and never having tasted defeat, the second son gained a reputation for persistence. He had an understanding of both his strengths and his shortcomings, and had learned from his mistakes - traits that all capable leaders need to have. More importantly, he understood how to recover from failure.

A different takeaway? 

Somehow, my buddy's takeaway from this was that the Lord had made a foolish decision - one looks at track record and it was unquestionable that the first son should be the one to lead. From this, I can only conclude that this was because my friend had never been part of the tech industry, or its very specialized cousin, the cybersecurity industry.

You see, techies generally know (any techies who don't know this should probably find another, less taxing line of work) that in the world of cybersecurity, defense is always playing catch-up to attack. After all, a defense can only be devised for an attack method that has already been invented. Thus, if your system has never gone down due to a cybersecurity attack, it's usually down to two reasons. One, there are targets more tempting than you. You're simply not worth the effort. Two, you've just been lucky.

Anyone feel lucky?

Neither reason has anything to do with the excellence of your defenses. That's just not the world we live in.

Thus the real test is not whether one can remain undefeated in perpetuity but whether one can recover from defeat, and how quickly. That is why recovery plans are integral to I.T Security policies. Prevention strategies are also part of I.T Security policies, yes... but the general understanding is that it's not a matter of whether your system will be the victim of a strong and determined attack, but when.

In fact, one should be extremely aware of those who claim to have never failed or made a mistake. If they're telling the truth (and that's a huge if) it raises red flags as to their adaptability and resilience.

In this industry, you're judged by how well you recover from failure. Not by never having encountered it.

An MMA example 

There's a real-world example in Mixed Martial Arts that illustrates this perfectly.

Take Conor McGregor. I'm not a big fan of the man by any means. He seems like a jerk with a big mouth. Where it concerns his job as a fighting athlete, however, I have considerably more respect for him than I do for Ronda Rousey. (That's not to say I think I could take her; she'd probably kick my ass in ten seconds flat).

You see, they're both legends in the sport. Their athleticism and skill. Their personalities. The way they popularized the sport. However, there is one significant difference. Ronda Rousey dominated the women's circuit for years, until she encountered two defeats. After the first defeat, she went to pieces and it was a while before she came back. The second defeat, at the capable hands of Amanda Nunes, was an utter annihilation and ended in her retirement.



McGregor, on the other hand, has lost a hell of a lot more fights than Rousey. However, each time, he's always come back strong, louder and more obnoxious than ever, none the worse for wear. Never has he allowed a defeat to end his career the way it did for Rousey. While McGregor was used to defeat and took it in stride, a couple of defeats was all it took for Rousey to crumble. Because she had never tasted it and didn't know how to handle it.

While McGregor knew he wasn't infallible (and had made peace with the fact), Rousey allowed herself to believe she was.

That's why I feel that an experience that incorporates failures (and the lessons we learn from them) are infinitely more useful than an experience with nothing but success.

Conclusion 

True value lies in adaptability, not invincibility.

It's a matter of when, rather than if, you taste defeat. And when that happens, what really matters is how you react to it. Nobody is invincible and nothing is perfect. And until we come to terms with that fact, we're doomed to repeated failure.

For the win,
T___T

Monday, 16 June 2025

Web Tutorial: D3 Combo Chart

A couple months ago, I pontificated at length about quitting smoking and working on my chinup game. There was a combo chart mentioned, and collected data. It is this combo chart that we will build today, in D3. The data is in this CSV file.

This is going to be a bare bones chart, no fancy animations and whatnot. To that end, let's begin with the HTML and D3 import. You will see that in the HTML, I have also included a h1 tag, an SVG tag and a script tag.
<!DOCTYPE html>
<html>
  <head>
    <title>Quit Smoking!</title>

    <style>

    </style>

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

  <body>
    <h1>The Chinups - Cigarettes Combo Chart</h1>

    <svg>

    </svg>

    <script>

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


In the CSS, we set a width and height for the SVG, and some specs for the h1 tag. The specs for the SVG are more pertinent to this tutorial.
<style>
svg
{
  width: 800px;
  height: 500px;
}

h1
{
  font-family: verdana;
  font-size: 16px;
}

</style>


That really is it for the HTML. From this point on, it will be mostly about the JavaScript. In the script tag, we have a comboChart object. Aside from that, we also make a call to d3's csv() method, passing in the name of the file, combo.csv, and a callback. We're going to be developing these two blocks somewhat concurrently.
<script>
  let comboChart =
  {

  };

  d3.csv("combo.csv", function(data)
  {

  });
</script>


In comboChart, we have properties cigData and chinupData, which are arrays. We then have maxUnits and dataPoints, which are integers with a default value of 1.
<script>
  let comboChart =
  {
    cigData: [],
    chinupData: [],
    maxUnits: 1,
    dataPoints: 1

  };

  d3.csv("combo.csv", function(data)
  {

  });
</script>


When parsing combo.csv, the dataPoints property is set to the length of data.
<script>
  let comboChart =
  {
    cigData: [],
    chinupData: [],
    maxUnits: 1,
    dataPoints: 1
  };

  d3.csv("combo.csv", function(data)
  {
    comboChart.dataPoints = data.length;
  });
</script>


We then have a For loop to iterate through data. The figure in the Reps column gets pushed into the chinupData array. The figure in the Cigs column gets pushed into the cigData array.
<script>
  let comboChart =
  {
    cigData: [],
    chinupData: [],
    maxUnits: 1,
    dataPoints: 1
  };

  d3.csv("combo.csv", function(data)
  {
    comboChart.dataPoints = data.length;

    for (var i = 0; i < data.length; i++)
    {
      comboChart.chinupData.push(parseInt(data[i].Reps));
      comboChart.cigData.push(parseInt(data[i].Cigs));
    }

  });
</script>


We then define maxReps. Using the max() method of d3, we want maxReps to contain the maximum value in chinupData. We then do the same for maxCigs, using it to contain the maximum value in cigData.
d3.csv("combo.csv", function(data)
{
  comboChart.dataPoints = data.length;

  for (var i = 0; i < data.length; i++)
  {
    comboChart.chinupData.push(parseInt(data[i].Reps));
    comboChart.cigData.push(parseInt(data[i].Cigs));
  }

  var maxReps = d3.max(comboChart.chinupData);
  var maxCigs = d3.max(comboChart.cigData);

});


And then we set maxUnits to maxReps or maxCigs, whichever value is higher.
d3.csv("combo.csv", function(data)
{
  comboChart.dataPoints = data.length;

  for (var i = 0; i < data.length; i++)
  {
    comboChart.chinupData.push(parseInt(data[i].Reps));
    comboChart.cigData.push(parseInt(data[i].Cigs));
  }

  var maxReps = d3.max(comboChart.chinupData);
  var maxCigs = d3.max(comboChart.cigData);

  if (comboChart.maxUnits < maxReps) comboChart.maxUnits = maxReps;
  if (comboChart.maxUnits < maxCigs) comboChart.maxUnits = maxCigs;

});


Finally, we call the drawCharts() method from the comboChart object.
d3.csv("combo.csv", function(data)
{
  comboChart.dataPoints = data.length;

  for (var i = 0; i < data.length; i++)
  {
    comboChart.chinupData.push(parseInt(data[i].Reps));
    comboChart.cigData.push(parseInt(data[i].Cigs));
  }

  var maxReps = d3.max(comboChart.chinupData);
  var maxCigs = d3.max(comboChart.cigData);

  if (comboChart.maxUnits < maxReps) comboChart.maxUnits = maxReps;
  if (comboChart.maxUnits < maxCigs) comboChart.maxUnits = maxCigs;

  comboChart.drawCharts();
});


We've not created drawCharts() yet. Well, no time like the present! In it, use the select() method of d3 to get the sole SVG object, and set it to the variable svgChart. Then clear svgChart.
let comboChart =
{
  cigData: [],
  chinupData: [],
  maxUnits: 1,
  dataPoints: 1,
  drawCharts: function()
  {
    var svgChart = d3.select("svg");
    svgChart.html("");
  }

}


Next, run the drawAxes() method, passing in svgChart as an argument.
let comboChart =
{
  cigData: [],
  chinupData: [],
  maxUnits: 1,
  dataPoints: 1,
  drawCharts: function()
  {
    var svgChart = d3.select("svg");
    svgChart.html("");

    this.drawAxes(svgChart);
  }
}


We'll want to create this method. It has a parameter, chart. In drawCharts(), chart was already passed when drawAxes() was called.
drawCharts: function()
{
  var svgChart = d3.select("svg");
  svgChart.html("");

  this.drawAxes(svgChart);
},
drawAxes: function(chart)
{
    
}


We append one horizontal line and one vertical line. Bearing in mind that we have a 800 by 500 pixel square, and we want a 50 pixel buffer around the perimeter, this is how the x1, x2, y1 and y2 attributes are calculated. We use the CSS classes axesVertical and axesHorizontal.
drawAxes: function(chart)
{
  chart
  .append("line")
  .attr("class", "axesVertical")
  .attr("x1", "50px")
  .attr("y1", "50px")
  .attr("x2", "50px")
  .attr("y2", "450px");

  chart
  .append("line")
  .attr("class", "axesHorizontal")
  .attr("x1", "50px")
  .attr("y1", "450px")
  .attr("x2", "750px")
  .attr("y2", "450px");    

}


These two CSS classes described a thin grey line.
svg
{
  width: 800px;
  height: 500px;
}

h1
{
  font-family: verdana;
  font-size: 16px;
}

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


Here be your starting lines.


We then want to determine the amount of pixels dedicated to each point plotted on the horizontal and vertical axes. Since we have already determined maxUnits (the highest value in the entire dataset) and dataPoints (the total number of data rows in the entire dataset), we can define pxPerUnit as available vertical space (500 - 50 - 50 = 400) divided by maxUnits and pxPerPoint as (800 - 50 - 50 = 700) divided by dataPoints. And since we only want whole numbers, we'll use the floor() method on these results.
drawAxes: function(chart)
{
  chart
  .append("line")
  .attr("class", "axesVertical")
  .attr("x1", "50px")
  .attr("y1", "50px")
  .attr("x2", "50px")
  .attr("y2", "450px");

  chart
  .append("line")
  .attr("class", "axesHorizontal")
  .attr("x1", "50px")
  .attr("y1", "450px")
  .attr("x2", "750px")
  .attr("y2", "450px");

  var pxPerUnit = Math.floor(400 / this.maxUnits);
  var pxPerPoint = Math.floor(700 / this.dataPoints);  
  
}


We then define scale as an empty array. We will populate scale with the y-positions we want on the vertical axis. Remember that we start from 450 and end with 50 because we're implementing a 50 pixel buffer on the maximum heigh of 500 pixels. And we do in in steps of pxPerUnit.
drawAxes: function(chart)
{
  chart
  .append("line")
  .attr("class", "axesVertical")
  .attr("x1", "50px")
  .attr("y1", "50px")
  .attr("x2", "50px")
  .attr("y2", "450px");

  chart
  .append("line")
  .attr("class", "axesHorizontal")
  .attr("x1", "50px")
  .attr("y1", "450px")
  .attr("x2", "750px")
  .attr("y2", "450px");

  var pxPerUnit = Math.floor(400 / this.maxUnits);
  var pxPerPoint = Math.floor(700 / this.dataPoints);

  var scale = [];
  for (var i = 450; i >= 50; i -= pxPerUnit)
  {
    scale.push(i);
  }   
 
}


And then we go through chart to append line tags that are styled using the CSS class scaleTick. These will be about 10 pixels in length. The data used will be the scale array, which we've already populated with the appropriated values.
drawAxes: function(chart)
{
  chart
  .append("line")
  .attr("class", "axesVertical")
  .attr("x1", "50px")
  .attr("y1", "50px")
  .attr("x2", "50px")
  .attr("y2", "450px");

  chart
  .append("line")
  .attr("class", "axesHorizontal")
  .attr("x1", "50px")
  .attr("y1", "450px")
  .attr("x2", "750px")
  .attr("y2", "450px");

  var pxPerUnit = Math.floor(400 / this.maxUnits);
  var pxPerPoint = Math.floor(700 / this.dataPoints);

  var scale = [];
  for (var i = 450; i >= 50; i -= pxPerUnit)
  {
    scale.push(i);
  }

  chart.selectAll("line.scaleTick")
  .data(scale)
  .enter()
  .append("line")
  .attr("class", "scaleTick")
  .attr("x1", "40px")
  .attr("y1", function(d)
  {
    return d + "px";
  })
  .attr("x2", "50px")
  .attr("y2", function(d)
  {
    return d + "px";
  });    

}


We'll add scaleTick to this CSS specification.
.scaleTick, .axesVertical, .axesHorizontal
{
  stroke: rgba(100, 100, 100, 1);
  stroke-width: 1px;
}


And the ticks appear.


Then of course, we're going to need text. We will insert text tags, styled using the scaleText CSS class. The data used will be scale again, and the y attribute will depend on the current element of scale. The text itself, will reflect what the current index of scale is. And this, not-so-coincidentally, will be the value that the specific tick on the axis represents!
drawAxes: function(chart)
{
  chart
  .append("line")
  .attr("class", "axesVertical")
  .attr("x1", "50px")
  .attr("y1", "50px")
  .attr("x2", "50px")
  .attr("y2", "450px");

  chart
  .append("line")
  .attr("class", "axesHorizontal")
  .attr("x1", "50px")
  .attr("y1", "450px")
  .attr("x2", "750px")
  .attr("y2", "450px");

  var pxPerUnit = Math.floor(400 / this.maxUnits);
  var pxPerPoint = Math.floor(700 / this.dataPoints);

  var scale = [];
  for (var i = 450; i >= 50; i -= pxPerUnit)
  {
    scale.push(i);
  }

  chart.selectAll("line.scaleTick")
  .data(scale)
  .enter()
  .append("line")
  .attr("class", "scaleTick")
  .attr("x1", "40px")
  .attr("y1", function(d)
  {
    return d + "px";
  })
  .attr("x2", "50px")
  .attr("y2", function(d)
  {
    return d + "px";
  });
  
  chart.selectAll("text.scaleText")
  .data(scale)
  .enter()
  .append("text")
  .attr("class", "scaleText")
  .attr("x", "30px")
  .attr("y", function(d)
  {
    return d + "px";
  })
  .text(
  function(d, i)
  {
    return i;
  });  
    
}


In the CSS, scaleText is defined like this.
.scaleTick, .axesVertical, .axesHorizontal
{
  stroke: rgba(100, 100, 100, 1);
  stroke-width: 1px;
}

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


Here, the text appears. Note that your maximum value is 10!


Now we want to plot data points along the horizontal axes. To that end, we create the axes array. Then we populate it in a way similar to what we did for scale, instead using 750 and 50 as the end points in the For loop, and pxPerPoint as the decrementor.
drawAxes: function(chart)
{
  chart
  .append("line")
  .attr("class", "axesVertical")
  .attr("x1", "50px")
  .attr("y1", "50px")
  .attr("x2", "50px")
  .attr("y2", "450px");

  chart
  .append("line")
  .attr("class", "axesHorizontal")
  .attr("x1", "50px")
  .attr("y1", "450px")
  .attr("x2", "750px")
  .attr("y2", "450px");

  var pxPerUnit = Math.floor(400 / this.maxUnits);
  var pxPerPoint = Math.floor(700 / this.dataPoints);

  var scale = [];
  for (var i = 450; i >= 50; i -= pxPerUnit)
  {
    scale.push(i);
  }

  chart.selectAll("line.scaleTick")
  .data(scale)
  .enter()
  .append("line")
  .attr("class", "scaleTick")
  .attr("x1", "40px")
  .attr("y1", function(d)
  {
    return d + "px";
  })
  .attr("x2", "50px")
  .attr("y2", function(d)
  {
    return d + "px";
  });
  
  chart.selectAll("text.scaleText")
  .data(scale)
  .enter()
  .append("text")
  .attr("class", "scaleText")
  .attr("x", "30px")
  .attr("y", function(d)
  {
    return d + "px";
  })
  .text(
  function(d, i)
  {
    return i;
  });  

  var axes = [];
  for (var i = 750; i >= 50; i -= pxPerPoint)
  {
    axes.push(i);
  }    

}


Then we insert a bunch of 10 pixel vertical lines, styled using axesTick, into the chart. The data used is axes.
drawAxes: function(chart)
{
  chart
  .append("line")
  .attr("class", "axesVertical")
  .attr("x1", "50px")
  .attr("y1", "50px")
  .attr("x2", "50px")
  .attr("y2", "450px");

  chart
  .append("line")
  .attr("class", "axesHorizontal")
  .attr("x1", "50px")
  .attr("y1", "450px")
  .attr("x2", "750px")
  .attr("y2", "450px");

  var pxPerUnit = Math.floor(400 / this.maxUnits);
  var pxPerPoint = Math.floor(700 / this.dataPoints);

  var scale = [];
  for (var i = 450; i >= 50; i -= pxPerUnit)
  {
    scale.push(i);
  }

  chart.selectAll("line.scaleTick")
  .data(scale)
  .enter()
  .append("line")
  .attr("class", "scaleTick")
  .attr("x1", "40px")
  .attr("y1", function(d)
  {
    return d + "px";
  })
  .attr("x2", "50px")
  .attr("y2", function(d)
  {
    return d + "px";
  });
  
  chart.selectAll("text.scaleText")
  .data(scale)
  .enter()
  .append("text")
  .attr("class", "scaleText")
  .attr("x", "30px")
  .attr("y", function(d)
  {
    return d + "px";
  })
  .text(
  function(d, i)
  {
    return i;
  });  

  var axes = [];
  for (var i = 750; i >= 50; i -= pxPerPoint)
  {
    axes.push(i);
  }

  chart.selectAll("line.axesTick")
  .data(axes)
  .enter()
  .append("line")
  .attr("class", "axesTick")
  .attr("x1", function(d)
  {
    return d + "px";
  })
  .attr("y1", "450px")
  .attr("x2", function(d)
  {
    return d + "px";
  })
  .attr("y2", "460px");   
 
}


We'll add axesTick to this CSS specification.
.scaleTick, .axesTick, .axesVertical, .axesHorizontal
{
  stroke: rgba(100, 100, 100, 1);
  stroke-width: 1px;
}


And the horizontal axis is populated with ticks.


At the end of this method, we will run the methods drawLines() and drawBars(), passing in chart, pxPerUnit and pxPerPoint as arguments.
drawAxes: function(chart)
{
  chart
  .append("line")
  .attr("class", "axesVertical")
  .attr("x1", "50px")
  .attr("y1", "50px")
  .attr("x2", "50px")
  .attr("y2", "450px");

  chart
  .append("line")
  .attr("class", "axesHorizontal")
  .attr("x1", "50px")
  .attr("y1", "450px")
  .attr("x2", "750px")
  .attr("y2", "450px");

  var pxPerUnit = Math.floor(400 / this.maxUnits);
  var pxPerPoint = Math.floor(700 / this.dataPoints);

  var scale = [];
  for (var i = 450; i >= 50; i -= pxPerUnit)
  {
    scale.push(i);
  }

  chart.selectAll("line.scaleTick")
  .data(scale)
  .enter()
  .append("line")
  .attr("class", "scaleTick")
  .attr("x1", "40px")
  .attr("y1", function(d)
  {
    return d + "px";
  })
  .attr("x2", "50px")
  .attr("y2", function(d)
  {
    return d + "px";
  });
  
  chart.selectAll("text.scaleText")
  .data(scale)
  .enter()
  .append("text")
  .attr("class", "scaleText")
  .attr("x", "30px")
  .attr("y", function(d)
  {
    return d + "px";
  })
  .text(
  function(d, i)
  {
    return i;
  });  

  var axes = [];
  for (var i = 750; i >= 50; i -= pxPerPoint)
  {
    axes.push(i);
  }

  chart.selectAll("line.axesTick")
  .data(axes)
  .enter()
  .append("line")
  .attr("class", "axesTick")
  .attr("x1", function(d)
  {
    return d + "px";
  })
  .attr("y1", "450px")
  .attr("x2", function(d)
  {
    return d + "px";
  })
  .attr("y2", "460px");
  
  this.drawBars(chart, pxPerUnit, pxPerPoint);  
  this.drawLines(chart, pxPerUnit, pxPerPoint);  

}


Of course, we will need to create these methods.
  this.drawBars(chart, pxPerUnit, pxPerPoint);  
  this.drawLines(chart, pxPerUnit, pxPerPoint);  
},
drawLines: function(chart, pxPerUnit, pxPerPoint)
{

},
drawBars: function(chart, pxPerUnit, pxPerPoint)
{

}


Let's begin with drawLines(). We want to use the line portion of the combo chart to represent the smoking data. Thus, we declare cigData and set it to the value of the cigData array.
drawLines: function(chart, pxPerUnit, pxPerPoint)
{
  var cigData = this.cigData;
},


Now, in chart, we want to append line tags. cigData is the dataset we will use. The CSS class used for this is lineChart.
drawLines: function(chart, pxPerUnit, pxPerPoint)
{
  var cigData = this.cigData;

  chart.selectAll("line.lineChart")
  .data(cigData)
  .enter()
  .append("line")
  .attr("class", "lineChart");

},


In the CSS, lineChart is a two pixel red line.
.scaleText
{
  font: 8px verdana;
  fill: rgba(100, 100, 100, 1);
  text-anchor: end;
}

.lineChart
{
  stroke: rgba(255, 0, 0, 1);
  stroke-width: 2px;
}


We set x1 like this. First, we define the variable val which is the index of the dataset, i. If it is currently not the first element in the dataset (i.e, i is greater than 0), then we decrement val.
drawLines: function(chart, pxPerUnit, pxPerPoint)
{
  var cigData = this.cigData;

  chart.selectAll("line.lineChart")
  .data(cigData)
  .enter()
  .append("line")
  .attr("class", "lineChart")
  .attr("x1", function(d, i)
  {
   var val = i;
  
   if (i > 0)
   {
    val = val - 1;
   }  
  })
;
},


And then we set x1 to val multiplied by pxPerPoint, plus 50 for the buffer. In effect, if it's the first element in the dataset, x1 does pretty much nothing because it's at 0. If not, it uses the previous position in the dataset.
drawLines: function(chart, pxPerUnit, pxPerPoint)
{
  var cigData = this.cigData;

  chart.selectAll("line.lineChart")
  .data(cigData)
  .enter()
  .append("line")
  .attr("class", "lineChart")
  .attr("x1", function(d, i)
  {
   var val = i;
  
   if (i > 0)
   {
    val = val - 1;
   }
  
   return ((val * pxPerPoint) + 50) + "px";
  });
},


x2, of course, is the current index, i, multipled by pxPerPoint and plus 50.
drawLines: function(chart, pxPerUnit, pxPerPoint)
{
  var cigData = this.cigData;

  chart.selectAll("line.lineChart")
  .data(cigData)
  .enter()
  .append("line")
  .attr("class", "lineChart")
  .attr("x1", function(d, i)
  {
   var val = i;
  
   if (i > 0)
   {
    val = val - 1;
   }
  
   return ((val * pxPerPoint) + 50) + "px";
  })
  .attr("x2", function(d, i)
  {
   return ((i * pxPerPoint) + 50) + "px";
  })
;
},


For y1, we do something similar to what we did for x1. Except that we initially set val to the current value of the dataset instead of the index. And if it's not the first element in the dataset, we set val to the previous value of cigData.
drawLines: function(chart, pxPerUnit, pxPerPoint)
{
  var cigData = this.cigData;

  chart.selectAll("line.lineChart")
  .data(cigData)
  .enter()
  .append("line")
  .attr("class", "lineChart")
  .attr("x1", function(d, i)
  {
   var val = i;
  
   if (i > 0)
   {
    val = val - 1;
   }
  
   return ((val * pxPerPoint) + 50) + "px";
  })
  .attr("x2", function(d, i)
  {
   return ((i * pxPerPoint) + 50) + "px";
  })
  .attr("y1", function(d, i)
  {
   var val = d;
  
   if (i > 0)
   {
    val = cigData[i - 1];
   }  
  })
;
},


And then we set y1 by taking 450, which is position 0 on the vertical axis, and subtracting the product of val and pxPerUnit from it.
drawLines: function(chart, pxPerUnit, pxPerPoint)
{
  var cigData = this.cigData;

  chart.selectAll("line.lineChart")
  .data(cigData)
  .enter()
  .append("line")
  .attr("class", "lineChart")
  .attr("x1", function(d, i)
  {
   var val = i;
  
   if (i > 0)
   {
    val = val - 1;
   }
  
   return ((val * pxPerPoint) + 50) + "px";
  })
  .attr("x2", function(d, i)
  {
   return ((i * pxPerPoint) + 50) + "px";
  })
  .attr("y1", function(d, i)
  {
   var val = d;
  
   if (i > 0)
   {
    val = cigData[i - 1];
   }
  
   return (450 - (val * pxPerUnit)) + "px";
  });
},


And we set y2 similarly, except we just use d in a more straightforward way.
drawLines: function(chart, pxPerUnit, pxPerPoint)
{
  var cigData = this.cigData;

  chart.selectAll("line.lineChart")
  .data(cigData)
  .enter()
  .append("line")
  .attr("class", "lineChart")
  .attr("x1", function(d, i)
  {
   var val = i;
  
   if (i > 0)
   {
    val = val - 1;
   }
  
   return ((val * pxPerPoint) + 50) + "px";
  })
  .attr("x2", function(d, i)
  {
   return ((i * pxPerPoint) + 50) + "px";
  })
  .attr("y1", function(d, i)
  {
   var val = d;
  
   if (i > 0)
   {
    val = cigData[i - 1];
   }
  
   return (450 - (val * pxPerUnit)) + "px";
  })
  .attr("y2", function(d)
  {
   return (450 - (d * pxPerUnit)) + "px";
  })
;
},


You see that red line? That shows me starting the programme at 10 cigarettes a day, then moving my way down to 5 gradually over the next few months, and finally dropping to 0.


The next part, drawing the bar chart portion of the combo chart, is simple by comparison. We begin by declaring chinupData and setting it to the value of the chinupData array.
drawBars: function(chart, pxPerUnit, pxPerPoint)
{
  var chinupData = this.chinupData;
}


In chart, we append rect tags that are styled using the CSS class barChart. The data used for this is chinupData.
drawBars: function(chart, pxPerUnit, pxPerPoint)
{
  var chinupData = this.chinupData;

  chart.selectAll("rect.barChart")
  .data(chinupData)
  .enter()
  .append("rect")
  .attr("class", "barChart");


In the CSS, the barChart CSS class has an orange background.
.lineChart
{
  stroke: rgba(255, 0, 0, 1);
  stroke-width: 2px;
}

.barChart
{
  fill: rgba(255, 200, 100, 1);
  stroke-width: 0px;
}


Let's do the easy part first. Every bar has the same width - pxPerPoint pixels.
drawBars: function(chart, pxPerUnit, pxPerPoint)
{
  var chinupData = this.chinupData;

  chart.selectAll("rect.barChart")
  .data(chinupData)
  .enter()
  .append("rect")
  .attr("class", "barChart")
  .attr("width", pxPerPoint + "px");
}


The height depends on the value of the current element in chinupData, d. We multiply d by pxPerUnit to get the height.
drawBars: function(chart, pxPerUnit, pxPerPoint)
{
  var chinupData = this.chinupData;

  chart.selectAll("rect.barChart")
  .data(chinupData)
  .enter()
  .append("rect")
  .attr("class", "barChart")
  .attr("width", pxPerPoint + "px")
  .attr("height", function(d)
  {
    return (d * pxPerUnit) + "px";
  })
;
}


The x attribute depends on the index, i, of the current element. We multiply it by pxPerPoint and add 50 for the horizontal buffer.
drawBars: function(chart, pxPerUnit, pxPerPoint)
{
  var chinupData = this.chinupData;

  chart.selectAll("rect.barChart")
  .data(chinupData)
  .enter()
  .append("rect")
  .attr("class", "barChart")
  .attr("x", function(d, i)
  {
    return ((i * pxPerPoint) + 50) + "px";
  })

  .attr("width", pxPerPoint + "px")
  .attr("height", function(d)
  {
    return (d * pxPerUnit) + "px";
  });
}


For y, we take 450 (which is position 0 on the vertical axis) and subtract from it the product of d and pxPerUnit. In essence, we subtract the height of the bar from 450 to get the starting y-position of the rect tag.
drawBars: function(chart, pxPerUnit, pxPerPoint)
{
  var chinupData = this.chinupData;

  chart.selectAll("rect.barChart")
  .data(chinupData)
  .enter()
  .append("rect")
  .attr("class", "barChart")
  .attr("x", function(d, i)
  {
    return ((i * pxPerPoint) + 50) + "px";
  })
  .attr("y", function(d)
  {
    return (450 - (d * pxPerUnit)) + "px";
  })

  .attr("width", pxPerPoint + "px")
  .attr("height", function(d)
  {
    return (d * pxPerUnit) + "px";
  });
}


And there's the orange bar chart. You can see where I started off at 1 chinup, then shot up to 3 within a week, and how I gradually worked my way up from there.


There, all done!

This was simple in comparison to all the stuff we've gone through before with D3. That is a deliberate choice on my part - to give you the Combo Chart without throwing too much stuff in.

Also - and yes, I just wanna brag here - I'm three days away from making it an entire year without a cigarette. Go, me!

Cig-nificantly yours,
T___T

Thursday, 12 June 2025

The Sheepshank Analogy

In seamanship, there are various shortening knots, in what I like to call the "shank family". These are used to take up slack in ropes, and only hold up under a certain amount of tension (too little or too much tension, and they collapse). Here are some of them...

The Catshank.




The Dogshank.



And arguably the most famous and basic, the Sheepshank, upon which the previous two were based. I know all this sounds deliciously violent (shanking various animals, really?!) but bear with me, a point is being made here.

The Sheepshank, loosely tied for illustration.

When the knot is tied, the rope is shortened. Once untied, we see that the rope is really a lot longer than we initially gave it credit for. And that leads us to...

Code editors!

Let's just use Sublime Text as an example. Sometimes the code can get really long, but Sublime Text (indeed, many other Code Editors) has this feature where you can visually compress the code.

A HTML example.

Before.

After.


A JavaScript example.

Before.

After.


Kind of like a Sheepshank! The code is still there, but the presentation hides it. And then it can be revealed again, easily.

Final words

I know this doesn't seem like much of an analogy. As analogies go, admittedly, it's pretty tame. But honestly, that was exactly what I was thinking of one day when I was expanding some code in Sublime Text - wow, this is kind of like untying a Sheepshank!

Shanks for reading,
T___T

Monday, 9 June 2025

The Dark Years of COVID-19: A Software Developer's Perspective (Part 3/3)

The latter half of 2022 was when the masks came off. In August, it was announced that masking up was no longer mandatory except in medical facilities and food preparation, and on public transport.

It seemed like a small thing, but wow, the sensation of wind on my face after years of this shit was... incredible.

By February of 2024, masks were no longer required on public transport. There was a real sense that this was the last remnants of COVID-19 restrictions being lifted.

Masking no longer
required on public
transport.

Cinemas were allowed to open to full capacity. Swimming complexes and gyms no longer required users to go through a daily booking process, with penalties for no-shows. People tossed their contact tracing tokens. I kept mine around because it was cute.

Large gatherings were allowed again. With that, employers began pushing for a return to office. People started travelling again - ticket prices stopped being ridiculously expensive.

It felt like Singapore, after years of slumber, was starting to wake up once more.

2024 to now

COVID-19 didn't go away. However, it rapidly began to feel like it had become yesterday's news. COVID-19 was no longer the extinction event it had once threatened to be.

As a nation, we took cautious steps on the road to recovery. Some things were never quite the same again. Some businesses folded even after restrictions were lifted. Professionals pivoted to different careers. There was a limit, after all, to how much Government intervention could accomplish.

Mental health and depression.

And for some reason - perhaps this was just my imagination - the issue of mental health seemed to come to the forefront of societal consciousness.  Either the causes of depression predated COVID-19 and this was merely exacerbated due to social distancing and lockdowns, or the long-term effects of having contracted COVID-19 had some effect on brain chemistry.
Because it wasn't just the medical effects of COVID-19. The damage dealt to the economy was not inconsiderable.

Even now, big tech is in the process of laying off all the tech people they snapped up during the dark years of COVID-19. That bubble has burst. It seems especially bad in Silicon Valley, though Singapore is affected as well due to the simple fact that big tech also has offices in Singapore.

As a software developer, I am just profoundly glad that I never hopped on to that gravy train. Sometimes being completely mediocre is a blessing.

Final thoughts

I've said earlier that Singaporeans generally seemed to be becoming alarmingly stupid during the pandemic. Upon further reflection, the truth may have been far worse.

You see, Singaporeans are generally well-educated. We can speak and even write in a variety of languages. We are capable of rational thought. Therefore I find it hard to believe that the average Singaporean would have difficulty understanding the concepts I outlined earlier. No, what Singaporeans suffer from isn't stupidity; rather, it is Main Character Syndrome. The inability to grasp that there are things bigger than them, and that not everything is about them. The inability to get over themselves.

However, for good or ill, this was what the Singapore Government had to work with. It wasn't all bad. Some of us stepped up - some in small ways like myself, some in more substantive ways. We responded as a society, and while there might have been plenty of grumbling as we did it, it is part and parcel of the Singapore DNA. These were heartening signs in what looked like a sea of negativity. On my part, the crists had awakened some kind of social conscience within myself. I wasn't exactly rushing out to the frontlines to offer my aid. I didn't become a Safe Distancing Ambassador or help deliver supplies. I didn't help swab thousands of people a day to test for COVID-19. And I certainly did not participate in helping to write new and interesting software that could help Singapore tide over this crisis. That all said, I gained a new appreciation for how well-run Singapore generally is, and did my small part as a citizen.
Small corner stores.

I spared a few thoughts for my immediate community rather than focus on my own survival. Any money that the Singapore Government disbursed to citizens as aid during this period, I promptly donated to charity. I took pains to patronize small stores and hawkers, as these were the most vulnerable to changes in the economy.
And, despite the fact that I didn't know for sure that vaccination jabs were absolutely safe, I took them.

What does that have to do with a social conscience? You see, Singapore is in a unique situation. The size of this country, with its lack of manpower and natural resources, means that we must open our borders to foreign investment in order to survive. Had we insisted on staying closed, Singapore would have sunk just as surely as if all of us had been overrun by the coronavirus. Sure, we would have sunk slower, but we would have sunk nonetheless. Singapore required at least eighty percent of her citizens to be vaccinated before we could open our borders. There was a chance I could die if I accepted the jab. I took that chance. I will never be great. I'll never be a Bill Gates or a Steve Jobs. I will never cure cancer, create art that will transcend generations, or otherwise achieve anything monumental. But whatever small thing I could do for my country, I did. The jab was never about saving my statistically insignificant life. At the risk of sounding cliche, it was for the greater good. I do value my life... I'm just not vain enough to think it's more important than the millions of livelihoods at stake.

Epilogue

It's 2025. COVID-19 is a distant memory, but not so distant that the turmoil has not left its mark. Now and again, we see some remnant of the measures that were in place during the pandemic. The posters. The tracing tokens. Hand sanitizer. Abandoned gantries.

Remnants of those
dark years.

It's recent enough that any talk of a possible pandemic is instantly compared to COVID-19. Not the Spanish Flu. Not the Red Death, or the Bubonic Plague.

The world seems to have recovered, but the scars run deep.

COVID-19 was nothing to sneeze at, y'all!
T___T

Saturday, 7 June 2025

The Dark Years of COVID-19: A Software Developer's Perspective (Part 2/3)

Singapore, indeed the entire world, continued grappling with COVID-19 well into the latter half of 2020. By that point, there was a pervasive sense of resignation - that this was to be our new normal. Up to this point, cases in Singapore had remained at a manageable number. The lockdowns had lasted for a while - and mark this well, even though lockdowns were observed in various ways internationally, they were still being observed in some form or other.

A election during
COVID-19.

This was also the year where Singapore held her General Elections during the pandemic. I remember being particularly infuriated by politicians taking the opportunity to grandstand and politicize despite the fact that lives and livelihoods were at stake. That level of selfishness boggled the mind.

Singapore was being lauded by the international community for the way she had handled the pandemic thus far. But I took little pride in it. This was a highly fluid situation, and all that could be flipped on its head in the course of twenty-four hours.

All this was not sustainable. Our economy, reliant on remaining open and accessible to the rest of the world, suffered. Jobs were lost. Hiring was slow except for certain sectors, such as healthcare (big surprise there), tech and logistics.

I, too, lost my job in the month of September. By then, I had known of so many people who had suffered a similar fate, that I thought nothing of it. Even when advised by various people to apply for grants meted out to the unemployed during the pandemic, I consistently declined. Compared to people who had been jobless for months, my own situation did not seem so dire. I would much prefer Government money to go towards more deserving cases. For starters, I actually had money in the bank from years of not spending much. Further, I was from the tech industry. It was booming. With the advent of remote work, it seemed like tech firms could not get enough of developers. I predicted that by the time I got off my ass to fill out the form, I would have landed another job.

And I was proven right - within two weeks of losing my job, I was hired. This was less a testament to my value as an employee, then to the nature of my industry.

2021: Optimistically easing up.

Singapore plodded on, and mandated vaccination jabs while slowly opening up. This caused quite a bit of consternation among the general populace, though older folks were generally happy to trust the Government's direction on this. I had my own reasons for being willing to vaccinate, which I'll get into later.

Professionally, I was working from home the majority of the time. I had settled down into my work and even managed to juggle school on top of it. I was living with the restrictions just fine because they didn't interfere with my lifestyle... much.

But there was one problem - it was getting lonely. Not being much of an extrovert, social distancing was actually a good thing as far as I was concerned. Until it wasn't. As the months wore on, I began to discover that anti-social as I was, even I had my limits.

Isolation.

For that, there was the Clubhouse app. It was novel at the time; a way to connect with random strangers in the rest of the world, get updates and take solace in knowing we weren't alone in our suffering. Of course, like all Social Media, it went to shit pretty quickly because when human beings are involved, toxic behaviors are always par for the course.

That said, Clubhouse did a lot for me. I made friends and eventually visited them. Some of them came over to Singapore to visit, in turn. Listening to real-time conversations was also a good way to familiarize myself with foreign languages. It got me comfortable with public speaking, at the very least. I spent countless nights leaving it on in the background throughout. Sometimes there would be endless heat - shouting matches, drama, that kind of thing - and I would literally sleep through it all, like a baby. In the absence of my wife, this felt like I was in a room full of people.

And then the Missus came back from China, where she had been stuck for the past year. This coincided with the Singapore Government's vaccine mandate, and some of the rules were confusing where foreigners were concerned. I don't really have a problem with the vaccine mandates, but communication was often unclear, contradictory and generally inadequate. Really not what I've come to expect from the civil sector, but these were extraordinary times, I guess.

She caught COVID-19 a few months later, and to my everlasting perplexity, I didn't catch so much as a sniffle despite lying beside her every night for a week. I wasn't dumb enough to believe that I had any sort of superhuman immunity or something; this was just one of those inexplicable things.

2022: Catching COVID-19

I did catch the virus about a year later. My wife had caught it a second time, and this time my luck ran out. It should have been the Omicron variant by then. COVID-19, at this time, was nowhere as deadly as it had been when it first surfaced, and we survived it with nothing more severe than a running nose.

Was it because we were vaccinated by then? That may have played a part, sure.

Taking the jab.

Many people were quick to point out that vaccines aren't a hundred percent effective because people can still catch the disease after being vaccinated. Well of course vaccines aren't a hundred percent effective - nothing ever is. Vaccines weren't meant to prevent people from catching the coronavirus - they were meant to help train the body to fight it, and lower the chances of dying painfully from it.

I could say a lot more, but again, perhaps that's best left for later.

More Thoughts

It may be an oversimplification to say that Singapore emerged relatively unscathed due to good governance. I give our Government their due, certainly. It could not have been easy running a country during such trying times.

Plenty of people worried about dying. Some worried about not dying (at least, not right away) and instead suffering till their last rasping breath. I pretty much accepted this was out of my (frequently-washed) hands and focused on what I could control.

Job losses and the suffering economy were next on the list of the average Singaporean's worries. The Food and Beverage industry seemed to be among the hardest hit (though Food Delivery boomed), as restaurants started encountering Government-mandated limits on restaurant capacity. Many closed down. I made it a point to buy from hawkers as often as I could. It gave me an excuse to go out, and at the same time hopefully contribute to their dwindling coffers. 

Socially, life was a mixed bag. I was mostly hanging out with my tenants but nobody else. My WhatsApp chats were abuzz with activity and plenty of quarrels erupted. With those who were spreading unhelpful vibes and bad advice, I was particularly savage. Normally I'm a live-and-let-live kind of guy, but I needed an outlet and idiots were an irresistable target.

Masks were
controversial.

You see, there were plenty of overnight experts claiming that Government directives such as mask mandates, safe distancing and vaccinations, did not work because despite their existence, people were still catching the virus. The sheer stupidity of statements like these were mind-boggling. Policies aren't like software - they don't work magically on the flip of a switch and results are not a hundred percent guaranteed. Even in the field of medicine, where there are outliers to everything, nothing is absolute. Expecting the number of cases to drop to zero overnight just because the majority of people started wearing masks, was idiocy on a mind-boggling scale.

Sure, the science may not have been entirely correct. But that's always been the nature of science - the knowledge is never a hundred percent. How do people complete a basic education and yet fail to understand this? Bizarre.
 

Next

To today.

Thursday, 5 June 2025

The Dark Years of COVID-19: A Software Developer's Perspective (Part 1/3)

Just last month, I made contact with something that had well and truly left behind: COVID-19. It began with a mild burning sensation behind the eyes, feeling chilly and an ache in the bones. A day or so later, the sore throat came, along with the phlegm and snot. It did not even occur to me to test for COVID-19 until the third day, and even then I had to test twice!

Bummer.

It was relatively mild; not like how it had been the first time the world noticed COVID-19's ugly spiked head. Singapore and some parts of Southeast Asia have been experiencing a resurgence of COVID-19 cases. I just happened to get caught up in it.

This little episode inspired me to recall the turmoil back then, and how the world, Singapore in particular, survived the dark years of the pandemic.

How It Began

The tail end of 2019 marked a significant chapter in human history, a period which many of us lived through in trepidation. COVID-19 made its presence felt as governments around the world imposed strict, sometimes draconian, safety measures on their citizens.

Thus, unless one was living on a desert island somewhere in the middle of the ocean, the effects of COVID-19's presence was felt, at least indirectly, if not on a far more lethal level. Singapore was no exception. Even now in 2025, with the traumatic experiences of that chapter behind us, COVID-19 remains a relevant talking point; if not as a real and present danger to life and well-being, as a cautionary tale.

This was a common sight.

How did we survive this? Honestly, the answers vary from person to person. My COVID-19 experience as a Singaporean will differ in places from the rest of the world. My experiences as a software developer may differ from other Singaporeans. Therefore, I can speak only for myself, and perhaps other Singaporean software developers.

2019 to early 2020: COVID-19 creeps up on the world

When the Coronavirus was first announced in China, self-preservation wasn't the first thing on my mind. After all, the world had dealt with SARS and MERS, and Singapore had barely been touched before the fuss died down. 

No, my concern was as a husband. My newly-wedded wife was in China at the time, and she was stuck in a strict lockdown

Soon, COVID-19 began making its presence felt in Singapore, which had inevitability written all over it. After all, Singapore is an international hub where foreigners land hourly, even if just for transit purposes. Cases began spreading through the island.

Changi Airport was unusually
quiet.

At first, Government handled it well. Perhaps taking our safety for granted, people were still disregarding Government advisories on group gatherings, and not being altogether truthful when they caught COVID-19 and had to be questioned on their activities.

All I could really do at this point, was watch the news and avoid going out too much. Honestly, as a guy who only really hung out at a very limited number of places, this just didn't represent any great hardship for me. I was perfectly content to stay home and watch YouTube content, write my code, and work on my numerous side-projects.

Up to this point, gantries had been set up at office buildings. We all had to have our temperatures checked, and logged. Mask mandates were on. Groups of larger than five, were discouraged.

There was something in the air (other than the obvious!), a vibe of dread mixed with optimism. Things were at s stage where it could get better, or get a whole lot worse. And even if things got better for Singapore, as connected as we were to the rest of the world, it would not do us much good.

Mid-2020: DORSCONs

Singaporeans watched as the case count crept higher despite the best efforts of the COVID Task Force. And soon, Singapore moved to DORSCON Orange.

Things started escalating further with a work-from-home order for non-sessential personnel. As software developers, this described our jobs perfectly. Our jobs were important, but also perfect for remote work. Furthermore, as we were doing work for the Economic Development Board (i.e, doing work for the Singapore Government) and the Singapore Government needed to lead by example, the software developer team would be relegated to our own homes, with office equipment. Meetings would be held via video call.

Many of my colleagues liked the idea of working from home. My prior experience with it wasn't so great; however, at that time, I had already secured a new job and was counting down the days.

Around this time was when an alarming percentage of the population began to go full retard lose their collective marbles. There were people crowding the supermarkets for one last supply buying spree. The most popular item? Toilet paper. I shit you not. Pun fully intended.

This was really popular in
those days, for some odd
reason.

People were finding all sorts of excuses to go out and hang out, never mind that this was what got us into this mess in the first place. Queues for things like bubble tea and haircuts became the in-thing as businesses were given a deadline to temporarily shut down. Anti-foreigner sentiment was at an all-time high, despite the fact that Singapore was largely built on the backs of foreign worker labor. For real, at this point I was starting to wonder if Singaporeans had only recently become this stupid. Then again, the world had never encountered anything on this scale; perhaps I could have extended a little understanding.

In the larger picture, it wasn't exactly only Singaporeans that were reacting due to panic. Small consolation there, but for a while it did seem like the world had gone mad and there was no going back.

There was no end in sight once the restrictions took hold. Gantries were now a fact of life in all major air-conditioned buildings such as shopping malls, not just office buildings. Contact tracing tokens were issued to everyone on the island, where they were used against scanners built into the gantries. Even individual shops (and taxis!) had QR codes for customers to scan in and out when they were done! This only made me more determined not to go out.

Sporting facilities such as swimming complexes were off-limits. That put a huge crimp in my workout schedule, and now I was forced to actually go out and run instead of swim. Put some stress on these aging knees.

Software development, as an industry, had already been constantly hiring up to then; now it positively erupted as big tech began recruiting with an aggression not previously seen. It appeared that since remote work was now a thing, hiring software developers had just become easier. And with no end in sight of remote work, more software products were being pushed that would ride this wave.

Some thoughts

Some say there are opportunities within a crisis. I've also heard people claim that the Chinese words for "danger" and "opportunity" are the same... and unsurprisingly, the people peddling this garbage don't actually know any Chinese.

But yes, the general principle stands. There were lessons to be learned in the midst of a crisis such as the COVID-19 pandemic.

The necessity of limiting physical contact as much as possible meant that Government agencies had to modernize their services, updating them to match the Internet age. Now instead of having to show up in-person to apply for new passports, the ICA implemented a process where the documents could be mailed to addresses instead. Instead of a similarly clunky process in CPF nomination, for example, where the supplicant had to fill up a paper form and have two people physically present to witness, now applicants and their witnesses can log on to the CPF portal to participate in the processes.

Meetings were
conducted like this.

Companies learned to cope with remote working, once considered unthinkable. Grudgingly, almost certainly, but they did it. And even though many wasted precious little time reverting to in-office work years later, the fact remains that they did it, and it worked.

What I'm trying to say is that COVID-19 forced us to up our game on many levels. We ventured out of our comfort zones. Instead of adopting the good old "if it ain't broke, don't fix it" cop-out, we came to terms with the fact that during COVID-19, a lot of it was, in fact, broken and needed to be fixed. Pronto.

Next

A look at the latter half of 2020 and the aftermath.