Showing posts with label Data Analytics. Show all posts
Showing posts with label Data Analytics. Show all posts

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, 3 March 2026

Ten Years of GitHub Usage

There was a time I shuttled code between the workplace to home, in the most comically retro manner possible - via email. I would be fiddling with some stuff at home, think it would be interesting to use in a workplace project, and send it to my work email account. And at work, if I didn't feel like leaving my code in the workplace and wanted to continue over the weekend, I would send it to my personal email. As the frequency of this increased, it soon became untenable. I actually only really started a GitHub account almost a full two years after starting this blog.

It's been ten years since. I looked at the heatmaps generated from my activity, and there were some interesting patterns. Really took me back. I realize that just going by the number of commits is a poor metric. Almost as poor as the number of lines of code for measuring code quality. But we've all got to start somewhere...

1. 2016 (46 commits)

At this time, I had just set up my GitHub account the previous year. The line chart of my contributions could be charitably described as "tentative".


Looking at the heatmap of my activity, the word that comes to mind is "sporadic". I used it a couple times a month, each time to commit a bunch of stuff.


Looks pitiful, eh? Basically I was just feeling my way around. Getting comfortable with the interface. There were even months where I had no activity whatsoever.

2. 2017 (96 commits)

The second year wasn't that much better, at least in terms of consistency. There were still a couple months where I failed to register any activity.


The line chart shows a marked improvement over the previous year. Though, to be fair, it's hard to do worse.


However, in the months where I did do shit, there was an uptick. Instead of a couple commits here and there, I was starting to register double digits on a semi-regular basis. This was definitely an improvement. Much of this could be attributed to me coding more ambitious projects. Projects that couldn't just be finished in a couple hours, and had to be periodically saved.

3. 2018 (179 commits)

This was the year one could practically see me shifting into third gear. There were no months where I neglected GitHub. More and more months were registering double digit commits. At the highest point at the end of the year, I even registered 50 commits. Compared to what I do consistently today, this is nothing. But it marked a start of something.


The trend line shows that I was still finding my feet, though some months were better than others. I was struggling for consistency as far as GitHub usage was concerned. This was at least partly due to me being busy studying for my ACTA.


Room for improvement? Definitely. But I was still using GitHub pretty much like a layperson. I used it to store code and not much else. I wasn't using GitHub anywhere close to its full potential.

4. 2019 (138 commits)

Things dropped off slightly in 2019. I suspect a lot of it was due to adjusting to my first year of being married and all. (Yeah, way to blame the wife, dude.)


My GitHub contribution was a jagged line. I would be a GitHub hero for a couple of months, then almost a zero the next month.


And in October, I even registered an entire month without a commit. This might have been due to the impending dominion of COVID-19. Suddenly we were all distracted by this potential life-or-death issue.

But for sure, my use of GitHub was still going strong, just not as strong as the previous year.

5. 2020 (286 commits)

This is where it started to get interesting. Some months, my usage climbed sharply, and plummeted just as quickly the next few months.

If you look at the heatmap below, it almost mirrors the line chart - periods of increased activity punctuated by periods of low activity. If there was any consolation, there were less fluctuations than the previous year.


This was because I was retroactively going through all my Readme files, and reformatting them for better readability. Also, now I was actually establishing proper code commits instead of merely updating via copy-pasting directly into repositories. I had GitHub Desktop open on my Lenovo, and open on my MacBook. This led to a whole lot of increased activity.

6. 2021 (227 commits)

This year, activity dropped off slightly, though my usage was arguably more consistent than it had been the previous year. The line chart shows a jagged line, though less jagged than previous years, with a peak near year end.


The heatmap of my activity was looking more spread out.


I was going through school for Data Analytics, and this affected the time I had for code experimentation. I'm proud to say, though, I managed to dedicate at least this much time.

7. 2022 (651 commits)

This was the year my usage really started to take off. For one, I was really seriously beginning to commit code the way GitHub was meant to be used. The lowest number of commits I tracked was 34 in January, and it never fell below that number for the rest of the year. The highest at one point was 76 in March. The line chart still shows a jagged line, but the minimum has risen dramatically.


The heatmap shows an ever wider spread of contributions over the year. A whole lot more heat. Almost three times the previous year's.


I had also begun to manage the contents of the website on GitHub, under a private repository. This was so I could look up previous versions of files and potentially restore them. I really should have done this a lot sooner. The thing was, my Lenovo was beginning to sputter and I really didn't feel comfortable having all my content stored there. Thus, my hand was forced.

That was pretty much how the number of commits jumped that much.

8. 2023 (872 commits)

The number of commits continued to jump. Compared to the peals and valleys registered in previous years for monthly commits, it was a relatively straight, consistent line.


I had begun to use GitHub to store my blogging drafts. Now this may not sound like much, until one considers how I blog. It starts as a skeleton made out of ideas in point form, and slowly I flesh them out bit by bit. Along the way, I may make revisions - rewording and rearranging stuff. I may only have over 70 blogposts a year, but that's multiple commits per blogpost!

This was partly due to my Lenovo being on its last legs. I began the process of writing drafts in GitHub instead of storing them in text files in my Lenovo, and when my Lenovo finally died in the latter half of 2023, my caution was rewarded.


The end result was, there were only three dates in the entire year where I didn't register a single commit. Compared to last year, the number of commits in a single month ranged from 61 to 80. In the heatmap, you can see that almost the entire map is shades of green.

9. 2024 (991 commits)

Now that I was writing drafts in GitHub full-time, that translated to daily commits. I would get an idea, open up GitHub, and commit it. I registered maybe one or two dates the entire year where I didn't commit anything. Most of the time, though, I was supremely consistent. If you look at the graph, the line was even smoother than the previous year's!


Looking at the chart, my usage started out at 70 plus commits per month, then steadily climbed to the high 80s through the course of the year. I remember at that point trying to rein myself in. I didn't want to end up setting a bar I couldn't commit to long term. The heatmap, as in 2024, shows almost total coverage of shades of green, but a lot more is bright green.


In addition to that, some of my projects were a little complex. They required frequent commits. I could push ten commits in an hour on a ReactJS project. This was also the year I started with NodeJS, and as you can probably tell, this also translated to a lot of commits.



10. 2025 (1007 commits)

The trend continued. I was hitting my stride in my usage of GitHub, and the consistency was really starting to show. Commits per month were now in the 80 plus range, until near the end of the year where I decided to give myself a bit of breathing room. Looking at the trend line, it was almost a straight line except for that year-end dip.


In the case of blogpost drafts, sometimes my updates were just little typo corrections and adding a few sentences here and there. Most bloggers will tell you that the incremental nature of writing a blogpost means that potentially a whole bunch of corrections accompany every one. While this was already the case in previous years, I took it up a few notches.


Here, the heatmap shows an entire year with no gaps. There is obviously higher usage during weekends. Of course, one commit could be as small as correcting a single typo, or be as big as including new functions into the code base. Thus, it can't be a complete representation of how hard I work here. But it's a decent indication.

What a decade!

It's interesting to me how my usage of GitHub evolved through the years. From just another online file system to a means of tracking code changes, and from there expanding to tracking all document changes. Even with code, my usage also changed, with more frequent commits due to establishing CI/CD pipelines.

Most of all, I think it shows my growth as a techie. My usage could still be improved, but at this point I think I'm getting close to a sweet spot. What has your usage been like?

With much commitment,
T___T