Monday, 13 April 2026

Film Review: Black Mirror Series Seven (Part 1/3)

Black Mirror Series Seven has arrived! For those not in the know, Black Mirror is an antholotgcal series revolving around tech and media, and its sometimes negative effects on life.

Will Black Mirror Series Seven be even more of a disappointment than its predecessor, or will this offering be a worthy inclusion in the exalted halls of the Black Mirror franchise? That's a question for the ages.


No time like the present - let's dive in and see what we're getting this time.

Warning - not safe for children! 

Bad language, sexual themes, the works. Also, spoilers.

The first episode is Common People.

The Premise

Mike and Amanda are a married couple who find themselves in financial dire straits when Amanda develops a severe medical condition which can be alleviated via means of a new technological breakthrough. They manage at first, but things get progressively worse as services degrade and the costs of maintaining the service keep rising to unsustainable levels.

The Characters

Chris O'Dowd as Mike Waters. He has this very goofy puppy-dog expression that really sells it - the horror of what he has to do to take care of his wife, the frustration at the escalating costs, the weariness at the neverending shitshow. It's all there, and I felt every bit of it. This guy didn't look like a hero, but damn if Chris O'Dowd didn't bring that Everyman Superman vibe.

Rashida Jones as his wife Amanda. She portrays a woman who, through no fault of her own, becomes a walking talking billboard. I really did enjoy the instances where she started spouting ads, so naturally. It was a remarkable performance. One moment she's a caring and empathetic schoolteacher, and next moment she's a shell of her former self, though we see this in progressive stages.

Tracee Ellis Ross as Gaynor, Rivermind's sales rep. I actually thoguht thatbwas quite a nuanced performance. She first appeared as a sympathetic voice of hope, later on appearing still sympathetic even as she started tightening the screws, and at the end of it she was a soulless corporate shill, complete with shoulder-padded blazers and all. The way she delivers the line "pregnancy costs extra", is just diabolical. It's so good!

Nicholas Cirillo is Shane. This guy played the kind of asshole one loves to hate. At first he was just an irresponsible jackass, until somewhere in the third quarter of the episode he goes full douchebag. It was a one-note character but I have to give credit where it's due - his portrayal made me cheer when the character got his comeuppance.


Lisa Gilroy makes an appearance as the unnamed woman in the Rivermind Lux ad. It was delightfully corny and she looked like she was having a ball.

Sabrina Jalees as Angie. Seems like a pointless casting. I suspect her role was originally bigger than just a few throwaway lines. But if they cut her participation, that much, they should have just gone all the way and excised her completely from this episode. WHat did her character do, really? A big fat nothing, that's what.

Donald Sales as Kyle, the foreman. Almost another pointless casting.

Carolyn Taylor as the middle-aged principal Penelope, Amanda's boss. Not a big role but Taylor nailed it as the otherwise sympathetic boss who's sensitive about her middle-aged spinster status.

Lucy Turnbull as Eva. She comes across as emotionally vulnerable, like most kids, but does very little else that is plot-relevant.

Huxley Fisher as Oscar. This kid is something else. In the limited screen time he had, his judgey stare while talking to Amanda had me in stitches. I don't know if that was intentional.

The Mood

It's a picture of domestic bliss at the start, and soon gets bleak and depressing, vintage Black Mirror. The mood rarely shifts. It's constantly slow-moving and dreary, forcing the viewer to face the inexorably miserable ending that is the eventuality.

What I liked

There's nothing sinister going on, just the combination of technology, corporate greed and human sadism. In short, tech and human beings being horrible to each other. Love it! Did I mention I adore the story? Throughout it all, Mike and Amanda never give up until they absolutely have to. It's awesome and heartbreaking at the same goddamn time. Even without any real shocking twists, this episode delivers.


Using Dum Dummies as the platform on which Mike debases himself in order to raise funds, seemed to be a clever parody of the platform known as OnlyFans, or perhaps other livestreaming platforms.


And speaking of Dum Dummies, there was an amusing callback to a previous episode, The Waldo Moment.

The dynamic between Mike and Amanda was really sweet and funny at the same time. I especially like how they refer to sex as a "happy accident".

That ending was so deliciously downer! Oh man. It looks like we find out just how Mike paid for that last thirty minutes' worth of Rivermind Lux... with his life!


Gaynor using the app to increase her nonchalance level to max, in order to deal with Mike's angry outburst, was such black humor.

What I didn't

I could have done without that bit about Amanda really enjoying the sex under the influence of the Rivermind Lux package, to be honest.

The tech being able to transfer language and motor skills. Like, OK, that bit was overkill and didn't really add anything to the plot. Also, things like parkour are as much physical as they are mental, if not more so. So this bit made no goddamn sense.

Conclusion

