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

Sunday, 22 March 2026

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

Earlier this month, we took a look at my contributions over ten years of GitHub usage. I have the data, and we're going to do some data visualization on this one! Here's a sample of what hcdata_github.csv looks like. For the purpose of this exercise, I saved it at this link.
Year,Month,Contributions
2016,1,0
2016,2,4
2016,3,2
2016,4,2
2016,5,2
2016,6,7
2016,7,6
2016,8,6
2016,9,3
2016,10,0
2016,11,0
2016,12,7
2017,1,5
2017,2,3
2017,3,17
...


For this, we want something quick and dirty, so HighCharts it is! Here, we have some boilerplate HTML. Note the script link to the HighCharts library.
<!DOCTYPE html>
<html>
  <head>
    <title>GitHub Contributions</title>

    <style>
  
    </style>

    <script src="https://code.highcharts.com/highcharts.js"></script>

    <script>

    </script>
  </head>

  <body>

  </body>
</html>


We'll have two divs - ids container and dashboard respectively. I've included the styling. They both take up full screen width, though container has a bigger height than dashboard. dashboard has an additional specification to say text must be aligned in the middle (this will be relevant very soon). I've set divs to have a red outline, temporarily, so we can have a better visual.
<!DOCTYPE html>
<html>
  <head>
    <title>GitHub Contributions</title>

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

      #container
      {
        width: 100%;
        height: 600px;
      }

      #dashboard
      {
        width: 100%;
        height: 100px;
        text-align: center;
      }
    </style>

    <script src="https://code.highcharts.com/highcharts.js"></script>

    <script>

    </script>
  </head>

  <body>
    <div id="container">

    </div>

    <div id="dashboard">
    
    </div>
  </body>
</html>


Simple enough so far?


Over here in the dashboard div, we add a checkbox, cbSeparateYears, within a label tag.
<div id="dashboard">
    <label for="rngYearFrom">
        <input type="checkbox" id="cbSeparateYears">
        SEPARATE YEARS
    </label>
    <br />  
</div>


And two sliders, rngYearFrom and rngYearTo, also within label tags. Beside each slider we have corresponding output tags.
<div id="dashboard">
    <label for="rngYearFrom">
        <input type="checkbox" id="cbSeparateYears">
        SEPARATE YEARS
    </label>
    <br />
    <label for="rngYearFrom">
        FROM
        <input id="rngYearFrom" type="range" />
        <output id="opYearFrom" for="rngYearFrom"></output>
    </label>
    <br />
    <label for="rngYearTo">
        TO
        <input id="rngYearTo" type="range" />
        <output id="opYearTo" for="rngYearTo"></output>
    </label>      
</div>


This styling here for labels is just meant to align stuff nicely at the bottom of the chart. Basically, the labels have a fixed width and are aligned right... within that fixed width of 20em. And then the whole 20em worth of labels is aligned smack in the middle of dashboard! Remember we set text-align to middle for dashboard? I also set the color to orange. Sorry, the color just speaks to me, y'know?
<style>
    div { outline:1px solid rgb(255, 0, 0); }

    #container
    {
        width: 100%;
        height: 600px;
    }

    #dashboard
    {
        width: 100%;
        height: 100px;
        text-align: center;
    }

    label
    {
        display: inline-block;
        width: 20em;
        font-family: verdana;
        color: rgba(200, 150, 0, 1);
        text-align: right;
    }
</style>


OK, so here's a preview. The output tags won't be visible simply because they have no values yet.


We want these sliders to behave a certain way. They will both call the function setYear(), but with different arguments.
<label for="rngYearFrom">
  FROM
  <input id="rngYearFrom" type="range" oninput="setYear('from')" />
  <output id="opYearFrom" for="rngYearFrom"></output>
</label>
<br />
<label for="rngYearTo">
  TO
  <input id="rngYearTo" type="range" oninput="setYear('to')" />
  <output id="opYearTo" for="rngYearTo"></output>
</label>  


Start writing some code to handle events when the page loads. At the same time, we'll define the setYear() function. It has a parameter, target. Also, declare currentData and yearSeriesData as empty arrays.
<script>
    document.addEventListener("DOMContentLoaded", function () {

      });      
    })
;

    function setYear(target)
    {

    }

    let currentData = [];
    let yearSeriesData = [];
</script>


