Thursday, 4 December 2025

Film Review: War of the Worlds (2025)

Following in the footsteps of screen marvels like Unfriended, Searching and Missing, comes a new tech thriller in the same vein. Well, "new" might be stretching the truth a wee bit. War of the Worlds was produced back in 2020 during the COVID-19 pandemic, and it must have seemed like a good idea at the time, what with people being made to stay at home and all. A movie that requires very little face-to-face interaction between its actors? Holy guacamole pandemic loopholes, Batman!


Except that sadly, while the idea was decent, execution was found rather wanting. I watched this movie out of sheer obligation rather than because I truly wanted to, and this was made more apparent the more it went on.

This movie is in screenlife format, where the entire story takes place on TV monitors and computer screens. I kept calling it the "found footage format" in the past, but I guess this has kind of evolved into its own sub-genre by this point.

Warning - spoilers, I guess...

I don't even know why I bother telling you this. There are no surprises here (barring how dumb this movie is) that me saying it out here would even count as much of a spoiler. Still, you've been forewarned, if not forearmed.

The Premise

William Radford is a Government tech agent (with high security clearance and all) who is in his office when an alien invasion begins. What ensues is a series of catastrophic events as he shuttles back and forth between his job, helping to repel the invasion and keeping his family safe... all from behind the keyboard and monitor.

The Characters

Ice Cube as William Radford, a techie employed in a Government tech surveillance program. It's almost impossible to overstate how much I love watching Ice Cube on screen. Next to Samuel L Jackson and Denzel Washington, he might just be the coolest black actor ever. I'm biased, of course. From XXX: State of the Union to 22 Jump Street, his gruff angry black man persona is a joy to watch. However, even Ice Cube has his limits and his inclusion in this movie exposed them cruelly. The screenlife format of this movie basically means that for a large part of the show, we're reduced to watching Ice Cube emote close up... basically underreacting and overreacting to everything. Not his best role, that's for sure.

Eva Longoria appears in the movie as Dr Sandra Salas, who seems to be pretty useless, all things considered. She's just here to deliver footage and exposition to Radford (and us, by extension) and does nothing to make us care for her character in the slightest. Honestly, I didn't even remember the character's name, that's how little she mattered to the plot. Eva Longoria has had a long and storied career, and let's just say that this role is not something she will be remembered fondly for, if at all.

Henry Hunter Hall as Radford's son David, who's apparently a game reviewer. Radford thinks his son is a huge disappointment and loser... until he discovers that he's actually the hacker known as The Disruptor. Hall is not very interesting in that role, I gotta say. My reaction to the reveal was an overwhelming "meh".

Iman Benson as daughter Faith. Happens to be a biologist who somehow discovers how to synthesize a virus to kill off the aliens. She's pregnant during the events of this film and mostly spends her time either looking terrified or constipated. I don't know if it's Benson's acting or just the shitty writing (maybe both?) but she just doesn't come across as a particularly competent scientist... at least not at the level that would result in producing a virus that would take down the aliens.

Devon Bostick as son-in-law Mark Goodman, whom Radford might consider an even bigger loser than his son. He's an Amazon delivery guy, but even he has a role to play in defeating the aliens, though it's possible just to add some product placement since it is being featured on Amazon Prime.

Clark Gregg is the unscrupulous Director Donald Briggs, whose insane plan to spy on every living person on Earth is the cause of all this mess. It's nice to see Gregg again after his stint in the Marvel Cinemetic Universe, but at the same time really sad to see that he's reduced to this.

Michael O'Neill makes a couple appearances as US Secretary of Defense. A very one-note role.

Andrea Savage is FBI Agent Sheila Jeffries, a role so colorless I mistook her for Eva Longoria's character a few times.

The Mood

They try hard to convey the end-of-the-world high-stakes terror of an alien invasion using the screenlife format. And it falls flat a lot. Even when Ice Cube swears right at the screen, it comes across as hilarious rather than scary. The alien robots are meant to inspire dread, but the whole effect is cheesy. I get what they were trying to do, but it just fell flat.

What I liked

The whole bio-mechanical nature of the aliens is kind of cool. Tentacles within tripod-shaped domes? Tiny bugs that are equal parts blood and silicon chips? Corny but also intriguing.