Just one word - wow. What. An. Episode. Not only was it quintessential Black Mirror, it had at its core, a story of true, enduring love. There's that bleak ending, the midway twists, and the absurdly dark humor. This episode had it all, and it was the first episode. Honestly, how is the rest of the series going to top this?!

My Rating

9.5 / 10

Next

BĂȘte Noire

Wednesday, 8 April 2026

In defense of small steps

It was 1997. I was a Temasek Polytechnic student waiting to go into National Service. While hanging out at a running track (imagine, in those days I actually had knees that didn't creak.) I bumped into a schoolmate also waiting for National Service.

He told me he had been preparing for National Service by running three rounds around the running track every day. Three. That was 1.2 kilometers. I was baffled. I laughed in his face and said he might as well not bother. This was a pitifully short distance. What good would it do?

Running in preparation.

I know, I know, I was a jerk. And if I ever see him again, even if he's forgotten all about it, I really should offer him an apology. I'm a kinder, more evolved individual now and all. More importantly, I've spent decades as a web developer and it's not just compassion behind the apology - it's professional clarity.

The lesson behind this

The story here isn't about me saying stupid, needlessly cruel things like some empty-headed moron. I was nineteen, FFS. Saying dumb shit was par for the course.

No, the story is that, in my youthful arrogance, I was blind to the simple fact that everything begins with small steps. Everything. And years later, life would beat that lesson into me over and over. I would attempt a tech career in sunny Singapore. I would stumble repeatedly. Sometimes I would take a few steps forward only to be shunted one step back.

In many ways, life was not kind. It could have been worse, of course. A lot worse. But the fact remains that there were challenges. Challenges that weren't going to be navigated in a single bound. I was dismissed from jobs. Failed probationary periods. Screwed up at work. Joined companies that crashed and burned. Worked for exploitative bosses with the ethics of a swamp leech.

But you know what? It was OK because my failures were due to me actually trying. That was valuable. The thing about freedom, and having agency, is that you own all your success and your failures.

Moving slowly.

The thing is, I wasn't shooting up the corporate ladder. Sometimes I wasn't anywhere near any kind of ladder. It took a few (OK, several) iterations before my career got any kind of traction, and even then, it was a slow grind.

In retrospect, my career was like how software is typically written - iteratively. "Do it once, do it good" is music to the ears. Unfortunately, that's all it often is. The ugly reality is that software evolves. You start off with an MVP that gets fleshed out over time, and requirements are going to change. That's business. That's life, even.

What I know now

A new appreciation for small steps is one of the things I've gained. It's not about how big the step is. It's about the fact that the step is being taken at all. It's a statement of intent, and this can be powerful. Without even a small step in the right direction, you ain't getting anywhere, son.

Pretty much like my ex-schoolmate and his 1.2 kilometer run. A tiny step? Yes. Very tiny. But no less important. Because it was the intention that counted. And later on, whatever success he enjoyed in that area should be attributed to that intention.

It wasn't like he simply went on Social Media and talked about doing it without actually doing it. No, he was doing it when I encountered him. And what did I do? Minimized his efforts like a jackass, that's what.

Wishing for software.

I've come to understand the need to do things in iterations. Small incremental steps if necessary. And it's often necessary. Software isn't simply conjured out of thin air after verbalizing a wish, no matter what the likes of Jensen Huang or Elon Musk would have you believe. It follows a process of repeated refinement. A lot of eventual success is predicated upon simply showing up to do the work, and showing up consistently.

This has also manifested in my reading habits, and even my consumption of media. Before, I always felt the need to watch YouTube videos to the end. Finish the book I borrowed from the library. If I started on a TV series, I would binge it to the end.

No longer.

Now I understand that reading a book to the end, for example, means very little. It doesn't actually mean that I'm finished with it. I can always gain new insights from rereading it. Conversely, if it's not worth my time the first round, I can feel free to cut it loose without wasting any more time on it. Perhaps one day I'll find the motivation to get back to it... and if not, it's not a big deal. Basically, I no longer feel the pressure to finish things I started. Because the "finish" is ultimately artificial.

Just like software. Software is never "finished". It's only ready for shipping, but it will evolve in its lifetime.

Small conclusions

Be comfortable with small beginnings. Showing up is more important than showing up loud. "Go big or go home" is ego and stupidity talking, nothing more.

One small step for man...
T___T

Saturday, 4 April 2026

Web Tutorial: Easter Animation 2026

It's that time of the year again. Easter arrives tomorrow, and I'd like to herald its coming with a HTML animation.

This one uses no images. Just good old CSS sleight-of-hand. For the animation, we'll use jQuery UI. Is it absolutely necessary? No, but I feel like it.

The starting HTML looks like this. We have a div, id container. Below it, we'll have a h1 tag. All divs are set to have red outlines. Trust me, we'll need it.
<!DOCTYPE html>
<html>
  <head>
    <title>Easter 2026</title>

    <style>
      div
      {
        border: 1px solid rgb(255, 0, 0);
      }        
    </style>

    <script src="https://code.jquery.com/jquery-3.7.1.js"></script>
    <script src="https://code.jquery.com/ui/1.13.3/jquery-ui.js"></script>

    <script>

    </script>
  </head>

  <body>
    <div id="container">

    </div>

    <h1>HAPPY EASTER</h1>
  </body>
</html>


Here, we define h1 with some aesthetics. container is set to a small size, centered in the screen via use of margin properties. Round corners and all, because it's cute. I also gave it a thick orange border.
<!DOCTYPE html>
<html>
  <head>
    <title>Easter 2026</title>

    <style>
      div
      {
        border: 1px solid rgb(255, 0, 0);
      }  

      h1
      {
        font-family: verdana;
        text-align: center;
        color: rgb(255, 200, 0);
      }

      #container
      {
        margin: 100px auto 0 auto;
        border-radius: 50px;
        border: 5px solid rgb(255, 200, 0);
        width: 300px;
        height: 200px;
      }
    </style>

    <script src="https://code.jquery.com/jquery-3.7.1.js"></script>
    <script src="https://code.jquery.com/ui/1.13.3/jquery-ui.js"></script>

    <script>

    </script>
  </head>

  <body>
    <div id="container">

    </div>

    <h1>HAPPY EASTER</h1>
  </body>
</html>


In container, we have two divs - ids aura and hills.
<div id="container">
      <div id="aura">

      </div>  

      <div id="hills">

      </div>
</div>


In hills, we have five other divs. The first three are styled using the CSS class hill. The fourth and fifth have the ids cross and shadow. They contain the HTML crucfix symbol.
<div id="container">
      <div id="aura" class="unrotated">

      </div>  

      <div id="hills">
            <div class="hill"></div>
            <div class="hill"></div>
            <div class="hill"></div>
            <div id="cross">&#10013;</div>
            <div id="shadow">&#10013;</div>
      </div>
</div>


Here's some styling. We want aura to overlap the entirety of container. I've made it bigger than container's width and height, offset the margins accordingly, and turned it into a circle using the border-radius property. This isn't strictly necessary, but we're going to rotate it later, and it's easier to gauge if aura will still cover container that way. As for hills, we want it to cover container's full width but only the bottom half. So I've set the margin-top property accordingly. I've set position to relative and z-index to a positive number to ensure that hills always stays on top of aura in that order. This will be very relevant later.
<style>
      div
      {
        border: 1px solid rgb(255, 0, 0);
      }

      h1
      {
        font-family: verdana;
        text-align: center;
        color: rgb(255, 200, 0);
      }

      #container
      {
        margin: 100px auto 0 auto;
        border-radius: 50px;
        border: 5px solid rgb(255, 200, 0);
        width: 300px;
        height: 200px;
      }
    
      #aura
      {
        width: 400px;
        height: 400px;
        margin-top: -100px;
        margin-left: -50px;
        border-radius: 50%;
      }

      #hills
      {
        width: 300px;
        height: 100px;
        margin-top: -120px;
       position: relative;
       z-index: 2;
      }

</style>


So far, it's all here.


Now, what happens if we style hill this way? All 150 pixel squares with rounded corners, and rotated 45 degrees.
      #hills
      {
        width: 300px;
        height: 100px;
        margin-top: -120px;
       position: relative;
       z-index: 2;
      }

      .hill
      {
        width: 150px;
        height: 150px;
        border-radius: 10px;
        background-color: rgb(0, 0, 0);
        transform-origin: 50% 50%;
        transform: rotate(45deg);
      }

</style>


This is what you should have.


We use the nth-of-type pseudoselector because each of these will now have their own properties at this point.
      #hills
      {
        width: 300px;
        height: 100px;
        margin-top: -120px;
        position: relative;
        z-index: 2;
      }

      .hill
      {
        width: 150px;
        height: 150px;
        border-radius: 10px;
        background-color: rgb(0, 0, 0);
        transform-origin: 50% 50%;
        transform: rotate(45deg);
      }

      .hill:nth-of-type(1)
      {

      }

      .hill:nth-of-type(2)
      {

      }

      .hill:nth-of-type(3)
      {

      }

</style>


The first hill gets moved 50 pixels left.
.hill:nth-of-type(1)
{
  margin-left: -50px;
}

.hill:nth-of-type(2)
{

}

.hill:nth-of-type(3)
{

}


So far so good.


We move the middle "hill" up and to the right. It should placed higher than the other two.
.hill:nth-of-type(1)
{
  margin-left: -50px;
}

.hill:nth-of-type(2)
{
  margin-top: -180px;
  margin-left: 75px;

}

.hill:nth-of-type(3)
{

}


See this?


Finally, we use margin-top and margin-left properties to move the last "hill".
.hill:nth-of-type(1)
{
  margin-left: -50px;
}

.hill:nth-of-type(2)
{
  margin-top: -180px;
  margin-left: 75px;
}

.hill:nth-of-type(3)
{
  margin-top: -120px;
  margin-left: 200px;

}


Yep. It's coming along nicely.


What's next? The cross, of course! For cross, ensure that it's a large font size. margin-top is the property that moves the div vertically upwards while text-align is set to center so that it rests on top of the middle "hill".
  .hill:nth-of-type(3)
  {
    margin-top: -120px;
    margin-left: 200px;
  }  

  #cross
  {
    font-size: 5em;
    font-family: arial;
    font-weight: bold;
    margin-top: -270px;
    text-align: center;
  }   
       
</style>


See? There's the cross. Where's shadow? Look to the left "hill", it's hiding there.


What we do here is repeat the styling for shadow, but with a slightly different margin-top property.
  #cross
  {
    font-size: 5em;
    font-family: arial;
    font-weight: bold;
    margin-top: -270px;
    text-align: center;
  }

  #shadow
  {
    font-size: 5em;
    font-family: arial;
    font-weight: bold;
    margin-top: -100px;
    text-align: center;
  }       
 
</style>


Now shadow overlaps cross, but not perfectly. So the cross beam looks thicker. But that's not a problem. The animation later will take care of this.


For aura, we want the background to have spokes radiating from the center. So that means it's going to be a repeating conic gradient of yellow and orange.
#aura
{
  width: 400px;
  height: 400px;
  margin-top: -100px;
  margin-left: -50px;
  border-radius: 50%;
  background-image: repeating-conic-gradient(rgb(255, 255, 0) 5deg 10deg, rgb(255, 200, 0) 15deg 20deg, rgb(255, 255, 0) 25deg 30deg);
}


Now would you look at that!


At this point, remove the red outlines. And set the overflow property of container to hidden.
div
{
  border: 0px solid rgb(255, 0, 0);
}

h1
{
  font-family: verdana;
  text-align: center;
  color: rgb(255, 200, 0);
}

#container
{
  margin: 100px auto 0 auto;
  border-radius: 50px;
  border: 5px solid rgb(255, 200, 0);
  width: 300px;
  height: 200px;
  overflow: hidden;
}


Oh, this is looking so good.


Time to animate!

Add the classes unrotated and rotated. We want aura to rotate, see? So unrotated is the start state and rotated is the end state. I also use the transition property in both cases, to determine duration and style. Not a big issue, carry on.
#aura
{
  width: 400px;
  height: 400px;
  margin-top: -100px;
  margin-left: -50px;
  border-radius: 50%;
  background-image: repeating-conic-gradient(rgb(255, 255, 0) 5deg 10deg, rgb(255, 200, 0) 15deg 20deg, rgb(255, 255, 0) 25deg 30deg);
}

.unrotated
{
  transform: rotate(0deg);
  transition: 10s ease;
}

.rotated
{
  transform: rotate(360deg);      
  transition: 10s ease;
}


#hills
{
  width: 300px;
  height: 100px;
  margin-top: -120px;
  position: relative;
  z-index: 2;
}


What we'll want to do here is set the class of aura to unrotated.
<div id="aura" class="unrotated">
  
</div>  


Now use the toggleClass() method on aura.
<script>
  $(document).ready(function() {
    $("#aura").toggleClass("rotated");
  });
</script>


And then use jQuery UI's "puff" effect on shadow. We'll set the animation to last 1 second, and specify that the end size of shadow will be 250% of the original.
<script>
  $(document).ready(function() {
    $("#aura").toggleClass("rotated");
    $("#shadow").effect( "puff", {percent: 250}, 1000 );
  });
</script>


See the effect! It looks like the crucifix with a fading black aura amid the rotating spokes.


And if you want to make this continuous, put that in a setInterval() function.
<script>
  $(document).ready(function() {
    $("#aura").toggleClass("rotated");
    $("#shadow").effect("puff", {percent: 250}, 1000 );

    setInterval
    (
      function()
      {
        $("#aura").toggleClass("rotated");
        $("#shadow").effect("puff", {percent: 250}, 1000 );
      },
      10000
    );

  });
</script>


Auran't you glad it's Easter?
T___T

Sunday, 29 March 2026

Web Tutorial: Chuck Norris Memorial

A legend has left us. On the 19th of this month, one Chuck Norris, martial artist and action movie icon, passed away. One of the things that really stood out in the Chuck Norris mythos was... well, the Chuck Norris mythos. Remember back in the 2000s, how popular "Chuck Norris facts" became?

Well, today, in loving ass-kicking memory, we'll do something like this! It'll be a page that returns a random Chuck Norris fact each time. But this is a tech blog, so the fact has to be tech-based! And this is an image that we'll be using, which I generated using MetaAI.

chucknorris.jpg

Let's begin by creating a PHP page. We'll deal with the HTML portion first. We'll also use some jQuery UI to create nice animations. Note that in the body, we have div tags styled using the CSS classes number, fact and rip
<!DOCTYPE html>
<html>
  <head>
    <title>In Memory of Chuck Norris</title>

    <style>
  
    </style>

    <script src="https://code.jquery.com/jquery-3.7.1.js"></script>
    <script src="https://code.jquery.com/ui/1.13.3/jquery-ui.js"></script>

    <script>

    </script>
  </head>

  <body>
    <div class="number"></div>
    <br />
    <div class="fact"></div>
    <div class="rip">R.I.P 19<sup>th</sup> March 2026</div>
  </body>
</html>


Let's add some PHP. This currently is just one line, declaring fact as a string. The value is one of my favorite Chuck Norris "facts". In the div styled using the CSS class fact, display the value of fact. And in the div styled using the CSS class number, let's have a random number to humorously display which number this "fact" is supposed to be.
<?php
  $fact = "Chuck Norris can divide by zero";
?>


<!DOCTYPE html>
<html>
  <head>
    <title>In Memory of Chuck Norris</title>

    <style>
  
    </style>

    <script src="https://code.jquery.com/jquery-3.7.1.js"></script>
    <script src="https://code.jquery.com/ui/1.13.3/jquery-ui.js"></script>

    <script>

    </script>
  </head>

  <body>
    <div class="number">Fact #<?php echo rand(100, 100000); ?>:</div>
    <br />
    <div class="fact"><?php echo $fact; ?></div>
    <div class="rip">R.I.P 19<sup>th</sup> March 2026</div>
  </body>
</html>


This is just text right now.


Now let's style the body tag. We first want to use the image as a background.
<style>
  body
  {
    background: url(chucknorris.jpg) left top no-repeat;
    background-size: cover;
  }  

</style>


Then we want to style the text. I made it large font, with a white outline using the text-shadow property.
<style>
  body
  {
    background: url(chucknorris.jpg) left top no-repeat;
    background-size: cover;
    font-size: 60px;
    font-family: georgia;
    text-shadow: -2px -2px 2px rgb(255, 255, 255), 2px -2px 2px rgb(255, 255, 255), -2px 2px 2px rgb(255, 255, 255), 2px 2px 2px rgb(255, 255, 255);

  }  
</style>


Nice contrast, eh?


Now let's focus on the CSS classes number, fact and rip. It's mostly positional. I made number bolder and floated it left. fact is also floated left. rip has position property set to fixed and is anchored to the bottom right of the screen via the right and bottom properties. I'ave also adjusted the font size.
<style>
  body
  {
    background: url(chucknorris.jpg) left top no-repeat;
    background-size: cover;
    font-size: 60px;
    font-family: georgia;
    text-shadow: -2px -2px 2px rgb(255, 255, 255), 2px -2px 2px rgb(255, 255, 255), -2px 2px 2px rgb(255, 255, 255), 2px 2px 2px rgb(255, 255, 255);
  }

  .number
  {
    font-weight: bold;
    width: 10em;
    float: left;
  }

  .fact
  {
    width: 20em;
    float: left;
  }

  .rip
  {
    font-size: 0.5em;
    height: 1.5em;
    position: fixed;
    bottom: 0;
    right: 0;
  }    
  
</style>


See what I mean?


OK, next up... animation! For this, we want to set the display property of the fact CSS class, to none. This effectively hides the "fact". That's because we want to use jQuery to make it fade in.
.fact
{
  width: 20em;
  float: left;
  display: none;
}


In the script tag, do this so that the code only runs once the HTML is loaded.
<script>
  $(document).ready(function() {

  });

</script>


This causes the "fact" to fade in over the course of 5 seconds.
<script>
  $(document).ready(function() {
    $(".fact").fadeIn(5000);
  });
</script>


Then we use the effect() method on this element. The "bounce" effect belongs to jQuery UI, and here we specify a 1 second duration.
<script>
  $(document).ready(function() {
    $(".number").effect("bounce", 1000);
    $(".fact").fadeIn(5000);
  });
</script>


We include an object with the times property set to 5, so that it bounces 5 times in 1 second. (Sounds like a really lousy credit card, but there you go.)
<script>
  $(document).ready(function() {
    $(".number").effect("bounce", {times: 5}, 1000);
    $(".fact").fadeIn(5000);
  });
</script>


See how the "fact" fades in as the "number" bounces!


Now for the most exciting part... leveraging on OpenAI's ChatGPT to generate a random Chuck Norris "fact". For this, we're leveraging on ChatGPT's API. First, declare key, org and url. These should already have been set up as a new project in ChatGPT. Then create headers as an array of strings. This is what we'll be sending to the URL defined at url.
<?php
  $key = "sk-xxx";
  $org = "org-FUOhDblZb1pxvaY6YylF54gl";
  $url = "https://api.openai.com/v1/chat/completions";

  $headers = [
   "Authorization: Bearer " . $key,
   "OpenAI-Organization: " . $org,
   "Content-Type: application/json"
  ];


  $fact = "Chuck Norris can divide by zero";
?>


We then construct the prompt to send. Here. I specify the JSON object that ChatGPT should give me, and explicitly specify the value. I want a Chuck Norris "fact", and I also want it to be tech-related. Because those are the ones I love. That's for content. role is set to "user". All this is in the array, obj, which is in turn part of messages.
<?php
  $key = "sk-xxx";
  $org = "org-FUOhDblZb1pxvaY6YylF54gl";
  $url = "https://api.openai.com/v1/chat/completions";

  $headers = [
   "Authorization: Bearer " . $key,
   "OpenAI-Organization: " . $org,
   "Content-Type: application/json"
  ];

  $messages = [];
  $obj = [];
  $obj["role"] = "user";
  $obj["content"] = "Give me a JSON object with one property. The property should be named 'fact'. Its value should be a string. This should be a Chuck Norris 'fact', relating either to internet, email or software. An Example would be 'Chuck Norris can divide by zero.'.";
  $messages[] = $obj;


  $fact = "Chuck Norris can divide by zero";
?>


Then we create the parent, data. Here we specify the model. Then we set messages, and max_tokens. This one won't be text-heavy. I reckon 500 tokens should be enough.
<?php
  $key = "sk-xxx";
  $org = "org-FUOhDblZb1pxvaY6YylF54gl";
  $url = "https://api.openai.com/v1/chat/completions";

  $headers = [
   "Authorization: Bearer " . $key,
   "OpenAI-Organization: " . $org,
   "Content-Type: application/json"
  ];

  $messages = [];
  $obj = [];
  $obj["role"] = "user";
  $obj["content"] = "Give me a JSON object with one property. The property should be named 'fact'. Its value should be a string. This should be a Chuck Norris 'fact', relating either to internet, email or software. An Example would be 'Chuck Norris can divide by zero.'.";
  $messages[] = $obj;

  $data = [];
  $data["model"] = "gpt-3.5-turbo";
  $data["messages"] = $messages;
  $data["max_tokens"] = 500;


  $fact = "Chuck Norris can divide by zero";
?>


And here's the final use of cURL, to send data to the API endpoint.
<?php
  $key = "sk-xxx";
  $org = "org-FUOhDblZb1pxvaY6YylF54gl";
  $url = "https://api.openai.com/v1/chat/completions";

  $headers = [
   "Authorization: Bearer " . $key,
   "OpenAI-Organization: " . $org,
   "Content-Type: application/json"
  ];

  $messages = [];
  $obj = [];
  $obj["role"] = "user";
  $obj["content"] = "Give me a JSON object with one property. The property should be named 'fact'. Its value should be a string. This should be a Chuck Norris 'fact', relating either to internet, email or software. An Example would be 'Chuck Norris can divide by zero.'.";
  $messages[] = $obj;

  $data = [];
  $data["model"] = "gpt-3.5-turbo";
  $data["messages"] = $messages;
  $data["max_tokens"] = 500;

  $curl = curl_init($url);
  curl_setopt($curl, CURLOPT_POST, 1);
  curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
  curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
  curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);

  $result = curl_exec($curl);
  if (curl_errno($curl))
  {
   echo 'Error:' . curl_error($curl);
  }

  curl_close($curl);  


  $fact = "Chuck Norris can divide by zero";
