dbtester/vendor/github.com/gonum/plot/plotter/heat.go

218 lines
5.3 KiB
Go

// Copyright ©2015 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
import (
"image/color"
"math"
"github.com/gonum/plot"
"github.com/gonum/plot/palette"
"github.com/gonum/plot/vg"
"github.com/gonum/plot/vg/draw"
)
// GridXYZ describes three dimensional data where the X and Y
// coordinates are arranged on a rectangular grid.
type GridXYZ interface {
// Dims returns the dimensions of the grid.
Dims() (c, r int)
// Z returns the value of a grid value at (c, r).
// It will panic if c or r are out of bounds for the grid.
Z(c, r int) float64
// X returns the coordinate for the column at the index x.
// It will panic if c is out of bounds for the grid.
X(c int) float64
// Y returns the coordinate for the row at the index r.
// It will panic if r is out of bounds for the grid.
Y(r int) float64
}
// HeatMap implements the Plotter interface, drawing
// a heat map of the values in the GridXYZ field.
type HeatMap struct {
GridXYZ GridXYZ
// Palette is the color palette used to render
// the heat map. Palette must not be nil or
// return a zero length []color.Color.
Palette palette.Palette
// Underflow and Overflow are colors used to fill
// heat map elements outside the dynamic range
// defined by Min and Max.
Underflow color.Color
Overflow color.Color
// Min and Max define the dynamic range of the
// heat map.
Min, Max float64
}
// NewHeatMap creates as new heat map plotter for the given data,
// using the provided palette. If g has Min and Max methods that return
// a float, those returned values are used to set the respective HeatMap
// fields. If the returned HeatMap is used when Min is greater than or
// equal to Max, the Plot method will panic.
func NewHeatMap(g GridXYZ, p palette.Palette) *HeatMap {
var min, max float64
type minMaxer interface {
Min() float64
Max() float64
}
switch g := g.(type) {
case minMaxer:
min, max = g.Min(), g.Max()
default:
min, max = math.Inf(1), math.Inf(-1)
c, r := g.Dims()
for i := 0; i < c; i++ {
for j := 0; j < r; j++ {
v := g.Z(i, j)
if math.IsNaN(v) {
continue
}
min = math.Min(min, v)
max = math.Max(max, v)
}
}
}
return &HeatMap{
GridXYZ: g,
Palette: p,
Min: min,
Max: max,
}
}
// Plot implements the Plot method of the plot.Plotter interface.
func (h *HeatMap) Plot(c draw.Canvas, plt *plot.Plot) {
if h.Min >= h.Max {
panic("heatmap: non-positive Z range")
}
pal := h.Palette.Colors()
if len(pal) == 0 {
panic("heatmap: empty palette")
}
// ps scales the palette uniformly across the data range.
ps := float64(len(pal)-1) / (h.Max - h.Min)
trX, trY := plt.Transforms(&c)
var pa vg.Path
cols, rows := h.GridXYZ.Dims()
for i := 0; i < cols; i++ {
var right, left float64
switch i {
case 0:
right = (h.GridXYZ.X(i+1) - h.GridXYZ.X(i)) / 2
left = -right
case cols - 1:
right = (h.GridXYZ.X(i) - h.GridXYZ.X(i-1)) / 2
left = -right
default:
right = (h.GridXYZ.X(i+1) - h.GridXYZ.X(i)) / 2
left = -(h.GridXYZ.X(i) - h.GridXYZ.X(i-1)) / 2
}
for j := 0; j < rows; j++ {
v := h.GridXYZ.Z(i, j)
if math.IsNaN(v) || math.IsInf(v, 0) {
continue
}
pa = pa[:0]
var up, down float64
switch j {
case 0:
up = (h.GridXYZ.Y(j+1) - h.GridXYZ.Y(j)) / 2
down = -up
case rows - 1:
up = (h.GridXYZ.Y(j) - h.GridXYZ.Y(j-1)) / 2
down = -up
default:
up = (h.GridXYZ.Y(j+1) - h.GridXYZ.Y(j)) / 2
down = -(h.GridXYZ.Y(j) - h.GridXYZ.Y(j-1)) / 2
}
x, y := trX(h.GridXYZ.X(i)+left), trY(h.GridXYZ.Y(j)+down)
dx, dy := trX(h.GridXYZ.X(i)+right), trY(h.GridXYZ.Y(j)+up)
if !c.Contains(vg.Point{X: x, Y: y}) || !c.Contains(vg.Point{X: dx, Y: dy}) {
continue
}
pa.Move(vg.Point{X: x, Y: y})
pa.Line(vg.Point{X: dx, Y: y})
pa.Line(vg.Point{X: dx, Y: dy})
pa.Line(vg.Point{X: x, Y: dy})
pa.Close()
var col color.Color
switch {
case v < h.Min:
col = h.Underflow
case v > h.Max:
col = h.Overflow
default:
col = pal[int((v-h.Min)*ps+0.5)] // Apply palette scaling.
}
if col != nil {
c.SetColor(col)
c.Fill(pa)
}
}
}
}
// DataRange implements the DataRange method
// of the plot.DataRanger interface.
func (h *HeatMap) DataRange() (xmin, xmax, ymin, ymax float64) {
c, r := h.GridXYZ.Dims()
switch c {
case 1: // Make a unit length when there is no neighbour.
xmax = 0.5
xmin = -0.5
default:
xmax = h.GridXYZ.X(c-1) + (h.GridXYZ.X(c-1)-h.GridXYZ.X(c-2))/2
xmin = h.GridXYZ.X(0) - (h.GridXYZ.X(1)-h.GridXYZ.X(0))/2
}
switch r {
case 1: // Make a unit length when there is no neighbour.
ymax = 0.5
ymin = -0.5
default:
ymax = h.GridXYZ.Y(r-1) + (h.GridXYZ.Y(r-1)-h.GridXYZ.Y(r-2))/2
ymin = h.GridXYZ.Y(0) - (h.GridXYZ.Y(1)-h.GridXYZ.Y(0))/2
}
return xmin, xmax, ymin, ymax
}
// GlyphBoxes implements the GlyphBoxes method
// of the plot.GlyphBoxer interface.
func (h *HeatMap) GlyphBoxes(plt *plot.Plot) []plot.GlyphBox {
c, r := h.GridXYZ.Dims()
b := make([]plot.GlyphBox, 0, r*c)
for i := 0; i < c; i++ {
for j := 0; j < r; j++ {
b = append(b, plot.GlyphBox{
X: plt.X.Norm(h.GridXYZ.X(i)),
Y: plt.Y.Norm(h.GridXYZ.Y(j)),
Rectangle: vg.Rectangle{
Min: vg.Point{X: -5, Y: -5},
Max: vg.Point{X: +5, Y: +5},
},
})
}
}
return b
}