Sunday 6 November 2022

Web Tutorial: Highcharts Heatmap

The Heatmap makes a comeback!

In September, I walked you through how to create a Heatmap in D3. Now you will see how the process is like, using Highcharts. As to be expected, this has been reduced to largely a configuration task.

Let's begin with some HTML. You will see that the divs container and dashboard have already been styled, as has the label for the drop-down list in the dashboard, ddlStat.
<!DOCTYPE html>
<html>
    <head>
        <title>Heatmap</title>

        <style>
            #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, 0, 0, 1);
            }
        </style>

        <script>

        </script>
    </head>

    <body>
        <div id="container">

        </div>

        <div id="dashboard">
            <label for="ddlStat">
                STATISTICS
                <select id="ddlStat">
                    <option value="appearances" selected>Appearances</option>
                    <option value="goals">Goals</option>
                </select>
            </label>
        </div>
    </body>
</html>


This is what the code produces. Very bare bones at the moment. You can see a large space left for the Heatmap, and the drop-down list.




Add a link to the Highcharts code, then the Heatmap module.
<head>
  <title>Heatmap</title>

  <style>
    #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, 0, 0, 1);
    }
  </style>

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


  <script>

  </script>
</head>


In the JavaScript, create the data object upon page load. This data is the same data that has been loaded in all the other Highcharts web tutorials so far. Make sure data is declared, outside of this.
<script>
  document.addEventListener("DOMContentLoaded", function () {
      data["2016/2017"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Danny Ings"],
          "appearances": [41, 27, 29, 2],
          "goals": [12, 1, 13, 0]
      };

      data["2017/2018"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Danny Ings", "Alex Oxlade-Chamberlain", "Mohd Salah"],
          "appearances": [54, 20, 44, 14, 42, 52],
          "goals": [27, 1, 20, 1, 5, 44]
      };

      data["2018/2019"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Alex Oxlade-Chamberlain", "Mohd Salah", "Fabinho"],
          "appearances": [48, 44, 50, 2, 52, 41],
          "goals": [16, 1, 26, 0, 27, 1]
      };

      data["2019/2020"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Alex Oxlade-Chamberlain", "Mohd Salah", "Fabinho"],
          "appearances": [52, 40, 47, 43, 48, 39],
          "goals": [12, 4, 22, 8, 23, 2]
      };

      data["2020/2021"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Alex Oxlade-Chamberlain", "Mohd Salah", "Fabinho"],
          "appearances": [48, 28, 48, 17, 51, 42],
          "goals": [9, 1, 16, 1, 21, 0]
      };  
         
    });
    
    const data = [];
</script>


After that, run the renderHeatmap() function, and create it.
<script>
  document.addEventListener("DOMContentLoaded", function () {
      data["2016/2017"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Danny Ings"],
          "appearances": [41, 27, 29, 2],
          "goals": [12, 1, 13, 0]
      };

      data["2017/2018"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Danny Ings", "Alex Oxlade-Chamberlain", "Mohd Salah"],
          "appearances": [54, 20, 44, 14, 42, 52],
          "goals": [27, 1, 20, 1, 5, 44]
      };

      data["2018/2019"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Alex Oxlade-Chamberlain", "Mohd Salah", "Fabinho"],
          "appearances": [48, 44, 50, 2, 52, 41],
          "goals": [16, 1, 26, 0, 27, 1]
      };

      data["2019/2020"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Alex Oxlade-Chamberlain", "Mohd Salah", "Fabinho"],
          "appearances": [52, 40, 47, 43, 48, 39],
          "goals": [12, 4, 22, 8, 23, 2]
      };

      data["2020/2021"] =
      {
          "categories": ["Roberto Firminho", "Jordan Henderson", "Sadio Mané", "Alex Oxlade-Chamberlain", "Mohd Salah", "Fabinho"],
          "appearances": [48, 28, 48, 17, 51, 42],
          "goals": [9, 1, 16, 1, 21, 0]
      };

      renderHeatmap();                
    });

    function renderHeatmap()
    {

    }


    const data = [];
</script>


Make sure that renderHeatmap() runs when the value in ddlStat is changed, as well.
<select id="ddlStat" onchange="renderHeatmap()">
  <option value="appearances" selected>Appearances</option>
  <option value="goals">Goals</option>
</select>


Now, let us build on the renderHeatmap() function. In here, we want to grab the value of ddlStat. Assign the value to the variable stat.
function renderHeatmap()
{
  var stat = document.getElementById("ddlStat").value;
}