?>


We grab the response and extract the required value. And then we change fact's value from a hard-coded string, to that extracted value.
<?php
  $key = "sk-xxx";
  $org = "org-FUOhDblZb1pxvaY6YylF54gl";
  $url = "https://api.openai.com/v1/chat/completions";

  $headers = [
   "Authorization: Bearer " . $key,
   "OpenAI-Organization: " . $org,
   "Content-Type: application/json"
  ];

  $messages = [];
  $obj = [];
  $obj["role"] = "user";
  $obj["content"] = "Give me a JSON object with one property. The property should be named 'fact'. Its value should be a string. This should be a Chuck Norris 'fact', relating either to internet, email or software. An Example would be 'Chuck Norris can divide by zero.'.";
  $messages[] = $obj;

  $data = [];
  $data["model"] = "gpt-3.5-turbo";
  $data["messages"] = $messages;
  $data["max_tokens"] = 500;

  $curl = curl_init($url);
  curl_setopt($curl, CURLOPT_POST, 1);
  curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
  curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
  curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);

  $result = curl_exec($curl);
  if (curl_errno($curl))
  {
   echo 'Error:' . curl_error($curl);
  }

  curl_close($curl);  

  $data = [];
  $data["model"] = "gpt-3.5-turbo";
  $data["messages"] = $messages;
  $data["max_tokens"] = 500;

  $result = json_decode($result);
  $content = $result->choices[0]->message->content;
  $content = json_decode($content);


  $fact = $content->fact;  
