heatgrid: keyboard bindings, highlight box, more viewable popover.

The popover would go off the bottom or right edges of the screen if you
moved your mouse too far in that direction; now it tries to put itself to
the left or above your mouse cursor if you're in the right or bottom half of
the screen, respectively.

There's now a red box that surrounds whatever pixel you're trying to
highlight, giving a more precise view than the mouse pointer.

You can use the up/down/left/right arrows to more precisely adjust which
pixel you want to view.
diff --git a/heatgrid.js b/heatgrid.js
index 7894bb6..f40153a 100644
--- a/heatgrid.js
+++ b/heatgrid.js
@@ -43,8 +43,9 @@
 
   this.draw = function(grid) {
     console.debug('heatgrid.draw', grid);
-    this.el.html('<div id="heatgrid"><canvas></canvas>' +
-                 '<div id="heatgrid-popover"></div></div>');
+    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',
@@ -55,11 +56,20 @@
     var popover = this.el.find('#heatgrid-popover');
     popover.css({
       position: 'absolute',
-      top: 0, left: 0,
       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;
@@ -69,7 +79,7 @@
     canvas.css({
       background: '#fff',
       width: '100%',
-      height: ysize //'100%',
+      height: ysize
     });
     console.debug('heatgrid canvas size is: x y =', xsize, ysize);
     var ctx = canvas[0].getContext('2d');
@@ -78,11 +88,14 @@
       return;
     }
 
-    // TODO(apenwarr): offsetX/Y are flakey, use something else
-    var movefunc = function(offX, offY) {
-      var x = parseInt(offX / canvas.width() * grid.headers.length);
-      var y = parseInt(offY / canvas.height() * grid.data.length);
-      if (x > grid.headers.length || y > grid.data.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') {
@@ -94,24 +107,64 @@
       info.push(grid.headers[x]);
       info.push('value=' + grid.data[y][x]);
 
-      popover.css({
-        left: (x + 0.4) / grid.headers.length * canvas.width(),
-        top: (y + 0.4) / grid.data.length * canvas.height(),
-      });
+      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();
-      movefunc(ev.pageX - pos.left, ev.pageY - pos.top);
+      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++) {