| /* |
| * Copyright 2013 Google Inc. All Rights Reserved. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| 'use strict'; |
| |
| var HeatGrid = function(el) { |
| this.el = $(el); |
| |
| var frac = function(minval, maxval, fraction) { |
| return minval + fraction * (maxval - minval); |
| }; |
| |
| var gradient = function(mincolor, zerocolor, maxcolor, |
| ofs) { |
| if (ofs == 0) { |
| return zerocolor; |
| } else if (ofs < 0) { |
| return [frac(zerocolor[0], mincolor[0], -ofs / 2), |
| frac(zerocolor[1], mincolor[1], -ofs / 2), |
| frac(zerocolor[2], mincolor[2], -ofs / 2)]; |
| } else if (ofs > 0) { |
| return [frac(zerocolor[0], maxcolor[0], ofs / 2), |
| frac(zerocolor[1], maxcolor[1], ofs / 2), |
| frac(zerocolor[2], maxcolor[2], ofs / 2)]; |
| } |
| }; |
| |
| this.draw = function(grid) { |
| console.debug('heatgrid.draw', grid); |
| this.el.html('<div id="heatgrid" tabindex=0><canvas></canvas>' + |
| '<div id="heatgrid-popover"></div>' + |
| '<div id="heatgrid-highlight"></div></div>'); |
| var heatgrid = this.el.find('#heatgrid'); |
| heatgrid.css({ |
| position: 'relative', |
| overflow: 'scroll', |
| width: '100%', |
| height: '100%' |
| }); |
| var popover = this.el.find('#heatgrid-popover'); |
| popover.css({ |
| position: 'absolute', |
| background: '#aaa', |
| border: '1px dotted black', |
| 'white-space': 'pre' |
| }); |
| var highlight = this.el.find('#heatgrid-highlight'); |
| highlight.css({ |
| position: 'absolute', |
| //background: 'rgba(255,192,192,16)', |
| width: '3px', |
| height: '3px', |
| margin: '0', |
| padding: '0', |
| border: '1px solid red' |
| }); |
| var canvas = this.el.find('canvas'); |
| var xmult = parseInt(1000 / grid.headers.length); |
| if (xmult < 1) xmult = 1; |
| var xsize = grid.headers.length * xmult; |
| var ysize = grid.data.length; |
| canvas.attr({width: xsize, height: ysize}); |
| canvas.css({ |
| background: '#fff', |
| width: '100%', |
| height: ysize |
| }); |
| console.debug('heatgrid canvas size is: x y =', xsize, ysize); |
| var ctx = canvas[0].getContext('2d'); |
| |
| if (!grid.data.length || !grid.data[0].length) { |
| return; |
| } |
| |
| var lastPos = { x: 0, y: 0 }; |
| var movefunc = function(x, y) { |
| if (x >= grid.headers.length) x = grid.headers.length - 1; |
| if (y >= grid.data.length) y = grid.data.length - 1; |
| if (x < 0) x = 0; |
| if (y < 0) y = 0; |
| lastPos.x = x; |
| lastPos.y = y; |
| var info = []; |
| for (var i = 0; i < grid.headers.length; i++) { |
| if (grid.types[i] != 'number') { |
| info.push(grid.data[y][i]); |
| } else { |
| break; |
| } |
| } |
| info.push(grid.headers[x]); |
| info.push('value=' + grid.data[y][x]); |
| |
| var cx = x / grid.headers.length * canvas.width(); |
| var cy = y / grid.data.length * canvas.height(); |
| highlight.css({left: cx - 2, top: cy - 2}); |
| if (cx < canvas.width() / 2) { |
| popover.css({ |
| left: cx + 30, |
| right: 'auto' |
| }); |
| } else { |
| popover.css({ |
| left: 'auto', |
| right: heatgrid[0].clientWidth - cx + 10 |
| }); |
| } |
| if (cy < canvas.height() / 2) { |
| popover.css({ |
| top: cy + 10, |
| bottom: 'auto' |
| }); |
| } else { |
| popover.css({ |
| top: 'auto', |
| bottom: heatgrid[0].clientHeight - cy + 10 |
| }); |
| } |
| popover.text(info.join('\n')); |
| }; |
| heatgrid.mousemove(function(ev) { |
| var pos = canvas.position(); |
| var offX = ev.pageX - pos.left - 1; |
| var offY = ev.pageY - pos.top - 1; |
| var x = parseInt(offX / canvas.width() * grid.headers.length); |
| var y = parseInt(offY / canvas.height() * grid.data.length); |
| movefunc(x, y); |
| }); |
| heatgrid.keydown(function(ev) { |
| if (ev.which == 38) { // up |
| movefunc(lastPos.x, lastPos.y - 1); |
| } else if (ev.which == 40) { // down |
| movefunc(lastPos.x, lastPos.y + 1); |
| } else if (ev.which == 37) { // left |
| movefunc(lastPos.x - 1, lastPos.y); |
| } else if (ev.which == 39) { // right |
| movefunc(lastPos.x + 1, lastPos.y); |
| } else { |
| return true; // propagate event forward |
| } |
| ev.stopPropagation(); |
| return false; |
| }); |
| heatgrid.mouseleave(function() { |
| popover.hide(); |
| }); |
| heatgrid.mouseenter(function() { |
| heatgrid.focus(); // so that keyboard bindings work |
| popover.show(); |
| }); |
| |
| var total = 0, count = 0; |
| for (var y = 0; y < grid.data.length; y++) { |
| for (var x = 0; x < grid.data[y].length; x++) { |
| if (grid.types[x] != 'number') continue; |
| var cell = parseFloat(grid.data[y][x]); |
| if (!isNaN(cell)) { |
| total += cell; |
| count++; |
| } |
| } |
| } |
| var avg = total / count; |
| |
| var tdiff = 0; |
| for (var y = 0; y < grid.data.length; y++) { |
| for (var x = 0; x < grid.data[y].length; x++) { |
| if (grid.types[x] != 'number') continue; |
| var cell = parseFloat(grid.data[y][x]); |
| if (!isNaN(cell)) { |
| tdiff += (cell - avg) * (cell - avg); |
| } |
| } |
| } |
| var stddev = Math.sqrt(tdiff / count); |
| console.debug('total,count,mean,stddev = ', total, count, avg, stddev); |
| |
| var img = ctx.createImageData(xsize, ysize); |
| for (var y = 0; y < grid.data.length; y++) { |
| for (var x = 0; x < grid.data[y].length; x++) { |
| if (grid.types[x] != 'number') continue; |
| var cell = parseFloat(grid.data[y][x]); |
| if (isNaN(cell)) continue; |
| var ofs = stddev ? (cell - avg) / stddev : 3; |
| var color = gradient([192, 192, 192], |
| [192, 192, 255], |
| [0, 0, 255], ofs); |
| if (!color) continue; |
| var pix = (y * xsize + x * xmult) * 4; |
| for (var i = 0; i < xmult; i++) { |
| img.data[pix + 0] = color[0]; |
| img.data[pix + 1] = color[1]; |
| img.data[pix + 2] = color[2]; |
| img.data[pix + 3] = 255; |
| pix += 4; |
| } |
| } |
| } |
| ctx.putImageData(img, 0, 0); |
| }; |
| }; |