?>


See? The facts change now.


Different fact.


Another different fact.


R.I.P, Mr Norris!

Rumour has it that you've been dead for years. Death just hasn't plucked up the courage to tell you.

Did you know that Chuck Norris can delete the Recycle Bin?
T___T

Tuesday, 24 March 2026

Web Tutorial: The GitHub Commit Line Chart (Part 2/2)

Time to show the data!

Declare commits. It is an array of all the values of the third column in dataset, which is the contributions. Then set the data property of the object in the series array, to commits.
function renderLineChart()
{
  var rngYearFrom = document.getElementById("rngYearFrom");
  var rngYearTo = document.getElementById("rngYearTo");
  var yearFrom = parseInt(rngYearFrom.value);
  var yearTo = parseInt(rngYearTo.value);

  var dataset = currentData.filter((x) =>{ return parseInt(x[0]) >= yearFrom && parseInt(x[0]) <= yearTo});
  var years = dataset.map(col => monthToName(col[1]) + " " + col[0]);
  var commits = dataset.map(col => col[2]);

  var series = [];

  series = [
    {
      name: "commits",
      type: "spline",
      data: commits,
      lineColor: "rgba(250, 100, 0, 1)",
      lineWidth: 5,
      dashStyle: "Solid",
      marker:
      {
        fillColor: "none"
      }
    }
  ];
}