Declare players as an empty array (it won't be empty for long) and seasons as an array of keys from data. We need these for the x and y axes.
function renderHeatmap()
{
  var stat = document.getElementById("ddlStat").value;
  var players = [];
  var seasons = Object.keys(data);

}


Now we will populate the players array. Iterate through seasons using a For loop.
var players = [];
var seasons = Object.keys(data);

for(let i = 0; i < seasons.length; i++)
{

}


This will be a nested For loop. We iterate through the categories array of the current data element, pointed to by the current element in seasons.
for(let i = 0; i < seasons.length; i++)
{
    for(let j = 0; j < data[seasons[i]].categories.length; j++)
    {

    }

}


If the current element does not already exist in players, push the value in.
for(let i = 0; i < seasons.length; i++)
{
    for(let j = 0; j < data[seasons[i]].categories.length; j++)
    {
        if (players.indexOf(data[seasons[i]].categories[j]) == -1) players.push(data[seasons[i]].categories[j]);
    }
}


Next, it's time to define chart. Use the chart() method of the Highcharts object. Pass in the id of the container div, and a callback.
for(let i = 0; i < seasons.length; i++)
{
    for(let j = 0; j < data[seasons[i]].categories.length; j++)
    {
        if (players.indexOf(data[seasons[i]].categories[j]) == -1) players.push(data[seasons[i]].categories[j]);
    }
}

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

});


Define the chart property. It will have its own properties. Let's give it a deep red border, round borders and a border width. That's just aesthetics, but the really important property is type, and the value will be "heatmap". The colors property will be an array containing that same color we gave the border.
const chart = Highcharts.chart("container", {
    chart:
    {
        type: "heatmap",
        borderColor: "rgba(200, 0, 0, 1)",
        borderRadius: 10,
        borderWidth: 2,
    },
    colors: ["rgba(200, 0, 0, 1)"]

});

 
We will next add title and subtitle properties.
const chart = Highcharts.chart("container", {
    chart:
    {
        type: "heatmap",
        borderColor: "rgba(200, 0, 0, 1)",
        borderRadius: 10,
        borderWidth: 2,
    },
    title:
    {
        text: "Liverpool FC",
        style: { "color": "rgba(200, 0, 0, 1)", "font-size": "2.5em", "font-weight": "bold" }
    },
    subtitle:
    {
        text: "Football statistics by TeochewThunder",
        style: { "color": "rgba(200, 0, 0, 0.8)", "font-size": "0.8em" }
    },

    colors: ["rgba(200, 0, 0, 1)"]
});


So far so good!




Back to the renderHeatmap() function. Declare heatmap_values as an empty array.
for(let i = 0; i < seasons.length; i++)
{
    for(let j = 0; j < data[seasons[i]].categories.length; j++)
    {
        if (players.indexOf(data[seasons[i]].categories[j]) == -1) players.push(data[seasons[i]].categories[j]);
    }
}

var heatmap_values = [];


We are going to fill in heatmap_values, and it will be a slightly complicated process. Basically, heatmap_values is an array of arrays. Each of these sub-arrays has three elements - the X-axis index, the Y-axis index and the value. For that we have a nested For loop. For the outer loop, iterate through seasons. For the inner loop, iterate through players.
var heatmap_values = [];

for(let i = 0; i < seasons.length; i++)
{
    for(let j = 0; j < players.length; j++)
    {

    }
}


In the inner loop, declare index. The value of index is based on whether the current element of players is found in the categories array of the seasons array, pointed to by the current element of seasons.
for(let i = 0; i < seasons.length; i++)
{
    for(let j = 0; j < players.length; j++)
    {
        var index = data[seasons[i]].categories.indexOf(players[j]);
    }
}


If index is -1, it means the value is not found. That means you push an array containing j, i and null into heatmap_values.
for(let i = 0; i < seasons.length; i++)
{
    for(let j = 0; j < players.length; j++)
    {
        var index = data[seasons[i]].categories.indexOf(players[j]);

        if (index == -1)
        {
            heatmap_values.push([j, i, null]);
        }
        else
        {

        }

    }
}


If not, the value is present. Push an array containing j, i and the appropriate value into heatmap_values. The appropriate value in this case is based on i, stat and index.
for(let i = 0; i < seasons.length; i++)
{
    for(let j = 0; j < players.length; j++)
    {
        var index = data[seasons[i]].categories.indexOf(players[j]);

        if (index == -1)
        {
            heatmap_values.push([j, i, null]);
        }
        else
        {
            heatmap_values.push([j, i, data[seasons[i]][stat][index]]);
        }
    }
}


Now in the object, declare xAxis and yAxis properties. These properties are objects, each with a categories property. The value of these properties are the players and seasons arrays respectively.
const chart = Highcharts.chart("container", {
    chart:
    {
        type: "heatmap",
        borderColor: "rgba(200, 0, 0, 1)",
        borderRadius: 10,
        borderWidth: 2,
    },
    title:
    {
        text: "Liverpool FC",
        style: { "color": "rgba(200, 0, 0, 1)", "font-size": "2.5em", "font-weight": "bold" }
    },
    subtitle:
    {
        text: "Football statistics by TeochewThunder",
        style: { "color": "rgba(200, 0, 0, 0.8)", "font-size": "0.8em" }
    },
    colors: ["rgba(200, 0, 0, 1)"],
    xAxis:
    {
        categories: players
    },
    yAxis:
    {
        categories: seasons
    }

});


