Repository: gonum/plot
Pull request #631 (2020-07-25 09:49:12)

plotter/heat: Add RasterHeatMap
Please take a look.

This PR is based on the discussion [gonum-dev group](https://groups.google.com/g/gonum-dev/c/TmINmbBZXZc). This PR implements a new heat map type (RasterHeatMap). When plotting a heat map it first creates a raster image and then plots the image. By doing so we avoid creating an artificial grid. Below is an example of a result produced using the new heatmap

![demoRaster](https://user-images.githubusercontent.com/13123407/88454032-a8159d00-ce6c-11ea-89ff-279ef6dc894f.png)


I am only familiar with gonum/plot as a user and admittedly I don't know all the internal concepts of the package well. Therefore, I am not sure if I made the most consistent choices. In particular, should there be two heat map types or should there be an option in the current heatmap type to enable/disable raster-based drawing?

Code used to produce the plot

```go
package main

import (
	"fmt"
	"math"

	"gonum.org/v1/plot"
	"gonum.org/v1/plot/palette/moreland"
	"gonum.org/v1/plot/plotter"
)

// Gaussian represents a gaussian sampled on a "virtual" grid
// of dimension NxN.
type Gaussian struct {
	WidthX float64
	WidthY float64
	N      int
}

// X returns the x coordinate corresponding to column c
func (g *Gaussian) X(c int) float64 {
	return float64(c) - float64(g.N/2)
}

// Y returns the y coordinate corresponding to row r
func (g *Gaussian) Y(r int) float64 {
	return float64(r) - float64(g.N/2)
}

// Z returns the value correspondin
func (g *Gaussian) Z(c, r int) float64 {
	exponent := g.X(c)*g.X(c)/(g.WidthX*g.WidthX) + g.Y(r)*g.Y(r)/(g.WidthY*g.WidthY)
	return math.Exp(-exponent)
}

// Dims returns the dimension of the "virtual" grid
func (g *Gaussian) Dims() (int, int) {
	return g.N, g.N
}

func main() {
	data := Gaussian{
		WidthX: 10.0,
		WidthY: 30.0,
		N:      100,
	}

	pal := moreland.Kindlmann().Palette(255)
	plt, _ := plot.New()
	raster := plotter.NewRasterHeatMap(&data, pal)
	plt.Add(raster)
	plt.Save(200, 200, "demoRaster.png")
	fmt.Printf("Picture sucsessfully written\n")
}

```


sbinet: Ping @kortschak
davidkleiven: I added a `Rasterized` flag to the `HeatMap` type and removed the new type.
davidkleiven: @sbinet I reverted the dimension of the image from `5*vg.Centimer, 3.5*vg.Centimeter` to the original `250, 175` to make the test pass. Should I have upgraded the image it is compared against instead?
kortschak: I think this is fine as it is; it reflects the code in the other heat map example.

Patch:
diff --git a/plotter/heat.go b/plotter/heat.go
index c33103e..facdb46 100644
--- a/plotter/heat.go
+++ b/plotter/heat.go
@@ -5,6 +5,7 @@
 package plotter
 
 import (
+	"image"
 	"image/color"
 	"math"
 
@@ -57,6 +58,10 @@ type HeatMap struct {
 	// Min and Max define the dynamic range of the
 	// heat map.
 	Min, Max float64
+
+	// Rasterized indicates whether the heatmap
+	// should be produced using raster-based drawing.
+	Rasterized bool
 }
 
 // NewHeatMap creates as new heat map plotter for the given data,
@@ -98,6 +103,50 @@ func NewHeatMap(g GridXYZ, p palette.Palette) *HeatMap {
 
 // Plot implements the Plot method of the plot.Plotter interface.
 func (h *HeatMap) Plot(c draw.Canvas, plt *plot.Plot) {
+	if h.Rasterized {
+		h.plotRasterized(c, plt)
+	} else {
+		h.plotVectorized(c, plt)
+	}
+}
+
+// plotRasterized plots the heatmap using raster-based drawing.
+func (h *HeatMap) plotRasterized(c draw.Canvas, plt *plot.Plot) {
+	cols, rows := h.GridXYZ.Dims()
+	img := image.NewRGBA64(image.Rectangle{
+		Min: image.Point{X: 0, Y: 0},
+		Max: image.Point{X: cols, Y: rows},
+	})
+
+	pal := h.Palette.Colors()
+	ps := float64(len(pal)-1) / (h.Max - h.Min)
+	for i := 0; i < cols; i++ {
+		for j := 0; j < rows; j++ {
+			var col color.Color
+			switch v := h.GridXYZ.Z(i, j); {
+			case v < h.Min:
+				col = h.Underflow
+			case v > h.Max:
+				col = h.Overflow
+			case math.IsNaN(v), math.IsInf(ps, 0):
+				col = h.NaN
+			default:
+				col = pal[int((v-h.Min)*ps+0.5)] // Apply palette scaling.
+			}
+
+			if col != nil {
+				img.Set(i, j, col)
+			}
+		}
+	}
+
+	xmin, xmax, ymin, ymax := h.DataRange()
+	pImg := NewImage(img, xmin, ymin, xmax, ymax)
+	pImg.Plot(c, plt)
+}
+
+//  plotVectorized plots the heatmap using vector-based drawing.
+func (h *HeatMap) plotVectorized(c draw.Canvas, plt *plot.Plot) {
 	if h.Min > h.Max {
 		panic("contour: invalid Z range: min greater than max")
 	}


Test patch:
diff --git a/plotter/example_heat_rasterized_test.go b/plotter/example_heat_rasterized_test.go
new file mode 100644
index 0000000..3054272
--- /dev/null
+++ b/plotter/example_heat_rasterized_test.go
@@ -0,0 +1,40 @@
+// Copyright ©2020 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package plotter_test
+
+import (
+	"log"
+
+	"gonum.org/v1/gonum/mat"
+	"gonum.org/v1/plot"
+	"gonum.org/v1/plot/palette"
+	"gonum.org/v1/plot/plotter"
+)
+
+func ExampleHeatMap_rasterized() {
+	m := offsetUnitGrid{
+		XOffset: -2,
+		YOffset: -1,
+		Data: mat.NewDense(3, 4, []float64{
+			1, 2, 3, 4,
+			5, 6, 7, 8,
+			9, 10, 11, 12,
+		})}
+
+	pal := palette.Heat(12, 1)
+	plt, err := plot.New()
+	if err != nil {
+		log.Panic(err)
+	}
+
+	raster := plotter.NewHeatMap(&m, pal)
+	raster.Rasterized = true
+	plt.Add(raster)
+
+	err = plt.Save(250, 175, "testdata/rasterHeatMap.png")
+	if err != nil {
+		log.Panic(err)
+	}
+}
diff --git a/plotter/heat_test.go b/plotter/heat_test.go
index f363eff..f4e243a 100644
--- a/plotter/heat_test.go
+++ b/plotter/heat_test.go
@@ -90,3 +90,7 @@ func TestHeatMapDims(t *testing.T) {
 		}()
 	}
 }
+
+func TestRasterHeatMap(t *testing.T) {
+	cmpimg.CheckPlot(ExampleHeatMap_rasterized, t, "rasterHeatMap.png")
+}