Sunday, 29 June 2025

Film Review: M3GAN 2.0

We all knew a sequel was coming for the hit slasher M3GAN back in 2022, and here it is - the suitably titled M3GAN 2.0. It's a sequel in all but genre, as I'm about to explain shortly.


For some background, M3GAN was about a robot doll who interpreted its programming a little (OK, a lot) too literally and ended up killing people in very gruesome ways. At the end of the movie, she was destroyed, but her digital consciousness escaped into the ether. I mean, this is the Internet age, amirite?

Suffice to say, the titular robot returns in this sequel, but how does it stack up against the original? Let's find out.

Warning - bitchy murderbots ahead!

And also spoilers. But yeah, the killer robots are the meat of this movie - and with them, violence and profanity.

The Premise

M3GAN, the homicidal robot doll, has resurfaced in this sequel. In an obvious nod to the classic Terminator 2: Judgement Day, the former villain of the previous movie comes back as an ally to the protagonists of the sequel, defending them against another homicidal robot doll. What ensues is a wacky action movie and good popcorn fun.

But anyone hoping for a creepy horror-fest like the last outing, is going to be sorely disappointed. Right from the get-go, the movie wastes no time establishing that it is now more of a guns-ablazing action comedy. Still campy, snarky fun, but with a 180-degree twist.

The Characters

Allison Williams returns as Gemma. I was happy to see her back, if just for the continuity. I found her bland and uninspiring, even more so than in the last movie, if that were even possible. Which is a tragedy because I think Williams can actually act.

Violet McGraw also returns as Cady. She's grown up, and now she knows Aikido. I liked her in the original, and I like her here. Now she's moved on beyond being an orphan and has become the typical angsty teenager. The character has more to do beyond moping and grieving, and even helps move the plot along.

Jenna Davis and Amie Donald are, once again, the voice and body of M3GAN, respectively. Except that Amie Donald has now grown several inches, so they make M3GAN demand to be made "taller". It's hilarious if you watched the original and know the background of the joke. Jenna Davis seems to be having a blast delivering M3GAN's biting wit and deadpan sarcasm.

Ivanna Sakhno as AMELIA, acronym for Autonomous Military Engagement Logistics and Infiltration Android, the other murderous robot doll. She brings heavy vibes of Kristianna Lokken from Terminator 3: Rise of the Machines, in demeanor. She does OK, but to be fair, all she really has do do in terms of personality is... actually, nothing. They could have just put a CGI face on AMELIA and it wouldn't have changed much. I mean, her role here is purely physical anyway, right?

Jermaine Clement has the role of Alton Appleton, a disabled tech millionaire who comes off as a elitist sleazeball. He delivered some great ableist dad jokes ("a man of my standing", heh heh). I was sad that they killed him off so soon.

Timm Sharp is FBI agent Tim Sattler, a douchebag who learns the hard way that he's in over his head. The script went out of its way to paint him as incompetent, self-important and just not at all likeable. Perhaps just to give the audience something to cheer about when he inevitably got his ass handed to him.

Aristotle Athari as Christian Bradley, Gemma's's milquetoast boyfriend who - surprise, surprise - turns out to be the big bad. Could they have found someone whose face screamed I'm the bad guy any louder? Somehow I doubt it.

Jen Van Epps plays Tess once more. She has even less to do than the last time, and I think it's a damn shame because she's so watchable.

Brian Jordan Alvarez comes back as Cole, and he has significantly more to contribute this time, with some physical presence in the plot. He remains largely part of the background, though.

Amy Usherwood as Lydia, the therapist. It's good that she was included for the sake of continuity. It's even better that her one scene was kept mercifully short, because she really got on my nerves the last time round.

The Mood

M3GAN was dark and sinister. In a departure from its tech slasher horror roots, M3GAN 2.0 is now a tech action thriller. It begins with a scene in the Middle East that wouldn't be out of place in a James Bond movie, and then continues gleefully in that same vein throughout the movie. It's loud, colorful and vulgar, and M3GAN is its potty-mouthed avatar.

What I liked

M3GAN is a delightful shit-talker. If there's any part of this sequel that reminded me of why I enjoyed its predecessor, it has to be this. M3GAN delivers absolute zingers, decimating egos and self-esteem with measured vitriol. She's no longer creepy and eerily calm and seems stuck in the savage verbal mode she was in at the tail end of the last movie, but I liked it. This movie really missed the presence of Ronny Chieng, whose character died in the last movie. It feels like M3GAN inherited his lines.

This line, for instance.
Hold on to your vaginas!

The jokes landed, for the most part. The scenes of M3GAN's exasperation at getting stuck in the body of a Teletubby doll, were hilarious. M3GAN is getting increasingly humanized in this one, and I am here for it! There's also this part where AMELIA snaps a guy's neck and the camera immediately cuts to Breaking News on the TV. Absolutely hysterical.

AMELIA was fun to watch. I loved the scenes of her crawling down a pipe in the background, creeping out of a suitcase, and her murder sprees. Unlike most action flicks where you see a tiny girl beat up a team of trained henchmen twice her size, I didn't have to strain too hard to justify the sight to myself. We are talking about a robot with a metal skeleton here.

The previous Aesop is still in force - parents should be wary of leaving the parenting to tech devices. Though, this time, they've expanded it to responsible usage of A.I, or something to that effect, instead of summarily dismissing A.I as "good" or "bad". Interesting, but maybe a movie as obviously ridiculous as M3GAN 2.0 wasn't the best medium for it?

What I didn't

M3GAN broke out into song just like the last time, but this time I wasn't feeling it. This just felt like a waste of time.

This movie went on for two hours. Was there really that much content? The plot was way too convoluted, what with The Motherboard and shit. Trimming all this fat from the movie would have resulted in a leaner one with more time to spend on stuff that really mattered. Speaking of superfluous things, the gag of how Christian's name is pronounced, was silly, lame, and got too much air-time. Leaving it out altogether would have done this movie good.

I'm on the fence about the genre shift. It's always a huge gamble. Films like Happy Death Day and its sequel Happy Death Day 2U managed to make the switch from slasher to another genre (in this case, science-fiction) and not suck too much, but it's unreasonable to expect the same (questionable) success every time.

As clever as the movie tried to be with its plot twists, it was just as predictable as the last one. Lucky no one watches these movies for cleverness. Stick to your strengths, eh?

Conclusion

This was OK-ish. M3GAN 2.0 was a fun ride for the most part. But I will confess to feeling let down, because what originally attracted me to M3GAN was the slasher concept. As a consequence, I did not enjoy myself as much as I did watching the original.

Was it worth the watch though? Very much so, especially if you watched the original, and kept your expectations realistic. Exceedingly rare is the movie sequel that surpasses its predecessor. M3GAN 2.0 is not in the hallowed company of films like Terminator 2: Judgement Day and John Wick: Chapter 4, and no one should expect it to be.

I did want a sequel to M3GAN. I'm just not sure if I wanted this sequel.

My Rating

6.5 / 10

This was kind of m3h,
T___T

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.