The whole thing about the homeless guy accepting the bribe of an Amazon Gift Card was hysterical. It was so stupid (and shameless) I couldn't help but laugh. Kudos!

The biblical poetry of a hacker named David fighting a Government surveillance program named Goliath, is divine.

What I didn't

In general, the plot is just silly. I'll elaborate why as we go along.

Radford types a lot of textspeak, but his textspeak doesn't make a lick of sense. He types things like "Where are U" which is just weird. I can understand, even though I loathe it, the propensity to substitute "U" for "you". What I don't understand is, if you're going to be lazy enough to do that, why would you make the effort to properly captalize the "w" in "where"? While we're at it, why would you capitalize the "u"? Actually, why not just type "where r u"? Indeed, why not "wru"?

The aliens invade Earth because they want to eat our data. Read that again, until the sheer stupidity of that statement sinks in. Data is basically just 1s and 0s. The image of a cat is data. The recording of a song is data. A hastily-typed text message is also data. A YouTube video that's been corrupted, is still data. A random sequence of 1s and 0s? You guessed it, data. If the aliens had the tech to invade Earth, they definitely had the tech to create whatever data they needed, for consumption, rather than go to the trouble of invading Earth for it. I feel like whoever wrote this story doesn't know a whole lot about tech, and just thinks it's some kind of witchcraft.

The tentacles that make an appearance when the aliens attack, are goofy AF.

Faith was injured due to being stabbed by a piece of debris. Later on, she's seen removing it in order to "stop the bleeding". Wait, what? I could understand that if Faith Radford was supposed to be a clueless layperson. But a professional biologist?! This is some Grade A shit writing right there.

Amazon really got their claws in where product placement was concerned. Not only was one of the characters an Amazon employee, the script also had an extended sequence of Radford placing an order and having it shipped by drone. So cringe, OMG.

Conclusion

Was this movie a huge disappointment? Not really; that would have required that I had sizeable expectations for it. The fact that it disappointed me even a bit considering how forgiving as an audience I am, is testament to how little value it has. This is probably the most useless movie of 2025... and I say that both as an Ice Cube fan and someone who absolutely loves the screenlife format.

My Rating

2.5 / 10

This movie alienated me!
T___T

Friday, 28 November 2025

Tech skillsets: Go deep or go wide? (Part 2/2)

There are plenty of reasons, though, to choose to generalize rather than specialize. And by that, I mean good reasons, not things like a lack of focus or a limited attention span.

The case for generalizing over specializing

It's dangerous to put all your eggs in one basket. An extreme example is if programmers specialized in VBScript to the exclusion of all else. The probability of those programmers having a job as of 2023, are approaching zero. I love VBScript, but it is what it is. Professionally, I cut my teeth on ASP and VBScript back in the 2000s. But what would have happened had I stubbornly refused to branch out? The thought makes me shudder.

Speaking of branching out, what makes it relatively easy for me to pick up new programming languages and frameworks is that I've been doing it repeatedly over the years. Instead of focusing on one specific thing to get good at, I identified certain things one new area had with the one I already knew, and worked from there. Got my fingers in as many related pies as possible. And as I learned more new stuff, my frame of reference only got stronger. All the stuff I learned, fed into each other. It's unclear just how good I would be at picking up new stuff if I had specialized instead.

Fingers in many pies.

The other advantage being a generalist gave me, was that I got plenty of attention from cheapskate financially strapped employers who saw the wisdom of getting me to do many different things at once, because I was decent at all those things. And often, for small startups, that's just what they need. The end result was that even in poor economic times, I rarely had trouble finding employment. I did not have to apply only for front-end jobs, or back-end jobs. I could apply to all of them.

Back in the day, I had been working for a law firm and maintaining their corporate website as one of my many duties. When I eventually left for greener pastures, the guy they hired to replace me was a more accomplished network infrastructure guy. More specialized, I would say. I went on to ply my trade as a web developer, but occasionally I would check back on my former company's corporate website to see what was shaking. Imagine my surprise one day when I went to the ABOUT US page where the hotshot attorneys were presented, and found a whole bunch of distorted profile photos! And that was after waiting a good couple minutes for all these images to load.

Apparently my replacement not only did not understand optimizing images for the internet, he also did not know about resizing images to fit the HTML img tag placeholders. He had simply taken the photographs he had been presented with, and used them as-is.