When the page loads, we want to get the data, and populate the min and max attributes of rngDateFrom and rngDateTo, with the minimum and maximum value of the Year column. No point allowing the user to select an invalid year, amirite?! So first, we run fetch() to get the CSV data from the URL I've saved it in.
document.addEventListener("DOMContentLoaded", function () {
    fetch("http://www.teochewthunder.com/demo/hc_github/hcdata_github.csv")
  });      
});


We use then() to grab the output response once the data is loaded, and resolve it to text using the text() method.
document.addEventListener("DOMContentLoaded", function () {
    fetch("http://www.teochewthunder.com/demo/hc_github/hcdata_github.csv")
    .then(response => response.text())
  });      
});


Once that's done, the next step is to use a chained then() method call to grab the output, csvData, and start working on it.
document.addEventListener("DOMContentLoaded", function () {
  fetch("http://www.teochewthunder.com/demo/hc_github/hcdata_github.csv")
  .then(response => response.text())
  .then(csvData => {
    
  });

});


We first want to declare rows as an array obtained from running the split() method on csvData, splitting by newlines in the CSV text content.
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");  
  });
});


And now we can fill up currentData. Each element of rows is a CSV line, separated by commas. We first use the slice() method on rows, with the argument 1, to only take into account all the rows after the first one, which is the header. And then we run the map() method on the result to iterate through it.
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 => {

    });

  });
});


We define cols by running the split() method on row, with a comma as the argument. We declare year, month and contribution according to which part of cols we are referencing, and ensure that the result is an integer by using the parseInt() function. Then we return the array of all these. In short, we return cols, but with the values converted to integers.
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];

    });
  });
});


Now define years. This is an array that contains all the years in the dataset, currentData. For this, we run the map() method on currentData and just get the first column (index 0) as the value.
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]);
  });
});


Now that we have the array years, getting the minimum and maximum values is a simple matter of using the min() and max() methods of the Math object, and passing in as an argument all the values of years. Note the use of the Spread Syntax here.
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);

  });
});


Here, we grab the DOM elements from the inputs and output.
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);

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

  });
});


We then declare and set the range. yearFrom and yearTo are both set to yearMin. For the sliders, the min attributes of rngYearFrom and rngYearTo are set to yearMin, and the max attributes of rngYearFrom and rngYearTo are set to yearMax. The values of the sliders, as well as the outputs, are set to yearFrom and yearTo.
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);

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

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

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

  });
});


You see it! Both sliders are set to the minimum, 2016. And the output tags are showing.


We want these sliders to behave a certain way. They will both call the function setYear(), but with different arguments.
<label for="rngYearFrom">
  FROM
  <input id="rngYearFrom" type="range" oninput="setYear('from')" />
  <output id="opYearFrom" for="rngYearFrom"></output>
</label>
<br />
<label for="rngYearTo">
  TO
  <input id="rngYearTo" type="range" oninput="setYear('to')" />
  <output id="opYearTo" for="rngYearTo"></output>
</label>


In the JavaScript, we create this function. The idea here is that the value of rngDateFrom can never be greater than the value of rngDateTo. So rngDateTo's value needs to be adjusted to the value of rngdateFrom when that happens, and vice versa. if rngDateTo's value is less than that of rngDateFrom, the value of rngDateFrom needs to be adjusted to the value of rngdateTo.
    rngYearTo.min = yearMin;
    rngYearTo.max = yearMax;
    rngYearTo.value = yearTo;
    opYearTo.value = yearTo;
  });      
});

function setYear(target)
{

}


let currentData = [];
let yearSeriesData = [];


Let's begin by grabbing the required elements from the DOM - namely, the sliders and outputs.
function setYear(target)
{
  var rngYearFrom = document.getElementById("rngYearFrom");
  var rngYearTo = document.getElementById("rngYearTo");
  var opYearFrom = document.getElementById("opYearFrom");
  var opYearTo = document.getElementById("opYearTo");

}


And we grab the currently selected values.
function setYear(target)
{
  var rngYearFrom = document.getElementById("rngYearFrom");
  var rngYearTo = document.getElementById("rngYearTo");
  var opYearFrom = document.getElementById("opYearFrom");
  var opYearTo = document.getElementById("opYearTo");
  var yearFrom = parseInt(rngYearFrom.value);
  var yearTo = parseInt(rngYearTo.value);

}


