1
2
3
4
5 package plotter
6
7 import (
8 "image"
9 "image/color"
10 "math"
11
12 "gonum.org/v1/plot"
13 "gonum.org/v1/plot/palette"
14 "gonum.org/v1/plot/vg"
15 "gonum.org/v1/plot/vg/draw"
16 )
17
18
19
20 type GridXYZ interface {
21
22 Dims() (c, r int)
23
24
25
26 Z(c, r int) float64
27
28
29
30 X(c int) float64
31
32
33
34 Y(r int) float64
35 }
36
37
38
39 type HeatMap struct {
40 GridXYZ GridXYZ
41
42
43
44
45 Palette palette.Palette
46
47
48
49
50 Underflow color.Color
51 Overflow color.Color
52
53
54
55
56 NaN color.Color
57
58
59
60 Min, Max float64
61
62
63
64 Rasterized bool
65 }
66
67
68
69
70
71
72 func NewHeatMap(g GridXYZ, p palette.Palette) *HeatMap {
73 var min, max float64
74 type minMaxer interface {
75 Min() float64
76 Max() float64
77 }
78 switch g := g.(type) {
79 case minMaxer:
80 min, max = g.Min(), g.Max()
81 default:
82 min, max = math.Inf(1), math.Inf(-1)
83 c, r := g.Dims()
84 for i := 0; i < c; i++ {
85 for j := 0; j < r; j++ {
86 v := g.Z(i, j)
87 if math.IsNaN(v) {
88 continue
89 }
90 min = math.Min(min, v)
91 max = math.Max(max, v)
92 }
93 }
94 }
95
96 return &HeatMap{
97 GridXYZ: g,
98 Palette: p,
99 Min: min,
100 Max: max,
101 }
102 }
103
104
105 func (h *HeatMap) Plot(c draw.Canvas, plt *plot.Plot) {
106 if h.Rasterized {
107 h.plotRasterized(c, plt)
108 } else {
109 h.plotVectorized(c, plt)
110 }
111 }
112
113
114 func (h *HeatMap) plotRasterized(c draw.Canvas, plt *plot.Plot) {
115 cols, rows := h.GridXYZ.Dims()
116 img := image.NewRGBA64(image.Rectangle{
117 Min: image.Point{X: 0, Y: 0},
118 Max: image.Point{X: cols, Y: rows},
119 })
120
121 pal := h.Palette.Colors()
122 ps := float64(len(pal)-1) / (h.Max - h.Min)
123 for i := 0; i < cols; i++ {
124 for j := 0; j < rows; j++ {
125 var col color.Color
126 switch v := h.GridXYZ.Z(i, j); {
127 case v < h.Min:
128 col = h.Underflow
129 case v > h.Max:
130 col = h.Overflow
131 case math.IsNaN(v), math.IsInf(ps, 0):
132 col = h.NaN
133 default:
134 col = pal[int((v-h.Min)*ps+0.5)]
135 }
136
137 if col != nil {
138 img.Set(i, rows-j-1, col)
139 }
140 }
141 }
142
143 xmin, xmax, ymin, ymax := h.DataRange()
144 pImg := NewImage(img, xmin, ymin, xmax, ymax)
145 pImg.Plot(c, plt)
146 }
147
148
149 func (h *HeatMap) plotVectorized(c draw.Canvas, plt *plot.Plot) {
150 if h.Min > h.Max {
151 panic("contour: invalid Z range: min greater than max")
152 }
153 pal := h.Palette.Colors()
154 if len(pal) == 0 {
155 panic("heatmap: empty palette")
156 }
157
158 ps := float64(len(pal)-1) / (h.Max - h.Min)
159
160 trX, trY := plt.Transforms(&c)
161
162 var pa vg.Path
163 cols, rows := h.GridXYZ.Dims()
164 for i := 0; i < cols; i++ {
165 var right, left float64
166 switch i {
167 case 0:
168 if cols == 1 {
169 right = 0.5
170 } else {
171 right = (h.GridXYZ.X(1) - h.GridXYZ.X(0)) / 2
172 }
173 left = -right
174 case cols - 1:
175 right = (h.GridXYZ.X(cols-1) - h.GridXYZ.X(cols-2)) / 2
176 left = -right
177 default:
178 right = (h.GridXYZ.X(i+1) - h.GridXYZ.X(i)) / 2
179 left = -(h.GridXYZ.X(i) - h.GridXYZ.X(i-1)) / 2
180 }
181
182 for j := 0; j < rows; j++ {
183 var up, down float64
184 switch j {
185 case 0:
186 if rows == 1 {
187 up = 0.5
188 } else {
189 up = (h.GridXYZ.Y(1) - h.GridXYZ.Y(0)) / 2
190 }
191 down = -up
192 case rows - 1:
193 up = (h.GridXYZ.Y(rows-1) - h.GridXYZ.Y(rows-2)) / 2
194 down = -up
195 default:
196 up = (h.GridXYZ.Y(j+1) - h.GridXYZ.Y(j)) / 2
197 down = -(h.GridXYZ.Y(j) - h.GridXYZ.Y(j-1)) / 2
198 }
199
200 x, y := trX(h.GridXYZ.X(i)+left), trY(h.GridXYZ.Y(j)+down)
201 dx, dy := trX(h.GridXYZ.X(i)+right), trY(h.GridXYZ.Y(j)+up)
202
203 if !c.Contains(vg.Point{X: x, Y: y}) || !c.Contains(vg.Point{X: dx, Y: dy}) {
204 continue
205 }
206
207 pa = pa[:0]
208 pa.Move(vg.Point{X: x, Y: y})
209 pa.Line(vg.Point{X: dx, Y: y})
210 pa.Line(vg.Point{X: dx, Y: dy})
211 pa.Line(vg.Point{X: x, Y: dy})
212 pa.Close()
213
214 var col color.Color
215 switch v := h.GridXYZ.Z(i, j); {
216 case v < h.Min:
217 col = h.Underflow
218 case v > h.Max:
219 col = h.Overflow
220 case math.IsNaN(v), math.IsInf(ps, 0):
221 col = h.NaN
222 default:
223 col = pal[int((v-h.Min)*ps+0.5)]
224 }
225 if col != nil {
226 c.SetColor(col)
227 c.Fill(pa)
228 }
229 }
230 }
231 }
232
233
234
235 func (h *HeatMap) DataRange() (xmin, xmax, ymin, ymax float64) {
236 c, r := h.GridXYZ.Dims()
237 switch c {
238 case 1:
239 xmax = h.GridXYZ.X(0) + 0.5
240 xmin = h.GridXYZ.X(0) - 0.5
241 default:
242 xmax = h.GridXYZ.X(c-1) + (h.GridXYZ.X(c-1)-h.GridXYZ.X(c-2))/2
243 xmin = h.GridXYZ.X(0) - (h.GridXYZ.X(1)-h.GridXYZ.X(0))/2
244 }
245 switch r {
246 case 1:
247 ymax = h.GridXYZ.Y(0) + 0.5
248 ymin = h.GridXYZ.Y(0) - 0.5
249 default:
250 ymax = h.GridXYZ.Y(r-1) + (h.GridXYZ.Y(r-1)-h.GridXYZ.Y(r-2))/2
251 ymin = h.GridXYZ.Y(0) - (h.GridXYZ.Y(1)-h.GridXYZ.Y(0))/2
252 }
253 return xmin, xmax, ymin, ymax
254 }
255
256
257
258 func (h *HeatMap) GlyphBoxes(plt *plot.Plot) []plot.GlyphBox {
259 c, r := h.GridXYZ.Dims()
260 b := make([]plot.GlyphBox, 0, r*c)
261 for i := 0; i < c; i++ {
262 for j := 0; j < r; j++ {
263 b = append(b, plot.GlyphBox{
264 X: plt.X.Norm(h.GridXYZ.X(i)),
265 Y: plt.Y.Norm(h.GridXYZ.Y(j)),
266 Rectangle: vg.Rectangle{
267 Min: vg.Point{X: -5, Y: -5},
268 Max: vg.Point{X: +5, Y: +5},
269 },
270 })
271 }
272 }
273 return b
274 }
275
View as plain text