Was that his fault? No, not at all. My then-Manager had specifically wanted a network specialist, as opposed to me, a web developer who could follow simple network instructions. And his wish had been granted. But specialization comes at a cost. Things like image optimization for the web, just weren't covered in the guy's extensive networking knowledge. In the job, not only had I needed to help with network and infrastructure, I'd had to develop web applications and maintain websites, and my skillset had grown accordingly. These were the things the company had learned to take for granted where my position was concerned.

Ultimately...

Let me just say it again - the industry needs both specialists and generalists. Without generalists, specialists would have trouble plugging all the other technical gaps that their very specific skillsets do not cover. Hiring specialists for every area just isn't practical. On the other hand, without specialists, it would be devilishly hard for just generalists to raise the quality of the product to its optimal level.

Most of all, it would be really difficult to be a specialist in a world without generalists. And vice versa. Not only are both needed, the two need each other.

Specially for you,
T___T

Tuesday, 25 November 2025

Tech skillsets: Go deep or go wide? (Part 1/2)

"Jack of all trades, master of none" is a term I've heard all too often when fellow techies describe my stint as a web developer. Back then, I was a generalist dipping my sticky fingers into every new and shiny tech I encountered. In a sense, I'm still that web developer. Just with more money.

Go deep or go wide?

I want to explore the pros and cons of generalizing as opposed to specializing, especially in the context of today's tech landscape. The industry needs both specialists and generalists. That's a simple fact that shouldn't need some nobody tech blogger pointing out, but sometimes people get overly emotional about defending their stance. Want to specialize? Cool, do that. Want to generalize? Also cool, go crazy. But we need to be cognizant of the tradeoffs.

The case for specializing over generalizing

Before every tech and his dog started calling themselves "full-stack developers", specialists seemed to earn a whole lot of money. That was back when I was reading job ads asking for "deep expertise" and extensive work experience in more narrow scopes such as back-end programming and database administration. Some areas, such as COBOL programming, pay a lot for the simple reason that COBOL is still largely in use but COBOL programmers appear to be a dying breed. I've touched a lot of programming languages, but just enough to qualify as a hobbyist in each one. I have a decent frame for comparison between them - a Python lecturer of mine was actually quite entertained at my comparisons of the language to PHP and Ruby - but that often isn't very useful in the professional sense.

Specialists earn big.

The companies whose budgets are large enough to afford to pay for specialists, also tend to use these specialists for projects that are grand in scale. Projects that actually require deep expertise. Projects that add incalculable value to one's CV.

Deep knowledge is also useful for judging the extent of one's resolve and commitment. After all, it takes plenty of both to spend the effort. I personally know someone who specializes in HTML and CSS. It's a very narrow niche and perhaps not very profitable, but undeniably impressive.

Generalists - quite unfairly, I might add - seem to have developed a reputation for being easily distracted. Lacking the necessary discipline and focus required to specialize. I would actually say it takes an extraordinary amount of discipline to avoid going too deeply down one rabbit hoole and shuting out everything else, but that's not the general sentiment. Therefore, in some circles, generalists are seen as those who were unable to specialize as opposed to specialists being unable to generalize.

The conventional advice when it comes to tech specialization, is to specialize in one area while simultaneously being decent in a few related areas. For instance, if you're a Data Analytics guru, you may want to specialize in statistical analysis and mathematics, but at the same time get reasonably good with Data Visualization tools and learn a bit of Python. If you're a primarily a Front-end Developer, learning some simple database concepts would be a useful addition to HTML, CSS and JavaScript. Maybe pick up a few frameworks such as ReactJS or VueJS, just to round things out.

I won't argue against that advice, but these days it feels like nothing is ever enough.

Next

We explore the opposite case!

Wednesday, 19 November 2025

Web Tutorial: Color Troll Meme

Time for a little fun. There's a meme I encountered years back, where it claims that certain illnesses affect the eyes, and there's a test for it. If you can see what you're reading, you probably don't have that condition. So you'll be tested for serious stuff like "tuberculosis", "stress" or what-have-you, and then there will be one you can't quite make out. When you finally do, it says something embarrassing like "erectile dysfunction".

Something like this!

So yeah, let's do something like that today. Totally randomize the text and colors. We'll use VueJS for this because... why the hell not, eh?