Run this! You should see right now that both range sliders are at the minimum value, 2016.


Change the values. The chart should refresh!


Increase the range. See what happens?


We've created a line chart. But sometimes, we want comparisons. Like, How does the Line chart for 2021 compare to 2022?

That's what the checkbox is for. Remember this guy? Well, make sure it runs renderLineChart() when clicked.
<label for="rngYearFrom"><input type="checkbox" id="cbSeparateYears" onclick="renderLineChart()"> SEPARATE YEARS</label>


And in the code that runs when DOM is loaded, we want to populate yearSeriesData because we're going to use it. Right after defining yearMin and yearMax, we make sure yearSeriesData has index pointers corresponding to those years. And the value for each element is another array of all the elements in currentData where the first column (index 0, year) corresponds to the current year being referenced, i.
document.addEventListener("DOMContentLoaded", function () {
  fetch("http://www.teochewthunder.com/demo/hc_github/hcdata_github.csv")
  .then(response => response.text())
  .then(csvData => {
    const rows = csvData.split("\n");
    currentData = rows.slice(1).map(row => {
      const cols = row.split(",");
      var year = parseInt(cols[0]);
      var month = parseInt(cols[1]);
      var contributions = parseInt(cols[2]);

      return [year, month, contributions];
    });

    var years = currentData.map(col => col[0]);
    var yearMin = Math.min(...years);
    var yearMax = Math.max(...years);

    for (var i = yearMin; i <= yearMax; i++)
    {
      yearSeriesData[i] = currentData.filter((x) => { return x[0] == i; });
    }


    var yearFrom = yearMin;
    var yearTo = yearMin;

    var rngYearFrom = document.getElementById("rngYearFrom");
    var rngYearTo = document.getElementById("rngYearTo");
    var opYearFrom = document.getElementById("opYearFrom");
    var opYearTo = document.getElementById("opYearTo");

    rngYearFrom.min = yearMin;
    rngYearFrom.max = yearMax;
    rngYearFrom.value = yearFrom;
    opYearFrom.value = yearFrom;

    rngYearTo.min = yearMin;
    rngYearTo.max = yearMax;
    rngYearTo.value = yearTo;
    opYearTo.value = yearTo;

    renderLineChart();  
  });      
});


