Generating heatmaps with Go

Balazs Dianiska
4 min readJul 18, 2019

--

At Evotrex we build IoT solutions for Smart Warehousing, and we are using Go to power several of our key applications and microservices. And although we have enjoyed the benefits of having most of our services in a single language, there was one area where we still looked back at particular tools and weeped. And this was data visualisation.

Producing data heavy output (such as 3D vector fields with many important statistical properties) means we have to have the tools to validate it. With experienced Python programmers on the team it is often not trivial to decide if we want to stay in Go-land for a particular piece of data analysis tool.

This is when gonum/plot came into the picture. Gonum already enabled us to replicate several of our numerical calculations from NumPy and specialised C++ utilities into Go, however we desperately needed the right way of visualising our datasets. It is not obvious at first, that gonum/plot supports plotting 2D data as well apart from the Bubbles plotter as showcased in the wiki: there are two more plotters in code: Heatmap and Field. In this first post I will provide an example on how to use the former.

Data structure

Our goal is to visualise a 2D array, which in this case will have a single float64 value associated to each cell. In the following example I will use var dataset [][]float64 as the source of our data, which we want to plot.

As with all gonum/plot plotters, we need to store our data in a struct, which satisfies the expected interface, so the plotter can visualise it. The base data structure for plotter/heatmap is the GridXYZ interface, which has the following methods:

  1. Dims() int: returns the column and row count of our 2D array.
  2. Z(c, r int) float64: returns the actual value stored in the positions of the cand r indices, e.g. dataset[c][r]
  3. X(c int) float64: returns the value we want to use for the X axis at column c
  4. Y(r int) float64: returns the value we want to use for the Y axis at row r

While Z(c, r int) float64 is probably obvious, it is worthwhile to spend a minute to understand what the functions X and Y are doing. In a typical scenario you are plotting data, where the X and Y coordinates represent a quantity, e.g. they are width and height data. It is up to you, who defines this dataset to give the integer row and column data meaning, which is done through these methods.

In addition to the dataset to plot one needs to define the colour range, to visualise the range of values with. The gonum.org/v1/plot/palette package provides this quite nicely, and you can find a full range of palettes to chose from here: https://godoc.org/gonum.org/v1/plot/palette/brewer and here: https://godoc.org/gonum.org/v1/plot/palette/moreland.

Storing real-world data

Let’s build a heatmap for a grid, which represents a scalar value on a 0.5m x 0.5m physical grid, like a map of elevation. First we define our data storage structure, which will conform to the GridXYZ interface.

type plottable struct {
grid [][]float64
N int
M int
resolution float64
minX float64
minY float64
}

As you can see due to the physical interpretation of the basic grid we have to enable this struct with relevant parameters:

  1. The resolution of our grid, in this case 0.5 (in this example we assume the same resolution in both X and Y directions)
  2. minX and minY representing what physical coordinates the 0, 0 cell should assume
  3. and N and M to hold our width and height information, moved here into the struct for convenience mainly

Implementing the interface

Our plottable struct must implement the GridXYZ interface so we can eventually pass it into the NewHeatMap() constructor:

func (p plottable) Dims() (c, r int) {
return p.N, p.M
}
func (p plottable) X(c int) float64 {
return p.minX + float64(c)*p.resolution
}
func (p plottable) Y(r int) float64 {
return p.minY + float64(r)*p.resolution
}
func (p plottable) Z(c, r int) float64 {
return p.grid[c][r]
}

Thus the X and Y indices are now associated with the right meaningful values on our plot.

Plotting

The eventual plotting is now simple. Create an instance of the plottable struct, populate the grid, N and M fields (or use len()) and invoke the plotter as usual. If our initial dataset was var dataset [][]float64, then the following code will build a nice heatmap, which starts at (-0.5; 42.0) and has a resolution of 0.5:

plotData := plottable{
grid: dataset,
N: cols,
M: rows,
minX: -0.5,
minY: 42.0,
resolution: 0.5,
}
pal := moreland.SmoothBlueRed().Palette(255)
hm := plotter.NewHeatMap(plotData, pal)

Enjoy!

--

--