We're going to have some boilerplate HTML to start with. We have a div with the id ctmApp that will be Vue's placeholder. There's also a script remote link to VueJS.
<!DOCTYPE html>
<html>
  <head>
    <title>Color Troll Meme</title>

    <style>

    </style>
  </head>

  <body>
    <div id="ctmApp">

    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.18/vue.min.js"></script>

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


Inside the script tag, We declare app as a call to Vue(). In it, we pass an object. In the classic VueJS pattern, the object will have el, data, methods and createdel will be set to ctmApp.
<script>
var app = new Vue
(
  {
    el: "#ctmApp",
    data:
    {

    },
    methods:
    {

    },
    created: function()
    {
   
    }
  }
);
</script>


In data, we want two arrays - ctms, inUse and the object wordTypes.
data:
{
  ctms:
  [
                          
  ],
  inUse:
  [

  ],
  wordTypes:
  {  

  }

},


ctms is an array of objects. You can have as many as you want, though I'd go with 9. Each object has name, color and textColor properties.
data:
{
  ctms:
  [
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    }    
                        
  ],
  inUse:
  [

  ],
  wordTypes:
  {  

  }
},


inUse is a placeholder array which defaults to empty. We won't need to worry about it yet. wordTypes is an objects that has area, condition, condition_prefix and condition_suffix arrays as properties. These arrays contain strings.
data:
{
  ctms:
  [
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    }                            
  ],
  inUse:
  [

  ],
  wordTypes:
  {  
    area:
    [

    ],
    condition:
    [

    ],
    condition_prefix:
    [

    ],
    condition_suffix:
    [

    ]

  }
},


I've filled these in. area is supposed to be embarrassing areas of the human body to develop medical conditions in. condition is an array of medical conditions that range from mild to nasty. condition_prefix and condition_suffix are descriptors that are supposed to go with the words in area.
data:
{
  ctms:
  [
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    },
    {
      name: "",
      color: "",
      textColor: ""
    }                            
  ],
  inUse:
  [

  ],
  wordTypes:
  {  
    area:
    [
      "testicle",
      "anus",
      "vagina",
      "nipple",
      "groin",
      "penis",
      "labia",
      "cleavage",
      "rectum"

    ],
    condition:
    [
      "dehydration",
      "constipation",
      "arthritis",
      "bronchitis",
      "myopia",
      "baldness",
      "pinkeye",
      "narcolepsy",
      "epilepsy",
      "insomnia",
      "dementia",
      "psychosis",
      "indigestion",
      "heartburn"

    ],
    condition_prefix:
    [
      "detached",
      "bruised",
      "inverted",
      "ingrown",
      "misaligned",
      "ruptured"

    ],
    condition_suffix:
    [
      "lacerations",
      "tumor",
      "ulcers",
      "blisters",
      "blockage",
      "fracture",
      "ulcers",
      "inflammation",
      "necrosis",
      "discharge",
      "dysfunction",
      "fungus"

    ]
  }
},


For the methods object, we will put in five methods - fillInfo(), getColor(), getCondition(), getOne() and getRandomNo(). In the created method, we will run the fillInfo() method.
methods:
{
  fillInfo: function()
  {
                          
  },
  getColor: function()
  {

  },
  getCondition: function()
  {

  },
  getOne: function()
  {

  },
  getRandomNo:function()
  {

  }

},
created: function()
{
  this.fillInfo();
}


In the HTML, add this in the ctmApp div. We use v-for to dictate that we want a div for each element in the ctms array, styled using the CSS class ctm.
<div id="ctmApp">
  <div class="ctm" v-for="ctm in ctms">

  </div>

</div>


In the CSS, add the CSS class for ctm. It's basically a deep grey circle.
<style>
  .ctm
  {
    width: 300px;
    height: 300px;
    float: left; 
    margin-left: 10%;
    margin-top: 10%;
    border-radius: 50%;
    background-color: rgb(10, 10, 10);
  }

</style>


You'll see 9 deep grey circles. Because I put 9 elements inside the ctms array. Depending on your screen size, you may need to scroll.


Let's work on the methods now. This is getRandomNo(). Give it a parameter, max. It should return a random number from 0 to max minus 1.
getRandomNo:function(max)
{
  return Math.floor(Math.random() * max);
}


