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