Now, an If block handles the scenario of which slider was adjusted, based on the parameter, target. It's either "from" or "to".
function setYear(target)
{
  var rngYearFrom = document.getElementById("rngYearFrom");
  var rngYearTo = document.getElementById("rngYearTo");
  var opYearFrom = document.getElementById("opYearFrom");
  var opYearTo = document.getElementById("opYearTo");
  var yearFrom = parseInt(rngYearFrom.value);
  var yearTo = parseInt(rngYearTo.value);

  if (target == "from")
  {

  }
  else
  {
    
  }

}


Here, if the selected value of rngYearFrom, yearFrom, is greater than yearTo, that should not be allowed. We set yearTo to at least be equal to yearFrom. Then we adjust the slider and output. Don't forget to adjust the output for the "from" slider too.
function setYear(target)
{
  var rngYearFrom = document.getElementById("rngYearFrom");
  var rngYearTo = document.getElementById("rngYearTo");
  var opYearFrom = document.getElementById("opYearFrom");
  var opYearTo = document.getElementById("opYearTo");
  var yearFrom = parseInt(rngYearFrom.value);
  var yearTo = parseInt(rngYearTo.value);

  if (target == "from")
  {
    if (yearFrom > yearTo)
    {
      yearTo = yearFrom;
      rngYearTo.value = yearFrom;
      opYearTo.value = yearFrom;
    }

    opYearFrom.value = yearFrom;

  }
  else
  {
      
  }
}


And we do the reverse for the other slider!
function setYear(target)
{
  var rngYearFrom = document.getElementById("rngYearFrom");
  var rngYearTo = document.getElementById("rngYearTo");
  var opYearFrom = document.getElementById("opYearFrom");
  var opYearTo = document.getElementById("opYearTo");
  var yearFrom = parseInt(rngYearFrom.value);
  var yearTo = parseInt(rngYearTo.value);

  if (target == "from")
  {
    if (yearFrom > yearTo)
    {
      yearTo = yearFrom;
      rngYearTo.value = yearFrom;
      opYearTo.value = yearFrom;
    }

    opYearFrom.value = yearFrom;
  }
  else
  {
    if (yearTo < yearFrom)
    {
      yearFrom = yearTo;
      rngYearFrom.value = yearTo;
      opYearFrom.value = yearTo;
    }    

    opYearTo.value = yearTo;      

  }
}


Let's test this...

Both the Year FROM and TO start at 2016. Slide FROM to 2022. Does TO follow?


Now slide TO to a value lower than FROM, say, 2018. Does FROM follow?

Now slide TO to a value higher than FROM. FROM should stay put! In effect, FROM should always be lower than TO, or equal.


Rendering the Chart

Finally, eh? Create the renderLineChart() function.
  });      
});

function renderLineChart()
{

}


function setYear(target)
{


We get the values from the range sliders, and assign them to the variables yearFrom and yearTo, after coercing them to integers using the parseInt() function.
function renderLineChart()
{
  var rngYearFrom = document.getElementById("rngYearFrom");
  var rngYearTo = document.getElementById("rngYearTo");
  var yearFrom = parseInt(rngYearFrom.value);
  var yearTo = parseInt(rngYearTo.value);

}


We then declare dataset. It will be the subset of currentData whose year column (the first one at index 0) confirms to the range between yearFrom and yearTo, inclusive. We use the filter() method for this.
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});
}


Next, we declare years. This is actually a label that shows both the month and the year. For this, we use the second column (index 1) and the first column (index 0). We run the second column's value through the monthToName() function.
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]);
}


Here's the function. It really isn't anything special - just returns a month string based on the integer passed into the function.
function setYear(target)
{
  var rngYearFrom = document.getElementById("rngYearFrom");
  var rngYearTo = document.getElementById("rngYearTo");
  var opYearFrom = document.getElementById("opYearFrom");
  var opYearTo = document.getElementById("opYearTo");
  var yearFrom = parseInt(rngYearFrom.value);
  var yearTo = parseInt(rngYearTo.value);

  if (target == "from")
  {
    if (yearFrom > yearTo)
    {
      yearTo = yearFrom;
      rngYearTo.value = yearFrom;
      opYearTo.value = yearFrom;
    }

    opYearFrom.value = yearFrom;
  }
  else
  {
    if (yearTo < yearFrom)
    {
      yearFrom = yearTo;
      rngYearFrom.value = yearTo;
      opYearFrom.value = yearTo;
    }    

    opYearTo.value = yearTo;      
  }
}

function monthToName(month)
{
  monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
  return monthNames[parseInt(month) - 1];
}