For getOne(), we have the parameter, arr, which is an array. We return one of the elements of arr.
getOne: function(arr)
{
  return arr[];
},


Which one? Well, we let getRandomNo() decide.
getOne: function(arr)
{
  return arr[this.getRandomNo()];
},


But of course, we need to pass in the size of arr so that the random number generated is not out of bounds for arr.
getOne: function(arr)
{
  return arr[this.getRandomNo(arr.length)];
},


For getColor(), there is the parameter maxRGB. maxRGB is used to restrict how bright each color component is. In here, we declare r, g and b, and each one is set by using getRandomNo() to grab any number from 0 to maxRGB minus 1.
getColor: function(maxRGB)
{
  var r = this.getRandomNo(maxRGB);
  var b = this.getRandomNo(maxRGB);
  var g = this.getRandomNo(maxRGB);

},


Then we return an array containing these three values.
getColor: function(maxRGB)
{
  var r = this.getRandomNo(maxRGB);
  var b = this.getRandomNo(maxRGB);
  var g = this.getRandomNo(maxRGB);
  return [r, g, b];
},


And for the last small method, we have getCondition(). For this, we have the parameters area and prefix. Here, we assume that area has already been taken from the area array, and prefix is a Boolean that is true by default.
getCondition: function(area, prefix = true)
{

},


We declare cond as an empty string, then use an If block on prefix.
getCondition: function(area, prefix = true)
{
  var cond = "";

  if (prefix)
  {

  }
  else
  {

  }

},


If prefix is true, we set cond to a random element in condition_prefix using getOne(), and return a string of cond and area. If not, we set cond to a random element in condition_suffix, and return a string of area and cond.
getCondition: function(area, prefix = true)
{
  var cond = "";

  if (prefix)
  {
    cond = this.getOne(this.wordTypes.condition_prefix);
    return cond + " " + area;

  }
  else
  {
    cond = this.getOne(this.wordTypes.condition_suffix);
    return area + " " + cond;

  }
},


Now, time to work on fillInfo()! It will use all of the methods we've worked on up to now, either directly or otherwise. We begin by iterating through the ctms array.
fillInfo: function()
{
  this.ctms.forEach(
    (ctm, index) =>
    {

    }
  );  
                          
},


We declare isLast as a Boolean. If the current value of index is exactly the length of ctms minus 1, that means it's the last element. Then we have an If block based on isLast.
fillInfo: function()
{
  this.ctms.forEach(
    (ctm, index) =>
    {
      var isLast = (index === this.ctms.length - 1);

      if (isLast)
      {

      }
      else
      {

      }

    }
  );                            
},


We'll leave the first case along for now. For all other ctm objects in the ctms array, we set the name property to a random string from the condition array.
fillInfo: function()
{
  this.ctms.forEach(
    (ctm, index) =>
    {
      var isLast = (index === this.ctms.length - 1);

      if (isLast)
      {

      }
      else
      {
        ctm.name = this.getOne(this.wordTypes.condition);
      }
    }
  );                            
},


Now, here in the HTML, we have a h1 tag with the value of name.
<div class="ctm" v-for="ctm in ctms">
  <h1>{{ ctm.name }}</h1>
</div>


In the HTML, let's just set the h1 tag to have a certain size, center it, and so on. Most importantly, we set the color to a light grey so we can see the contrast.
<style>
  .ctm
  {
    width: 300px;
    height: 300px;
    float: left;
    margin-left: 10%;
    margin-top: 10%;
    border-radius: 50%;
    background-color: rgb(10, 10, 10);
  }

  .ctm h1
  {
    margin-top: 40%;
    font-size: 30px;
    text-align: center;
    color: rgb(100, 100, 100);
  }

</style>


Look at this! Conditions are populated, but "pinkeye" is repeated.


To fix this, check if the value of name is in the inUse array. And keep reassigning the name property until you get a value that isn't in the inUse array.
if (isLast)
{

}
else
{
  ctm.name = this.getOne(this.wordTypes.condition);

  while (this.inUse.indexOf(ctm.name) > -1)
  {
    ctm.name = this.getOne(this.wordTypes.condition);
  }

}


