Generating heatmaps with Go
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:
Dims() int
: returns the column and row count of our 2D array.Z(c, r int) float64
: returns the actual value stored in the positions of thec
andr
indices, e.g.dataset[c][r]
X(c int) float64
: returns the value we want to use for the X axis at columnc
Y(r int) float64
: returns the value we want to use for the Y axis at rowr
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:
- The resolution of our grid, in this case 0.5 (in this example we assume the same resolution in both X and Y directions)
minX
andminY
representing what physical coordinates the 0, 0 cell should assume- and
N
andM
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!