let currentData = [];
let yearSeriesData = [];


Now we have series, another array. For now, set series to contain one single object. Here are its properties.

name - this is the label that will appear on the chart's y-axis.
type - the line type. I chose "spline" because it's smooth and sexy.
data - the array of values. This is an empty array for now.
lineColor, lineWidth and dashStyle - Aesthetic choices. I went with a thick orange line.
marker - I don't want any damn markers, so it's an object with fillColor set to none.
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 series = [];

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

}


Now, to render the chart! We will use the container div as the target here, for the Highchart object's chart() method.
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 series = [];

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

  const chart = Highcharts.chart("container", {

  });

}


The first three properties we pass in, are chart, title and subtitle. As you can see, it's all visual styling and aesthetics. I've gone with an orange color scheme. (surprise, surprise)
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"
      }
    }
  ];

  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" }
    }

  });
}


The next two properties define the labelling and scale. For xAxis, we pass in the years array as the value of the category property. For yAxis, it's all color scheme and labelling. Feel free to play with different values.
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 series = [];

  series = [
    {
      name: "commits",
      type: "spline",
      data: [],
      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)"
    }

  });
}


And finally, for the series property, we pass in series, which we created earlier.
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 series = [];

  series = [
    {
      name: "commits",
      type: "spline",
      data: [],
      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
  });
}


Call the function here...
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);

    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();  
  });      
});


...and here, at the end of the setYear() function.
function setYear(target)
{
  var rngYearFrom = document.getElementById("rngYearFrom");
  var rngYearTo = document.getElementById("rngYearTo");
  var opYearFrom = document.getElementById("opYearFrom");
  var opYearTo = document.getElementById("opYearTo");
  var yearFrom = parseInt(rngYearFrom.value);
  var yearTo = parseInt(rngYearTo.value);

  if (target == "from")
  {
    if (yearFrom > yearTo)
    {
      yearTo = yearFrom;
      rngYearTo.value = yearFrom;
      opYearTo.value = yearFrom;
    }

    opYearFrom.value = yearFrom;
  }
  else
  {
    if (yearTo < yearFrom)
    {
      yearFrom = yearTo;
      rngYearFrom.value = yearTo;
      opYearFrom.value = yearTo;
    }    

    opYearTo.value = yearTo;      
  }

  renderLineChart();
}


Remove the red lines.
div { outline:0px solid rgb(255, 0, 0); }


And here is the current placeholder we have for the chart. You should see right now that both range sliders are at the minimum value, 2016.



Next

Displaying chart data.

Tuesday, 17 March 2026

The Bowknot Analogy

In seamanship, slipped knots are common, such as the Slippery Sheetbend, the Slippery Reef Knot, the Slippery Bowline, and so on. This is so that the knot in question has a quick-release mechanism. It is accomplished by doubling back the end of the rope under the last tuck, so that pulling on that end immediately undoes the knot.

Slippery Reef Knot

The Slippery Reef Knot is one such example. If we pull on the "slippery" end, it undoes the knot. If we pull on the other end, it tightens the knot.

But what if we made both ends slippery? Then, dear readers, we have what is known as a Bowknot.

Bowknot

Just like the kind you finish off wrapped presents with, or tie your shoelaces with.

The misconception here is that making both ends slippery, doubles the "slipperiness" of the knot. Not so! What has happened now is, instead of having one end being a quick-release mechanism, you now have two quick-release mechanisms. Pulling on either one end unties the entire knot. The quick-release mechanism functions the exact same way, and neither slippery end contributes to the other's quick-release mechanism.

Similarly in tech security...

You may have heard me speak of 2FA before. 2FA is an acronym for Two-factor Authentication. Meaning, a system that requires the user to authenticate in two different ways. Having two password fields does not count, because it just means that the user authenticates two times, the same way.

Biometrics!

Using a password and a fingerprint scan combination would count. Because then the user would have to authenticate two different ways.

Let's tie these two situations together (pun intended). Someone tying a Bowknot expecting the knot to become more slippery than a Slippery Reef Knot, is like adding an extra password field to a system expecting to make it more secure. It doesn't. It only makes the system more inconvenient for the user. If attackers can bypass one password field, they can bypass two.

The Takeaway

More of the same does not mean more of the benefits. That's really the common thread here. In security, the nature of the attacks are wide and varied. So, too, must your defenses be.

Taking a bow,
T___T