Once that's done, push the value into the inUse array, so that this value can be one of the values compared against further down.
if (isLast)
{

}
else
{
  ctm.name = this.getOne(this.wordTypes.condition);

  while (this.inUse.indexOf(ctm.name) > -1)
  {
    ctm.name = this.getOne(this.wordTypes.condition);
  }
  
  this.inUse.push(ctm.name);
}


Now you'll see no repeats! Bear in mind that this only works as long as you have less elements in the ctms array (OK, not counting the final element) than there are elements in the condition array. Otherwise you're going to get an infinite loop.


Now to handle the condition for the last element in ctms. Declare prefix as a Boolean, and use getRandomNo() with a argument of 2, to get a random number between 0 and 1. Then declare area, and use getOne() to obtain a random value from the area array, as its value.
if (isLast)
{
  var prefix = (this.getRandomNo(2) === 0);
  var area = this.getOne(this.wordTypes.area);

}
else
{
  ctm.name = this.getOne(this.wordTypes.condition);

  while (this.inUse.indexOf(ctm.name) > -1)
  {
    ctm.name = this.getOne(this.wordTypes.condition);
  }
  
  this.inUse.push(ctm.name);
}


Then set the name property by using getCondition() and passing in area and prefix as arguments.
if (isLast)
{
  var prefix = (this.getRandomNo(2) === 0);
  var area = this.getOne(this.wordTypes.area);
  ctm.name = this.getCondition(area, prefix);
}
else
{
  ctm.name = this.getOne(this.wordTypes.condition);

  while (this.inUse.indexOf(ctm.name) > -1)
  {
    ctm.name = this.getOne(this.wordTypes.condition);
  }
  
  this.inUse.push(ctm.name);
}


See? The very last circle at the bottom right now reads "misaligned rectum"!


Let's handle colors now. After the If block, declare colorArr as the array returned from calling the getColor() method. Pass in 100 as an argument so that the RGB values won't be greater than 100. That means it'll be a somewhat dark color. After that, formulate a CSS rgb() string based on colorArr's elements, and assign the value to the color property of the current element of ctms.
if (isLast)
{
  var prefix = (this.getRandomNo(2) === 0);
  var area = this.getOne(this.wordTypes.area);
  ctm.name = this.getCondition(area, prefix);
}
else
{
  ctm.name = this.getOne(this.wordTypes.condition);

  while (this.inUse.indexOf(ctm.name) > -1)
  {
    ctm.name = this.getOne(this.wordTypes.condition);
  }
  
  this.inUse.push(ctm.name);
}

var colorArr = this.getColor(100);
ctm.color = "rgb(" + colorArr[0] + "," + colorArr[1] + "," + colorArr[2] + ")";


Bake this value into the div's style attribute.
<div class="ctm" v-for="ctm in ctms" style="background-color:{{ ctm.color }}">
  <h1>{{ ctm.name }}</h1>
</div>


You can see that now the circles are colored different. Each one is a different color.


Now for the text! Each circle's text should have the same color as its div, but several shades brighter. Remember we capped the RGB values at 100 earlier? That was deliberate so we could have a buffer between 100 and 255, the actual maximim RGB value. Now declare opacity and set it to 1. Then set the textColor property to what we previously set color as, except that we use rgba() instead and use opacity as the last argument.
var colorArr = this.getColor(100);
ctm.color = "rgb(" + colorArr[0] + "," + colorArr[1] + "," + colorArr[2] + ")";

var opacity = 1;
ctm.textColor = "rgba(" + colorArr[0] + "," + colorArr[1] + "," + colorArr[2] + "," + opacity + ")";


For each value (except opacity), we add 150. This makes the text color much brighter, but keeps them at the same proportions.
var colorArr = this.getColor(100);
ctm.color = "rgb(" + colorArr[0] + "," + colorArr[1] + "," + colorArr[2] + ")";

var opacity = 1;
ctm.textColor = "rgba(" + (colorArr[0] + 150) + "," + (colorArr[1] + 150) + "," + (colorArr[2] + 150) + "," + opacity + ")";


Now bake the color of the text into the h1 tag.
<div class="ctm" v-for="ctm in ctms" style="background-color:{{ ctm.color }}">
  <h1 style="color:{{ ctm.textColor }}">{{ ctm.name }}</h1>
</div>


There you go. You can see all the text colors are basically brighter versions of their backgrounds.