Next we have renderLineChart(). Grab cbSeparateYears as the checkbox, then implement a conditional block that checks if cbSeparateYears is checked. If not, define series the way we did previously.
function renderLineChart()
{
  var rngYearFrom = document.getElementById("rngYearFrom");
  var rngYearTo = document.getElementById("rngYearTo");
  var cbSeparateYears = document.getElementById("cbSeparateYears");
  var yearFrom = parseInt(rngYearFrom.value);
  var yearTo = parseInt(rngYearTo.value);

  var dataset = currentData.filter((x) =>{ return parseInt(x[0]) >= yearFrom && parseInt(x[0]) <= yearTo});

  var years = dataset.map(col => monthToName(col[1]) + " " + col[0]);
  var commits = dataset.map(col => col[2]);

  var series = [];
  if (cbSeparateYears.checked)
  {

  }
  else
  {

    series = [
      {
        name: "commits",
        type: "spline",
        data: commits,
        lineColor: "rgba(250, 100, 0, 1)",
        lineWidth: 5,
        dashStyle: "Solid",
        marker:
        {
          fillColor: "none"
        }
      }
    ];
  }

  const chart = Highcharts.chart("container", {
    chart:
    {
      borderColor: "rgba(250, 100, 0, 1)",
      borderRadius: 10,
      borderWidth: 2,
    },
    title:
    {
      text: "My Contributions",
      style: { "color": "rgba(250, 100, 0, 1)", "font-size": "2.5em", "font-weight": "bold" }
    },
    subtitle:
    {
      text: "GitHub statistics by TeochewThunder",
      style: { "color": "rgba(250, 100, 0, 0.8)", "font-size": "0.8em" }
    },
    xAxis:
    {
      categories: years
    },
    yAxis:
    {
      title:
      {
        text: "Commits"
      },
      gridLineColor: "rgba(250, 100, 0, 0.2)",
      tickColor: "rgba(250, 100, 0, 0.2)"
    },
    series: series
  });
}

