Create Heatmaps in PHP (and other languages)

I’ve added new heatmaps to player and goalie pages. I am using the same information as the zone charts and just displaying it in a different way.

There are three steps to creating heatmaps.

  1. Create a gradient
  2. Create an alpha image of the heatmap (I’ll explain what that is later)
  3. Using the alpha image, colour points on the heatmap using the gradient

I did this in PHP, but can easily be adapted to other languages.

Creating our Gradient

So, what’s the gradient for? The gradient is going to determine what is hot and cold in our heatmap. I’ve chosen the most common colours for the heatmap: blue, green, yellow and red. We’re also going to increase the opacity over the course of the gradient. Our gradient image will be 256 pixels wide. It should become apparent why later.

Defining Our Colour Transitions

$gradientColours = [
    'blue' => [
        'red'   => 0,
        'green' => 0,
        'blue'  => 255,
    ],
    'green' => [
        'red'   => 0,
        'green' => 255,
        'blue'  => 0,
    ],
    'yellow' => [
        'red'   => 255,
        'green' => 255,
        'blue'  => 0
    ],
    'red' => [
        'red'   => 255,
        'green' => 0,
        'blue'  => 0
    ],
    'white' => [
        'red'   => 255,
        'green' => 255,
        'blue'  => 255
    ]
];

$transitions = [
    [
        'start' => 0,
        'end' => 128,
        'startColour' => $gradientColours['blue'],
        'endColour' => $gradientColours['green']
    ],
    [
        'start' => 128,
        'end' => 192,
        'startColour' => $gradientColours['green'],
        'endColour' => $gradientColours['yellow']
    ],
    [
        'start' => 192,
        'end' => 240,
        'startColour' => $gradientColours['yellow'],
        'endColour' => $gradientColours['red']
    ],
    [
        'start' => 240,
        'end' => 256,
        'startColour' => $gradientColours['red'],
        'endColour' => $gradientColours['white']
    ]
];

Feel free to re-organize how you’re storing colours. Perhaps using Symfony’s ParameterBag¬†if you’re using PHP or just create a class of your own in other languages.

So we’ve defined our colours and how our gradient should behave, let’s actually generate it. I’ve added inline comments to the code. I’m going to be using linear interpolation¬†here.

You’ll notice I am using something very similar to the code found on that Wiki page.

float lerp(float v0, float v1, float t) {
  return (1 - t) * v0 + t * v1;
}

 

// store our gradient colours in an array
$gradients = [];

foreach ($transitions as $transition) {
    // create easy access vars
    $start     = $transition['start'];
    $end       = $transition['end'];

    // this is important, how many steps should it take to switch from one colour to the other
    $steps     = $end - $start;
    $colourOne = $transition['startColour'];
    $colourTwo = $transition['endColour'];

    for ($i = 0; $i < $steps; $i++) { 
        // our t value for linear interpolation
        $t = $i / $steps; 
        // perform linear interpolation on all the colours 
        $r = $colourTwo['red'] * $t + $colourOne['red'] * (1 - $t); 
        $g = $colourTwo['green'] * $t + $colourOne['green'] * (1 - $t); 
        $b = $colourTwo['blue'] * $t + $colourOne['blue'] * (1 - $t); 

        // set our opacity (in php 127 = opaque), so multiply 127 by the percentage of how far along we are in the gradient 
        $a = 127 - (($i + $start) / 255 * 127); 

        // save 
        $gradients[$i + $start] = [
            'red' => $r,
            'green' => $g,
            'blue'  => $b,
            'alpha' => $a
        ];
    }
}

This code generating the gradient should not change, so feel free to actually generate an image and use imagesetpixel to cache your gradient image. You will need to set imagealphablending to false and imagesavealpha to true to keep your opacity.

Creating the Heatmap Alpha

What is the heatmap alpha? The heatmap alpha is going to determine how intense each pixel is on our heatmap. We are going to generate this by create semi-transparent black circles on a separate image. It is important that this image is separate because we don’t want your background picture (if you’re using one) to affect the intensity of each point. For instance, I don’t want the blue line in my rink background to affect the intensity.

$heatmapAlpha = imagecreatetruecolor($width, $height);
$white = imagecolorallocate($heatmapAlpha, 255, 255, 255);
imagefilledrectangle($heatmapAlpha, 0, 0, $width - 1, $height - 1, $white);

We’ve created an image and set the entire background to white. For the purposes of the tutorial let’s add some random points.

$width = 300;
$height = 100;
$points = [];

for ($i = 0; $i < 20; $i++) {
    $x = rand(0, $width); 
    $y = rand(0, $height); 
    $points[] = ['x' => $x, 'y' => $y];
}

Now, for every point we’re going to draw several circles.

// 92% opacity point
$black = imagecolorallocatealpha($heatmapAlpha, 0, 0, 0, 127 * 0.92);
$radius = 15;

// this will create black circles that have some transparency
// the idea is many of these transparent circles on top of each other
// will slowly create a more opaque spot (a more intense spot)
foreach ($points as $point) {
    for ($r = $radius; $r > 0; $r--) {
        imagefilledellipse($heatmapAlpha, $point['x'], $point['y'], $r, $r, $black);
    }
}

So there will be 15 circles, each one smaller than the next, with 92% opacity. So the data point will be the most intense.

And finally, add the telltale gaussian blur to your heatmap alpha.

imagefilter($heatmapAlpha, IMG_FILTER_GAUSSIAN_BLUR);

Draw the Heatmap

Now, for every point (x, y) we will set the colour at that point on a new image, based on the intensity of that point on the alpha image.

$heatmap = imagecreatetruecolor($width, $height);
imagesavealpha($heatmap, true);

for ($x = 0; $x < $width; $x++) {
    for ($y = 0; $y < $height; $y++) {
        // get intensity from heatmapAlpha
        $intensity = imagecolorsforindex($heatmapAlpha, imagecolorat($heatmapAlpha, $x, $y));

        // our mask colour, continue to next pixel
        if ($intensity['red'] == 255 && $intensity['green'] == 255 && $intensity['blue'] == 255) {
            continue;
        }

        // using the intensity get the colour
        $gradientColour = $gradients[255 - $intensity['red']];

        // create the colour
        $colour = imagecolorallocatealpha($heatmap, $gradientColour['red'], $gradientColour['green'], $gradientColour['blue'], $gradientColour['alpha']);

        // set the colour at that pixel
        imagesetpixel($heatmap, $x, $y, $colour);
    }
}

imagedestroy($heatmapAlpha);

imagepng($heatmap, '/save/heatmap/to/file.png');
imagedestroy($heatmap);

Here is where our gradient image width comes into play. Because the hex values (RGB) go from 0 – 255 our gradient is 256 pixels wide. We also know that because the heatmap alpha is in greyscale all RGB values should be the same.

You’ll also note I am taking the alpha (opacity) from the gradient as well. If you have an image underneath the heatmap, feel free to multiply $gradientColour[‘alpha’] by a number less than one, to see the background shine through the heatmap.

Hopefully, I explained this well enough and you get something that looks a little like this:

Auston Matthews goal locations

You can see the gist here

Leave a Comment

Your email address will not be published. Required fields are marked *

*
*

This site uses Akismet to reduce spam. Learn how your comment data is processed.