Finally, the opacity!

Remember the variable opacity? It's supposed to be very low for the final div, and varying higher levels for the others. The idea here is that the more clearly you can see the text, the less likely that you have that condition. So for the last medical condition, it's super unclear and when the user can finally read it, it says something embarrassing. That's the joke!

So here, if isLast is true, we set opacity at a really low value, such as 0.05.
var opacity = (isLast ? 0.05 : );


And if not, we get a random number from 0 to 7.
var opacity = (isLast ? 0.05 : this.getRandomNo(8));


We add 2, so we get a number from 2 to 9.
var opacity = (isLast ? 0.05 : this.getRandomNo(8) + 2);


Then we divide by 10, so the final result is between 0.2 to 0.9.
var opacity = (isLast ? 0.05 : (this.getRandomNo(8) + 2) / 10);


See that everything else is relatively easy to read, but the last one is almost invisible! It says "ingrown rectum", which sounds painful.


Have fun with this. Refresh. See what results you get. Add more to the arrays!

Better go see a doctor for that testicle necrosis!
T___T

Wednesday, 12 November 2025

Spot The Bug: The Underperforming Chart Scale

Hey, hey! Spot The Bug is back in town, and we're rolling out a new bug-fix story!

Can't wait to
squash another bug.

Working with D3 is one of my favorite pasttimes, especially when I have a new dataset to play with. However, there are some gotchas, and today's episode of Spot The Bug is a story of how one of these gotchas, well, got me.

I had some data that I wanted to plot a chart for. I truncated it a bit for brevity, but it will serve to illustrate the point. Note in particular, the column val.

datavals.csv
date,val
2024-11-01,8
2024-11-02,2
2024-11-03,17
2024-11-04,11
2024-11-05,25
2024-11-06,4
2024-11-07,7
2024-11-08,3
2024-11-09,8
2024-11-10,15


Here's the code I wrote to create an y-axis
<!DOCTYPE html>
<html>
  <head>
    <title>Chart</title>

    <style>
      svg
      {
        width: 800px;
        height: 500px;
      }
      
      .scaleTick, .scale
      {
        stroke: rgba(100, 100, 100, 1);
        stroke-width: 1px;
      }
      
      .scaleText
      {
        font: 8px verdana;
        fill: rgba(100, 100, 100, 1);
        text-anchor: end;
      }
    </style>

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

  <body>
    <svg>

    </svg>

    <script>
      d3.csv("datavals.csv", function(data)
      {
        var dataVals = [];

       for (var i = 0; i < data.length; i++)
       {
         dataVals.push(data[i].val);
       }

       var maxVal = d3.max(dataVals);

        var chart = d3.select("svg");
        chart.html("");

        chart
     .append("line")
     .attr("class", "scale")
     .attr("x1", "50px")
     .attr("y1", "50px")
     .attr("x2", "50px")
     .attr("y2", "450px");
    
        var pxPerUnit = Math.floor(400 / maxVal);
    
        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;
     });     
      });
    </script>
  </body>
</html>

Here, I got a scale. However, something was wrong. The largest value in my dataset was 25. Why was the largest value here 8?


What Went Wrong

My first clue was here, near the top of the script where I declared maxVal. Because according to this line, max() was returning 8 as the largest value.
var maxVal = d3.max(dataVals);


This would explain why the scale started with the value 8 and worked its way down from there.

Why It Went Wrong

If the largest value in the dataVals array was 25 and the script thought the largest value was 8, there was one explanation for that. The script was comparing alphabetically rather than numerically. As text, "8" is always larger than "25". Basically, the dataVals array was being filled with strings rather than integers from the CSV file.
var dataVals = [];

for (var i = 0; i < data.length; i++)
{
  dataVals.push(data[i].val);
}

How I Fixed It

I used the parseInt() function to convert the data before pushing it into the dataVals array.
var dataVals = [];

for (var i = 0; i < data.length; i++)
{
  dataVals.push(parseInt(data[i].val));
}


And now it took 25 as the largest value! Because now dataVals was filled with integers rather than text, and it was comparing accordingly.


Moral of the Story

You'd think I would have learned to sanitize these things by now, especially in JavaScript, or any loosely-typed language. Apparently not!

Stay int-tellectual,
T___T