Next we have the series property. This object's properties are set as given. name is an empty string and borderWidth is 1. For data, we use what we created for heatmap_values. nullColor is black. This is for all null values in heatmap_values.
const chart = Highcharts.chart("container", {
    chart:
    {
        type: "heatmap",
        borderColor: "rgba(200, 0, 0, 1)",
        borderRadius: 10,
        borderWidth: 2,
    },
    title:
    {
        text: "Liverpool FC",
        style: { "color": "rgba(200, 0, 0, 1)", "font-size": "2.5em", "font-weight": "bold" }
    },
    subtitle:
    {
        text: "Football statistics by TeochewThunder",
        style: { "color": "rgba(200, 0, 0, 0.8)", "font-size": "0.8em" }
    },
    colors: ["rgba(200, 0, 0, 1)"],
    xAxis:
    {
        categories: players
    },
    yAxis:
    {
        categories: seasons
    },                    
    series:
    [
        {
            name: "",
            borderWidth: 1,
            data: heatmap_values,
            nullColor: "rgba(0, 0, 0, 1)"
        }
    ]

});


OK, stuff is happening! You will see black spots in the Heatmap where values are null. Unfortunately, everything else is red.




What we will do here, is use the colorAxis property to create a color scheme. Like most things in Highcharts, it's an object itself. We set its min property to 0, which means the minimum value of the data set is set to 0. minColor is set to white, which means the color white corresponds to the value 0. maxColor is set to deep red.
xAxis:
{
    categories: players
},
yAxis:
{
    categories: seasons
},
colorAxis:
{
    min: 0,
    minColor: "rgba(255, 255, 255, 1)",
    maxColor: "rgba(200, 0, 0, 1)"
},

series:
[
    {
        name: "",
        borderWidth: 1,
        data: heatmap_values,
        nullColor: "rgba(0, 0, 0, 1)"
    }
]


Now you see that the colors reflect the number of goals or appearances! And if you mouse over each of the squares, you will see the x, y and values that make up the color!




Add the dataLabels property within the object. It is an object as well. It has the properties enabled, which you set to true, and color. I have chosen yellow.
series:
[
    {
        name: "",
        borderWidth: 1,
        data: heatmap_values,
        nullColor: "rgba(0, 0, 0, 1)",
        dataLabels:
        {
            enabled: true,
            color: "rgba(255, 255, 0, 1)"
        }

    }
]


Now you see the numbers appear.




Final touch!

Let's get to work on the tooltips. Right now, these tooltips are just showing x, y and number values. We can modify them to make sense. We will do that using the tooltip property.
colorAxis:
{
    min: 0,
    minColor: "rgba(255, 255, 255, 1)",
    maxColor: "rgba(200, 0, 0, 1)"
},
tooltip:
{

},  
                 
series:
[
    {
        name: "",
        borderWidth: 1,
        data: heatmap_values,
        nullColor: "rgba(0, 0, 0, 1)",
        dataLabels:
        {
            enabled: true,
            color: "rgba(255, 255, 0, 1)"
        }
    }
]


The formatter property is a callback.
tooltip:
{
    formatter: function ()
    {

    }

},    


Now at this point you need to be familiar with the point object. It's all documented at this link. We use the data in that object to format a string to present the data nicely.
tooltip:
{
    formatter: function ()
    {
            return this.point.series['xAxis'].categories[this.point.x] + " " + (stat == "appearances" ? "made" : "scored") + " " + this.point.value + " " + stat + " in the " + this.point.series['yAxis'].categories[this.point.y] + " season.";
    }
},    


If the data was null, we just state that the player was not part of the club at that time period.
tooltip:
{
    formatter: function ()
    {
            if (this.point.value == null) return this.point.series['xAxis'].categories[this.point.x] + " was not part of the club during the " + this.point.series['yAxis'].categories[this.point.y] + " season.";

            return this.point.series['xAxis'].categories[this.point.x] + " " + (stat == "appearances" ? "made" : "scored") + " " + this.point.value + " " + stat + " in the " + this.point.series['yAxis'].categories[this.point.y] + " season.";
    }
},    


Now see what happens when you mouse over a square.




And if you mouse over a black square, it gives you this!




That was easy, eh? Highcharts takes away most of the hassle. Pity the commercial version is so darn expensive.

With heat and passion,
T___T

No comments:

Post a Comment