UNPKG

8.85 kBJavaScriptView Raw
1'use strict'
2
3module.exports = createHeatmap2D
4
5var bsearch = require('binary-search-bounds')
6var iota = require('iota-array')
7var pool = require('typedarray-pool')
8var createShader = require('gl-shader')
9var createBuffer = require('gl-buffer')
10
11var shaders = require('./lib/shaders')
12
13function 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
35var proto = GLHeatmap2D.prototype
36
37var WEIGHTS = [
38 0, 0,
39 1, 0,
40 0, 1,
41 1, 0,
42 1, 1,
43 0, 1
44]
45
46proto.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
97proto.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
158proto.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
176proto.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 /* To get squares to centre on data values */
203 lox = bounds[0] = x[0] + (x[1] - x[0]) / 2 /* starting x value */
204 loy = bounds[1] = y[0] + (y[1] - y[0]) / 2 /* starting y value */
205
206 /* Bounds needs to add half a square on each end */
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 // N.B. Resolution = 1 / range
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
316proto.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
326function 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}