If cbSeparateYears is checked, we'll populate series with more elements than just one. We will use a For loop to go from yearFrom to yearTo, then extract the relevant year's data into an object, seriesData. Then we will push an object containing i and seriesData, into series.
var series = [];
if (cbSeparateYears.checked)
{
  for (var i = yearFrom; i <= yearTo; i++)
  {
    var seriesData = yearSeriesData[i].map(col => col[2]);
    series.push(
      {
        name: i,
        type: "spline",
        data: seriesData,
        lineColor: "rgba(250, 100, 0, 1)",
        lineWidth: 5,
        dashStyle: "Solid",
        marker:
        {
          fillColor: "none"
        }              
      }
    );
  }

}
else
{
  series = [
    {
      name: "commits",
      type: "spline",
      data: commits,
      lineColor: "rgba(250, 100, 0, 1)",
      lineWidth: 5,
      dashStyle: "Solid",
      marker:
      {
        fillColor: "none"
      }
    }
  ];
}


Just above, we change years. If cbSeparateYears is checked, this means that we are displaying multiple years on separate lines, so it wouldn't make sense to display both month and year on the x-axis any more. So in that case, we'll omit the year.
var dataset = currentData.filter((x) =>{ return parseInt(x[0]) >= yearFrom && parseInt(x[0]) <= yearTo});

