1 | 'use strict'
|
2 |
|
3 | module.exports = createHeatmap2D
|
4 |
|
5 | var bsearch = require('binary-search-bounds')
|
6 | var iota = require('iota-array')
|
7 | var pool = require('typedarray-pool')
|
8 | var createShader = require('gl-shader')
|
9 | var createBuffer = require('gl-buffer')
|
10 |
|
11 | var shaders = require('./lib/shaders')
|
12 |
|
13 | function GLHeatmap2D (
|
14 | plot,
|
15 | shader,
|
16 | pickShader,
|
17 | positionBuffer,
|
18 | weightBuffer,
|
19 | colorBuffer,
|
20 | idBuffer) {
|
21 | this.plot = plot
|
22 | this.shader = shader
|
23 | this.pickShader = pickShader
|
24 | this.positionBuffer = positionBuffer
|
25 | this.weightBuffer = weightBuffer
|
26 | this.colorBuffer = colorBuffer
|
27 | this.idBuffer = idBuffer
|
28 | this.xData = []
|
29 | this.yData = []
|
30 | this.shape = [0, 0]
|
31 | this.bounds = [Infinity, Infinity, -Infinity, -Infinity]
|
32 | this.pickOffset = 0
|
33 | }
|
34 |
|
35 | var proto = GLHeatmap2D.prototype
|
36 |
|
37 | var WEIGHTS = [
|
38 | 0, 0,
|
39 | 1, 0,
|
40 | 0, 1,
|
41 | 1, 0,
|
42 | 1, 1,
|
43 | 0, 1
|
44 | ]
|
45 |
|
46 | proto.draw = (function () {
|
47 | var MATRIX = [
|
48 | 1, 0, 0,
|
49 | 0, 1, 0,
|
50 | 0, 0, 1
|
51 | ]
|
52 |
|
53 | return function () {
|
54 | var plot = this.plot
|
55 | var shader = this.shader
|
56 | var bounds = this.bounds
|
57 | var numVertices = this.numVertices
|
58 |
|
59 | if (numVertices <= 0) {
|
60 | return
|
61 | }
|
62 |
|
63 | var gl = plot.gl
|
64 | var dataBox = plot.dataBox
|
65 |
|
66 | var boundX = bounds[2] - bounds[0]
|
67 | var boundY = bounds[3] - bounds[1]
|
68 | var dataX = dataBox[2] - dataBox[0]
|
69 | var dataY = dataBox[3] - dataBox[1]
|
70 |
|
71 | MATRIX[0] = 2.0 * boundX / dataX
|
72 | MATRIX[4] = 2.0 * boundY / dataY
|
73 | MATRIX[6] = 2.0 * (bounds[0] - dataBox[0]) / dataX - 1.0
|
74 | MATRIX[7] = 2.0 * (bounds[1] - dataBox[1]) / dataY - 1.0
|
75 |
|
76 | shader.bind()
|
77 |
|
78 | var uniforms = shader.uniforms
|
79 | uniforms.viewTransform = MATRIX
|
80 |
|
81 | uniforms.shape = this.shape
|
82 |
|
83 | var attributes = shader.attributes
|
84 | this.positionBuffer.bind()
|
85 | attributes.position.pointer()
|
86 |
|
87 | this.weightBuffer.bind()
|
88 | attributes.weight.pointer(gl.UNSIGNED_BYTE, false)
|
89 |
|
90 | this.colorBuffer.bind()
|
91 | attributes.color.pointer(gl.UNSIGNED_BYTE, true)
|
92 |
|
93 | gl.drawArrays(gl.TRIANGLES, 0, numVertices)
|
94 | }
|
95 | })()
|
96 |
|
97 | proto.drawPick = (function () {
|
98 | var MATRIX = [
|
99 | 1, 0, 0,
|
100 | 0, 1, 0,
|
101 | 0, 0, 1
|
102 | ]
|
103 |
|
104 | var PICK_VECTOR = [0, 0, 0, 0]
|
105 |
|
106 | return function (pickOffset) {
|
107 | var plot = this.plot
|
108 | var shader = this.pickShader
|
109 | var bounds = this.bounds
|
110 | var numVertices = this.numVertices
|
111 |
|
112 | if (numVertices <= 0) {
|
113 | return
|
114 | }
|
115 |
|
116 | var gl = plot.gl
|
117 | var dataBox = plot.dataBox
|
118 |
|
119 | var boundX = bounds[2] - bounds[0]
|
120 | var boundY = bounds[3] - bounds[1]
|
121 | var dataX = dataBox[2] - dataBox[0]
|
122 | var dataY = dataBox[3] - dataBox[1]
|
123 |
|
124 | MATRIX[0] = 2.0 * boundX / dataX
|
125 | MATRIX[4] = 2.0 * boundY / dataY
|
126 | MATRIX[6] = 2.0 * (bounds[0] - dataBox[0]) / dataX - 1.0
|
127 | MATRIX[7] = 2.0 * (bounds[1] - dataBox[1]) / dataY - 1.0
|
128 |
|
129 | for (var i = 0; i < 4; ++i) {
|
130 | PICK_VECTOR[i] = (pickOffset >> (i * 8)) & 0xff
|
131 | }
|
132 |
|
133 | this.pickOffset = pickOffset
|
134 |
|
135 | shader.bind()
|
136 |
|
137 | var uniforms = shader.uniforms
|
138 | uniforms.viewTransform = MATRIX
|
139 | uniforms.pickOffset = PICK_VECTOR
|
140 | uniforms.shape = this.shape
|
141 |
|
142 | var attributes = shader.attributes
|
143 | this.positionBuffer.bind()
|
144 | attributes.position.pointer()
|
145 |
|
146 | this.weightBuffer.bind()
|
147 | attributes.weight.pointer(gl.UNSIGNED_BYTE, false)
|
148 |
|
149 | this.idBuffer.bind()
|
150 | attributes.pickId.pointer(gl.UNSIGNED_BYTE, false)
|
151 |
|
152 | gl.drawArrays(gl.TRIANGLES, 0, numVertices)
|
153 |
|
154 | return pickOffset + this.shape[0] * this.shape[1]
|
155 | }
|
156 | })()
|
157 |
|
158 | proto.pick = function (x, y, value) {
|
159 | var pickOffset = this.pickOffset
|
160 | var pointCount = this.shape[0] * this.shape[1]
|
161 | if (value < pickOffset || value >= pickOffset + pointCount) {
|
162 | return null
|
163 | }
|
164 | var pointId = value - pickOffset
|
165 | var xData = this.xData
|
166 | var yData = this.yData
|
167 | return {
|
168 | object: this,
|
169 | pointId: pointId,
|
170 | dataCoord: [
|
171 | xData[pointId % this.shape[0]],
|
172 | yData[(pointId / this.shape[0]) | 0]]
|
173 | }
|
174 | }
|
175 |
|
176 | proto.update = function (options) {
|
177 | options = options || {}
|
178 |
|
179 | var shape = options.shape || [0, 0]
|
180 |
|
181 | var x = options.x || iota(shape[0])
|
182 | var y = options.y || iota(shape[1])
|
183 | var z = options.z || new Float32Array(shape[0] * shape[1])
|
184 |
|
185 | var isSmooth = options.zsmooth !== false
|
186 |
|
187 | this.xData = x
|
188 | this.yData = y
|
189 |
|
190 | var colorLevels = options.colorLevels || [0]
|
191 | var colorValues = options.colorValues || [0, 0, 0, 1]
|
192 | var colorCount = colorLevels.length
|
193 |
|
194 | var bounds = this.bounds
|
195 | var lox, loy, hix, hiy
|
196 | if (isSmooth) {
|
197 | lox = bounds[0] = x[0]
|
198 | loy = bounds[1] = y[0]
|
199 | hix = bounds[2] = x[x.length - 1]
|
200 | hiy = bounds[3] = y[y.length - 1]
|
201 | } else {
|
202 |
|
203 | lox = bounds[0] = x[0] + (x[1] - x[0]) / 2
|
204 | loy = bounds[1] = y[0] + (y[1] - y[0]) / 2
|
205 |
|
206 |
|
207 | hix = bounds[2] = x[x.length - 1] + (x[x.length - 1] - x[x.length - 2]) / 2
|
208 | hiy = bounds[3] = y[y.length - 1] + (y[y.length - 1] - y[y.length - 2]) / 2
|
209 |
|
210 |
|
211 | }
|
212 | var xs = 1.0 / (hix - lox)
|
213 | var ys = 1.0 / (hiy - loy)
|
214 |
|
215 | var numX = shape[0]
|
216 | var numY = shape[1]
|
217 |
|
218 | this.shape = [numX, numY]
|
219 |
|
220 | var numVerts = (
|
221 | isSmooth ? (numX - 1) * (numY - 1) : numX * numY
|
222 | ) * (WEIGHTS.length >>> 1)
|
223 |
|
224 | this.numVertices = numVerts
|
225 |
|
226 | var colors = pool.mallocUint8(numVerts * 4)
|
227 | var positions = pool.mallocFloat32(numVerts * 2)
|
228 | var weights = pool.mallocUint8 (numVerts * 2)
|
229 | var ids = pool.mallocUint32(numVerts)
|
230 |
|
231 | var ptr = 0
|
232 |
|
233 | var ni = isSmooth ? numX - 1 : numX
|
234 | var nj = isSmooth ? numY - 1 : numY
|
235 |
|
236 | for (var j = 0; j < nj; ++j) {
|
237 | var yc0, yc1
|
238 |
|
239 | if (isSmooth) {
|
240 | yc0 = ys * (y[j] - loy)
|
241 | yc1 = ys * (y[j + 1] - loy)
|
242 | } else {
|
243 | yc0 = j < numY - 1 ? ys * (y[j] - (y[j + 1] - y[j])/2 - loy) : ys * (y[j] - (y[j] - y[j - 1])/2 - loy)
|
244 | yc1 = j < numY - 1 ? ys * (y[j] + (y[j + 1] - y[j])/2 - loy) : ys * (y[j] + (y[j] - y[j - 1])/2 - loy)
|
245 | }
|
246 |
|
247 | for (var i = 0; i < ni; ++i) {
|
248 | var xc0, xc1
|
249 |
|
250 | if (isSmooth) {
|
251 | xc0 = xs * (x[i] - lox)
|
252 | xc1 = xs * (x[i + 1] - lox)
|
253 | } else {
|
254 | xc0 = i < numX - 1 ? xs * (x[i] - (x[i + 1] - x[i])/2 - lox) : xs * (x[i] - (x[i] - x[i - 1])/2 - lox)
|
255 | xc1 = i < numX - 1 ? xs * (x[i] + (x[i + 1] - x[i])/2 - lox) : xs * (x[i] + (x[i] - x[i - 1])/2 - lox)
|
256 | }
|
257 |
|
258 | for (var dd = 0; dd < WEIGHTS.length; dd += 2) {
|
259 | var dx = WEIGHTS[dd]
|
260 | var dy = WEIGHTS[dd + 1]
|
261 | var offset = isSmooth ? (j + dy) * numX + (i + dx) : j * numX + i
|
262 | var zc = z[offset]
|
263 | var colorIdx = bsearch.le(colorLevels, zc)
|
264 | var r, g, b, a
|
265 | if (colorIdx < 0) {
|
266 | r = colorValues[0]
|
267 | g = colorValues[1]
|
268 | b = colorValues[2]
|
269 | a = colorValues[3]
|
270 | } else if (colorIdx === colorCount - 1) {
|
271 | r = colorValues[4 * colorCount - 4]
|
272 | g = colorValues[4 * colorCount - 3]
|
273 | b = colorValues[4 * colorCount - 2]
|
274 | a = colorValues[4 * colorCount - 1]
|
275 | } else {
|
276 | var t = (zc - colorLevels[colorIdx]) /
|
277 | (colorLevels[colorIdx + 1] - colorLevels[colorIdx])
|
278 | var ti = 1.0 - t
|
279 | var i0 = 4 * colorIdx
|
280 | var i1 = 4 * (colorIdx + 1)
|
281 | r = ti * colorValues[i0] + t * colorValues[i1]
|
282 | g = ti * colorValues[i0 + 1] + t * colorValues[i1 + 1]
|
283 | b = ti * colorValues[i0 + 2] + t * colorValues[i1 + 2]
|
284 | a = ti * colorValues[i0 + 3] + t * colorValues[i1 + 3]
|
285 | }
|
286 |
|
287 | colors[4 * ptr] = 255 * r
|
288 | colors[4 * ptr + 1] = 255 * g
|
289 | colors[4 * ptr + 2] = 255 * b
|
290 | colors[4 * ptr + 3] = 255 * a
|
291 |
|
292 | positions[2*ptr] = xc0*.5 + xc1*.5;
|
293 | positions[2*ptr+1] = yc0*.5 + yc1*.5;
|
294 |
|
295 | weights[2*ptr] = dx;
|
296 | weights[2*ptr+1] = dy;
|
297 |
|
298 | ids[ptr] = j * numX + i
|
299 |
|
300 | ptr += 1
|
301 | }
|
302 | }
|
303 | }
|
304 |
|
305 | this.positionBuffer.update(positions)
|
306 | this.weightBuffer.update(weights)
|
307 | this.colorBuffer.update(colors)
|
308 | this.idBuffer.update(ids)
|
309 |
|
310 | pool.free(positions)
|
311 | pool.free(colors)
|
312 | pool.free(weights)
|
313 | pool.free(ids)
|
314 | }
|
315 |
|
316 | proto.dispose = function () {
|
317 | this.shader.dispose()
|
318 | this.pickShader.dispose()
|
319 | this.positionBuffer.dispose()
|
320 | this.weightBuffer.dispose()
|
321 | this.colorBuffer.dispose()
|
322 | this.idBuffer.dispose()
|
323 | this.plot.removeObject(this)
|
324 | }
|
325 |
|
326 | function createHeatmap2D (plot, options) {
|
327 | var gl = plot.gl
|
328 |
|
329 | var shader = createShader(gl, shaders.vertex, shaders.fragment)
|
330 | var pickShader = createShader(gl, shaders.pickVertex, shaders.pickFragment)
|
331 |
|
332 | var positionBuffer = createBuffer(gl)
|
333 | var weightBuffer = createBuffer(gl)
|
334 | var colorBuffer = createBuffer(gl)
|
335 | var idBuffer = createBuffer(gl)
|
336 |
|
337 | var heatmap = new GLHeatmap2D(
|
338 | plot,
|
339 | shader,
|
340 | pickShader,
|
341 | positionBuffer,
|
342 | weightBuffer,
|
343 | colorBuffer,
|
344 | idBuffer)
|
345 |
|
346 | heatmap.update(options)
|
347 | plot.addObject(heatmap)
|
348 |
|
349 | return heatmap
|
350 | }
|