var years = dataset.map(col => monthToName(col[1]) + (cbSeparateYears.checked ? "" : " " + col[0]));
var commits = dataset.map(col => col[2]);

var series = [];
if (cbSeparateYears.checked)
{


See? We have multiple lines when we track from 2016 to 2019... but it looks like spaghetti.


What we can do here is mitigate it by making them different shades of orange. We could make them different colors altogether, of course. But just for arguments sake, let's do orange. First, we define diff as the gap in years between yearTo and yearFrom. Because we'll be dividing by this number, add 1 so we don't get a Divide-By_Zero error.
if (cbSeparateYears.checked)
{
  for (var i = yearFrom; i <= yearTo; i++)
  {
    var diff = (yearTo - yearFrom + 1);

    var seriesData = yearSeriesData[i].map(col => col[2]);
    series.push(
      {
        name: i,
        type: "spline",
        data: seriesData,
        lineColor: "rgba(250, 100, 0, 1)",
        lineWidth: 5,
        dashStyle: "Solid",
        marker:
        {
          fillColor: "none"
        }              
      }
    );
  }
}


Now define r and g. r will always be greater than g if we're doing shades of orange. The formula here makes sure that the final results of r and g commensurate with the value of i, no matter how small.
if (cbSeparateYears.checked)
{
  for (var i = yearFrom; i <= yearTo; i++)
  {
    var diff = (yearTo - yearFrom + 1);
    var r = (250 / diff) * (1 + i - yearFrom);
    var g = (100 / diff) * (1 + i - yearFrom);

    var seriesData = yearSeriesData[i].map(col => col[2]);
    series.push(
      {
        name: i,
        type: "spline",
        data: seriesData,
        lineColor: "rgba(250, 100, 0, 1)",
        lineWidth: 5,
        dashStyle: "Solid",
        marker:
        {
          fillColor: "none"
        }              
      }
    );
  }
}


Then we ensure that r and g are part of the formula that defines lineColor.
if (cbSeparateYears.checked)
{
  for (var i = yearFrom; i <= yearTo; i++)
  {
    var diff = (yearTo - yearFrom + 1);
    var r = (250 / diff) * (1 + i - yearFrom);
    var g = (100 / diff) * (1 + i - yearFrom);

    var seriesData = yearSeriesData[i].map(col => col[2]);
    series.push(
      {
        name: i,
        type: "spline",
        data: seriesData,
        lineColor: "rgba(" + r + ", " + g + ", 0, 1)",
        lineWidth: 5,
        dashStyle: "Solid",
        marker:
        {
          fillColor: "none"
        }              
      }
    );
  }
}


Note: This works only if there aren't too many colors. Right now I'm just representing a handful, so color differences are distinct enough.


Go on, have fun with it. Adjust the years, check and uncheck the checkbox. See what these get you.

Stay committed,
T___T