changeset 0:95ff566506f4 draft

Uploaded
author adam-novak
date Tue, 11 Jun 2013 18:26:25 -0400
parents
children ec5ade08ac8a
files hexagram/color-0.4.1.js hexagram/drag.svg hexagram/filter.svg hexagram/hexagram.css hexagram/hexagram.html hexagram/hexagram.js hexagram/hexagram.py hexagram/hexagram.xml hexagram/jquery.tsv.js hexagram/jstat-1.0.0.js hexagram/layers.tab hexagram/maplabel-compiled.js hexagram/matrices.tab hexagram/matrix_0.tab hexagram/right.svg hexagram/select2-spinner.gif hexagram/select2.css hexagram/select2.js hexagram/select2.png hexagram/select2x2.png hexagram/statistics.js hexagram/statistics.svg hexagram/throbber.svg hexagram/tool_dependency.xml hexagram/tools.js hexagram/tsv.py hexagram/tsv.pyc
diffstat 27 files changed, 13886 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/color-0.4.1.js	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,1201 @@
+(function(){var global = this;function debug(){return debug};function require(p, parent){ var path = require.resolve(p) , mod = require.modules[path]; if (!mod) throw new Error('failed to require "' + p + '" from ' + parent); if (!mod.exports) { mod.exports = {}; mod.call(mod.exports, mod, mod.exports, require.relative(path), global); } return mod.exports;}require.modules = {};require.resolve = function(path){ var orig = path , reg = path + '.js' , index = path + '/index.js'; return require.modules[reg] && reg || require.modules[index] && index || orig;};require.register = function(path, fn){ require.modules[path] = fn;};require.relative = function(parent) { return function(p){ if ('debug' == p) return debug; if ('.' != p.charAt(0)) return require(p); var path = parent.split('/') , segs = p.split('/'); path.pop(); for (var i = 0; i < segs.length; i++) { var seg = segs[i]; if ('..' == seg) path.pop(); else if ('.' != seg) path.push(seg); } return require(path.join('/'), parent); };};require.register("color.js", function(module, exports, require, global){
+/* MIT license */
+var convert = require("color-convert"),
+    string = require("color-string");
+
+module.exports = function(cssString) {
+   return new Color(cssString);
+};
+
+var Color = function(cssString) {
+   this.values = {
+      rgb: [0, 0, 0],
+      hsl: [0, 0, 0],
+      hsv: [0, 0, 0],
+      cmyk: [0, 0, 0, 0],
+      alpha: 1
+   }
+
+   // parse Color() argument
+   if (typeof cssString == "string") {
+      var vals = string.getRgba(cssString);
+      if (vals) {
+         this.setValues("rgb", vals);
+      }
+      else if(vals = string.getHsla(cssString)) {
+         this.setValues("hsl", vals);
+      }
+   }
+   else if (typeof cssString == "object") {
+      var vals = cssString;
+      if(vals["r"] !== undefined || vals["red"] !== undefined) {
+         this.setValues("rgb", vals)
+      }
+      else if(vals["l"] !== undefined || vals["lightness"] !== undefined) {
+         this.setValues("hsl", vals)
+      }
+      else if(vals["v"] !== undefined || vals["value"] !== undefined) {
+         this.setValues("hsv", vals)
+      }
+      else if(vals["c"] !== undefined || vals["cyan"] !== undefined) {
+         this.setValues("cmyk", vals)
+      }
+   }
+}
+
+Color.prototype = {
+   rgb: function (vals) {
+      return this.setSpace("rgb", arguments);
+   },
+   hsl: function(vals) {
+      return this.setSpace("hsl", arguments);
+   },
+   hsv: function(vals) {
+      return this.setSpace("hsv", arguments);
+   },
+   cmyk: function(vals) {
+      return this.setSpace("cmyk", arguments);
+   },
+
+   rgbArray: function() {
+      return this.values.rgb;
+   },
+   hslArray: function() {
+      return this.values.hsl;
+   },
+   hsvArray: function() {
+      return this.values.hsv;
+   },
+   cmykArray: function() {
+      return this.values.cmyk;
+   },
+   rgbaArray: function() {
+      var rgb = this.values.rgb;
+      rgb.push(this.values.alpha);
+      return rgb;
+   },
+   hslaArray: function() {
+      var hsl = this.values.hsl;
+      hsl.push(this.values.alpha);
+      return hsl;
+   },
+
+   alpha: function(val) {
+      if (val === undefined) {
+         return this.values.alpha;
+      }
+      this.setValues("alpha", val);
+      return this;
+   },
+
+   red: function(val) {
+      return this.setChannel("rgb", 0, val);
+   },
+   green: function(val) {
+      return this.setChannel("rgb", 1, val);
+   },
+   blue: function(val) {
+      return this.setChannel("rgb", 2, val);
+   },
+   hue: function(val) {
+      return this.setChannel("hsl", 0, val);
+   },
+   saturation: function(val) {
+      return this.setChannel("hsl", 1, val);
+   },
+   lightness: function(val) {
+      return this.setChannel("hsl", 2, val);
+   },
+   saturationv: function(val) {
+      return this.setChannel("hsv", 1, val);
+   },
+   value: function(val) {
+      return this.setChannel("hsv", 2, val);
+   },
+   cyan: function(val) {
+      return this.setChannel("cmyk", 0, val);
+   },
+   magenta: function(val) {
+      return this.setChannel("cmyk", 1, val);
+   },
+   yellow: function(val) {
+      return this.setChannel("cmyk", 2, val);
+   },
+   black: function(val) {
+      return this.setChannel("cmyk", 3, val);
+   },
+
+   hexString: function() {
+      return string.hexString(this.values.rgb);
+   },
+   rgbString: function() {
+      return string.rgbString(this.values.rgb, this.values.alpha);
+   },
+   rgbaString: function() {
+      return string.rgbaString(this.values.rgb, this.values.alpha);
+   },
+   percentString: function() {
+      return string.percentString(this.values.rgb, this.values.alpha);
+   },
+   hslString: function() {
+      return string.hslString(this.values.hsl, this.values.alpha);
+   },
+   hslaString: function() {
+      return string.hslaString(this.values.hsl, this.values.alpha);
+   },
+   keyword: function() {
+      return string.keyword(this.values.rgb, this.values.alpha);
+   },
+
+   luminosity: function() {
+      // http://www.w3.org/TR/WCAG20/#relativeluminancedef
+      var rgb = this.values.rgb;
+      for (var i = 0; i < rgb.length; i++) {
+         var chan = rgb[i] / 255;
+         rgb[i] = (chan <= 0.03928) ? chan / 12.92
+                  : Math.pow(((chan + 0.055) / 1.055), 2.4)
+      }
+      return 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2];
+   },
+
+   contrast: function(color2) {
+      // http://www.w3.org/TR/WCAG20/#contrast-ratiodef
+      var lum1 = this.luminosity();
+      var lum2 = color2.luminosity();
+      if (lum1 > lum2) {
+         return (lum1 + 0.05) / (lum2 + 0.05)
+      };
+      return (lum2 + 0.05) / (lum1 + 0.05);
+   },
+
+   dark: function() {
+      // YIQ equation from http://24ways.org/2010/calculating-color-contrast
+      var rgb = this.values.rgb,
+          yiq = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000;
+   	return yiq < 128;
+   },
+
+   light: function() {
+      return !this.dark();
+   },
+
+   negate: function() {
+      var rgb = []
+      for (var i = 0; i < 3; i++) {
+         rgb[i] = 255 - this.values.rgb[i];
+      }
+      this.setValues("rgb", rgb);
+      return this;
+   },
+
+   lighten: function(ratio) {
+      this.values.hsl[2] += this.values.hsl[2] * ratio;
+      this.setValues("hsl", this.values.hsl);
+      return this;
+   },
+
+   darken: function(ratio) {
+      this.values.hsl[2] -= this.values.hsl[2] * ratio;
+      this.setValues("hsl", this.values.hsl);
+      return this;
+   },
+
+   saturate: function(ratio) {
+      this.values.hsl[1] += this.values.hsl[1] * ratio;
+      this.setValues("hsl", this.values.hsl);
+      return this;
+   },
+
+   desaturate: function(ratio) {
+      this.values.hsl[1] -= this.values.hsl[1] * ratio;
+      this.setValues("hsl", this.values.hsl);
+      return this;
+   },
+
+   greyscale: function() {
+      var rgb = this.values.rgb;
+      // http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale
+      var val = rgb[0] * 0.3 + rgb[1] * 0.59 + rgb[2] * 0.11;
+      this.setValues("rgb", [val, val, val]);
+      return this;
+   },
+
+   clearer: function(ratio) {
+      this.setValues("alpha", this.values.alpha - (this.values.alpha * ratio));
+      return this;
+   },
+
+   opaquer: function(ratio) {
+      this.setValues("alpha", this.values.alpha + (this.values.alpha * ratio));
+      return this;
+   },
+
+   rotate: function(degrees) {
+      var hue = this.values.hsl[0];
+      hue = (hue + degrees) % 360;
+      hue = hue < 0 ? 360 + hue : hue;
+      this.values.hsl[0] = hue;
+      this.setValues("hsl", this.values.hsl);
+      return this;
+   },
+
+   mix: function(color2, weight) {
+      weight = 1 - (weight || 0.5);
+
+      // algorithm from Sass's mix(). Ratio of first color in mix is
+      // determined by the alphas of both colors and the weight
+      var t1 = weight * 2 - 1,
+          d = this.alpha() - color2.alpha();
+
+      var weight1 = (((t1 * d == -1) ? t1 : (t1 + d) / (1 + t1 * d)) + 1) / 2;
+      var weight2 = 1 - weight1;
+
+      var rgb = this.rgbArray();
+      var rgb2 = color2.rgbArray();
+
+      for (var i = 0; i < rgb.length; i++) {
+         rgb[i] = rgb[i] * weight1 + rgb2[i] * weight2;
+      }
+      this.setValues("rgb", rgb);
+
+      var alpha = this.alpha() * weight + color2.alpha() * (1 - weight);
+      this.setValues("alpha", alpha);
+
+      return this;
+   },
+
+   toJSON: function() {
+     return this.rgb();
+   }
+}
+
+
+Color.prototype.getValues = function(space) {
+   var vals = {};
+   for (var i = 0; i < space.length; i++) {
+      vals[space[i]] = this.values[space][i];
+   }
+   if (this.values.alpha != 1) {
+      vals["a"] = this.values.alpha;
+   }
+   // {r: 255, g: 255, b: 255, a: 0.4}
+   return vals;
+}
+
+Color.prototype.setValues = function(space, vals) {
+   var spaces = {
+      "rgb": ["red", "green", "blue"],
+      "hsl": ["hue", "saturation", "lightness"],
+      "hsv": ["hue", "saturation", "value"],
+      "cmyk": ["cyan", "magenta", "yellow", "black"]
+   };
+
+   var maxes = {
+      "rgb": [255, 255, 255],
+      "hsl": [360, 100, 100],
+      "hsv": [360, 100, 100],
+      "cmyk": [100, 100, 100, 100],
+   };
+
+   var alpha = 1;
+   if (space == "alpha") {
+      alpha = vals;
+   }
+   else if (vals.length) {
+      // [10, 10, 10]
+      this.values[space] = vals.slice(0, space.length);
+      alpha = vals[space.length];
+   }
+   else if (vals[space[0]] !== undefined) {
+      // {r: 10, g: 10, b: 10}
+      for (var i = 0; i < space.length; i++) {
+        this.values[space][i] = vals[space[i]];
+      }
+      alpha = vals.a;
+   }
+   else if (vals[spaces[space][0]] !== undefined) {
+      // {red: 10, green: 10, blue: 10}
+      var chans = spaces[space];
+      for (var i = 0; i < space.length; i++) {
+        this.values[space][i] = vals[chans[i]];
+      }
+      alpha = vals.alpha;
+   }
+   this.values.alpha = Math.max(0, Math.min(1, alpha || this.values.alpha));
+   if (space == "alpha") {
+      return;
+   }
+
+   // convert to all the other color spaces
+   for (var sname in spaces) {
+      if (sname != space) {
+         this.values[sname] = convert[space][sname](this.values[space])
+      }
+
+      // cap values
+      for (var i = 0; i < sname.length; i++) {
+         var capped = Math.max(0, Math.min(maxes[sname][i], this.values[sname][i]));
+         this.values[sname][i] = Math.round(capped);
+      }
+   }
+   return true;
+}
+
+Color.prototype.setSpace = function(space, args) {
+   var vals = args[0];
+   if (vals === undefined) {
+      // color.rgb()
+      return this.getValues(space);
+   }
+   // color.rgb(10, 10, 10)
+   if (typeof vals == "number") {
+      vals = Array.prototype.slice.call(args);
+   }
+   this.setValues(space, vals);
+   return this;
+}
+
+Color.prototype.setChannel = function(space, index, val) {
+   if (val === undefined) {
+      // color.red()
+      return this.values[space][index];
+   }
+   // color.red(100)
+   this.values[space][index] = val;
+   this.setValues(space, this.values[space]);
+   return this;
+}
+
+});require.register("color-string", function(module, exports, require, global){
+/* MIT license */
+var convert = require("color-convert");
+
+module.exports = {
+   getRgba: getRgba,
+   getHsla: getHsla,
+   getRgb: getRgb,
+   getHsl: getHsl,
+   getAlpha: getAlpha,
+
+   hexString: hexString,
+   rgbString: rgbString,
+   rgbaString: rgbaString,
+   percentString: percentString,
+   percentaString: percentaString,
+   hslString: hslString,
+   hslaString: hslaString,
+   keyword: keyword
+}
+
+function getRgba(string) {
+   if (!string) {
+      return;
+   }
+   var abbr =  /^#([a-fA-F0-9]{3})$/,
+       hex =  /^#([a-fA-F0-9]{6})$/,
+       rgba = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d\.]+)\s*)?\)$/,
+       per = /^rgba?\(\s*([\d\.]+)\%\s*,\s*([\d\.]+)\%\s*,\s*([\d\.]+)\%\s*(?:,\s*([\d\.]+)\s*)?\)$/,
+       keyword = /(\D+)/;
+
+   var rgb = [0, 0, 0],
+       a = 1,
+       match = string.match(abbr);
+   if (match) {
+      match = match[1];
+      for (var i = 0; i < rgb.length; i++) {
+         rgb[i] = parseInt(match[i] + match[i], 16);
+      }
+   }
+   else if (match = string.match(hex)) {
+      match = match[1];
+      for (var i = 0; i < rgb.length; i++) {
+         rgb[i] = parseInt(match.slice(i * 2, i * 2 + 2), 16);
+      }
+   }
+   else if (match = string.match(rgba)) {
+      for (var i = 0; i < rgb.length; i++) {
+         rgb[i] = parseInt(match[i + 1]);
+      }
+      a = parseFloat(match[4]);
+   }
+   else if (match = string.match(per)) {
+      for (var i = 0; i < rgb.length; i++) {
+         rgb[i] = Math.round(parseFloat(match[i + 1]) * 2.55);
+      }
+      a = parseFloat(match[4]);
+   }
+   else if (match = string.match(keyword)) {
+      if (match[1] == "transparent") {
+         return [0, 0, 0, 0];
+      }
+      rgb = convert.keyword2rgb(match[1]);
+      if (!rgb) {
+         return;
+      }
+   }
+
+   for (var i = 0; i < rgb.length; i++) {
+      rgb[i] = scale(rgb[i], 0, 255);
+   }
+   if (!a) {
+      a = 1;
+   }
+   else {
+      a = scale(a, 0, 1);
+   }
+   rgb.push(a);
+   return rgb;
+}
+
+function getHsla(string) {
+   if (!string) {
+      return;
+   }
+   var hsl = /^hsla?\(\s*(\d+)\s*,\s*([\d\.]+)%\s*,\s*([\d\.]+)%\s*(?:,\s*([\d\.]+)\s*)?\)/;
+   var match = string.match(hsl);
+   if (match) {
+      var h = scale(parseInt(match[1]), 0, 360),
+          s = scale(parseFloat(match[2]), 0, 100),
+          l = scale(parseFloat(match[3]), 0, 100),
+          a = scale(parseFloat(match[4]) || 1, 0, 1);
+      return [h, s, l, a];
+   }
+}
+
+function getRgb(string) {
+   var rgba = getRgba(string);
+   return rgba && rgba.slice(0, 3);
+}
+
+function getHsl(string) {
+  var hsla = getHsla(string);
+  return hsla && hsla.slice(0, 3);
+}
+
+function getAlpha(string) {
+   var vals = getRgba(string);
+   if (vals) {
+      return vals[3];
+   }
+   else if (vals = getHsla(string)) {
+      return vals[3];
+   }
+}
+
+// generators
+function hexString(rgb) {
+   return "#" + hexDouble(rgb[0]) + hexDouble(rgb[1])
+              + hexDouble(rgb[2]);
+}
+
+function rgbString(rgba, alpha) {
+   if (alpha < 1 || (rgba[3] && rgba[3] < 1)) {
+      return rgbaString(rgba, alpha);
+   }
+   return "rgb(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2] + ")";
+}
+
+function rgbaString(rgba, alpha) {
+   return "rgba(" + rgba[0] + ", " + rgba[1] + ", " + rgba[2]
+           + ", " + (alpha || rgba[3] || 1) + ")";
+}
+
+function percentString(rgba, alpha) {
+   if (alpha < 1 || (rgba[3] && rgba[3] < 1)) {
+      return percentaString(rgba, alpha);
+   }
+   var r = Math.round(rgba[0]/255 * 100),
+       g = Math.round(rgba[1]/255 * 100),
+       b = Math.round(rgba[2]/255 * 100);
+
+   return "rgb(" + r + "%, " + g + "%, " + b + "%)";
+}
+
+function percentaString(rgba, alpha) {
+   var r = Math.round(rgba[0]/255 * 100),
+       g = Math.round(rgba[1]/255 * 100),
+       b = Math.round(rgba[2]/255 * 100);
+   return "rgba(" + r + "%, " + g + "%, " + b + "%, " + (alpha || rgba[3] || 1) + ")";
+}
+
+function hslString(hsla, alpha) {
+   if (alpha < 1 || (hsla[3] && hsla[3] < 1)) {
+      return hslaString(hsla, alpha);
+   }
+   return "hsl(" + hsla[0] + ", " + hsla[1] + "%, " + hsla[2] + "%)";
+}
+
+function hslaString(hsla, alpha) {
+   return "hsla(" + hsla[0] + ", " + hsla[1] + "%, " + hsla[2] + "%, "
+           + (alpha || hsla[3] || 1) + ")";
+}
+
+function keyword(rgb) {
+   return convert.rgb2keyword(rgb.slice(0, 3));
+}
+
+// helpers
+function scale(num, min, max) {
+   return Math.min(Math.max(min, num), max);
+}
+
+function hexDouble(num) {
+  var str = num.toString(16).toUpperCase();
+  return (str.length < 2) ? "0" + str : str;
+}
+
+});require.register("color-convert", function(module, exports, require, global){
+var conversions = require("conversions");
+
+var convert = function() {
+   return new Converter();
+}
+
+for (var func in conversions) {
+  // export Raw versions
+  convert[func + "Raw"] =  (function(func) {
+    // accept array or plain args
+    return function(arg) {
+      if (typeof arg == "number")
+        arg = Array.prototype.slice.call(arguments);
+      return conversions[func](arg);
+    }
+  })(func);
+
+  var pair = /(\w+)2(\w+)/.exec(func),
+      from = pair[1],
+      to = pair[2];
+
+  // export rgb2hsl and ["rgb"]["hsl"]
+  convert[from] = convert[from] || {};
+
+  convert[from][to] = convert[func] = (function(func) {
+    return function(arg) {
+      if (typeof arg == "number") {
+        arg = Array.prototype.slice.call(arguments);
+      }
+
+      var val = conversions[func](arg);
+      if (typeof val == "string" || val === undefined) {
+        return val; // keyword
+      }
+
+      round(val)
+      return val;
+    }
+  })(func);
+}
+
+
+/* Converter does lazy conversion and caching */
+var Converter = function() {
+   this.space = "rgb";
+   this.convs = {
+     'rgb': [0, 0, 0]
+   };
+};
+
+/* Either get the values for a space or
+  set the values for a space, depending on args */
+Converter.prototype.routeSpace = function(space, args) {
+   var values = args[0];
+   if (values === undefined) {
+      // color.rgb()
+      return this.getValues(space);
+   }
+   // color.rgb(10, 10, 10)
+   if (typeof values == "number") {
+      values = Array.prototype.slice.call(args);
+   }
+
+   return this.setValues(space, values);
+};
+
+/* Set the values for a space, invalidating cache */
+Converter.prototype.setValues = function(space, values) {
+   this.space = space;
+   this.convs = {};
+   this.convs[space] = values;
+   return this;
+};
+
+/* Get the values for a space. If there's already
+  a conversion for the space, fetch it, otherwise
+  compute it */
+Converter.prototype.getValues = function(space) {
+   var vals = this.convs[space];
+   if (!vals) {
+      var fspace = this.space,
+          from = this.convs[fspace];
+      vals = convert[fspace][space](from);
+
+      this.convs[space] = vals;
+   }
+   else {
+      round(vals);
+   }
+  return vals;
+};
+
+function round(val) {
+  for (var i = 0; i < val.length; i++) {
+    val[i] = Math.round(val[i]);
+  }
+};
+
+["rgb", "hsl", "hsv", "cmyk", "keyword"].forEach(function(space) {
+   Converter.prototype[space] = function(vals) {
+      return this.routeSpace(space, arguments);
+   }
+});
+
+module.exports = convert;
+});require.register("conversions", function(module, exports, require, global){
+/* MIT license */
+
+module.exports = {
+  rgb2hsl: rgb2hsl,
+  rgb2hsv: rgb2hsv,
+  rgb2cmyk: rgb2cmyk,
+  rgb2keyword: rgb2keyword,
+  rgb2xyz: rgb2xyz,
+  rgb2lab: rgb2lab,
+
+  hsl2rgb: hsl2rgb,
+  hsl2hsv: hsl2hsv,
+  hsl2cmyk: hsl2cmyk,
+  hsl2keyword: hsl2keyword,
+
+  hsv2rgb: hsv2rgb,
+  hsv2hsl: hsv2hsl,
+  hsv2cmyk: hsv2cmyk,
+  hsv2keyword: hsv2keyword,
+
+  cmyk2rgb: cmyk2rgb,
+  cmyk2hsl: cmyk2hsl,
+  cmyk2hsv: cmyk2hsv,
+  cmyk2keyword: cmyk2keyword,
+
+  keyword2rgb: keyword2rgb,
+  keyword2hsl: keyword2hsl,
+  keyword2hsv: keyword2hsv,
+  keyword2cmyk: keyword2cmyk,
+  keyword2lab: keyword2lab,
+  keyword2xyz: keyword2xyz,
+
+  xyz2rgb: xyz2rgb,
+  xyz2lab: xyz2lab,
+
+  lab2xyz: lab2xyz,
+}
+
+
+function rgb2hsl(rgb) {
+  var r = rgb[0]/255,
+      g = rgb[1]/255,
+      b = rgb[2]/255,
+      min = Math.min(r, g, b),
+      max = Math.max(r, g, b),
+      delta = max - min,
+      h, s, l;
+
+  if (max == min)
+    h = 0;
+  else if (r == max)
+    h = (g - b) / delta;
+  else if (g == max)
+    h = 2 + (b - r) / delta;
+  else if (b == max)
+    h = 4 + (r - g)/ delta;
+
+  h = Math.min(h * 60, 360);
+
+  if (h < 0)
+    h += 360;
+
+  l = (min + max) / 2;
+
+  if (max == min)
+    s = 0;
+  else if (l <= 0.5)
+    s = delta / (max + min);
+  else
+    s = delta / (2 - max - min);
+
+  return [h, s * 100, l * 100];
+}
+
+function rgb2hsv(rgb) {
+  var r = rgb[0],
+      g = rgb[1],
+      b = rgb[2],
+      min = Math.min(r, g, b),
+      max = Math.max(r, g, b),
+      delta = max - min,
+      h, s, v;
+
+  if (max == 0)
+    s = 0;
+  else
+    s = (delta/max * 1000)/10;
+
+  if (max == min)
+    h = 0;
+  else if (r == max)
+    h = (g - b) / delta;
+  else if (g == max)
+    h = 2 + (b - r) / delta;
+  else if (b == max)
+    h = 4 + (r - g) / delta;
+
+  h = Math.min(h * 60, 360);
+
+  if (h < 0)
+    h += 360;
+
+  v = ((max / 255) * 1000) / 10;
+
+  return [h, s, v];
+}
+
+function rgb2cmyk(rgb) {
+  var r = rgb[0] / 255,
+      g = rgb[1] / 255,
+      b = rgb[2] / 255,
+      c, m, y, k;
+
+  k = Math.min(1 - r, 1 - g, 1 - b);
+  c = (1 - r - k) / (1 - k);
+  m = (1 - g - k) / (1 - k);
+  y = (1 - b - k) / (1 - k);
+  return [c * 100, m * 100, y * 100, k * 100];
+}
+
+function rgb2keyword(rgb) {
+  return reverseKeywords[JSON.stringify(rgb)];
+}
+
+function rgb2xyz(rgb) {
+  var r = rgb[0] / 255,
+      g = rgb[1] / 255,
+      b = rgb[2] / 255;
+
+  // assume sRGB
+  r = r > 0.04045 ? Math.pow(((r + 0.055) / 1.055), 2.4) : (r / 12.92);
+  g = g > 0.04045 ? Math.pow(((g + 0.055) / 1.055), 2.4) : (g / 12.92);
+  b = b > 0.04045 ? Math.pow(((b + 0.055) / 1.055), 2.4) : (b / 12.92);
+
+  var x = (r * 0.4124) + (g * 0.3576) + (b * 0.1805);
+  var y = (r * 0.2126) + (g * 0.7152) + (b * 0.0722);
+  var z = (r * 0.0193) + (g * 0.1192) + (b * 0.9505);
+
+  return [x * 100, y *100, z * 100];
+}
+
+function rgb2lab(rgb) {
+  var xyz = rgb2xyz(rgb),
+        x = xyz[0],
+        y = xyz[1],
+        z = xyz[2],
+        l, a, b;
+
+  x /= 95.047;
+  y /= 100;
+  z /= 108.883;
+
+  x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116);
+  y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116);
+  z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116);
+
+  l = (116 * y) - 16;
+  a = 500 * (x - y);
+  b = 200 * (y - z);
+
+  return [l, a, b];
+}
+
+
+function hsl2rgb(hsl) {
+  var h = hsl[0] / 360,
+      s = hsl[1] / 100,
+      l = hsl[2] / 100,
+      t1, t2, t3, rgb, val;
+
+  if (s == 0) {
+    val = l * 255;
+    return [val, val, val];
+  }
+
+  if (l < 0.5)
+    t2 = l * (1 + s);
+  else
+    t2 = l + s - l * s;
+  t1 = 2 * l - t2;
+
+  rgb = [0, 0, 0];
+  for (var i = 0; i < 3; i++) {
+    t3 = h + 1 / 3 * - (i - 1);
+    t3 < 0 && t3++;
+    t3 > 1 && t3--;
+
+    if (6 * t3 < 1)
+      val = t1 + (t2 - t1) * 6 * t3;
+    else if (2 * t3 < 1)
+      val = t2;
+    else if (3 * t3 < 2)
+      val = t1 + (t2 - t1) * (2 / 3 - t3) * 6;
+    else
+      val = t1;
+
+    rgb[i] = val * 255;
+  }
+
+  return rgb;
+}
+
+function hsl2hsv(hsl) {
+  var h = hsl[0],
+      s = hsl[1] / 100,
+      l = hsl[2] / 100,
+      sv, v;
+  l *= 2;
+  s *= (l <= 1) ? l : 2 - l;
+  v = (l + s) / 2;
+  sv = (2 * s) / (l + s);
+  return [h, sv * 100, v * 100];
+}
+
+function hsl2cmyk(args) {
+  return rgb2cmyk(hsl2rgb(args));
+}
+
+function hsl2keyword(args) {
+  return rgb2keyword(hsl2rgb(args));
+}
+
+
+function hsv2rgb(hsv) {
+  var h = hsv[0] / 60,
+      s = hsv[1] / 100,
+      v = hsv[2] / 100,
+      hi = Math.floor(h) % 6;
+
+  var f = h - Math.floor(h),
+      p = 255 * v * (1 - s),
+      q = 255 * v * (1 - (s * f)),
+      t = 255 * v * (1 - (s * (1 - f))),
+      v = 255 * v;
+
+  switch(hi) {
+    case 0:
+      return [v, t, p];
+    case 1:
+      return [q, v, p];
+    case 2:
+      return [p, v, t];
+    case 3:
+      return [p, q, v];
+    case 4:
+      return [t, p, v];
+    case 5:
+      return [v, p, q];
+  }
+}
+
+function hsv2hsl(hsv) {
+  var h = hsv[0],
+      s = hsv[1] / 100,
+      v = hsv[2] / 100,
+      sl, l;
+
+  l = (2 - s) * v;
+  sl = s * v;
+  sl /= (l <= 1) ? l : 2 - l;
+  l /= 2;
+  return [h, sl * 100, l * 100];
+}
+
+function hsv2cmyk(args) {
+  return rgb2cmyk(hsv2rgb(args));
+}
+
+function hsv2keyword(args) {
+  return rgb2keyword(hsv2rgb(args));
+}
+
+function cmyk2rgb(cmyk) {
+  var c = cmyk[0] / 100,
+      m = cmyk[1] / 100,
+      y = cmyk[2] / 100,
+      k = cmyk[3] / 100,
+      r, g, b;
+
+  r = 1 - Math.min(1, c * (1 - k) + k);
+  g = 1 - Math.min(1, m * (1 - k) + k);
+  b = 1 - Math.min(1, y * (1 - k) + k);
+  return [r * 255, g * 255, b * 255];
+}
+
+function cmyk2hsl(args) {
+  return rgb2hsl(cmyk2rgb(args));
+}
+
+function cmyk2hsv(args) {
+  return rgb2hsv(cmyk2rgb(args));
+}
+
+function cmyk2keyword(args) {
+  return rgb2keyword(cmyk2rgb(args));
+}
+
+
+function xyz2rgb(xyz) {
+  var x = xyz[0] / 100,
+      y = xyz[1] / 100,
+      z = xyz[2] / 100,
+      r, g, b;
+
+  r = (x * 3.2406) + (y * -1.5372) + (z * -0.4986);
+  g = (x * -0.9689) + (y * 1.8758) + (z * 0.0415);
+  b = (x * 0.0557) + (y * -0.2040) + (z * 1.0570);
+
+  // assume sRGB
+  r = r > 0.0031308 ? ((1.055 * Math.pow(r, 1.0 / 2.4)) - 0.055)
+    : r = (r * 12.92);
+
+  g = g > 0.0031308 ? ((1.055 * Math.pow(g, 1.0 / 2.4)) - 0.055)
+    : g = (g * 12.92);
+
+  b = b > 0.0031308 ? ((1.055 * Math.pow(b, 1.0 / 2.4)) - 0.055)
+    : b = (b * 12.92);
+
+  r = (r < 0) ? 0 : r;
+  g = (g < 0) ? 0 : g;
+  b = (b < 0) ? 0 : b;
+
+  return [r * 255, g * 255, b * 255];
+}
+
+function xyz2lab(xyz) {
+  var x = xyz[0],
+      y = xyz[1],
+      z = xyz[2],
+      l, a, b;
+
+  x /= 95.047;
+  y /= 100;
+  z /= 108.883;
+
+  x = x > 0.008856 ? Math.pow(x, 1/3) : (7.787 * x) + (16 / 116);
+  y = y > 0.008856 ? Math.pow(y, 1/3) : (7.787 * y) + (16 / 116);
+  z = z > 0.008856 ? Math.pow(z, 1/3) : (7.787 * z) + (16 / 116);
+
+  l = (116 * y) - 16;
+  a = 500 * (x - y);
+  b = 200 * (y - z);
+
+  return [l, a, b];
+}
+
+function lab2xyz(lab) {
+  var l = lab[0],
+      a = lab[1],
+      b = lab[2],
+      x, y, z, y2;
+
+  if (l <= 8) {
+    y = (l * 100) / 903.3;
+    y2 = (7.787 * (y / 100)) + (16 / 116);
+  } else {
+    y = 100 * Math.pow((l + 16) / 116, 3);
+    y2 = Math.pow(y / 100, 1/3);
+  }
+
+  x = x / 95.047 <= 0.008856 ? x = (95.047 * ((a / 500) + y2 - (16 / 116))) / 7.787 : 95.047 * Math.pow((a / 500) + y2, 3);
+
+  z = z / 108.883 <= 0.008859 ? z = (108.883 * (y2 - (b / 200) - (16 / 116))) / 7.787 : 108.883 * Math.pow(y2 - (b / 200), 3);
+
+  return [x, y, z];
+}
+
+function keyword2rgb(keyword) {
+  return cssKeywords[keyword];
+}
+
+function keyword2hsl(args) {
+  return rgb2hsl(keyword2rgb(args));
+}
+
+function keyword2hsv(args) {
+  return rgb2hsv(keyword2rgb(args));
+}
+
+function keyword2cmyk(args) {
+  return rgb2cmyk(keyword2rgb(args));
+}
+
+function keyword2lab(args) {
+  return rgb2lab(keyword2rgb(args));
+}
+
+function keyword2xyz(args) {
+  return rgb2xyz(keyword2rgb(args));
+}
+
+var cssKeywords = {
+  aliceblue:  [240,248,255],
+  antiquewhite: [250,235,215],
+  aqua: [0,255,255],
+  aquamarine: [127,255,212],
+  azure:  [240,255,255],
+  beige:  [245,245,220],
+  bisque: [255,228,196],
+  black:  [0,0,0],
+  blanchedalmond: [255,235,205],
+  blue: [0,0,255],
+  blueviolet: [138,43,226],
+  brown:  [165,42,42],
+  burlywood:  [222,184,135],
+  cadetblue:  [95,158,160],
+  chartreuse: [127,255,0],
+  chocolate:  [210,105,30],
+  coral:  [255,127,80],
+  cornflowerblue: [100,149,237],
+  cornsilk: [255,248,220],
+  crimson:  [220,20,60],
+  cyan: [0,255,255],
+  darkblue: [0,0,139],
+  darkcyan: [0,139,139],
+  darkgoldenrod:  [184,134,11],
+  darkgray: [169,169,169],
+  darkgreen:  [0,100,0],
+  darkgrey: [169,169,169],
+  darkkhaki:  [189,183,107],
+  darkmagenta:  [139,0,139],
+  darkolivegreen: [85,107,47],
+  darkorange: [255,140,0],
+  darkorchid: [153,50,204],
+  darkred:  [139,0,0],
+  darksalmon: [233,150,122],
+  darkseagreen: [143,188,143],
+  darkslateblue:  [72,61,139],
+  darkslategray:  [47,79,79],
+  darkslategrey:  [47,79,79],
+  darkturquoise:  [0,206,209],
+  darkviolet: [148,0,211],
+  deeppink: [255,20,147],
+  deepskyblue:  [0,191,255],
+  dimgray:  [105,105,105],
+  dimgrey:  [105,105,105],
+  dodgerblue: [30,144,255],
+  firebrick:  [178,34,34],
+  floralwhite:  [255,250,240],
+  forestgreen:  [34,139,34],
+  fuchsia:  [255,0,255],
+  gainsboro:  [220,220,220],
+  ghostwhite: [248,248,255],
+  gold: [255,215,0],
+  goldenrod:  [218,165,32],
+  gray: [128,128,128],
+  green:  [0,128,0],
+  greenyellow:  [173,255,47],
+  grey: [128,128,128],
+  honeydew: [240,255,240],
+  hotpink:  [255,105,180],
+  indianred:  [205,92,92],
+  indigo: [75,0,130],
+  ivory:  [255,255,240],
+  khaki:  [240,230,140],
+  lavender: [230,230,250],
+  lavenderblush:  [255,240,245],
+  lawngreen:  [124,252,0],
+  lemonchiffon: [255,250,205],
+  lightblue:  [173,216,230],
+  lightcoral: [240,128,128],
+  lightcyan:  [224,255,255],
+  lightgoldenrodyellow: [250,250,210],
+  lightgray:  [211,211,211],
+  lightgreen: [144,238,144],
+  lightgrey:  [211,211,211],
+  lightpink:  [255,182,193],
+  lightsalmon:  [255,160,122],
+  lightseagreen:  [32,178,170],
+  lightskyblue: [135,206,250],
+  lightslategray: [119,136,153],
+  lightslategrey: [119,136,153],
+  lightsteelblue: [176,196,222],
+  lightyellow:  [255,255,224],
+  lime: [0,255,0],
+  limegreen:  [50,205,50],
+  linen:  [250,240,230],
+  magenta:  [255,0,255],
+  maroon: [128,0,0],
+  mediumaquamarine: [102,205,170],
+  mediumblue: [0,0,205],
+  mediumorchid: [186,85,211],
+  mediumpurple: [147,112,219],
+  mediumseagreen: [60,179,113],
+  mediumslateblue:  [123,104,238],
+  mediumspringgreen:  [0,250,154],
+  mediumturquoise:  [72,209,204],
+  mediumvioletred:  [199,21,133],
+  midnightblue: [25,25,112],
+  mintcream:  [245,255,250],
+  mistyrose:  [255,228,225],
+  moccasin: [255,228,181],
+  navajowhite:  [255,222,173],
+  navy: [0,0,128],
+  oldlace:  [253,245,230],
+  olive:  [128,128,0],
+  olivedrab:  [107,142,35],
+  orange: [255,165,0],
+  orangered:  [255,69,0],
+  orchid: [218,112,214],
+  palegoldenrod:  [238,232,170],
+  palegreen:  [152,251,152],
+  paleturquoise:  [175,238,238],
+  palevioletred:  [219,112,147],
+  papayawhip: [255,239,213],
+  peachpuff:  [255,218,185],
+  peru: [205,133,63],
+  pink: [255,192,203],
+  plum: [221,160,221],
+  powderblue: [176,224,230],
+  purple: [128,0,128],
+  red:  [255,0,0],
+  rosybrown:  [188,143,143],
+  royalblue:  [65,105,225],
+  saddlebrown:  [139,69,19],
+  salmon: [250,128,114],
+  sandybrown: [244,164,96],
+  seagreen: [46,139,87],
+  seashell: [255,245,238],
+  sienna: [160,82,45],
+  silver: [192,192,192],
+  skyblue:  [135,206,235],
+  slateblue:  [106,90,205],
+  slategray:  [112,128,144],
+  slategrey:  [112,128,144],
+  snow: [255,250,250],
+  springgreen:  [0,255,127],
+  steelblue:  [70,130,180],
+  tan:  [210,180,140],
+  teal: [0,128,128],
+  thistle:  [216,191,216],
+  tomato: [255,99,71],
+  turquoise:  [64,224,208],
+  violet: [238,130,238],
+  wheat:  [245,222,179],
+  white:  [255,255,255],
+  whitesmoke: [245,245,245],
+  yellow: [255,255,0],
+  yellowgreen:  [154,205,50]
+};
+
+var reverseKeywords = {};
+for (var key in cssKeywords) {
+  reverseKeywords[JSON.stringify(cssKeywords[key])] = key;
+}
+
+});Color = require('color.js');
+})();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/drag.svg	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="20"
+   height="20"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.3.1 r9886"
+   sodipodi:docname="drag.svg">
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient3837">
+      <stop
+         style="stop-color:#000000;stop-opacity:0;"
+         offset="0"
+         id="stop3839" />
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="1"
+         id="stop3841" />
+    </linearGradient>
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3837"
+       id="radialGradient3845"
+       gradientUnits="userSpaceOnUse"
+       cx="7.1428571"
+       cy="7.3214283"
+       fx="7.1428571"
+       fy="7.3214283"
+       r="4.875" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3837"
+       id="radialGradient2989"
+       gradientUnits="userSpaceOnUse"
+       cx="7.1428571"
+       cy="7.3214283"
+       fx="7.1428571"
+       fy="7.3214283"
+       r="4.875" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3837"
+       id="radialGradient2993"
+       gradientUnits="userSpaceOnUse"
+       cx="7.1428571"
+       cy="7.3214283"
+       fx="7.1428571"
+       fy="7.3214283"
+       r="4.875" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="11.2"
+     inkscape:cx="-2.8571428"
+     inkscape:cy="25.721374"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1215"
+     inkscape:window-height="1000"
+     inkscape:window-x="65"
+     inkscape:window-y="24"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1032.3622)">
+    <rect
+       style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.83672959;stroke-opacity:1"
+       id="rect2995"
+       width="17.857143"
+       height="3.1255198"
+       x="1.0714283"
+       y="1040.7994" />
+    <rect
+       y="1034.9958"
+       x="1.0714283"
+       height="3.1255198"
+       width="17.857143"
+       id="rect2997"
+       style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.83672959;stroke-opacity:1" />
+    <rect
+       style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.83672959;stroke-opacity:1"
+       id="rect2999"
+       width="17.857143"
+       height="3.1255198"
+       x="1.0714283"
+       y="1046.603" />
+  </g>
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/filter.svg	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="20"
+   height="20"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.3.1 r9886"
+   sodipodi:docname="filter.svg">
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient3837">
+      <stop
+         style="stop-color:#000000;stop-opacity:0;"
+         offset="0"
+         id="stop3839" />
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="1"
+         id="stop3841" />
+    </linearGradient>
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3837"
+       id="radialGradient3845"
+       gradientUnits="userSpaceOnUse"
+       cx="7.1428571"
+       cy="7.3214283"
+       fx="7.1428571"
+       fy="7.3214283"
+       r="4.875" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3837"
+       id="radialGradient2989"
+       gradientUnits="userSpaceOnUse"
+       cx="7.1428571"
+       cy="7.3214283"
+       fx="7.1428571"
+       fy="7.3214283"
+       r="4.875" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3837"
+       id="radialGradient2993"
+       gradientUnits="userSpaceOnUse"
+       cx="7.1428571"
+       cy="7.3214283"
+       fx="7.1428571"
+       fy="7.3214283"
+       r="4.875" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="11.2"
+     inkscape:cx="-13.482143"
+     inkscape:cy="25.721374"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1215"
+     inkscape:window-height="1000"
+     inkscape:window-x="65"
+     inkscape:window-y="24"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1032.3622)">
+    <g
+       id="g2995"
+       transform="translate(-0.10218065,-0.25515)">
+      <path
+         inkscape:connector-curvature="0"
+         id="path2991"
+         d="m 1.7857143,1035.4618 6.6329331,6.8148 0,7.4963"
+         style="fill:none;stroke:#000000;stroke-width:1.29905522px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+      <path
+         style="fill:none;stroke:#000000;stroke-width:1.29905522px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+         d="m 18.418647,1035.4618 -6.632933,6.8148 0,7.4963"
+         id="path2993"
+         inkscape:connector-curvature="0" />
+    </g>
+  </g>
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/hexagram.css	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,441 @@
+/* Global font stuff */
+body {
+    font-family: sans-serif;
+}
+
+/*
+The visualization element needs to take up all available space.
+*/
+#visualization {
+    position: absolute;
+    left: 0;
+    right: 0;
+    top: 0;
+    bottom: 0;
+    overflow: hidden;
+}
+
+/* 
+Row and column layout boilerplate
+From http://blog.stevensanderson.com/2011/10/05/full-height-app-layouts-a-css-trick-to-make-it-easier/
+*/
+
+body {
+    margin: 0;
+}
+
+.row, .col {
+    overflow: hidden;
+    position: absolute;
+}
+
+.row { 
+    left: 0;
+    right: 0; 
+}
+
+.col { 
+    top: 0; 
+    bottom: 0;
+}
+
+.scroll-x {
+    overflow-x: auto;
+}
+
+.scroll-y {
+    overflow-y: auto;
+}
+
+/*
+Read as "The content row", distinct from whatever else that element might be.
+*/
+.content.row {
+    top: 6em;
+    bottom: 0;
+    z-index: 1;
+}
+
+.header.row {
+    height: 4em;
+    top: 2em;
+    border-bottom: 1px solid black;
+    overflow: visible;
+}
+
+.browse.col {
+    left: 0;
+    width: 22em;
+    /* Not sure what's up with the lack of dynamic height here, but... */
+    height: 4em;
+}
+
+.shortlist.col {
+    right: 0;
+    width: 21em;
+    overflow: visible;
+}
+
+.error.row{
+    height: 2em;
+    top: 0;
+    border-bottom: 1px solid black;
+    background: #F7EFAD;
+    z-index: 101;
+    display: none;
+}
+
+.tools.row {
+    height: 2em;
+    top: 0em;
+    border-bottom: 1px solid black;
+    background: #e0e0e0;
+}
+
+/*
+These are all supposed to stack against the left end in a toolbar thingy.
+*/
+.stacker {
+    float: left;
+    line-height: 2em;
+    margin-left: 0.5em;
+    height: 2em;
+}
+
+#error-notification {
+    color: red;
+    font-weight: bold;
+    text-align: center;
+    line-height: 2em;
+    margin-left: 0.5em;
+    height: 2em;
+}
+
+/* Except these which stack on the right */
+.stacker.right {
+    float: right;
+    margin-left: 0;
+    margin-right: 0.5em;
+}
+
+/* Code for fancy expandy side pannels */
+
+.panel-holder {
+    overflow: hidden;
+    height: 3.9em;
+    max-height: 4em;
+    top: 2.1em;
+    width: 20em;
+    position: fixed;
+    z-index: 100;
+    transition: max-height 100ms;
+}
+
+/* When a holder gets moused over, open it up to 100% window height */
+.panel-holder:hover {
+    height: auto;
+    max-height: 100%;
+    bottom: 0;
+}
+
+.panel {
+    position: relative;
+    top: 0;
+    left: 0;
+    height: 100%;
+    border-radius: 10px;
+    border: 1px solid gray;
+    background: white;
+    z-index: 100;
+}
+
+.panel-contents {
+    overflow-y: auto;
+    overflow-x: hidden;
+    position: absolute;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    top: 1em;
+}
+
+.panel-title {
+    text-align: center;
+    font-weight: bold;
+    font-family: sans-serif;
+    height: 1em;
+}
+
+/* Browsing stuff */
+
+#browse-holder {
+    margin-left: 0.5em;
+}
+
+#search {
+    width: 20em;
+}
+
+#recalculate-statistics {
+    padding: 0.2em;
+}
+
+.recalculate-throbber {
+    display: none;
+}
+
+/* Do some custom styling of browse results */
+.layer-entry {
+    padding-right: 20px;
+}
+.select2-results .select2-highlighted {
+    background-image: url("right.svg");
+    background-repeat: no-repeat;
+    background-position: right center;
+}
+
+.layer-name {
+    /* Force silly underscore names into shape */
+    word-wrap: break-word;
+}
+
+/* Make the browse dropdown tall */
+.results-dropdown.select2-container .select2-results {
+    max-height: 20em;
+}
+
+.results-dropdown .select2-results {
+    max-height: 20em;
+}
+
+.layer-metadata {
+    font-size: 70%;
+    /* Serif is more visible at small sizes */
+    font-family: serif;
+}
+
+/* Vertical alignment tables */
+.vertical-table {
+    display: table;
+}
+
+.vertical-cell {
+    display: table-cell;
+    vertical-align: middle;
+}
+
+/* 
+    Not much room for the results for this column. Just get them out of the way.
+*/
+#browse-results.panel-contents {
+    top: 4em;
+}
+
+/* Shortlist UI stuff */
+
+.shortlist-controls {
+    position: absolute;
+    left: 0;
+    top: 0;
+    bottom: 0;
+    width: 2em;
+    background-image: url("drag.svg");
+    background-repeat: no-repeat;
+    background-position: 50% 70%; 
+    border-radius: 5px;
+    padding: 5px;
+}
+
+.shortlist-entry {
+    position: relative;
+    height: auto;
+    width: auto;
+    background: #E0E0FF;
+    border-radius: 5px;
+    padding: 5px;
+    padding-left: 3em;
+    margin: 0.5em;
+    word-wrap: break-word;
+}
+
+.shortlist-entry.selection {
+    background: #FFE0FF;
+}
+
+.shortlist-controls {
+    cursor: grab;
+    cursor: -moz-grab;
+    cursor: -webkit-grab;
+}
+
+.shortlist-controls:activate {
+    cursor: grabbing;
+    cursor: -moz-grabbing;
+    cursor: -webkit-grabbing;
+}
+
+.shortlist-entry:after {
+    content: "";
+    display: block;
+    clear: both;
+}
+
+#shortlist-holder {
+    right: 0;
+}
+
+.layer-on {
+
+}
+
+.radio-label, .radio-clear {
+    margin-right: 0.1em;
+    margin-left: 0.1em;
+}
+
+/* Hide the radio button clearing links unless the radio button is selected. */
+input[type="radio"] + .radio-clear {
+    display: none;
+}
+
+input[type="radio"]:checked + .radio-clear {
+    display: inline;
+}
+
+
+/* These are the layer scaling sliders */
+.range-slider {
+    width: 10em;
+    margin-left: 0.3em;
+    margin-right: 0.3em;
+    margin-top: 0.5em;
+}
+
+/* Filtering stuff */
+.filter {
+    line-height: 1em;
+    vertical-align: center;
+}
+
+.filter-threshold, .filter-value {
+    display: none;
+    width: 5em;
+}
+
+/* This is the color key */
+.key {
+    overflow: visible;
+    position: absolute;
+    z-index: 2;
+    top: 2em;
+    right: 2em;
+    height: 150px;
+    width: 150px;
+    display: none;
+    pointer-events: none;
+    font-family: serif;
+}
+
+#color-key {
+    width: 100px;
+    height: 100px;
+    position: absolute;
+    display: block;
+    left:25px;
+    top: 25px;
+    border: 1px solid white;
+}
+
+.label {
+    color: white;
+}
+
+.axis {
+    word-wrap: break-word;
+}
+
+#low-both {
+    position: absolute;
+    left: 0;
+    bottom: 0;
+}
+
+#high-x {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+}
+
+#high-y {
+    position: absolute;
+    left: 0;
+    top: 0;
+}
+
+#x-axis {
+    position: absolute;
+    top: 155px;
+    width: 150px;
+    margin: auto;
+    text-align: center;
+}
+
+/* 
+    Complicated table centering thing from 
+    http://blog.themeforest.net/tutorials/vertical-centering-with-css/
+*/
+
+#y-axis-holder {
+    display: table;
+    right: 155px;
+    width: 150px;
+    top: 0;
+    height: 100%;
+    position: absolute;
+}
+
+#y-axis-cell {
+    display: table-cell;
+    vertical-align: middle;
+}
+
+#y-axis {
+    text-align: right;
+}
+
+/* This is the info window/infocard styling */
+.infocard {
+    word-break: break-all;
+    font-family: sans-serif;
+}
+
+.info-row {
+    margin-bottom: 0.1em;
+}
+
+.info-key {
+    background: green;
+    color: white;
+    text-align: center;
+    font-weight: bold;
+}
+
+.info-value {
+    background: #F0F0F0;
+}
+
+/* Tool stuff */
+textarea.import {
+    height: 10em;
+    width: 100%;
+    font-size: 10pt;
+}
+
+input.import {
+    width: 100%;
+    /* 
+        Apparently Firefox ignores your width and makes it some random size 
+        depending on font size. Make that small enough. 
+    */
+    font-size: 0.5em;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/hexagram.html	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,71 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="UTF-8" />
+        <link rel="stylesheet" type="text/css" href="http://code.jquery.com/ui/1.10.2/themes/smoothness/jquery-ui.css" />
+        <link rel="stylesheet" type="text/css" href="select2.css" />
+        <link rel="stylesheet" type="text/css" href="hexagram.css" />
+        <script type="text/javascript" src="http://maps.googleapis.com/maps/api/js?sensor=false"></script>
+        <script type="text/javascript" src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
+        <script type="text/javascript" src="http://code.jquery.com/ui/1.10.2/jquery-ui.js"></script>
+        <script type="text/javascript" src="jquery.tsv.js"></script>
+        <script type="text/javascript" src="color-0.4.1.js"></script>
+        <script type="text/javascript" src="maplabel-compiled.js"></script>
+        <script type="text/javascript" src="jstat-1.0.0.js"></script>
+        <script type="text/javascript" src="select2.js"></script>
+        <script type="text/javascript" src="hexagram.js"></script>
+        <script type="text/javascript" src="tools.js"></script>
+        <title>Hexagram Visualization</title>
+    </head>
+    <body>
+        <div class="tools row" id="toolbar">
+            <div class="stacker">
+                Tools:
+            </div>
+            <div class="stacker right">
+                <img src="throbber.svg" class="recalculate-throbber" title="Recalculating..." />
+                <span id="jobs-running">0</span>/<span id="jobs-ever">0</span> jobs running.
+            </div>
+        </div>
+        <div class="header row" id="header">
+            <div class="browse col vertical-table">
+                <div class="vertical-cell">
+                    <div id="browse-holder">
+                        <input id="search" type="hidden"/>
+                        <img src="statistics.svg" id="recalculate-statistics" title="Recalculate Statistics"/>
+                        <img src="throbber.svg" class="recalculate-throbber" title="Recalculating..." />
+                    </div>
+                </div>
+            </div>
+            <div class="shortlist col">
+                <div id="shortlist-holder" class="panel-holder">
+                    <div id="shortlist-panel" class="panel">
+                        <div class="panel-title">Shortlist</div>
+                        <div id="shortlist" class="panel-contents">
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="error row" id="error">
+            <div id="error-notification">
+            </div>
+        </div>
+        <div class="content row" id="content">
+            <div class="key">
+                <canvas id="color-key" width="100" height="100"></canvas>
+                <div class="label" id="low-both">Low</div>
+                <div class="label" id="high-x">High</div>
+                <div class="label y" id="high-y">High</div>
+                <div class="label axis" id="x-axis"></div>
+                <div id="y-axis-holder">
+                    <div id="y-axis-cell">
+                        <div class="label axis" id="y-axis"></div>
+                    </div>
+                </div>
+            </div>
+            <div id="visualization">
+            </div>
+        </div>
+    </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/hexagram.js	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,2776 @@
+// hexagram.js
+// Run the hexagram visualizer client.
+
+// Globals
+// This is a mapping from coordinates [x][y] in the global hex grid to signature
+// name
+var signature_grid = [];
+
+// This holds a global list of layer pickers in layer order. It is also the
+// authority on what layers are currently selected.
+var layer_pickers = [];
+
+// This holds a list of layer objects by name.
+// Layer objects have:
+// A downloading function "downloader"
+// A data object (from hex name to float) "data"
+// A magnitude "magnitude"
+// A boolean "selection" that specifies whether this is a user selection or not.
+// (This may be absent, which is the same as false.)
+// Various optional metadata fields
+var layers = {};
+
+// This is a list of layer names maintained in sorted order.
+var layer_names_sorted = [];
+
+// This holds an array of layer names that the user has added to the "shortlist"
+// They can be quickly selected for display.
+var shortlist = [];
+
+// This holds an object form shortlisted layer names to jQuery shortlist UI
+// elements, so we can efficiently tell if e.g. one is selected.
+var shortlist_ui = {};
+
+// This holds colormaps (objects from layer values to category objects with a 
+// name and color). They are stored under the name of the layer they apply to.
+var colormaps = {}
+
+// This holds an array of the available score matrix filenames
+var available_matrices = [];
+
+// This holds the Google Map that we use for visualization
+var googlemap = null;
+
+// This is the global Google Maps info window. We only want one hex to have its
+// info open at a time.
+var info_window = null;
+
+// This holds the signature name of the hex that the info window is currently
+// about.
+var selected_signature = undefined;
+
+// Which tool is the user currently using (string name or undefined for no tool)
+// TODO: This is a horrible hack, replace it with a unified tool system at once.
+var selected_tool = undefined;
+
+// This holds the grid of hexagon polygons on that Google Map.
+var polygon_grid = [];
+
+// This holds an object of polygons by signature name
+var polygons = {};
+
+// How big is a hexagon in google maps units? This gets filled in once we have 
+// the hex assignment data. (This is really the side length.)
+var hex_size;
+
+// This holds a handle for the currently enqueued view redrawing timeout.
+var redraw_handle;
+
+// This holds all the currently active tool event listeners.
+// They are indexed by handle, and are objects with a "handler" and an "event".
+var tool_listeners = {};
+
+// This holds the next tool listener handle to give out
+var tool_listener_next_id = 0;
+
+// This holds the next selection number to use. Start at 1 since the user sees 
+// these.
+var selection_next_id = 1;
+
+// This is a pool of statistics Web Workers.
+var rpc_workers = [];
+
+// This holds which RPC worker we ought to give work to next.
+// TODO: Better scheduling, and wrap all this into an RPC object.
+var next_free_worker = 0;
+
+// This holds how namy RPC jobs are currently running
+var jobs_running = 0;
+
+// This is the object of pending callbacks by RPC id
+var rpc_callbacks = {};
+
+// This is the next unallocated RPC id
+var rpc_next_id = 0;
+
+// How many statistics Web Workers should we start?
+var NUM_RPC_WORKERS = 10;
+
+// What's the minimum number of pixels that hex_size must represent at the 
+// current zoom level before we start drawing hex borders?
+var MIN_BORDER_SIZE = 10;
+
+// And how thick should the border be when drawn?
+var HEX_STROKE_WEIGHT = 2;
+
+// How many layers do we know how to draw at once?
+var MAX_DISPLAYED_LAYERS = 2;
+
+// How many layer search results should we display at once?
+var SEARCH_PAGE_SIZE = 10;
+
+// How big is our color key in pixels?
+var KEY_SIZE = 100;
+
+// This is an array of all Google Maps events that tools can use.
+var TOOL_EVENTS = [
+    "click",
+    "mousemove"
+];
+
+function print(text) {
+    // Print some logging text to the browser console
+        
+    if(console && console.log) {
+        // We know the console exists, and we can log to it.
+        console.log(text);
+    }
+}
+
+function complain(text) {
+    // Display a temporary error message to the user.
+    $("#error-notification").text(text);
+    $(".error").show().delay(1000).fadeOut(400);
+    
+    if(console && console.error) {
+        // Inform the browser console of this problem.as
+        console.error(text);
+    }
+}
+
+function make_hexagon(row, column, hex_side_length, grid_offset) {
+    // Make a new hexagon representing the hexagon at the given grid coordinates.
+    // hex_side_length is the side length of hexagons in Google Maps world 
+    // coordinate units. grid_offset specifies a distance to shift the whole 
+    // grid down and right from the top left corner of the map. This lets us 
+    // keep the whole thing away from the edges of the "earth", where Google 
+    // Maps likes to wrap.
+    // Returns the Google Maps polygon.
+    
+    // How much horizontal space is needed per hex on average, stacked the 
+    // way we stack them (wiggly)?
+    var hex_column_width = 3.0/2.0 * hex_side_length;
+    
+    // How tall is a hexagon?
+    var hex_height = Math.sqrt(3) * hex_side_length;
+    
+    // How far apart are hexagons on our grid, horizontally (world coordinate units)?
+    var hex_padding_horizontal = 0;
+    
+    // And vertically (world coordinate units)?
+    var hex_padding_veritcal = 0;
+    
+    // First, what are x and y in 0-256 world coordinates fo this grid position?
+    var x = column * (hex_column_width + hex_padding_horizontal);
+    var y = row * (hex_height + hex_padding_veritcal);
+    if(column % 2 == 1) {
+        // Odd columns go up
+        y -= hex_height / 2; 
+    }
+    
+    // Apply the grid offset to this hex
+    x += grid_offset;
+    y += grid_offset;
+    
+    // That got X and Y for the top left corner of the bounding box. Shift to 
+    // the center.
+    x += hex_side_length;
+    y += hex_height / 2;
+    
+    // Offset the whole thing so no hexes end up off the map when they wiggle up
+    y += hex_height / 2;
+    
+    // This holds an array of all the hexagon corners
+    var coords = [
+        get_LatLng(x - hex_side_length, y),
+        get_LatLng(x - hex_side_length / 2, y - hex_height / 2),
+        get_LatLng(x + hex_side_length / 2, y - hex_height / 2),
+        get_LatLng(x + hex_side_length, y),
+        get_LatLng(x + hex_side_length / 2, y + hex_height / 2),
+        get_LatLng(x - hex_side_length / 2, y + hex_height / 2),
+    ];
+    
+    // We don't know whether the hex should start with a stroke or not without 
+    // looking at the current zoom level.
+    // Get the current zoom level (low is out)
+    var zoom = googlemap.getZoom();
+        
+    // API docs say: pixelCoordinate = worldCoordinate * 2 ^ zoomLevel
+    // So this holds the number of pixels that the global length hex_size 
+    // corresponds to at this zoom level.
+    var hex_size_pixels = hex_size * Math.pow(2, zoom);
+    
+    // Construct the Polygon
+    var hexagon = new google.maps.Polygon({
+        paths: coords,
+        strokeColor: "#000000",
+        strokeOpacity: 1.0,
+        // Only turn on the border if we're big enough
+        strokeWeight: hex_size_pixels < MIN_BORDER_SIZE ? 0 : HEX_STROKE_WEIGHT, 
+        fillColor: "#FF0000",
+        fillOpacity: 1.0
+    });
+    
+    // Attach the hexagon to the global map
+    hexagon.setMap(googlemap);
+    
+    // Set up the click listener to move the global info window to this hexagon
+    // and display the hexagon's information
+    google.maps.event.addListener(hexagon, "click", function(event) {
+        if(selected_tool == undefined) {
+            // The user isn't trying to use a tool currently, so we can use 
+            // their clicks for the infowindow.
+            
+            // Remove the window from where it currently is
+            info_window.close();
+
+            // Place the window in the center of this hexagon.
+            info_window.setPosition(get_LatLng(x, y));
+            
+            // Record that this signature is selected now
+            selected_signature = hexagon.signature;
+            
+            // Calculate the window's contents and make it display them.
+            redraw_info_window();
+        }
+    });
+    
+    // Subscribe the tool listeners to events on this hexagon
+    subscribe_tool_listeners(hexagon);
+    
+    return hexagon;
+} 
+
+function set_hexagon_signature(hexagon, text) {
+    // Given a polygon representing a hexagon, set the signature that the
+    // hexagon represents.
+    hexagon.signature = text;
+}
+
+function set_hexagon_color(hexagon, color) {
+    // Given a polygon, set the hexagon's current background 
+    // color.
+    
+    hexagon.setOptions({
+        fillColor: color
+    });
+}
+
+function set_hexagon_stroke_weight(hexagon, weight) {
+    // Given a polygon, set the weight of hexagon's border stroke, in number of
+    // screen pixels.
+    
+    hexagon.setOptions({
+        strokeWeight: weight
+    });
+}
+
+function redraw_info_window() {
+    // Set the contents of the global info window to reflect the currently 
+    // visible information about the global selected signature. 
+    
+    if(selected_signature == undefined) {
+        // No need to update anything
+        return;
+    }
+
+    // Go get the infocard that goes in the info_window and, when it's 
+    // prepared, display it.
+    with_infocard(selected_signature, function(infocard) {
+        // The [0] is supposed to get the DOM element from the jQuery 
+        // element.
+        info_window.setContent(infocard[0]);
+        
+        // Open the window. It may already be open, or it may be closed but 
+        // properly positioned and waiting for its initial contents before 
+        // opening.
+        info_window.open(googlemap);
+    });
+}
+
+function with_infocard(signature, callback) {
+    // Given a signature, call the callback with a jQuery element representing 
+    // an "info card" about that signature. It's the contents of the infowindow 
+    // that we want to appear when the user clicks on the hex representing this 
+    // signature, and it includes things like the signature name and its values
+    // under any displayed layers (with category names if applicable).
+    // We return by callback because preparing the infocard requires reading 
+    // from the layers, which are retrieved by callback.
+    // TODO: Can we say that we will never have to download a layer here and 
+    // just directly access them? Is that neater or less neat?
+    
+    // Using jQuery to build this saves us from HTML injection by making jQuery
+    // do all the escaping work (we only ever set text).
+    
+    function row(key, value) {
+        // Small helper function that returns a jQuery element that displays the
+        // given key being the given value.
+        
+        // This holds the root element of the row
+        var root = $("<div/>").addClass("info-row");
+        
+        // Add the key and value elements
+        root.append($("<div/>").addClass("info-key").text(key));
+        root.append($("<div/>").addClass("info-value").text(value));
+        
+        return root;
+    }
+    
+    // This holds a list of the string names of the currently selected layers,
+    // in order.
+    // Just use everything on the shortlist.
+    var current_layers = shortlist;
+    
+    // Obtain the layer objects (mapping from signatures/hex labels to colors)
+    with_layers(current_layers, function(retrieved_layers) { 
+        
+        // This holds the root element of the card.
+        var infocard = $("<div/>").addClass("infocard");
+        
+        infocard.append(row("Name", signature).addClass("info-name"));
+    
+        for(var i = 0; i < current_layers.length; i++) {
+            // This holds the layer's value for this signature
+            var layer_value = retrieved_layers[i].data[signature];
+            
+            if(have_colormap(current_layers[i])) {
+                // This is a color map
+                
+                // This holds the category object for this category number, or
+                // undefined if there isn't one.
+                var category = colormaps[current_layers[i]][layer_value];
+                
+                if(category != undefined) {
+                    // There's a specific entry for this category, with a 
+                    // human-specified name and color.
+                    // Use the name as the layer value
+                    layer_value = category.name;
+                }
+            }
+            
+            if(layer_value == undefined) {
+                // Let the user know that there's nothing there in this layer.
+                layer_value = "<undefined>";
+            }
+            
+            // Make a listing for this layer's value
+            infocard.append(row(current_layers[i], layer_value));
+        }
+        
+        // Return the infocard by callback
+        callback(infocard);
+    }); 
+    
+}
+
+function add_layer_url(layer_name, layer_url, attributes) {
+    // Add a layer with the given name, to be downloaded from the given URL, to
+    // the list of available layers.
+    // Attributes is an object of attributes to copy into the layer.
+    
+    // Store the layer. Just keep the URL, since with_layer knows what to do
+    // with it.
+    layers[layer_name] = {
+        url: layer_url,
+        data: undefined,
+        magnitude: undefined
+    };
+    
+    for(var name in attributes) {
+        // Copy over each specified attribute
+        layers[layer_name][name] = attributes[name];
+    }
+    
+    // Add it to the sorted layer list.
+    layer_names_sorted.push(layer_name);
+    
+    // Don't sort because our caller does that when they're done adding layers.
+
+}
+
+function add_layer_data(layer_name, data, attributes) {
+    // Add a layer with the given name, with the given data to the list of 
+    // available layers.
+    // Attributes is an object of attributes to copy into the layer.
+    
+    // Store the layer. Just put in the data. with_layer knows what to do if the
+    // magnitude isn't filled in.
+    layers[layer_name] = {
+        url: undefined,
+        data: data,
+        magnitude: undefined
+    };
+    
+    for(var name in attributes) {
+        // Copy over each specified attribute
+        layers[layer_name][name] = attributes[name];
+    }
+    
+    // Add it to the sorted layer list and sort
+    layer_names_sorted.push(layer_name);
+
+    // Don't sort because our caller does that when they're done adding layers.
+}
+
+function with_layer(layer_name, callback) {
+    // Run the callback, passing it the layer (object from hex label/signature
+    // to float) with the given name.
+    // This is how you get layers, and allows for layers to be downloaded 
+    // dynamically. 
+    // have_layer must return true for the given name.
+    
+    // First get what we have stored for the layer
+    var layer = layers[layer_name];
+    
+    if(layer.data == undefined) {
+        // We need to download the layer.
+        print("Downloading \"" + layer.url + "\"");
+        
+        // Go get it (as text!)
+        $.get(layer.url, function(layer_tsv_data) {
+
+            // This is the TSV as parsed by our TSV-parsing plugin
+            var layer_parsed = $.tsv.parseRows(layer_tsv_data);
+
+            // This is the layer we'll be passing out. Maps from 
+            // signatures to floats on -1 to 1.
+            var layer_data = {};
+
+            for(var j = 0; j < layer_parsed.length; j++) {
+                // This is the label of the hex
+                var label = layer_parsed[j][0];
+                
+                if(label == "") {
+                    // Skip blank lines
+                    continue;
+                }
+                
+                // This is the heat level (-1 to 1)
+                var heat = parseFloat(layer_parsed[j][1]);
+                
+                // Store in the layer
+                layer_data[label] = heat;
+            }
+    
+            // Save the layer data locally
+            layers[layer_name].data = layer_data;
+ 
+            // Now the layer has been properly downloaded, but it may not have
+            // metadata. Recurse with the same callback to get metadata.
+            with_layer(layer_name, callback);
+        }, "text");
+    } else if(layer.magnitude == undefined) {
+        // We've downloaded it already, or generated it locally, but we don't
+        // know the magnitude. Compute that and check if it's a colormap.
+       
+        // Grab the data, which we know is defined.
+        var layer_data = layers[layer_name].data;
+       
+        // Store the maximum magnitude in the layer
+        // -1 is a good starting value since this always comes out positive
+        var magnitude = -1;
+        
+        // We also want to know if all layer entries are non-negative 
+        // integers (and it is thus valid as a colormap).
+        // If so, we want to display it as a colormap, so we will add an 
+        // empty entry to the colormaps object (meaning we should 
+        // auto-generate the colors on demand).
+        // This stores whether the layer is all integrs
+        all_nonnegative_integers = true;
+        
+        for(var signature_name in layer_data) {
+            // Take the new max if it's bigger (and thus not something silly
+            // like NaN).
+            // This holds the potential new max magnitude.
+            var new_magnitude = Math.abs(layer_data[signature_name]);
+            if(new_magnitude > magnitude) {
+                magnitude = new_magnitude;
+            }
+            
+            if(layer_data[signature_name] % 1 !== 0 || 
+                layer_data[signature_name] < 0 ) {
+                
+                // If we have an illegal value for a colormap, record that
+                // fact
+                // See http://stackoverflow.com/a/3886106
+                
+                all_nonnegative_integers = false;
+            }
+        }
+        
+        // Save the layer magnitude for later.
+        layer.magnitude = magnitude;
+        
+        if(!have_colormap(layer_name) && all_nonnegative_integers) {
+            // Add an empty colormap for this layer, so that 
+            // auto-generated discrete colors will be used.
+            // TODO: Provide some way to override this if you really do want
+            // to see integers as a heatmap?
+            // The only overlap with the -1 to 1 restricted actual layers
+            // is if you have a data set with only 0s and 1s. Is it a
+            // heatmap layer or a colormap layer?
+            colormaps[layer_name] = {};
+            print("Inferring that " + layer_name + 
+                " is really a colormap");
+        }
+        
+        // Now layer metadata has been filled in. Call the callback.
+        callback(layer);
+    } else {
+        // It's already downloaded, and already has metadata.
+        // Pass it to our callback
+        callback(layer);
+    }
+}
+
+function with_layers(layer_list, callback) {
+    // Given an array of layer names, call the callback with an array of the 
+    // corresponding layer objects (objects from signatures to floats).
+    // Conceptually it's like calling with_layer several times in a loop, only
+    // because the whole thing is continuation-based we have to phrase it in
+    // terms of recursion.
+    
+    // See http://marijnhaverbeke.nl/cps/
+    // "So, we've created code that does exactly the same as the earlier 
+    // version, but is twice as confusing."
+    
+    if(layer_list.length == 0) {
+        // Base case: run the callback with an empty list
+        callback([]);
+    } else {
+        // Recursive case: handle the last thing in the list
+        with_layers(layer_list.slice(0, layer_list.length - 1), 
+            function(rest) {
+            
+            // We've recursively gotten all but the last layer
+            // Go get the last one, and pass the complete array to our callback.
+            
+            with_layer(layer_list[layer_list.length - 1], 
+                function(last) {
+            
+                // Mutate the array. Shouldn't matter because it won't matter 
+                // for us if callback does it.
+                rest.push(last);
+                
+                // Send the complete array to the callback.
+                callback(rest);
+            
+            });
+            
+        });
+       
+    }
+}
+
+function have_layer(layer_name) {
+    // Returns true if a layer exists with the given name, false otherwise.
+    return layers.hasOwnProperty(layer_name);
+}
+
+function make_shortlist_ui(layer_name) {
+    // Return a jQuery element representing the layer with the given name in the
+    // shortlist UI.
+    
+    // This holds the root element for this shortlist UI entry
+    var root = $("<div/>").addClass("shortlist-entry");
+    root.data("layer", layer_name);
+    
+    // If this is a selection, give the layer a special class
+    // TODO: Justify not having to use with_layer because this is always known 
+    // client-side
+    if(layers[layer_name].selection) {
+        root.addClass("selection");
+    }
+    
+    // We have some configuration stuff and then the div from the dropdown
+    // This holds all the config stuff
+    var controls = $("<div/>").addClass("shortlist-controls");
+    
+    // Add a remove link
+    var remove_link = $("<a/>").addClass("remove").attr("href", "#").text("X");
+    
+    controls.append(remove_link);
+    
+    // Add a checkbox for whether this is enabled or not
+    var checkbox = $("<input/>").attr("type", "checkbox").addClass("layer-on");
+    
+    controls.append(checkbox);
+    
+    root.append(controls);
+    
+    var contents = $("<div/>").addClass("shortlist-contents");
+    
+    // Add the layer name
+    contents.append($("<span/>").text(layer_name));
+    
+    // Add all of the metadata. This is a div to hold it
+    var metadata_holder = $("<div/>").addClass("metadata-holder");
+    
+    // Fill it in
+    fill_layer_metadata(metadata_holder, layer_name);
+    
+    contents.append(metadata_holder);
+    
+    // Add a div to hold the filtering stuff so it wraps together.
+    var filter_holder = $("<div/>").addClass("filter-holder");
+    
+    // Add an image label for the filter control.
+    // TODO: put this in a label
+    var filter_image = $("<img/>").attr("src", "filter.svg")
+    filter_image.addClass("control-icon");
+    filter_image.attr("title", "Filter on Layer");
+    filter_image.addClass("filter");
+    
+    // Add a control for filtering
+    var filter_control = $("<input/>").attr("type", "checkbox");
+    filter_control.addClass("filter-on");
+    
+    filter_holder.append(filter_image);
+    filter_holder.append(filter_control);
+    
+    // Add a text input to specify a filtering threshold for continuous layers
+    var filter_threshold = $("<input/>").addClass("filter-threshold");
+    // Initialize to a reasonable value.
+    filter_threshold.val(0);
+    filter_holder.append(filter_threshold);
+    
+    // Add a select input to pick from a discrete list of values to filter on
+    var filter_value = $("<select/>").addClass("filter-value");
+    filter_holder.append(filter_value);
+    
+    contents.append(filter_holder);
+    if(layers[layer_name].selection) {
+        // We can do statistics on this layer.
+        
+        // Add a div to hold the statistics stuff so it wraps together.
+        var statistics_holder = $("<div/>").addClass("statistics-holder");
+        
+        // Add an icon
+        var statistics_image = $("<img/>").attr("src", "statistics.svg");
+        statistics_image.addClass("control-icon");
+        statistics_image.attr("title", "Statistics Group");
+        statistics_holder.append(statistics_image);
+        
+        // Label the "A" radio button.
+        var a_label = $("<span/>").addClass("radio-label").text("A");
+        statistics_holder.append(a_label);
+        
+        // Add a radio button for being the "A" group
+        var statistics_a_control = $("<input/>").attr("type", "radio");
+        statistics_a_control.attr("name", "statistics-a");
+        statistics_a_control.addClass("statistics-a");
+        // Put the layer name in so it's easy to tell which layer is A.
+        statistics_a_control.data("layer-name", layer_name);
+        statistics_holder.append(statistics_a_control);
+        
+        // And a link to un-select it if it's selected
+        var statistics_a_clear = $("<a/>").attr("href", "#").text("X");
+        statistics_a_clear.addClass("radio-clear");
+        statistics_holder.append(statistics_a_clear);
+        
+        // Label the "B" radio button.
+        var b_label = $("<span/>").addClass("radio-label").text("B");
+        statistics_holder.append(b_label);
+        
+        // Add a radio button for being the "B" group
+        var statistics_b_control = $("<input/>").attr("type", "radio");
+        statistics_b_control.attr("name", "statistics-b");
+        statistics_b_control.addClass("statistics-b");
+        // Put the layer name in so it's easy to tell which layer is A.
+        statistics_b_control.data("layer-name", layer_name);
+        statistics_holder.append(statistics_b_control);
+        
+        // And a link to un-select it if it's selected
+        var statistics_b_clear = $("<a/>").attr("href", "#").text("X");
+        statistics_b_clear.addClass("radio-clear");
+        statistics_holder.append(statistics_b_clear);
+        
+        contents.append(statistics_holder);
+        
+        // Statistics UI logic
+    
+        // Make the clear links work
+        statistics_a_clear.click(function() {
+            statistics_a_control.prop("checked", false);
+        });
+        statistics_b_clear.click(function() {
+            statistics_b_control.prop("checked", false);
+        });
+    }
+    
+    // Add a div to contain layer settings
+    var settings = $("<div/>").addClass("settings");
+    
+    // Add a slider for setting the min and max for drawing
+    var range_slider = $("<div/>").addClass("range range-slider");
+    settings.append($("<div/>").addClass("stacker").append(range_slider));
+    
+    // And a box that tells us what we have selected in the slider.
+    var range_display = $("<div/>").addClass("range range-display");
+    range_display.append($("<span/>").addClass("low"));
+    range_display.append(" to ");
+    range_display.append($("<span/>").addClass("high"));
+    settings.append($("<div/>").addClass("stacker").append(range_display));
+    
+    contents.append(settings);
+    
+    root.append(contents);
+    
+    // Handle enabling and disabling
+    checkbox.change(function() {
+        if($(this).is(":checked") && get_current_layers().length > 
+            MAX_DISPLAYED_LAYERS) {
+                
+            // Enabling this checkbox puts us over the edge, so un-check it
+            $(this).prop("checked", false);
+            
+            // Skip the redraw
+            return;
+        }
+    
+        refresh();
+    });
+    
+    // Run the removal process
+    remove_link.click(function() {
+        // Remove this layer from the shortlist
+        shortlist.splice(shortlist.indexOf(layer_name), 1);
+        
+        // Remove this from the DOM
+        root.remove();
+        
+        // Make the UI match the list.
+        update_shortlist_ui();
+        
+        if(checkbox.is(":checked") || filter_control.is(":checked")) {
+            // Re-draw the view since we were selected (as coloring or filter) 
+            // before removal.
+            refresh();
+        }
+    });
+    
+    // Functionality for turning filtering on and off
+    filter_control.change(function() {
+        if(filter_control.is(":checked")) {
+            // First, figure out what kind of filter settings we take based on 
+            // what kind of layer we are.
+            with_layer(layer_name, function(layer) {
+                if(have_colormap(layer_name)) {
+                    // A discrete layer.
+                    // Show the value picker.
+                    filter_value.show();
+                    
+                    // Make sure we have all our options
+                    if(filter_value.children().length == 0) {
+                        // No options available. We have to add them.
+                        // TODO: Is there a better way to do this than asking 
+                        // the DOM?
+                        
+                        for(var i = 0; i < layer.magnitude + 1; i++) {
+                            // Make an option for each value.
+                            var option = $("<option/>").attr("value", i);
+                            
+                            if(colormaps[layer_name].hasOwnProperty(i)) {
+                                // We have a real name for this value
+                                option.text(colormaps[layer_name][i].name);
+                            } else {
+                                // No name. Use the number.
+                                option.text(i);
+                            }
+                            
+                            filter_value.append(option);
+                            
+                        }
+                        
+                        // Select the last option, so that 1 on 0/1 layers will 
+                        // be selected by default.
+                        filter_value.val(
+                            filter_value.children().last().attr("value"));
+                        
+                    }
+                } else {
+                    // Not a discrete layer, so we take a threshold.
+                    filter_threshold.show();
+                }
+                
+                // Now that the right controls are there, assume they have 
+                refresh();
+            });
+        } else {
+            // Hide the filtering settings
+            filter_value.hide();
+            filter_threshold.hide();
+            
+            // Draw view since we're no longer filtering on this layer.
+            refresh();
+        }
+    });
+    
+    // Respond to changes to filter configuration
+    filter_value.change(refresh);
+    
+    // TODO: Add a longer delay before refreshing here so the user can type more
+    // interactively.
+    filter_threshold.keyup(refresh);
+    
+    // Configure the range slider
+    
+    // First we need a function to update the range display, which we will run 
+    // on change and while sliding (to catch both user-initiated and 
+    //programmatic changes).
+    var update_range_display = function(event, ui) {
+        range_display.find(".low").text(ui.values[0].toFixed(3));
+        range_display.find(".high").text(ui.values[1].toFixed(3));
+    }
+    
+    range_slider.slider({
+        range: true,
+        min: -1,
+        max: 1,
+        values: [-1, 1],
+        step: 1E-9, // Ought to be fine enough
+        slide: update_range_display,
+        change: update_range_display,
+        stop: function(event, ui) {
+            // The user has finished sliding
+            // Draw the view. We will be asked for our values
+            refresh();
+        }
+    });
+    
+    // When we have time, go figure out whether the slider should be here, and 
+    // what its end values should be.
+    reset_slider(layer_name, root)
+    
+    return root;
+}
+
+function update_shortlist_ui() {
+    // Go through the shortlist and make sure each layer there has an entry in 
+    // the shortlist UI, and that each UI element has an entry in the shortlist.
+    // Also make sure the metadata for all existing layers is up to date.
+    
+    // Clear the existing UI lookup table
+    shortlist_ui = {};
+    
+    for(var i = 0; i < shortlist.length; i++) {
+        // For each shortlist entry, put a false in the lookup table
+        shortlist_ui[shortlist[i]] = false;
+    }
+    
+    
+    $("#shortlist").children().each(function(index, element) {
+        if(shortlist_ui[$(element).data("layer")] === false) {
+            // There's a space for this element: it's still in the shortlist
+            
+            // Fill it in
+            shortlist_ui[$(element).data("layer")] = $(element);
+            
+            // Update the metadata in the element. It make have changed due to
+            // statistics info coming back.
+            fill_layer_metadata($(element).find(".metadata-holder"), 
+                $(element).data("layer"));
+        } else {
+            // It wasn't in the shortlist, so get rid of it.
+            $(element).remove();
+        }
+    });
+    
+    for(var layer_name in shortlist_ui) {
+        // For each entry in the lookup table
+        if(shortlist_ui[layer_name] === false) {
+             // If it's still false, make a UI element for it.
+             shortlist_ui[layer_name] = make_shortlist_ui(layer_name);
+             $("#shortlist").prepend(shortlist_ui[layer_name]);
+             
+             // Check it's box if possible
+             shortlist_ui[layer_name].find(".layer-on").click();
+        }
+    }
+    
+    // Make things re-orderable
+    // Be sure to re-draw the view if the order changes, after the user puts 
+    // things down.
+    $("#shortlist").sortable({
+        update: refresh,
+        // Sort by the part with the lines icon, so we can still select text.
+        handle: ".shortlist-controls" 
+    });
+    
+}
+
+function layer_sort_order(a, b) {
+    // A sort function defined on layer names.
+    // Return <0 if a belongs before b, >0 if a belongs after
+    // b, and 0 if their order doesn't matter.
+    
+    // Sort by selection status, then p_value, then clumpiness, then (for binary
+    // layers that are not selections) the frequency of the less common value,
+    // then alphabetically by name if all else fails.
+
+    // Note that we can consult the layer metadata "n" and "positives" fields to
+    // calculate the frequency of the least common value in binary layers,
+    // without downloading them.
+    
+    if(layers[a].selection && !layers[b].selection) {
+        // a is a selection and b isn't, so put a first.
+        return -1;
+    } else if(layers[b].selection && !layers[a].selection) {
+        // b is a selection and a isn't, so put b first.
+        return 1;
+    }
+    
+    if(layers[a].p_value < layers[b].p_value) {
+        // a has a lower p value, so put it first.
+        return -1;
+    } else if(layers[b].p_value < layers[a].p_value) {
+        // b has a lower p value. Put it first instead.
+        return 1;
+    } else if(isNaN(layers[b].p_value) && !isNaN(layers[a].p_value)) {
+        // a has a p value and b doesn't, so put a first
+        return -1;
+    } else if(!isNaN(layers[b].p_value) && isNaN(layers[a].p_value)) {
+        // b has a p value and a doesn't, so put b first.
+        return 1;
+    }
+    
+    if(layers[a].clumpiness < layers[b].clumpiness) {
+        // a has a lower clumpiness score, so put it first.
+        return -1;
+    } else if(layers[b].clumpiness < layers[a].clumpiness) {
+        // b has a lower clumpiness score. Put it first instead.
+        return 1;
+    } else if(isNaN(layers[b].clumpiness) && !isNaN(layers[a].clumpiness)) {
+        // a has a clumpiness score and b doesn't, so put a first
+        return -1;
+    } else if(!isNaN(layers[b].clumpiness) && isNaN(layers[a].clumpiness)) {
+        // b has a clumpiness score and a doesn't, so put b first.
+        return 1;
+    }
+    
+    
+    
+    if(!layers[a].selection && !isNaN(layers[a].positives) && layers[a].n > 0 &&
+        !layers[b].selection && !isNaN(layers[b].positives) && 
+        layers[b].n > 0) {
+        
+        // We have checked to see each layer is supposed to be bianry layer
+        // without downloading.  TODO: This is kind of a hack. Redesign the
+        // whole system with a proper concept of layer type.
+        
+        // We've also verified they both have some data in them. Otherwise we
+        // might divide by 0 trying to calculate frequency.
+            
+        // Two binary layers (not selections).
+        // Compute the frequency of the least common value for each
+        
+        // This is the frequency of the least common value in a (will be <=1/2)
+        var minor_frequency_a = layers[a].positives / layers[a].n;
+        if(minor_frequency_a > 0.5) {
+            minor_frequency_a = 1 - minor_frequency_a;
+        }
+        
+        // And this is the same frequency for the b layer
+        var minor_frequency_b = layers[b].positives / layers[b].n;
+        if(minor_frequency_b > 0.5) {
+            minor_frequency_b = 1 - minor_frequency_b;
+        }
+
+        if(minor_frequency_a > minor_frequency_b) {
+            // a is more evenly split, so put it first
+            return -1;
+        } else if(minor_frequency_a < minor_frequency_b) {
+            // b is more evenly split, so put it first
+            return 1;
+        } 
+       
+    } else if (!layers[a].selection && !isNaN(layers[a].positives) && 
+        layers[a].n > 0) {
+        
+        // a is a binary layer we can nicely sort by minor value frequency, but
+        // b isn't. Put a first so that we can avoid intransitive sort cycles.
+        
+        // Example: X and Z are binary layers, Y is a non-binary layer, Y comes
+        // after X and before Z by name ordering, but Z comes before X by minor
+        // frequency ordering. This sort is impossible.
+        
+        // The solution is to put both X and Z in front of Y, because they're
+        // more interesting.
+        
+        return -1;
+    
+    } else if (!layers[b].selection && !isNaN(layers[b].positives) && 
+        layers[b].n > 0) {
+        
+        // b is a binary layer that we can evaluate based on minor value
+        // frequency, but a isn't. Put b first.
+        
+        return 1;
+        
+    }
+    
+    // We couldn't find a difference in selection status, p-value, or clumpiness
+    // score, or the binary layer minor value frequency, or whether eqach layer
+    // *had* a binary layer minor value frequency, so use lexicographic ordering
+    // on the name.
+    return a.localeCompare(b);
+    
+}
+
+function sort_layers(layer_array) {
+    // Given an array of layer names, sort the array in place as we want layers
+    // to appear to the user.
+    // We should sort by p value, with NaNs at the end. But selections should be
+    // first.
+    
+    layer_array.sort(layer_sort_order);
+}
+
+function fill_layer_metadata(container, layer_name) {
+    // Empty the given jQuery container element, and fill it with layer metadata
+    // for the layer with the given name.
+    
+    // Empty the container.
+    container.html("");
+    
+    for(attribute in layers[layer_name]) {
+        // Go through everything we know about this layer
+        if(attribute == "data" || attribute == "url" || 
+            attribute == "magnitude" || attribute == "selection") {
+            
+            // Skip built-in things
+            // TODO: Ought to maybe have all metadata in its own object?
+            continue;
+        }
+        
+        // This holds the metadata value we're displaying
+        var value = layers[layer_name][attribute];
+        
+        if(typeof value == "number" && isNaN(value)) {
+            // If it's a numerical NaN (but not a string), just leave it out.
+            continue;
+        }
+        
+        // If we're still here, this is real metadata.
+        // Format it for display.
+        var value_formatted;
+        if(typeof value == "number") {
+            if(value % 1 == 0) {
+                // It's an int!
+                // Display the default way
+                value_formatted = value;
+            } else {
+                // It's a float!
+                // Format the number for easy viewing
+                value_formatted = value.toExponential(2);
+            }
+        } else {
+            // Just put the thing in as a string
+            value_formatted = value;
+        }
+        
+        // Make a spot for it in the container and put it in
+        var metadata = $("<div\>").addClass("layer-metadata");
+        metadata.text(attribute + " = " + value_formatted);
+        
+        container.append(metadata);
+        
+    }
+}
+
+function make_browse_ui(layer_name) {
+    // Returns a jQuery element to represent the layer with the given name in 
+    // the browse panel.
+    
+    // This holds a jQuery element that's the root of the structure we're
+    // building.
+    var root = $("<div/>").addClass("layer-entry");
+    root.data("layer-name", layer_name);
+    
+    // Put in the layer name in a div that makes it wrap.
+    root.append($("<div/>").addClass("layer-name").text(layer_name));
+    
+    // Put in a layer metadata container div
+    var metadata_container = $("<div/>").addClass("layer-metadata-container");
+    
+    fill_layer_metadata(metadata_container, layer_name);
+    
+    root.append(metadata_container);
+    
+    return root;
+}
+
+function update_browse_ui() {
+    // Make the layer browse UI reflect the current list of layers in sorted
+    // order.
+    
+    // Re-sort the sorted list that we maintain
+    sort_layers(layer_names_sorted);
+    
+    // Close the select if it was open, forcing the data to refresh when it
+    // opens again.
+    $("#search").select2("close");
+}
+
+function get_slider_range(layer_name) {
+    // Given the name of a layer, get the slider range from its shortlist UI 
+    // entry.
+    // Assumes the layer has a shortlist UI entry.
+    return shortlist_ui[layer_name].find(".range-slider").slider("values");
+}
+
+function reset_slider(layer_name, shortlist_entry) {
+    // Given a layer name and a shortlist UI entry jQuery element, reset the 
+    // slider in the entry to its default values, after downloading the layer. 
+    // The default value may be invisible because we decided the layer should be
+    // a colormap.
+        
+    // We need to set its boundaries to the min and max of the data set
+    with_layer(layer_name, function(layer) {
+        if(have_colormap(layer_name)) {
+            // This is a colormap, so don't use the range slider at all.
+            // We couldn't know this before because the colormap may need to be 
+            // auto-detected upon download.
+            shortlist_entry.find(".range").hide();
+            return;
+        } else {
+            // We need the range slider
+            shortlist_entry.find(".range").show();
+        
+            // TODO: actually find max and min
+            // For now just use + and - magnitude
+            // This has the advantage of letting us have 0=black by default
+            var magnitude = layer.magnitude;
+            
+            // This holds the limit to use, which should be 1 if the magnitude 
+            // is <1. This is sort of heuristic, but it's a good guess that 
+            // nobody wants to look at a layer with values -0.2 to 0.7 on a 
+            // scale of -10 to 10, say, but they might want it on -1 to 1.
+            var range = Math.max(magnitude, 1.0)
+            
+            // Set the min and max.
+            shortlist_entry.find(".range-slider").slider("option", "min", 
+                -range);
+            shortlist_entry.find(".range-slider").slider("option", "max", 
+                range);
+            
+            // Set slider to autoscale for the magnitude.
+            shortlist_entry.find(".range-slider").slider("values", [-magnitude, 
+                magnitude]);
+                
+            print("Scaled to magnitude " + magnitude);
+                
+            // Redraw the view in case this changed anything
+            refresh();
+        }
+        
+    });
+}
+
+function get_current_layers() {
+    // Returns an array of the string names of the layers that are currently
+    // supposed to be displayed, according to the shortlist UI.
+    // Not responsible for enforcing maximum selected layers limit.
+    
+    // This holds a list of the string names of the currently selected layers,
+    // in order.
+    var current_layers = [];
+    
+    $("#shortlist").children().each(function(index, element) {
+        // This holds the checkbox that determines if we use this layer
+        var checkbox = $(element).find(".layer-on");
+        if(checkbox.is(":checked")) {
+            // Put the layer in if its checkbox is checked.
+            current_layers.push($(element).data("layer"));
+        }
+    });
+    
+    // Return things in reverse order relative to the UI.
+    // Thus, layer-added layers will be "secondary", and e.g. selecting 
+    // something with only tissue up behaves as you might expect, highlighting 
+    // those things.
+    current_layers.reverse();
+    
+    return current_layers;
+}
+
+function get_current_filters() {
+    // Returns an array of filter objects, according to the shortlist UI.
+    // Filter objects have a layer name and a boolean-valued filter function 
+    // that returns true or false, given a value from that layer.
+    var current_filters = [];
+    
+    $("#shortlist").children().each(function(index, element) {
+        // Go through all the shortlist entries.
+        // This function is also the scope used for filtering function config 
+        // variables.
+    
+        // This holds the checkbox that determines if we use this layer
+        var checkbox = $(element).find(".filter-on");
+        if(checkbox.is(":checked")) {
+            // Put the layer in if its checkbox is checked.
+            
+            // Get the layer name
+            var layer_name = $(element).data("layer");
+            
+            // This will hold our filter function. Start with a no-op filter.
+            var filter_function = function(value) {
+                return true;
+            }
+            
+            // Get the filter parameters
+            // This holds the input that specifies a filter threshold
+            var filter_threshold = $(element).find(".filter-threshold");
+            // And this the element that specifies a filter match value for 
+            // discrete layers
+            var filter_value = $(element).find(".filter-value");
+            
+            // We want to figure out which of these to use without going and 
+            // downloading the layer.
+            // So, we check to see which was left visible by the filter config
+            // setup code.
+            if(filter_threshold.is(":visible")) {
+                // Use a threshold. This holds the threshold.
+                var threshold = parseInt(filter_threshold.val());
+                
+                filter_function = function(value) {
+                    return value > threshold;
+                }
+            }
+            
+            if(filter_value.is(":visible")) {
+                // Use a discrete value match instead. This hodls the value we
+                // want to match.
+                var desired = filter_value.val();
+                
+                filter_function = function(value) {
+                    return value == desired;
+                }
+            }
+            
+            // Add a filter on this layer, with the function we've prepared.
+            current_filters.push({
+                layer_name: layer_name,
+                filter_function: filter_function
+            });
+        }
+    });
+    
+    return current_filters;
+}
+
+function with_filtered_signatures(filters, callback) {
+    // Takes an array of filters, as produced by get_current_filters. Signatures 
+    // pass a filter if the filter's layer has a value >0 for that signature. 
+    // Computes an  array of all signatures passing all filters, and passes that
+    // to the given callback.
+    
+    // TODO: Re-organize this to do filters one at a time, recursively, like a 
+    // reasonable second-order filter.
+    
+    // Prepare a list of all the layers
+    var layer_names = [];
+    
+    for(var i = 0; i < filters.length; i++) {
+        layer_names.push(filters[i].layer_name);
+    }
+    
+    with_layers(layer_names, function(filter_layers) {
+        // filter_layers is guaranteed to be in the same order as filters.
+        
+        // This is an array of signatures that pass all the filters.
+        var passing_signatures = [];
+    
+        for(var signature in polygons) {
+            // For each signature
+            
+            // This holds whether we pass all the filters
+            var pass = true;
+            
+            for(var i = 0; i < filter_layers.length; i++) {
+                // For each filtering layer
+                if(!filters[i].filter_function(
+                    filter_layers[i].data[signature])) {
+                    
+                    // If the signature fails the filter function for the layer,
+                    // skip the signature.
+                    pass = false;
+                    break;
+                }
+            }
+            
+            if(pass) {
+                // Record that the signature passes all filters
+                passing_signatures.push(signature);
+            }
+        }
+        
+        // Now we have our list of all passing signatures, so hand it off to the
+        // callback.
+        callback(passing_signatures);
+    });
+}
+
+function select_list(to_select) {
+    // Given an array of signature names, add a new selection layer containing
+    // just those hexes. Only looks at hexes that are not filtered out by the
+    // currently selected filters.
+    
+    // Make the requested signature list into an object for quick membership
+    // checking. This holds true if a signature was requested, undefined
+    // otherwise.
+    var wanted = {};
+    
+    for(var i = 0; i < to_select.length; i++) {
+        wanted[to_select[i]] = true;
+    }
+    
+    // This is the data object for the layer: from signature names to 1/0
+    var data = {};
+    
+    // How many signatures will we have any mention of in this layer
+    var signatures_available = 0;
+    
+    // Start it out with 0 for each signature. Otherwise we wil have missing 
+    // data for signatures not passing the filters.
+    for(var signature in polygons) {
+        data[signature] = 0;
+        signatures_available += 1;
+    }
+    
+    // This holds the filters we're going to use to restrict our selection
+    var filters = get_current_filters();
+
+    // Go get the list of signatures passing the filters and come back.
+    with_filtered_signatures(filters, function(signatures) {   
+        // How many signatures get selected?
+        var signatures_selected = 0;
+     
+        for(var i = 0; i < signatures.length; i++) {
+            if(wanted[signatures[i]]) {
+                // This signature is both allowed by the filters and requested.
+                data[signatures[i]] = 1;
+                signatures_selected++;           
+            }
+        }
+        
+        // Make up a name for the layer
+        var layer_name = "Selection " + selection_next_id;
+        selection_next_id++;
+        
+        // Add the layer. Say it is a selection
+        add_layer_data(layer_name, data, {
+            selection: true,
+            selected: signatures_selected, // Display how many hexes are in
+            n: signatures_available // And how many have a value at all
+        });
+        
+        // Update the browse UI with the new layer.
+        update_browse_ui();
+        
+        // Immediately shortlist it
+        shortlist.push(layer_name);
+        update_shortlist_ui();
+    });
+    
+}
+
+function select_rectangle(start, end) {
+    // Given two Google Maps LatLng objects (denoting arbitrary rectangle 
+    // corners), add a new selection layer containing all the hexagons 
+    // completely within that rectangle.
+    // Only looks at hexes that are not filtered out by the currently selected 
+    // filters.
+    
+    // Sort out the corners to get the rectangle limits in each dimension
+    var min_lat = Math.min(start.lat(), end.lat());
+    var max_lat = Math.max(start.lat(), end.lat());
+    var min_lng = Math.min(start.lng(), end.lng());
+    var max_lng = Math.max(start.lng(), end.lng());
+    
+    // This holds an array of all signature names in our selection box.
+    var in_box = [];
+    
+    // Start it out with 0 for each signature. Otherwise we wil have missing 
+    // data for signatures not passing the filters.
+    for(var signature in polygons) {
+         // Get the path for its hex
+        var path = polygons[signature].getPath();
+        
+        // This holds if any points of the path are outside the selection
+        // box
+        var any_outside = false;
+        
+        path.forEach(function(point, index) {
+            // Check all the points. Runs synchronously.
+            
+            if(point.lat() < min_lat || point.lat() > max_lat || 
+                point.lng() < min_lng || point.lng() > max_lng) {
+                
+                // This point is outside the rectangle
+                any_outside = true;
+                
+            }
+        });
+        
+        // Select the hex if all its corners are inside the selection
+        // rectangle.
+        if(!any_outside) {
+            in_box.push(signature);
+        }
+    }
+    
+    // Now we have an array of the signatures that ought to be in the selection
+    // (if they pass filters). Hand it off to select_list.
+    
+    select_list(in_box);
+    
+}
+
+function recalculate_statistics(passed_filters) {
+    // Interrogate the UI to determine signatures that are "in" and "out", and
+    // run an appropriate statisical test for each layer between the "in" and
+    // "out" signatures, and update all the "p_value" fields for all the layers
+    // with the p values. Takes in a list of signatures that passed the filters,
+    // and ignores any signatures not on that list.
+    
+    // Build an efficient index of passing signatures
+    var passed = {};
+    for(var i = 0; i < passed_filters.length; i++) {
+        passed[passed_filters[i]] = true;
+    }
+    
+    // Figure out what the in-list should be (statistics group A)
+    var layer_a_name = $(".statistics-a:checked").data("layer-name");
+    var layer_b_name = $(".statistics-b:checked").data("layer-name");
+    
+    print("Running statistics between " + layer_a_name + " and " + 
+        layer_b_name);
+    
+    if(!layer_a_name) {
+        complain("Can't run statistics without an \"A\" group.");
+        
+        // Get rid of the throbber
+        // TODO: Move this UI code out of the backend code.
+        $(".recalculate-throbber").hide();
+        $("#recalculate-statistics").show();
+        
+        return;
+    }
+    
+    // We know the layers have data since they're selections, so we can just go
+    // look at them.
+    
+    // This holds the "in" list: hexes from the "A" group.
+    var in_list = [];
+    
+    for(var signature in layers[layer_a_name].data) {
+        if(passed[signature] && layers[layer_a_name].data[signature]) {
+            // Add all the signatures in the "A" layer to the in list.
+            in_list.push(signature);
+        }
+    }
+    
+    if(in_list.length == 0) {
+        complain("Can't run statistics with an empty \"A\" group.");
+        
+        // Get rid of the throbber
+        // TODO: Move this UI code out of the backend code.
+        $(".recalculate-throbber").hide();
+        $("#recalculate-statistics").show();
+        
+        return;
+    }
+    
+    // This holds the "out" list: hexes in the "B" group, or, if that's not
+    // defined, all hexes. It's a little odd to run A vs. a set that includes
+    // some members of A, but Prof. Stuart wants that and it's not too insane
+    // for a Binomial test (which is the only currently implemented test
+    // anyway).
+    var out_list = [];
+    
+    if(layer_b_name) {
+        // We have a layer B, so take everything that's on in it.
+        for(var signature in layers[layer_b_name].data) {
+            if(passed[signature] && layers[layer_b_name].data[signature]) {
+                // Add all the signatures in the "B" layer to the out list.
+                out_list.push(signature);
+            }
+        }
+    } else {
+        // The out list is all hexes
+        for(var signature in polygons) {
+            if(passed[signature]) {
+                // Put it on the out list.
+                out_list.push(signature);
+            }
+        }
+    }
+    
+    // So now we have our in_list and our out_list
+    
+    for(var layer_name in layers) {
+        // Do the stats on each layer between those lists. This only processes
+        // layers that don't have URLs. Layers with URLs are assumed to be part
+        // of the available matrices.
+        recalculate_statistics_for_layer(layer_name, in_list, out_list,
+            passed_filters);
+    }
+    
+    // Now do all the layers with URLs. They are in the available score
+    // matrices.
+    for(var i = 0; i < available_matrices.length; i++) {
+        recalculate_statistics_for_matrix(available_matrices[i], in_list, 
+            out_list, passed_filters);
+    }
+    
+    print("Statistics jobs launched.");
+    
+}
+
+function recalculate_statistics_for_layer(layer_name, in_list, out_list, all) {
+    // Re-calculate the stats for the layer with the given name, between the
+    // given in and out arrays of signatures. Store the re-calculated statistics
+    // in the layer. all is a list of "all" signatures, from which we can
+    // calculate pseudocounts.
+    
+    // All we do is send the layer data or URL (whichever is more convenient) to
+    // the workers. They independently identify the data type and run the
+    // appropriate test, returning a p value or NaN by callback.
+    
+    // This holds a callback for setting the layer's p_value to the result of
+    // the statistics.
+    var callback = function(result) {
+        layers[layer_name].p_value = result;
+        if(jobs_running == 0) {
+            // All statistics are done!
+            // TODO: Unify this code with similar callback below.
+            // Re-sort everything and draw all the new p values.
+            update_browse_ui();
+            update_shortlist_ui();
+            
+            // Get rid of the throbber
+            $(".recalculate-throbber").hide();
+            $("#recalculate-statistics").show();
+        }
+    };
+    
+    if(layers[layer_name].data != undefined) {
+        // Already have this downloaded. A local copy to the web worker is
+        // simplest, and a URL may not exist anyway.
+        
+        rpc_call("statistics_for_layer", [layers[layer_name].data, in_list, 
+            out_list, all], callback);
+    } else if(layers[layer_name].url != undefined) {
+        // We have a URL, so the layer must be in a matrix, too.
+        // Skip it here.
+    } else {
+        // Layer has no data and no way to get data. Should never happen.
+        complain("Layer " + layer_name + " has no data and no url.");
+    }
+}
+
+function recalculate_statistics_for_matrix(matrix_url, in_list, out_list, all) {
+    // Given the URL of one of the visualizer generator's input score matrices,
+    // download the matrix, calculate statistics for each layer in the matrix
+    // between the given in and out lists, and update the layer p values. all is
+    // a list of "all" signatures, from which we can calculate pseudocounts.
+
+    rpc_call("statistics_for_matrix", [matrix_url, in_list, out_list, all], 
+        function(result) {
+        
+        // The return value is p values by layer name
+        for(var layer_name in result) {
+            // Copy over p values
+            layers[layer_name].p_value = result[layer_name];
+        }
+        
+        if(jobs_running == 0) {
+            // All statistics are done!
+            // TODO: Unify this code with similar callback above.
+            // Re-sort everything and draw all the new p values.
+            update_browse_ui();
+            update_shortlist_ui();
+            
+            // Get rid of the throbber
+            $(".recalculate-throbber").hide();
+            $("#recalculate-statistics").show();
+        }
+    });    
+    
+}
+
+function rpc_initialize() {
+    // Set up the RPC system. Must be called before rpc_call is used.
+    
+    for(var i = 0; i < NUM_RPC_WORKERS; i++) {
+        // Start the statistics RPC (remote procedure call) Web Worker
+        var worker = new Worker("statistics.js");
+        
+        // Send all its messages to our reply processor
+        worker.onmessage = rpc_reply;
+        
+        // Add it to the list of workers
+        rpc_workers.push(worker);
+    }
+}
+
+function rpc_call(function_name, function_args, callback) {
+    // Given a function name and an array of arguments, send a message to a Web 
+    // Worker thread to ask it to run the given job. When it responds with the 
+    // return value, pass it to the given callback.
+    
+    // Allocate a new call id
+    var call_id = rpc_next_id;
+    rpc_next_id++;
+    
+    // Store the callback
+    rpc_callbacks[call_id] = callback;
+    
+    // Launch the call. Pass the function name, function args, and id to send 
+    // back with the return value.
+    rpc_workers[next_free_worker].postMessage({
+        name: function_name,
+        args: function_args,
+        id: call_id
+    });
+    
+    // Next time, use the next worker on the list, wrapping if we run out.
+    // This ensures no one worker gets all the work.
+    next_free_worker = (next_free_worker + 1) % rpc_workers.length;
+    
+    // Update the UI with the number of jobs in flight. Decrement jobs_running
+    // so the callback knows if everything is done or not.
+    jobs_running++;
+    $("#jobs-running").text(jobs_running);
+    
+    // And the number of jobs total
+    $("#jobs-ever").text(rpc_next_id);
+}
+
+function rpc_reply(message) {
+    // Handle a Web Worker message, which may be an RPC response or a log entry.
+    
+    if(message.data.log != undefined) {
+        // This is really a log entry
+        print(message.data.log);
+        return;
+    }
+    
+    // This is really a job completion message (success or error).
+    
+    // Update the UI with the number of jobs in flight.
+    jobs_running--;
+    $("#jobs-running").text(jobs_running);
+    
+    if(message.data.error) {
+        // The RPC call generated an error.
+        // Inform the page.
+        print("RPC error: " + message.data.error);
+        
+        // Get rid of the callback
+        delete rpc_callbacks[message.data.id];
+        
+        return;
+    }
+    
+    // Pass the return value to the registered callback.
+    rpc_callbacks[message.data.id](message.data.return_value);
+    
+    // Get rid of the callback
+    delete rpc_callbacks[message.data.id];
+}
+
+function initialize_view() {
+    // Initialize the global Google Map.
+    
+    // Configure a Google map
+    var mapOptions = {
+        // Look at the center of the map
+        center: get_LatLng(128, 128),
+        // Zoom all the way out
+        zoom: 0,
+        mapTypeId: "blank",
+        // Don't show a map type picker.
+        mapTypeControlOptions: {
+              mapTypeIds: []
+        },
+        // Or a street view man that lets you walk around various Earth places.
+        streetViewControl: false
+    };
+    
+    // Create the actual map
+    googlemap = new google.maps.Map(document.getElementById("visualization"),
+        mapOptions);
+        
+    // Attach the blank map type to the map
+    googlemap.mapTypes.set("blank", new BlankMapType());
+    
+    // Make the global info window
+    info_window = new google.maps.InfoWindow({
+        content: "No Signature Selected",
+        position: get_LatLng(0, 0)
+    });
+    
+    // Add an event to close the info window when the user clicks outside of any
+    // hexagon
+    google.maps.event.addListener(googlemap, "click", function(event) {
+        info_window.close();
+        
+        // Also make sure that the selected signature is no longer selected,
+        // so we don't pop the info_window up again.
+        selected_signature = undefined;
+        
+        // Also un-focus the search box
+        $("#search").blur();
+    });
+    
+    
+    // And an event to clear the selected hex when the info_window closes.
+    google.maps.event.addListener(info_window, "closeclick", function(event) {
+        selected_signature = undefined;
+    });
+    
+    // We also have an event listener that checks when the zoom level changes,
+    // and turns off hex borders if we zoom out far enough, and turns them on
+    // again if we come back.
+    google.maps.event.addListener(googlemap, "zoom_changed", function(event) {
+        // Get the current zoom level (low is out)
+        var zoom = googlemap.getZoom();
+        
+        // API docs say: pixelCoordinate = worldCoordinate * 2 ^ zoomLevel
+        // So this holds the number of pixels that the global length hex_size 
+        // corresponds to at this zoom level.
+        var hex_size_pixels = hex_size * Math.pow(2, zoom);
+        
+        if(hex_size_pixels < MIN_BORDER_SIZE) {
+            // We're too small for borders
+            for(var signature in polygons) {
+                set_hexagon_stroke_weight(polygons[signature], 0);
+            }
+        } else {
+            // We can fit borders on the hexes
+            for(var signature in polygons) {
+                set_hexagon_stroke_weight(polygons[signature], 
+                    HEX_STROKE_WEIGHT);
+            }
+        }
+        
+    });
+    
+    // Subscribe all the tool listeners to the map
+    subscribe_tool_listeners(googlemap);
+    
+}
+
+function add_tool(tool_name, tool_menu_option, callback) {
+    // Given a programmatic unique name for a tool, some text for the tool's
+    // button, and a callback for when the user clicks that button, add a tool
+    // to the tool menu.
+    
+    // This hodls a button to activate the tool.
+    var tool_button = $("<a/>").attr("href", "#").addClass("stacker");
+    tool_button.text(tool_menu_option);
+    tool_button.click(function() {
+        // New tool. Remove all current tool listeners
+        clear_tool_listeners();
+        
+        // Say that the select tool is selected
+        selected_tool = tool_name;
+        callback();
+        
+        // End of tool workflow must set current_tool to undefined.
+    });
+    
+    $("#toolbar").append(tool_button);
+}
+
+function add_tool_listener(name, handler, cleanup) {
+    // Add a global event listener over the Google map and everything on it. 
+    // name specifies the event to listen to, and handler is the function to be
+    // set up as an event handler. It should take a single argument: the Google 
+    // Maps event. A handle is returned that can be used to remove the event 
+    // listen with remove_tool_listener.
+    // Only events in the TOOL_EVENTS array are allowed to be passed for name.
+    // TODO: Bundle this event thing into its own object.
+    // If "cleanup" is specified, it must be a 0-argument function to call when
+    // this listener is removed.
+    
+    // Get a handle
+    var handle = tool_listener_next_id;
+    tool_listener_next_id++;
+    
+    // Add the listener for the given event under that handle.
+    // TODO: do we also need to index this for O(1) event handling?
+    tool_listeners[handle] = {
+        handler: handler,
+        event: name,
+        cleanup: cleanup
+    };
+    return handle;  
+}
+
+function remove_tool_listener(handle) {
+    // Given a handle returned by add_tool_listener, remove the listener so it
+    // will no longer fire on its event. May be called only once on a given 
+    // handle. Runs any cleanup code associated with the handle being removed.
+    
+    if(tool_listeners[handle].cleanup) {
+        // Run cleanup code if applicable
+        tool_listeners[handle].cleanup();
+    }
+    
+    // Remove the property from the object
+    delete tool_listeners[handle];
+}
+
+function clear_tool_listeners() {
+    // We're starting to use another tool. Remove all current tool listeners. 
+    // Run any associated cleanup code for each listener.
+    
+    for(var handle in tool_listeners) {
+        remove_tool_listener(handle);
+    }
+}
+
+function subscribe_tool_listeners(maps_object) {
+    // Put the given Google Maps object into the tool events system, so that 
+    // events on it will fire global tool events. This can happen before or 
+    // after the tool events themselves are enabled.
+    
+    for(var i = 0; i < TOOL_EVENTS.length; i++) {
+        // For each event name we care about,
+        // use an inline function to generate an event name specific handler,
+        // and attach that to the Maps object.
+        google.maps.event.addListener(maps_object, TOOL_EVENTS[i], 
+            function(event_name) {
+                return function(event) {
+                    // We are handling an event_name event
+                    
+                    for(var handle in tool_listeners) {
+                        if(tool_listeners[handle].event == event_name) {
+                            // The handler wants this event
+                            // Fire it with the Google Maps event args
+                            tool_listeners[handle].handler(event);
+                        }
+                    }
+                };
+        }(TOOL_EVENTS[i]));
+    }
+    
+}
+
+function have_colormap(colormap_name) {
+    // Returns true if the given string is the name of a colormap, or false if 
+    // it is only a layer.
+    
+    return !(colormaps[colormap_name] == undefined);
+}
+
+function get_range_position(score, low, high) {
+    // Given a score float, and the lower and upper bounds of an interval (which
+    // may be equal, but not backwards), return a number in the range -1 to 1
+    // that expresses the position of the score in the [low, high] interval.
+    // Positions out of bounds are clamped to -1 or 1 as appropriate.
+    
+    // This holds the length of the input interval
+    var interval_length = high - low;
+    
+    if(interval_length > 0) {
+        // First rescale 0 to 1
+        score = (score - low) / interval_length
+        
+        // Clamp
+        score = Math.min(Math.max(score, 0), 1);
+            
+        // Now re-scale to -1 to 1
+        score = 2 * score - 1;
+    } else {
+        // The interval is just a point
+        // Just use 1 if we're above the point, and 0 if below.
+        score = (score > low)? 1 : -1
+    }
+    
+    return score;
+}
+
+function refresh() {
+    // Schedule the view to be redrawn after the current event finishes.
+    
+    // Get rid of the previous redraw request, if there was one. We only want 
+    // one.
+    window.clearTimeout(redraw_handle);
+    
+    // Make a new one to happen as soon as this event finishes
+    redraw_handle = window.setTimeout(redraw_view, 0);
+}
+
+function redraw_view() {
+    // Make the view display the correct hexagons in the colors of the current 
+    // layer(s), as read from the values of the layer pickers in the global
+    // layer pickers array.
+    // All pickers must have selected layers that are in the object of 
+    // layers.
+    // Instead of calling this, you probably want to call refresh().
+    
+    // This holds a list of the string names of the currently selected layers,
+    // in order.
+    var current_layers = get_current_layers();
+    
+    // This holds arrays of the lower and upper limit we want to use for 
+    // each layer, by layer number. The lower limit corresponds to u or 
+    // v = -1, and the upper to u or v = 1. The entries we make for 
+    // colormaps are ignored.
+    // Don't do this inside the callback since the UI may have changed by then.
+    var layer_limits = []
+    for(var i = 0; i < current_layers.length; i++) {
+        layer_limits.push(get_slider_range(current_layers[i]));
+    }
+    
+    // This holds all the current filters
+    var filters = get_current_filters();
+    
+    // Obtain the layer objects (mapping from signatures/hex labels to colors)
+    with_layers(current_layers, function(retrieved_layers) {  
+        print("Redrawing view with " + retrieved_layers.length + " layers.");
+        
+        // Turn all the hexes the filtered-out color, pre-emptively
+        for(var signature in polygons) {
+            set_hexagon_color(polygons[signature], "black");
+        }
+        
+        // Go get the list of filter-passing hexes.
+        with_filtered_signatures(filters, function(signatures) {
+            for(var i = 0; i < signatures.length; i++) {
+                // For each hex passign the filter
+                // This hodls its signature label
+                var label = signatures[i];
+                
+                // This holds the color we are calculating for this hexagon.
+                // Start with the missing data color.
+                var computed_color = "grey";
+                
+                if(retrieved_layers.length >= 1) {
+                    // Two layers. We find a point in u, v cartesian space, map
+                    // it to polar, and use that to compute an HSV color.
+                    // However, we map value to the radius instead of
+                    // saturation.
+
+                    // Get the heat along u and v axes. This puts us in a square
+                    // of side length 2. Fun fact: undefined / number = NaN, but
+                    // !(NaN == NaN)
+                    var u = retrieved_layers[0].data[label];
+                    
+                    if(!have_colormap(current_layers[0])) {
+                        // Take into account the slider values and re-scale the 
+                        // layer value to express its position between them.
+                        u = get_range_position(u, layer_limits[0][0], 
+                            layer_limits[0][1]);
+                    }
+                    
+                    if(retrieved_layers.length >= 2) {
+                        // There's a second layer, so use the v axis.
+                        var v = retrieved_layers[1].data[label];
+                        
+                        if(!have_colormap(current_layers[1])) {
+                            // Take into account the slider values and re-scale
+                            // the layer value to express its position between
+                            // them.
+                            v = get_range_position(v, layer_limits[1][0], 
+                                layer_limits[1][1]);
+                        }
+                        
+                    } else {
+                        // No second layer, so v axis is unused. Don't make it 
+                        // undefined (it's not missing data), but set it to 0.
+                        var v = 0;
+                    }
+                    
+                    // Either of u or v may be undefined (or both) if the layer
+                    // did not contain an entry for this signature. But that's
+                    // OK. Compute the color that we should use to express this
+                    // combination of layer values. It's OK to pass undefined
+                    // names here for layers.
+                    computed_color = get_color(current_layers[0], u, 
+                        current_layers[1], v);
+                }
+                
+                // Set the color by the composed layers.
+                set_hexagon_color(polygons[label], computed_color);
+            }
+        });
+        
+        // Draw the color key.
+        if(retrieved_layers.length == 0) {
+            // No color key to draw
+            $(".key").hide();
+        } else {
+            // We do actually want the color key
+            $(".key").show();
+        
+            // This holds the canvas that the key gets drawn in
+            var canvas = $("#color-key")[0];
+            
+            // This holds the 2d rendering context
+            var context = canvas.getContext("2d");
+            
+            for(var i = 0; i < KEY_SIZE; i++) {
+                // We'll use i for the v coordinate (-1 to 1) (left to right)
+                var v = 0;
+                if(retrieved_layers.length >= 2) {
+                    v = i / (KEY_SIZE / 2) - 1;
+                    
+                    if(have_colormap(current_layers[1])) {
+                        // This is a color map, so do bands instead.
+                        v = Math.floor(i / KEY_SIZE * 
+                            (retrieved_layers[1].magnitude + 1));
+                    }
+                    
+                }
+                
+                for(var j = 0; j < KEY_SIZE; j++) {
+                    // And j spacifies the u coordinate (bottom to top)
+                    var u = 0;
+                    if(retrieved_layers.length >= 1) {
+                        u = 1 - j / (KEY_SIZE / 2);
+
+                        if(have_colormap(current_layers[0])) {
+                            // This is a color map, so do bands instead.
+                            // Make sure to flip sign, and have a -1 for the 
+                            // 0-based indexing.
+                            u = Math.floor((KEY_SIZE - j - 1) / KEY_SIZE * 
+                                (retrieved_layers[0].magnitude + 1));
+                        }
+                    }
+                    
+                    // Set the pixel color to the right thing for this u, v
+                    // It's OK to pass undefined names here for layers.
+                    context.fillStyle = get_color(current_layers[0], u, 
+                        current_layers[1], v);
+                    
+                    // Fill the pixel
+                    context.fillRect(i, j, 1, 1);
+                }
+            }
+        
+        }
+        
+        if(have_colormap(current_layers[0])) {
+            // We have a layer with horizontal bands
+            // Add labels to the key if we have names to use.
+            // TODO: Vertical text for vertical bands?
+        
+            // Get the colormap
+            var colormap = colormaps[current_layers[0]]
+            
+            if(colormap.length > 0) {
+                // Actually have any categories (not auto-generated)
+                print("Drawing key text for " + colormap.length + 
+                    " categories.");
+                
+                // How many pixels do we get per label, vertically
+                var pixels_per_label = KEY_SIZE / colormap.length;
+                
+                // Configure for text drawing
+                context.font = pixels_per_label + "px Arial";
+                context.textBaseline = "top";
+                
+                for(var i = 0; i < colormap.length; i++) {
+                    
+                    // This holds the pixel position where our text goes
+                    var y_position = KEY_SIZE - (i + 1) * pixels_per_label;
+                    
+                    // Get the background color here as a 1x1 ImageData
+                    var image = context.getImageData(0, y_position, 1, 1);
+                    
+                    // Get the components r, g, b, a in an array
+                    var components = image.data;
+                    
+                    // Make a Color so we can operate on it
+                    var background_color = Color({
+                        r: components[0],
+                        g: components[1],
+                        b: components[2]
+                    });
+                    
+                    if(background_color.light()) {
+                        // This color is light, so write in black.
+                        context.fillStyle = "black";
+                    } else {
+                        // It must be dark, so write in white.
+                        context.fillStyle = "white";
+                    }
+                
+                    // Draw the name on the canvas
+                    context.fillText(colormap[i].name, 0, y_position);
+                }
+            }
+        }
+        
+        // We should also set up axis labels on the color key.
+        // We need to know about colormaps to do this
+        
+        // Hide all the labels
+        $(".label").hide();
+        
+        if(current_layers.length > 0) {
+            // Show the y axis label
+            $("#y-axis").text(current_layers[0]).show();
+            
+            if(!have_colormap(current_layers[0])) {
+                // Show the low to high markers for continuous values
+                $("#low-both").show();
+                $("#high-y").show();
+            }
+        }
+        
+        if(current_layers.length > 1) {
+            // Show the x axis label
+            $("#x-axis").text(current_layers[1]).show();
+            
+            if(!have_colormap(current_layers[1])) {
+                // Show the low to high markers for continuous values
+                $("#low-both").show();
+                $("#high-x").show();
+            }
+        }
+        
+        
+    });
+    
+    // Make sure to also redraw the info window, which may be open.
+    redraw_info_window();
+}
+
+function get_color(u_name, u, v_name, v) {
+    // Given u and v, which represent the heat in each of the two currently 
+    // displayed layers, as well as u_name and v_name, which are the 
+    // corresponding layer names, return the computed CSS color.
+    // Either u or v may be undefined (or both), in which case the no-data color
+    // is returned. If a layer name is undefined, that layer dimension is 
+    // ignored.
+    
+    if(have_colormap(v_name) && !have_colormap(u_name)) {
+        // We have a colormap as our second layer, and a layer as our first.
+        // Swap everything around so colormap is our first layer instead.
+        // Now we don't need to think about drawing a layer first with a 
+        // colormap second.
+        // This is a temporary swapping variable.
+        var temp = v_name;
+        v_name = u_name;
+        u_name = temp;
+        
+        temp = v;
+        v = u;
+        u = temp;
+    }
+    
+    if(isNaN(u) || isNaN(v) || u == undefined || v == undefined) {
+        // At least one of our layers has no data for this hex.
+        return "grey";
+    }
+    
+    if(have_colormap(u_name) && have_colormap(v_name) && 
+        !colormaps[u_name].hasOwnProperty(u) && 
+        !colormaps[v_name].hasOwnProperty(v) &&
+        layers[u_name].magnitude <= 1 && layers[v_name].magnitude <= 1) {
+        
+        // Special case: two binary or unary auto-generated colormaps.
+        // Use dark grey/red/blue/purple color scheme
+    
+        if(u == 1) {
+            if(v == 1) {    
+                // Both are on
+                return "#FF00FF";
+            } else {
+                // Only the first is on
+                return "#FF0000";
+            }
+        } else {
+            if(v == 1) {
+                // Only the second is on
+                return "#0000FF";
+            } else {
+                // Neither is on
+                return "#545454";
+            }
+        }    
+        
+    }
+    
+    if(have_colormap(u_name) && !colormaps[u_name].hasOwnProperty(u) && 
+        layers[u_name].magnitude <= 1 && v_name == undefined) {
+        
+        // Special case: a single binary or unary auto-generated colormap.
+        // Use dark grey/red to make 1s stand out.
+        
+        if(u == 1) {
+            // Red for on
+            return "#FF0000";
+        } else {
+            // Dark grey for off
+            return "#545454";
+        }        
+    }
+   
+    
+    if(have_colormap(u_name)) {
+        // u is a colormap
+        if(colormaps[u_name].hasOwnProperty(u)) {
+            // And the colormap has an entry here. Use it as the base color.
+            var to_clone = colormaps[u_name][u].color;
+            
+            var base_color = Color({
+                hue: to_clone.hue(),
+                saturation: to_clone.saturationv(),
+                value: to_clone.value()
+            });
+        } else {
+            // The colormap has no entry. Assume we're calculating all the 
+            // entries. We do this by splitting the color circle evenly.
+            
+            // This holds the number of colors, which is 1 more than the largest
+            // value used (since we start at color 0), which is the magnitude.
+            // It's OK to go ask for the magnitude of this layer since it must 
+            // have already been downloaded.
+            var num_colors = layers[u_name].magnitude + 1;
+            
+            // Calculate the hue for this number.
+            var hsv_hue = u / (num_colors + 1) * 360;
+    
+            // The base color is a color at that hue, with max saturation and 
+            // value
+            var base_color = Color({
+                hue: hsv_hue, 
+                saturation: 100,
+                value: 100
+            })
+        }
+        
+        // Now that the base color is set, consult v to see what shade to use.
+        if(v_name == undefined) {
+            // No v layer is actually in use. Use whatever is in the base 
+            // color
+            // TODO: This code path is silly, clean it up.
+            var hsv_value = base_color.value();
+        } else if(have_colormap(v_name)) {
+            // Do discrete shades in v
+            // This holds the number of shades we need.
+            // It's OK to go ask for the magnitude of this layer since it must 
+            // have already been downloaded.
+            var num_shades = layers[v_name].magnitude + 1;
+            
+            // Calculate what shade we need from the nonnegative integer v
+            // We want 100 to be included (since that's full brightness), but we
+            // want to skip 0 (since no color can be seen at 0), so we add 1 to 
+            // v.
+            var hsv_value = (v + 1) / num_shades * 100;
+        } else {
+            // Calculate what shade we need from v on -1 to 1
+            var hsv_value = 50 + v * 50;
+        }
+        
+        // Set the color's value component.
+        base_color.value(hsv_value);
+        
+        // Return the shaded color
+        return base_color.hexString();
+    }
+    
+    
+    // If we get here, we only have non-colormap layers.
+    
+    // This is the polar angle (hue) in degrees, forced to be 
+    // positive.
+    var hsv_hue = Math.atan2(v, u) * 180 / Math.PI;
+    if(hsv_hue < 0) {
+        hsv_hue += 360;
+    }
+    
+    // Rotate it by 60 degrees, so that the first layer is 
+    // yellow/blue
+    hsv_hue += 60;
+    if(hsv_hue > 360) {
+        hsv_hue -= 360;
+    }
+    
+    // This is the polar radius (value). We inscribe our square
+    // of side length 2 in a circle of radius 1 by dividing by
+    // sqrt(2). So we get a value from 0 to 1
+    var hsv_value = (Math.sqrt(Math.pow(u, 2) + 
+        Math.pow(v, 2)) / Math.sqrt(2));
+        
+    // This is the HSV saturation component of the color on 0 to 1.
+    // Just fix to 1.
+    var hsv_saturation = 1.0;
+    
+    // Now scale saturation and value to percent
+    hsv_saturation *= 100;
+    hsv_value *= 100;
+    
+    // Now we have the color as HSV, but CSS doesn't support it.
+    
+    // Make a Color object and get the RGB string
+    try {
+        return Color({
+            hue: hsv_hue, 
+            saturation: hsv_saturation,
+            value: hsv_value,
+        }).hexString();
+    } catch(error) {
+        print("(" + u + "," + v + ") broke with color (" + hsv_hue +
+        "," + hsv_saturation + "," + hsv_value + ")");
+        
+        // We'll return an error color
+        return "white";
+    }
+}
+
+// Define a flat projection
+// See https://developers.google.com/maps/documentation/javascript/maptypes#Projections
+function FlatProjection() {
+}
+
+
+FlatProjection.prototype.fromLatLngToPoint = function(latLng) {
+    // Given a LatLng from -90 to 90 and -180 to 180, transform to an x, y Point 
+    // from 0 to 256 and 0 to 256   
+    var point = new google.maps.Point((latLng.lng() + 180) * 256 / 360, 
+        (latLng.lat() + 90) * 256 / 180);
+    
+    return point;
+
+}
+
+
+FlatProjection.prototype.fromPointToLatLng = function(point, noWrap) {
+    // Given a an x, y Point from 0 to 256 and 0 to 256, transform to a LatLng from
+    // -90 to 90 and -180 to 180
+    var latLng = new google.maps.LatLng(point.y * 180 / 256 - 90, 
+        point.x * 360 / 256 - 180, noWrap);
+    
+    return latLng;
+}
+
+// Define a Google Maps MapType that's all blank
+// See https://developers.google.com/maps/documentation/javascript/examples/maptype-base
+function BlankMapType() {
+}
+
+BlankMapType.prototype.tileSize = new google.maps.Size(256,256);
+BlankMapType.prototype.maxZoom = 19;
+
+BlankMapType.prototype.getTile = function(coord, zoom, ownerDocument) {
+    // This is the element representing this tile in the map
+    // It should be an empty div
+    var div = ownerDocument.createElement("div");
+    div.style.width = this.tileSize.width + "px";
+    div.style.height = this.tileSize.height + "px";
+    div.style.backgroundColor = "#000000";
+    
+    return div;
+}
+
+BlankMapType.prototype.name = "Blank";
+BlankMapType.prototype.alt = "Blank Map";
+
+BlankMapType.prototype.projection = new FlatProjection();
+
+
+
+function get_LatLng(x, y) {
+    // Given a point x, y in map space (0 to 256), get the corresponding LatLng
+    return FlatProjection.prototype.fromPointToLatLng(
+        new google.maps.Point(x, y));
+}
+
+$(function() {
+
+    // Set up the RPC system for background statistics
+    rpc_initialize();
+
+    // Set up the Google Map
+    initialize_view();
+    
+    // Set up the layer search
+    $("#search").select2({
+        placeholder: "Add Attribute...",
+        query: function(query) {
+            // Given a select2 query object, call query.callback with an object
+            // with a "results" array.
+            
+            // This is the array of result objects we will be sending back.
+            var results = [];
+        
+            // Get where we should start in the layer list, from select2's
+            // infinite scrolling.
+            var start_position = 0;
+            if(query.context != undefined) {
+                start_position = query.context;
+            }
+        
+            for(var i = start_position; i < layer_names_sorted.length; i++) {
+                // For each possible result
+                if(layer_names_sorted[i].toLowerCase().indexOf(
+                    query.term.toLowerCase()) != -1) {
+                    
+                    // Query search term is in this layer's name. Add a select2
+                    // record to our results. Don't specify text: our custom
+                    // formatter looks up by ID and makes UI elements
+                    // dynamically.
+                    results.push({
+                        id: layer_names_sorted[i]
+                    });
+                    
+                    if(results.length >= SEARCH_PAGE_SIZE) {
+                        // Page is full. Send it on.
+                        break;
+                    }
+                    
+                }
+            }
+            
+            // Give the results back to select2 as the results parameter.
+            query.callback({
+                results: results,
+                // Say there's more if we broke out of the loop.
+                more: i < layer_names_sorted.length,
+                // If there are more results, start after where we left off.
+                context: i + 1
+            });
+        },
+        formatResult: function(result, container, query) {
+            // Given a select2 result record, the element that our results go
+            // in, and the query used to get the result, return a jQuery element
+            // that goes in the container to represent the result.
+            
+            // Get the layer name, and make the browse UI for it.
+            return make_browse_ui(result.id);
+        },
+        // We want our dropdown to be big enough to browse.
+        dropdownCssClass: "results-dropdown"
+    });
+    
+    // Handle result selection
+    $("#search").on("select2-selecting", function(event) {
+        // The select2 id of the thing clicked (the layer's name) is event.val
+        var layer_name = event.val;
+        
+        // User chose this layer. Add it to the global shortlist.
+        
+        // Only add to the shortlist if it isn't already there
+        // Was it already there?
+        var found = false;
+        for(var j = 0; j < shortlist.length; j++) {
+            if(shortlist[j] == layer_name) {
+                found = true;
+                break;
+            }
+        }
+        
+        if(!found) {
+            // It's new. Add it to the shortlist
+            shortlist.push(layer_name);
+            
+            // Update the UI to reflect this. This may redraw the view.
+            update_shortlist_ui();
+            
+        }
+        
+        // Don't actually change the selection.
+        // This keeps the dropdown open when we click.
+        event.preventDefault();
+    });
+
+    $("#recalculate-statistics").button().click(function() {
+        // Re-calculate the statistics between the currently filtered hexes and
+        // everything else.
+        
+        // Put up the throbber instead of us.
+        $("#recalculate-statistics").hide();
+        $(".recalculate-throbber").show();
+        
+        // This holds the currently enabled filters.
+        var filters = get_current_filters();
+    
+        with_filtered_signatures(filters, function(signatures) {
+            // Find everything passing the filters and run the statistics.
+            recalculate_statistics(signatures);
+        });
+    });
+
+    // Download the signature assignments to hexagons and fill in the global 
+    // hexagon assignment grid.
+    $.get("assignments.tab", function(tsv_data) {        
+        // This is an array of rows, which are arrays of values:
+        // id, x, y
+        var parsed = $.tsv.parseRows(tsv_data);
+
+        // This holds the maximum observed x
+        var max_x = 0;
+        // And y
+        var max_y = 0;
+        
+        // Fill in the global signature grid and ploygon grid arrays.
+        for(var i = 0; i < parsed.length; i++) {
+            // Get the label
+            var label = parsed[i][0];
+            
+            if(label == "") {
+                // Blank line
+                continue;
+            }
+            
+            // Get the x coord
+            var x = parseInt(parsed[i][1]);
+            // And the y coord
+            var y = parseInt(parsed[i][2]);
+
+
+            // Update maxes
+            max_x = Math.max(x, max_x);
+            max_y = Math.max(y, max_y);
+            
+
+            // Make sure we have a row
+            if(signature_grid[y] == null) {
+                signature_grid[y] = [];
+                // Pre-emptively add a row to the polygon grid.
+                polygon_grid[y] = [];
+            }
+            
+            // Store the label in the global signature grid.
+            signature_grid[y][x] = label;
+        }
+        
+        // We need to fit this whole thing into a 256x256 grid.
+        // How big can we make each hexagon?
+        // TODO: Do the algrbra to make this exact. Right now we just make a 
+        // grid that we know to be small enough.
+        // Divide the space into one column per column, and calculate 
+        // side length from column width. Add an extra column for dangling
+        // corners.
+        var side_length_x = 256 / (max_x + 2) * (2.0 / 3.0);
+        
+        print("Max hexagon side length horizontally is " + side_length_x);
+        
+        // Divide the space into rows and calculate the side length
+        // from hex height. Remember to add an extra row for wggle.
+        var side_length_y = (256 / (max_y + 2)) / Math.sqrt(3);
+        
+        print("Max hexagon side length vertically is " + side_length_y);
+        
+        // How long is a hexagon side in world coords?
+        // Shrink it from the biggest we can have so that we don't wrap off the 
+        // edges of the map.
+        var hexagon_side_length = Math.min(side_length_x, side_length_y) / 2.0;
+
+        // Store this in the global hex_size, so we can later calculate the hex
+        // size in pixels and make borders go away if we are too zoomed out.
+        hex_size = hexagon_side_length;
+
+        // How far in should we move the whole grid from the top left corner of 
+        // the earth?
+        // Let's try leaving a 1/4 Earth gap at least, to stop wrapping in 
+        // longitude that we can't turn off.
+        // Since we already shrunk the map to half max size, this would put it 
+        // 1/4 of the 256 unit width and height away from the top left corner.
+        grid_offset = 256 / 4;
+        
+        // Loop through again and draw the polygons, now that we know how big 
+        // they have to be
+        for(var i = 0; i < parsed.length; i++) {
+            // TODO: don't re-parse this info
+            // Get the label
+            var label = parsed[i][0];
+            
+            if(label == "") {
+                // Blank line
+                continue;
+            }
+            
+            // Get the x coord
+            var x = parseInt(parsed[i][1]);
+            // And the y coord
+            var y = parseInt(parsed[i][2]);
+
+            // Make a hexagon on the Google map and store that.
+            var hexagon = make_hexagon(y, x, hexagon_side_length, grid_offset);
+            // Store by x, y in grid
+            polygon_grid[y][x] = hexagon;
+            // Store by label
+            polygons[label] = hexagon;
+            
+            // Set the polygon's signature so we can look stuff up for it when 
+            // it's clicked.
+            set_hexagon_signature(hexagon, label);     
+            
+        }
+        
+        // Now that the ploygons exist, do the initial redraw to set all their 
+        // colors corectly. In case someone has messed with the controls.
+        // TODO: can someone yet have messed with the controlls?
+        refresh();
+        
+
+    }, "text");
+    
+    // Download the DrL position data, and make it into a layer
+    $.get("drl.tab", function(tsv_data) {
+        // This is an array of rows, which are arrays of values:
+        // id, x, y
+        // Only this time X and Y are Cartesian coordinates.
+        var parsed = $.tsv.parseRows(tsv_data);
+        
+        // Compute two layers: one for x position, and one for y position.
+        var layer_x = {};
+        var layer_y = {};
+        
+        for(var i = 0; i < parsed.length; i++) {
+            // Pull out the parts of the TSV entry
+            var label = parsed[i][0];
+            
+            if(label == "") {
+                // DrL ends its output with a blank line, which we skip 
+                // here.
+                continue;
+            }
+            
+            var x = parseFloat(parsed[i][1]);
+            // Invert the Y coordinate since we do that in the hex grid
+            var y = -parseFloat(parsed[i][2]);
+            
+            // Add x and y to the appropriate layers
+            layer_x[label] = x;
+            layer_y[label] = y;
+        }
+        
+        // Register the layers with no priorities. By default they are not 
+        // selections.
+        add_layer_data("DrL X Position", layer_x);
+        add_layer_data("DrL Y Position", layer_y);
+        
+        // Make sure the layer browser has the up-to-date layer list
+        update_browse_ui();
+        
+    }, "text");
+    
+    // Download the layer index
+    $.get("layers.tab", function(tsv_data) {
+        // Layer index is <name>\t<filename>\t<clumpiness>
+        var parsed = $.tsv.parseRows(tsv_data);
+        
+        for(var i = 0; i < parsed.length; i++) {
+            // Pull out the parts of the TSV entry
+            // This is the name of the layer.
+            var layer_name = parsed[i][0];
+            
+            if(layer_name == "") {
+                // Skip any blank lines
+                continue;
+            }
+            
+            // This is the URL from which to download the TSV for the actual 
+            // layer.
+            var layer_url = parsed[i][1];
+            
+            // This is the layer's clumpiness score
+            var layer_clumpiness = parseFloat(parsed[i][2]);
+            
+            // This is the number of hexes that the layer has any values for.
+            // We need to get it from the server so we don't have to download 
+            // the layer to have it.
+            var layer_count = parseFloat(parsed[i][3]);
+            
+            // This is the number of 1s in a binary layer, or NaN in other
+            // layers
+            var layer_positives = parseFloat(parsed[i][4]);       
+                   
+            // Add this layer to our index of layers
+            add_layer_url(layer_name, layer_url, {
+                clumpiness: layer_clumpiness,
+                positives: layer_positives,
+                n: layer_count
+            });
+        }
+        
+        // Now we have added layer downloaders for all the layers in the 
+        // index. Update the UI
+        update_browse_ui();
+        
+         
+    }, "text");
+    
+    // Download full score matrix index, which we later use for statistics. Note
+    // that stats won't work unless this finishes first. TODO: enforce this.
+    $.get("matrices.tab", function(tsv_data) {
+        // Matrix index is just <filename>
+        var parsed = $.tsv.parseRows(tsv_data);
+        
+        for(var i = 0; i < parsed.length; i++) {
+            // Pull out the parts of the TSV entry
+            // This is the filename of the matrix.
+            var matrix_name = parsed[i][0];
+            
+            if(matrix_name == "") {
+                // Not a real matrix
+                continue;
+            }
+            
+            // Add it to the global list
+            available_matrices.push(matrix_name);
+        }
+    }, "text");
+    
+    // Download color map information
+    $.get("colormaps.tab", function(tsv_data) {
+        // Colormap data is <layer name>\t<value>\t<category name>\t<color>
+        // \t<value>\t<category name>\t<color>...
+        var parsed = $.tsv.parseRows(tsv_data);
+        
+        for(var i = 0; i < parsed.length; i++) {
+            // Get the name of the layer
+            var layer_name = parsed[i][0];
+            
+            // Skip blank lines
+            if(layer_name == "") {
+                continue;
+            }
+            
+            // This holds all the categories (name and color) by integer index
+            var colormap = [];
+            
+            print("Loading colormap for " + layer_name);
+            
+            for(j = 1; j < parsed[i].length; j += 3) {
+                // Store each color assignment.
+                // Doesn't run if there aren't any assignments, leaving an empty
+                // colormap object that just forces automatic color selection.
+                
+                // This holds the index of the category
+                var category_index = parseInt(parsed[i][j]);
+                
+                // The colormap gets an object with the name and color that the
+                // index number refers to. Color is stored as a color object.
+                colormap[category_index] = {
+                    name: parsed[i][j + 1],
+                    color: Color(parsed[i][j + 2])
+                };
+                
+                print( colormap[category_index].name + " -> " +  
+                    colormap[category_index].color.hexString());
+            }
+            
+            // Store the finished color map in the global object
+            colormaps[layer_name] = colormap;
+            
+            
+        }
+        
+        // We may need to redraw the view in response to having new color map 
+        // info, if it came particularly late.
+        refresh();
+            
+    }, "text");
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/hexagram.py	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,1065 @@
+#!/usr/bin/env python2.7
+"""
+hexagram.py: Given a matrix of similarities, produce a hexagram visualization.
+
+This script takes in the filename of a tab-separated value file containing a
+sparse similarity matrix (with string labels) and several matrices of
+layer/score data. It produces an HTML file (and several support files) that
+provide an interactive visualization of the items clustered on a hexagonal grid.
+
+This script depends on the DrL graph alyout package, binaries for which must be
+present in your PATH.
+
+Re-uses sample code and documentation from 
+<http://users.soe.ucsc.edu/~karplus/bme205/f12/Scaffold.html>
+"""
+
+import argparse, sys, os, itertools, math, numpy, subprocess, shutil, tempfile
+import collections, scipy.stats, multiprocessing, traceback, numpy.ma
+import os.path
+import tsv
+
+def parse_args(args):
+    """
+    Takes in the command-line arguments list (args), and returns a nice argparse
+    result with fields for all the options.
+    Borrows heavily from the argparse documentation examples:
+    <http://docs.python.org/library/argparse.html>
+    """
+    
+    # The command line arguments start with the program name, which we don't
+    # want to treat as an argument for argparse. So we remove it.
+    args = args[1:]
+    
+    # Construct the parser (which is stored in parser)
+    # Module docstring lives in __doc__
+    # See http://python-forum.com/pythonforum/viewtopic.php?f=3&t=36847
+    # And a formatter class so our examples in the docstring look good. Isn't it
+    # convenient how we already wrapped it to 80 characters?
+    # See http://docs.python.org/library/argparse.html#formatter-class
+    parser = argparse.ArgumentParser(description=__doc__, 
+        formatter_class=argparse.RawDescriptionHelpFormatter)
+    
+    # Now add all the options to it
+    # Options match the ctdHeatmap tool options as much as possible.
+    parser.add_argument("similarities", type=argparse.FileType("r"), 
+        help="the TSV file with the similarities for signatures we're using")
+    parser.add_argument("--scores", type=str,
+        action="append", default=[],
+        help="a TSV to read scores for each signature from")
+    parser.add_argument("--colormaps", type=argparse.FileType("r"), 
+        default=None,
+        help="a TSV defining coloring and value names for discrete scores")
+    parser.add_argument("--html", "-H", type=str, 
+        default="index.html",
+        help="where to write HTML report")
+    parser.add_argument("--directory", "-d", type=str, default=".",
+        help="directory in which to create other output files")
+    parser.add_argument("--query", type=str, default=None,
+        help="Galaxy-escaped name of the query signature")
+    parser.add_argument("--window_size", type=int, default=20,
+        help="size of the window to use when looking for clusters")
+    parser.add_argument("--no-stats", dest="stats", action="store_false", 
+        default=True,
+        help="disable cluster-finding statistics")
+        
+    return parser.parse_args(args)
+
+def hexagon_center(x, y, scale=1.0):
+    """
+    Given a coordinate on a grid of hexagons (using wiggly rows in x), what is 
+    the 2d Euclidian coordinate of its center?
+    
+    x and y are integer column and row coordinates of the hexagon in the grid.
+    
+    scale is a float specifying hexagon side length.
+    
+    The origin in coordinate space is defined as the upper left corner of the 
+    bounding box of the hexagon wityh indices x=0 and y=0.
+    
+    Returns a tuple of floats.
+    """
+    # The grid looks like this:
+    #
+    #   /-\ /-\ /-\ /-\ 
+    # /-\-/-\-/-\-/-\-/-\
+    # \-/-\-/-\-/-\-/-\-/
+    # /-\-/-\-/-\-/-\-/-\
+    # \-/-\-/-\-/-\-/-\-/
+    # /-\-/-\-/-\-/-\-/-\
+    # \-/ \-/ \-/ \-/ \-/
+    #   
+    # Say a hexagon side has length 1
+    # It's 2 across corner to corner (x), and sqrt(3) across side to side (y)
+    # X coordinates are 1.5 per column
+    # Y coordinates (down from top) are sqrt(3) per row, -1/2 sqrt(3) if you're 
+    # in an odd column.
+    
+    center_y = math.sqrt(3) * y
+    if x % 2 == 1:
+        # Odd column: shift up
+        center_y -= 0.5 * math.sqrt(3)
+        
+    return (1.5 * x * scale + scale, center_y * scale + math.sqrt(3.0) / 2.0 * 
+        scale)
+
+def hexagon_pick(x, y, scale=1.0):
+    """
+    Given floats x and y specifying coordinates in the plane, determine which 
+    hexagon grid cell that point is in.
+    
+    scale is a float specifying hexagon side length.
+    
+    See http://blog.ruslans.com/2011/02/hexagonal-grid-math.html
+    But we flip the direction of the wiggle. Odd rows are up (-y)
+    """
+    
+    # How high is a hex?
+    hex_height = math.sqrt(3) * scale
+    
+    # First we pick a rectangular tile, from the point of one side-traingle to 
+    # the base of the other in width, and the whole hexagon height in height.
+    
+    # How wide are these tiles? Corner to line-between-far-corners distance
+    tile_width = (3.0 / 2.0 * scale)
+    
+    # Tile X index is floor(x / )
+    tile_x = int(math.floor(x / tile_width))
+    
+    # We need this intermediate value for the Y index and for tile-internal
+    # picking
+    corrected_y = y + (tile_x % 2) * hex_height / 2.0
+    
+    # Tile Y index is floor((y + (x index mod 2) * hex height/2) / hex height)
+    tile_y = int(math.floor(corrected_y / hex_height))
+    
+    # Find coordinates within the tile
+    internal_x = x - tile_x * tile_width
+    internal_y = corrected_y - tile_y * hex_height
+    
+    # Do tile-scale picking
+    # Are we in the one corner, the other corner, or the bulk of the tile?
+    if internal_x > scale * abs(0.5 - internal_y / hex_height):
+        # We're in the bulk of the tile
+        # This is the column (x) of the picked hexagon
+        hexagon_x = tile_x
+        
+        # This is the row (y) of the picked hexagon
+        hexagon_y = tile_y
+    else:
+        # We're in a corner.
+        # In an even column, the lower left is part of the next row, and the 
+        # upper left is part of the same row. In an odd column, the lower left 
+        # is part of the same row, and the upper left is part of the previous 
+        # row.
+        if internal_y > hex_height / 2.0:
+            # It's the lower left corner
+            # This is the offset in row (y) that being in this corner gives us
+            # The lower left corner is always 1 row below the upper left corner.
+            corner_y_offset = 1
+        else:
+            corner_y_offset = 0
+            
+        # TODO: verify this for correctness. It seems to be right, but I want a
+        # unit test to be sure.
+        # This is the row (y) of the picked hexagon
+        hexagon_y = tile_y - tile_x % 2 + corner_y_offset
+        
+        # This is the column (x) of the picked hexagon
+        hexagon_x = tile_x - 1
+    
+    # Now we've picked the hexagon
+    return (hexagon_x, hexagon_y)    
+
+def radial_search(center_x, center_y):
+    """
+    An iterator that yields coordinate tuples (x, y) in order of increasing 
+    hex-grid distance from the specified center position.
+    """
+    
+    # A hexagon has neighbors at the following relative coordinates:
+    # (-1, 0), (1, 0), (0, -1), (0, 1)
+    # and ((-1, 1) and (1, 1) if in an even column)
+    # or ((-1, -1) and (1, -1) if in an odd column)
+    
+    # We're going to go outwards using breadth-first search, so we need a queue 
+    # of hexes to visit and a set of already visited hexes.
+    
+    # This holds a queue (really a deque) of hexes waiting to be visited.
+    # A list has O(n) pop/insert at left.
+    queue = collections.deque()
+    # This holds a set of the (x, y) coordinate tuples of already-seen hexes,
+    # so we don't enqueue them again.
+    seen = set()
+    
+    # First place to visit is the center.
+    queue.append((center_x, center_y))
+    
+    while len(queue) > 0:
+        # We should in theory never run out of items in the queue.
+        # Get the current x and y to visit.
+        x, y = queue.popleft()
+        
+        # Yield the location we're visiting
+        yield (x, y)
+        
+        # This holds a list of all relative neighbor positions as (x, y) tuples.
+        neighbor_offsets = [(-1, 0), (1, 0), (0, -1), (0, 1)]
+        if y % 2 == 0:
+            # An even-column hex also has these neighbors
+            neighbor_offsets += [(-1, 1), (1, 1)]
+        else:
+            # An odd-column hex also has these neighbors
+            neighbor_offsets += [(-1, -1), (1, -1)]
+    
+        for x_offset, y_offset in neighbor_offsets:
+            # First calculate the absolute position of the neighbor in x
+            neighbor_x = x + x_offset
+            # And in y
+            neighbor_y = y + y_offset
+            
+            if (neighbor_x, neighbor_y) not in seen:
+                # This is a hex that has never been in the queue. Add it.
+                queue.append((neighbor_x, neighbor_y))
+                
+                # Record that it has ever been enqueued
+                seen.add((neighbor_x, neighbor_y))
+    
+    
+    
+
+def assign_hexagon(hexagons, node_x, node_y, node, scale=1.0):
+    """
+    This function assigns the given node to a hexagon in hexagons. hexagons is a
+    defaultdict from tuples of hexagon (x, y) integer indices to assigned nodes,
+    or None if a hexagon is free. node_x and node_y are the x and y coordinates 
+    of the node, adapted so that the seed node lands in the 0, 0 hexagon, and 
+    re-scaled to reduce hexagon conflicts. node is the node to be assigned. 
+    scale, if specified, is the hexagon side length in node space units.
+    
+    This function assigns nodes to their closest hexagon, reprobing outwards if 
+    already occupied.
+    
+    When the function completes, node is stored in hexagons under some (x, y) 
+    tuple.
+    
+    Returns the distance this hexagon is from its ideal location.
+    """
+    
+    # These hold the hexagon that the point falls in, which may be taken.
+    best_x, best_y = hexagon_pick(node_x, node_y, scale=scale)
+    
+    for x, y in radial_search(best_x, best_y):
+        # These hexes are enumerated in order of increasign distance from the 
+        # best one, starting with the best hex itself.
+        
+        if hexagons[(x, y)] is None:
+            # This is the closest free hex. Break out of the loop, leaving x and
+            # y pointing here.
+            break
+    
+    # Assign the node to the hexagon
+    hexagons[(x, y)] = node
+    
+    return math.sqrt((x - best_x) ** 2 + (y - best_y) ** 2)
+    
+    
+
+def assign_hexagon_local_radial(hexagons, node_x, node_y, node, scale=1.0):
+    """
+    This function assigns the given node to a hexagon in hexagons. hexagons is a
+    defaultdict from tuples of hexagon (x, y) integer indices to assigned nodes,
+    or None if a hexagon is free. node_x and node_y are the x and y coordinates 
+    of the node, adapted so that the seed node lands in the 0, 0 hexagon, and 
+    re-scaled to reduce hexagon conflicts. node is the node to be assigned. 
+    scale, if specified, is the hexagon side length in node space units.
+    
+    This function assigns nodes to their closest hexagon. If thast hexagon is 
+    full, it re-probes in the direction that the node is from the closest 
+    hexagon's center.
+    
+    When the function completes, node is stored in hexagons under some (x, y) 
+    tuple.
+    
+    Returns the distance this hexagon is from its ideal location.
+    """
+    
+    # These hold the hexagon that the point falls in, which may be taken.
+    best_x, best_y = hexagon_pick(node_x, node_y, scale=scale)
+    
+    # These hold the center of that hexagon in float space
+    center_x, center_y = hexagon_center(best_x, best_y, scale=scale)
+    
+    # This holds the distance from this point to the center of that hexagon
+    node_distance = math.sqrt((node_x - center_x) ** 2 + (node_y - center_y) **
+        2)
+    
+    # These hold the normalized direction of this point, relative to the center 
+    # of its best hexagon
+    direction_x = (node_x - center_x) / node_distance
+    direction_y = (node_y - center_y) / node_distance
+    
+    # Do a search in that direction, starting at the best hex.
+
+    # These are the hexagon indices we're considering
+    x, y = best_x, best_y
+    
+    # These are the Cartesian coordinates we're probing. Must be in the x, y hex
+    # as a loop invariant.
+    test_x, test_y = center_x, center_y
+    
+    while hexagons[(x, y)] is not None:
+        # Re-probe outwards from the best hex in scale/2-sized steps
+        # TODO: is that the right step size? Scale-sized steps seemed slightly 
+        # large.
+        test_x += direction_x * scale
+        test_y += direction_y * scale
+        
+        # Re-pick x and y for the hex containing our test point
+        x, y = hexagon_pick(test_x, test_y, scale=scale)
+        
+    # We've finally reached the edge of the cluster.
+    # Drop our hexagon
+    hexagons[(x, y)] = node
+    
+    return math.sqrt((x - best_x) ** 2 + (y - best_y) ** 2)
+
+def assign_hexagon_radial(hexagons, node_x, node_y, node, scale=1.0):
+    """
+    This function assigns the given node to a hexagon in hexagons. hexagons is a
+    defaultdict from tuples of hexagon (x, y) integer indices to assigned nodes,
+    or None if a hexagon is free. node_x and node_y are the x and y coordinates 
+    of the node, adapted so that the seed node lands in the 0, 0 hexagon, and 
+    re-scaled to reduce hexagon conflicts. node is the node to be assigned. 
+    scale, if specified, is the hexagon side length in node space units.
+    
+    This function assigns nodes to hexagons based on radial distance from 0, 0.
+    This makes hexagon assignment much more dense, but can lose spatial 
+    structure.
+    
+    When the function completes, node is stored in hexagons under some (x, y) 
+    tuple.
+    
+    Returns the distance this hexagon is from its ideal location. Unfortunately,
+    this doesn't really make sense for this assignment scheme, so it is always
+    0.
+    """
+    
+    # Compute node's distance from the origin
+    node_distance = math.sqrt(node_x ** 2 + node_y ** 2)
+    
+    # Compute normalized direction from the origin for this node
+    direction_x = node_x / node_distance
+    direction_y = node_y / node_distance
+    
+    # These are the coordinates we are testing
+    test_x = 0
+    test_y = 0
+    
+    # These are the hexagon indices that correspond to that point
+    x, y = hexagon_pick(test_x, test_y, scale=scale)
+    
+    while hexagons[(x, y)] is not None:
+        # Re-probe outwards from the origin in scale-sized steps
+        # TODO: is that the right step size?
+        test_x += direction_x * scale
+        test_y += direction_y * scale
+        
+        # Re-pick
+        x, y = hexagon_pick(test_x, test_y, scale=scale)
+        
+    # We've finally reached the edge of the cluster.
+    # Drop our hexagon
+    # TODO: this has to be N^2 if we line them all up in a line
+    hexagons[(x, y)] = node
+    
+    return 0
+
+def hexagons_in_window(hexagons, x, y, width, height):
+    """
+    Given a dict from (x, y) position to signature names, return the list of all
+    signatures in the window starting at hexagon x, y and extending width in the
+    x direction and height in the y direction on the hexagon grid.
+    """        
+    
+    # This holds the list of hexagons we've found
+    found = []
+    
+    for i in xrange(x, x + width):
+        for j in xrange(y, y + height):
+            if hexagons.has_key((i, j)):
+                # This position in the window has a hex.
+                found.append(hexagons[(i, j)])
+                
+    return found
+
+class ClusterFinder(object):
+    """
+    A class that can be invoked to find the p value of the best cluster in its 
+    layer. Instances are pickleable.
+    """
+    
+    def __init__(self, hexagons, layer, window_size=5):
+        """
+        Keep the given hexagons dict (from (x, y) to signature name) and the 
+        given layer (a dict from signature name to a value), and the given 
+        window size, in a ClusterFinder object.
+        """
+        
+        # TODO: This should probably all operate on numpy arrays that we can 
+        # slice efficiently.
+        
+        # Store the layer
+        self.hexagons = hexagons
+        # Store the hexagon assignments
+        self.layer = layer
+        
+        # Store the window size
+        self.window_size = window_size
+    
+    @staticmethod
+    def continuous_p(in_values, out_values):
+        """
+        Get the p value for in_values and out_values being distinct continuous 
+        distributions.
+        
+        in_values and out_values are both Numpy arrays. Returns the p value, or 
+        raises a ValueError if the statistical test cannot be run for some
+        reason.
+        
+        Uses the Mann-Whitney U test.
+        """
+    
+        # Do a Mann-Whitney U test to see how different the data  
+        # sets are.
+        u_statistic, p_value = scipy.stats.mannwhitneyu(in_values, 
+            out_values)
+            
+        return p_value
+    
+    @staticmethod    
+    def dichotomous_p(in_values, out_values):
+        """
+        Given two one-dimensional Numpy arrays of 0s and 1s, compute a p value 
+        for the in_values having a different probability of being 1 than the 
+        frequency of 1s in the out_values.
+        
+        This test uses the scipy.stats.binom_test function, which does not claim
+        to use the normal approximation. Therefore, this test should be valid
+        for arbitrarily small frequencies of either 0s or 1s in in_values.
+        
+        TODO: What if out_values is shorter than in_values?
+        """
+        
+        if len(out_values) == 0:
+            raise ValueError("Background group is empty!")
+        
+        # This holds the observed frequency of 1s in out_values
+        frequency = numpy.sum(out_values) / len(out_values)
+        
+        # This holds the number of 1s in in_values
+        successes = numpy.sum(in_values)
+        
+        # This holds the number of "trials" we got that many successes in
+        trials = len(in_values)
+        
+        # Return how significantly the frequency inside differs from that 
+        # outside.
+        return scipy.stats.binom_test(successes, trials, frequency)
+    
+    @staticmethod    
+    def categorical_p(in_values, out_values):
+        """
+        Given two one-dimensional Numpy arrays of integers (which may be stored
+        as floats), which represent items being assigned to different 
+        categories, return a p value for the distribution of categories observed
+        in in_values differing from that observed in out_values.
+        
+        The normal way to do this is with a chi-squared goodness of fit test. 
+        However, that test has invalid assumptions when there are fewer than 5 
+        expected and 5 observed observations in every category. 
+        See http://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.chis
+        quare.html
+        
+        However, we will use it anyway, because the tests that don't break down
+        are prohibitively slow.
+        """
+        
+        # Convert our inputs to integer arrays
+        in_values = in_values.astype(int)
+        out_values = out_values.astype(int)
+        
+        # How many categories are there (count 0 to the maximum value)
+        num_categories = max(numpy.max(in_values), numpy.max(out_values)) + 1
+        
+        # Count the number of in_values and out_values in each category
+        in_counts = numpy.array([len(in_values[in_values == i]) for i in 
+            xrange(num_categories)])
+        out_counts = numpy.array([len(out_values[out_values == i]) for i in 
+            xrange(num_categories)])
+        
+        # Get the p value for the window being from the estimated distribution
+        # None of the distribution parameters count as "estimated from data" 
+        # because they aren't estimated from the data under test.
+        _, p_value = scipy.stats.chisquare(in_counts, out_counts)
+        
+        return p_value
+        
+    def __call__(self):
+        """
+        Find the best p value for any window of size window_size. Return it.
+        """
+
+        # Calculate the bounding box where we want to look for windows.
+        # TODO: This would just be all of a numpy array
+        min_x = min(coords[0] for coords in self.hexagons.iterkeys())
+        min_y = min(coords[1] for coords in self.hexagons.iterkeys()) 
+        max_x = max(coords[0] for coords in self.hexagons.iterkeys())
+        max_y = max(coords[1] for coords in self.hexagons.iterkeys()) 
+        
+        # This holds a Numpy array of all the data by x, y
+        layer_data = numpy.empty((max_x - min_x + 1, max_y - min_y + 1))
+        
+        # Fill it with NaN so we can mask those out later
+        layer_data[:] = numpy.NAN
+        
+        for (hex_x, hex_y), name in self.hexagons.iteritems():
+            # Copy the layer values into the Numpy array
+            if self.layer.has_key(name):
+                layer_data[hex_x - min_x, hex_y - min_y] = self.layer[name]
+        
+        # This holds a masked version of the layer data
+        layer_data_masked = numpy.ma.masked_invalid(layer_data, copy=False) 
+        
+        # This holds the smallest p value we have found for this layer
+        best_p = float("+inf")
+        
+        # This holds the statistical test to use (a function from two Numpy 
+        # arrays to a p value)
+        # The most specific test is the dichotomous test (0 or 1)
+        statistical_test = self.dichotomous_p
+        
+        if numpy.sum(~layer_data_masked.mask) == 0: 
+            # There is actually no data in this layer at all.
+            # nditer complains if we try to iterate over an empty thing.
+            # So quit early and say we couldn't find anything.
+            return best_p
+ 
+        for value in numpy.nditer(layer_data_masked[~layer_data_masked.mask]):
+            # Check all the values in the layer.
+            # If this value is out of the domain of the current statistical 
+            # test, upgrade to a more general test.
+            
+            if statistical_test == self.dichotomous_p and (value > 1 or 
+                value < 0):
+                
+                # We can't use a dichotomous test on things outside 0 to 1
+                # But we haven't yet detected any non-integers
+                # Use categorical
+                statistical_test = self.categorical_p
+            
+            if value % 1 != 0:
+                # This is not an integer value
+                # So, we must use a continuous statistical test
+                statistical_test = self.continuous_p
+                
+                # This is the least specific test, so we can stop now
+                break
+        
+                
+        for i in xrange(min_x, max_x - self.window_size):
+            for j in xrange(min_y, max_y - self.window_size):
+
+                # Get the layer values for hexes in the window, as a Numpy
+                # masked array.
+                in_region = layer_data_masked[i:i + self.window_size, 
+                    j:j + self.window_size]
+                    
+                # And as a 1d Numpy array
+                in_values = numpy.reshape(in_region[~in_region.mask], -1).data
+                
+                # And out of the window (all the other hexes) as a masked array
+                out_region = numpy.ma.copy(layer_data_masked)
+                # We get this by masking out everything in the region
+                out_region.mask[i:i + self.window_size, 
+                    j:j + self.window_size] = True
+                
+                # And as a 1d Numpy array
+                out_values = numpy.reshape(out_region[~out_region.mask], 
+                    -1).data
+                 
+                    
+                if len(in_values) == 0 or len(out_values) == 0:
+                    # Can't do any stats on this window
+                    continue
+                    
+                if len(in_values) < 0.5 * self.window_size ** 2:
+                    # The window is less than half full. Skip it.
+                    # TODO: Make this threshold configurable.
+                    continue
+                
+                try:    
+                    
+                    # Get the p value for this window under the selected 
+                    # statistical test
+                    p_value = statistical_test(in_values, out_values)
+                        
+                    # If this is the best p value so far, record it
+                    best_p = min(best_p, p_value)
+                except ValueError:
+                    # Probably an all-zero layer, or something else the test 
+                    # can't handle.
+                    # But let's try all the other windows to be safe. 
+                    # Maybe one will work.
+                    pass
+                    
+                
+                
+        # We have now found the best p for any window for this layer.
+        print "Best p found: {}".format(best_p)
+        sys.stdout.flush()
+        
+        return best_p                
+
+def run_functor(functor):
+    """
+    Given a no-argument functor (like a ClusterFinder), run it and return its 
+    result. We can use this with multiprocessing.map and map it over a list of 
+    job functors to do them.
+    
+    Handles getting more than multiprocessing's pitiful exception output
+    """
+    
+    try:
+        return functor()
+    except:
+        # Put all exception text into an exception and raise that
+        raise Exception(traceback.format_exc())
+                
+def main(args):
+    """
+    Parses command line arguments, and makes visualization.
+    "args" specifies the program arguments, with args[0] being the executable
+    name. The return value should be used as the program's exit code.
+    """
+    
+    options = parse_args(args) # This holds the nicely-parsed options object
+    
+    # Test our picking
+    x, y = hexagon_center(0, 0)
+    if hexagon_pick(x, y) != (0, 0):
+        raise Exception("Picking is broken!")
+    
+    # First bit of stdout becomes annotation in Galaxy
+    
+    # Make sure our output directory exists.
+    if not os.path.exists(options.directory):
+        # makedirs is the right thing to use here: recursive
+        os.makedirs(options.directory)
+    
+    # Work in a temporary directory
+    drl_directory = tempfile.mkdtemp()
+    
+    # This is the base name for all the files that DrL uses to do the layout
+    # We're going to put it in a temporary directory.
+    drl_basename = os.path.join(drl_directory, "layout")
+    
+    # We can just pass our similarity matrix to DrL's truncate
+    # But we want to run it through our tsv parser to strip comments and ensure
+    # it's valid
+    
+    # This holds a reader for the similarity matrix
+    sim_reader = tsv.TsvReader(options.similarities)
+    
+    # This holds a writer for the sim file
+    sim_writer = tsv.TsvWriter(open(drl_basename + ".sim", "w"))
+    
+    print "Regularizing similarity matrix..."
+    sys.stdout.flush()
+    
+    for parts in sim_reader:
+        sim_writer.list_line(parts)
+        
+    sim_reader.close()
+    sim_writer.close()
+    
+    # Now our input for DrL is prepared!
+    
+    # Do DrL truncate.
+    # TODO: pass a truncation level
+    print "DrL: Truncating..."
+    sys.stdout.flush()
+    subprocess.check_call(["truncate", drl_basename]) 
+        
+    # Run the DrL layout engine.
+    print "DrL: Doing layout..."
+    sys.stdout.flush()
+    subprocess.check_call(["layout", drl_basename]) 
+    
+    # Put the string names back
+    print "DrL: Restoring names..."
+    sys.stdout.flush()
+    subprocess.check_call(["recoord", drl_basename]) 
+        
+    # Now DrL has saved its coordinates as <signature name>\t<x>\t<y> rows in 
+    # <basename>.coord
+    
+    # We want to read that.
+    # This holds a reader for the DrL output
+    coord_reader = tsv.TsvReader(open(drl_basename + ".coord", "r"))
+    
+    # This holds a dict from signature name string to (x, y) float tuple
+    nodes = {}
+    
+    print "Reading DrL output..."
+    sys.stdout.flush()
+    for parts in coord_reader:
+        nodes[parts[0]] = (float(parts[1]), float(parts[2])) 
+            
+    coord_reader.close()
+    
+    # Save the DrL coordinates in our bundle, to be displayed client-side for 
+    # debugging.
+    coord_writer = tsv.TsvWriter(open(
+        os.path.join(options.directory, "drl.tab"), "w"))
+        
+    for signature_name, (x, y) in nodes.iteritems():
+        # Write a tsv with names instead of numbers, like what DrL recoord would
+        # have written. This is what the Javascript on the client side wants.
+        coord_writer.line(signature_name, x, y)
+        
+    coord_writer.close()
+    
+    # Do the hexagon layout
+    # We do the squiggly rows setup, so express everything as integer x, y
+    
+    # This is a defaultdict from (x, y) integer tuple to id that goes there, or
+    # None if it's free.
+    hexagons = collections.defaultdict(lambda: None)
+    
+    # This holds the side length that we use
+    side_length = 1.0
+    
+    # This holds what will be a layer of how badly placed each hexagon is
+    # A dict from node name to layer value
+    placement_badnesses = {}
+    
+    for node, (node_x, node_y) in nodes.iteritems():
+        # Assign each node to a hexagon
+        # This holds the resulting placement badness for that hexagon (i.e. 
+        # distance from ideal location)
+        badness = assign_hexagon(hexagons, node_x, node_y, node,
+            scale=side_length)
+            
+        # Put the badness in the layer
+        placement_badnesses[node] = float(badness)
+   
+    # Normalize the placement badness layer
+    # This holds the max placement badness
+    max_placement_badness = max(placement_badnesses.itervalues())
+    print "Max placement badness: {}".format(max_placement_badness)
+
+    if max_placement_badness != 0:
+        # Normalize by the max if possible.
+        placement_badnesses = {node: value / max_placement_badness for node, 
+            value in placement_badnesses.iteritems()}
+   
+    # The hexagons have been assigned. Make hexagons be a dict instead of a 
+    # defaultdict, so it pickles.
+    # TODO: I should change it so I don't need to do this.
+    hexagons = dict(hexagons)  
+   
+    # Now dump the hexagon assignments as an id, x, y tsv. This will be read by
+    # the JavaScript on the static page and be used to produce the 
+    # visualization.        
+    hexagon_writer = tsv.TsvWriter(open(os.path.join(options.directory, 
+        "assignments.tab"), "w"))
+
+    # First find the x and y offsets needed to make all hexagon positions 
+    # positive
+    min_x = min(coords[0] for coords in hexagons.iterkeys())
+    min_y = min(coords[1] for coords in hexagons.iterkeys())  
+        
+    for coords, name in hexagons.iteritems():
+        # Write this hexagon assignment, converted to all-positive coordinates.
+        hexagon_writer.line(name, coords[0] - min_x, coords[1] - min_y)    
+    hexagon_writer.close()
+    
+    # Now that we have hex assignments, compute layers.
+    
+    # In addition to making per-layer files, we're going to copy all the score
+    # matrices to our output directoy. That way, the client can download layers
+    # in big chunks when it wants all layer data for statistics. We need to
+    # write a list of matrices that the client can read, which is written by
+    # this TSV writer.
+    matrix_index_writer = tsv.TsvWriter(open(os.path.join(options.directory, 
+        "matrices.tab"), "w"))
+        
+    # Read in all the layer data at once
+    # TODO: Don't read in all the layer data at once
+    
+    # This holds a dict from layer name to a dict from signature name to 
+    # score.
+    layers = {}
+    
+    # This holds the names of all layers
+    layer_names = []
+    
+    for matrix_number, score_filename in enumerate(options.scores):
+        # First, copy the whole matrix into our output. This holds its filename.
+        output_filename = "matrix_{}.tab".format(matrix_number)
+        shutil.copy2(score_filename, os.path.join(options.directory, 
+            output_filename))
+            
+        # Record were we put it
+        matrix_index_writer.line(output_filename)
+    
+        # This holds a reader for the scores TSV
+        scores_reader = tsv.TsvReader(open(score_filename, "r"))
+        
+        # This holds an iterator over lines in that file
+        # TODO: Write a proper header/data API
+        scores_iterator = scores_reader.__iter__()
+        
+        try:
+            # This holds the names of the columns (except the first, which is 
+            # labels). They also happen to be layer names
+            file_layer_names = scores_iterator.next()[1:]
+            
+            # Add all the layers in this file to the complete list of layers.
+            layer_names += file_layer_names
+            
+            # Ensure that we have a dict for every layer mentioned in the file
+            # (even the ones that have no data below). Doing it this way means
+            # all score matrices need disjoint columns, or the last one takes
+            # precedence.
+            for name in file_layer_names:
+                layers[name] = {} 
+            
+            for parts in scores_iterator:
+                # This is the signature that this line is about
+                signature_name = parts[0]
+                
+                # These are the scores for all the layers for this signature
+                layer_scores = parts[1:]
+                
+                for (layer_name, score) in itertools.izip(file_layer_names, 
+                    layer_scores):
+                    
+                    # Store all the layer scores in the appropriate
+                    # dictionaries.
+                    try:
+                        layers[layer_name][signature_name] = float(score)
+                    except ValueError:
+                        # This is not a float.
+                        # Don't set that entry for this layer.
+                        # TODO: possibly ought to complain to the user? But then
+                        # things like "N/A" won't be handled properly.
+                        continue
+                    
+        except StopIteration:
+            # We don't have any real data here. Couldn't read the header line.
+            # Skip to the next file
+            pass
+            
+        # We're done with this score file now
+        scores_reader.close()
+    
+    # We're done with all the input score matrices, so our index is done too.
+    matrix_index_writer.close()
+    
+    # We have now loaded all layer data into memory as Python objects. What
+    # could possibly go wrong?
+    
+    # Stick our placement badness layer on the end
+    layer_names.append("Placement Badness")
+    layers["Placement Badness"] = placement_badnesses
+       
+    # Now we need to write layer files.
+        
+    # Generate some filenames for layers that we can look up by layer name.
+    # We do this because layer names may not be valid filenames.
+    layer_files = {name: os.path.join(options.directory, 
+        "layer_{}.tab".format(number)) for (name, number) in itertools.izip(
+        layer_names, itertools.count())}
+        
+    for layer_name, layer in layers.iteritems():
+        # Write out all the individual layer files
+        # This holds the writer for this layer file
+        scores_writer = tsv.TsvWriter(open(layer_files[layer_name], "w"))
+        for signature_name, score in layer.iteritems():
+            # Write the score for this signature in this layer
+            scores_writer.line(signature_name, score)
+        scores_writer.close()
+    
+    # We need something to sort layers by. We have "priority" (lower is 
+    # better)
+    
+    if len(layer_names) > 0 and options.stats:
+        # We want to do this fancy parallel stats thing.
+        # We skip it when there are no layers, so we don't try to join a
+        # never-used pool, which seems to hang.
+        
+        print "Running statistics..."
+        
+        # This holds an iterator that makes ClusterFinders for all out layers
+        cluster_finders = [ClusterFinder(hexagons, layers[layer_name], 
+            window_size=options.window_size) for layer_name in layer_names]
+        
+        print "{} jobs to do.".format(len(cluster_finders))
+       
+        # This holds a multiprocessing pool for parallelization
+        pool = multiprocessing.Pool()
+       
+        # This holds all the best p values in the same order
+        best_p_values = pool.map(run_functor, cluster_finders)
+        
+        # Close down the pool so multiprocessing won't die sillily at the end
+        pool.close()
+        pool.join()
+        
+        # This holds a dict from layer name to priority (best p value)
+        # We hope the order of the dict items has not changed
+        layer_priorities = {layer_name: best_p_value for layer_name, 
+            best_p_value in itertools.izip(layer_names, best_p_values)}
+    else:
+        # We aren't doing any stats.
+        
+        print "Skipping statistics."
+        
+        # Make up priorities.
+        layer_priorities = {name: float("+inf") for name in layer_names}
+    
+    # Count how many layer entries are greater than 0 for each binary layer, and
+    # store that number in this dict by layer name. Things with the default
+    # empty string instead of a number aren't binary layers, but they can use
+    # the empty string as their TSV field value, so we can safely pull any layer
+    # out of this by name.
+    layer_positives = collections.defaultdict(str)
+    
+    for layer_name in layer_names:
+        # Assume it's a binary layer until proven otherwise
+        layer_positives[layer_name] = 0
+        for value in layers[layer_name].itervalues():
+            if value == 1:
+                # Count up all the 1s in the layer
+                layer_positives[layer_name] += 1
+            elif value != 0:
+                # It has something that isn't 1 or 0, so it can't be a binary
+                # layer. Throw it out and try the next layer.
+                layer_positives[layer_name] = ""
+                break
+    
+    # Write an index of all the layers we have, in the form:
+    # <layer>\t<file>\t<priority>\t<number of signatures with data>\t<number of 
+    # signatures that are 1 for binary layers, or empty>
+    # This is the writer to use.
+    index_writer = tsv.TsvWriter(open(os.path.join(options.directory, 
+        "layers.tab"), "w"))
+        
+    for layer_name, layer_file in layer_files.iteritems():
+        # Write the index entry for this layer
+        index_writer.line(layer_name, os.path.basename(layer_file), 
+            layer_priorities[layer_name], len(layers[layer_name]), 
+            layer_positives[layer_name])
+        
+    index_writer.close()
+    
+    # Copy over the user-specified colormaps file, or make an empty TSV if it's
+    # not specified.
+    
+    
+    
+    # This holds a writer for the sim file. Creating it creates the file.
+    colormaps_writer = tsv.TsvWriter(open(os.path.join(options.directory, 
+        "colormaps.tab"), "w"))
+    
+    if options.colormaps is not None:
+        # The user specified colormap data, so copy it over
+        # This holds a reader for the colormaps file
+        colormaps_reader = tsv.TsvReader(options.colormaps)
+        
+        print "Regularizing colormaps file..."
+        sys.stdout.flush()
+        
+        for parts in colormaps_reader:
+            colormaps_writer.list_line(parts)
+        
+        colormaps_reader.close()
+    
+    # Close the colormaps file we wrote. It may have gotten data, or it may 
+    # still be empty.
+    colormaps_writer.close()
+    
+    # Now copy any static files from where they live next to this Python file 
+    # into the web page bundle.
+    # This holds the directory where this script lives, which also contains 
+    # static files.
+    tool_root = os.path.dirname(os.path.realpath(__file__))
+    
+    # Copy over all the static files we need for the web page
+    # This holds a list of them
+    static_files = [
+        # Static images
+        "drag.svg",
+        "filter.svg",
+        "statistics.svg",
+        "right.svg",
+        "throbber.svg",
+        
+        # jQuery itself is pulled from a CDN.
+        # We can't take everything offline since Google Maps needs to be sourced
+        # from Google, so we might as well use CDN jQuery.
+        
+        # Select2 scripts and resources:
+        "select2.css",
+        "select2.js",
+        "select2.png",
+        "select2-spinner.gif",
+        "select2x2.png",
+        
+        # The jQuery.tsv plugin
+        "jquery.tsv.js",
+        # The color library
+        "color-0.4.1.js",
+        # The jStat statistics library
+        "jstat-1.0.0.js",
+        # The Google Maps MapLabel library
+        "maplabel-compiled.js",
+        # The main CSS file
+        "hexagram.css",
+        # The main JavaScript file that runs the page
+        "hexagram.js",
+        # Web Worker for statistics
+        "statistics.js",
+        # File with all the tool code
+        "tools.js"
+    ]
+    
+    # We'd just use a directory of static files, but Galaxy needs single-level
+    # output.
+    for filename in static_files:
+        shutil.copy2(os.path.join(tool_root, filename), options.directory)
+    
+    # Copy the HTML file to our output file. It automatically knows to read
+    # assignments.tab, and does its own TSV parsing
+    shutil.copy2(os.path.join(tool_root, "hexagram.html"), options.html)
+    
+    # Delete our temporary directory.
+    shutil.rmtree(drl_directory)
+    
+    print "Visualization generation complete!"
+        
+    return 0
+
+if __name__ == "__main__" :
+    try:
+        # Get the return code to return
+        # Don't just exit with it because sys.exit works by exceptions.
+        return_code = main(sys.argv)
+    except:
+        traceback.print_exc()
+        # Return a definite number and not some unspecified error code.
+        return_code = 1
+        
+    sys.exit(return_code)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/hexagram.xml	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,55 @@
+<tool id="hexagram" name="Hexagram Visualization" version="0.1">
+    <description>Interactive hex grid clustering visualization</description>
+    <requirements>
+        <requirement type="package" version="1.1">drl-graph-layout</requirement>
+    </requirements> 
+    <!-- 
+        This is the command to run as a Cheetah template.
+        We do fancy iteration over multiple score matrices (see
+        ../plotting/xy_plot.xml).
+    -->
+    <command interpreter="python">hexagram.py "$similarity"
+        #for $i, $s in enumerate( $scores )
+            --scores "${s.score_matrix.file_name}"
+        #end for
+        #if $query:
+            --query "$query"
+        #end if
+        #if $colormaps
+            --colormaps "$colormaps"
+        #end if
+        --html "$output"
+        --directory "$output.files_path"
+        #if $nostats
+            --no-stats
+        #end if
+    </command>
+    <inputs>
+        <param name="similarity" type="data" format="tabular" 
+            label="Similarity matrix of signatures to visualize"/>
+        <repeat name="scores" title="Scores">
+            <param name="score_matrix" type="data" format="tabular" 
+                label="Score matrix for signatures to visualize"/>
+        </repeat>       
+        <param name="colormaps" type="data" format="text" optional="true" 
+            label="Colormap configuration file"/>
+        <param name="query" type="text" 
+            label="Name of query signature"
+            help="A signature name, or empty for no query."/>
+        <param name="nostats" type="boolean" 
+            label="Skip calculation of heatmap clumpiness statistics"/>
+    </inputs>
+    <outputs>
+        <data name="output" label="Hexagram Visualization of $similarity.name" 
+            format="html" hidden="false"/>
+    </outputs>
+    <stdio>
+        <!-- 
+            The tool catches all errors and returns 1, or 0 if no errors 
+            happened. 
+        -->
+        <exit_code range="1" level="fatal" 
+            description="Error in visualization generator (see below)" />
+        <exit_code range="2:" level="fatal" description="Unhandleable error" />
+    </stdio>
+</tool>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/jquery.tsv.js	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,357 @@
+/**
+ *  jQuery-tsv (jQuery Plugin)
+ *
+ *  Inspired by jQuery-csv by Evan Plaice.
+ *
+ *  Copyright 2012 by Bob Kerns
+ *
+ *  This software is licensed as free software under the terms of the MIT License:
+ *  http://www.opensource.org/licenses/mit-license.php
+ */
+
+(function ($) {
+    // Make sure we have a copy, not original, of $.tsv.options.
+    function copyOptions(options) {
+        return $.extend({__copy: true}, options);
+    }
+    // Default the options.
+    function tsvOptions(options) {
+        if (options) {
+            if (options.__defaults_applied) {
+                return options;
+            }
+            return $.extend(copyOptions($.tsv.options), options);
+        }
+        return copyOptions($.tsv.options);
+    }
+
+    function tsvColumn(options, index) {
+        var opts = tsvOptions(options);
+        return String(opts.columns ? opts.columns[index] : index);
+    }
+
+    function tsvColumns(options, top) {
+        if (options.columns) {
+            return options.columns;
+        } else {
+            var cols = Object.keys(top || {}).sort();
+            options.columns = cols;
+            return cols;
+        }
+    }
+
+    $.tsv = {
+	version: "0.957",
+            /**
+             * The default set of options. It is not recommended to change these, as the impact will be global
+             */
+        options: {
+            /**
+             * If supplied, a function to format a value on output.
+             * The returned value is used in the output instead of the supplied value.
+             * If not supplied, it is simply converted to a string.
+             *
+             * @param value the value to be formatted.
+             * @param the options
+             * @param colnum the column number
+             * @param colname the column name, if known, or the column number as a string.
+             * @param rownum the row number
+             * @returns the value, formatted
+             */
+            formatValue: null,
+            /**
+             *  If supplied, a function to parse or canonicalize a value on input.
+             * The returned value is used in place of the input.
+             *
+             * @param value the value to be formatted.
+             * @param the options
+             * @param colnum the column number
+             * @param colname the column name, if known, or the column number as a string.
+             * @param rownum the row number
+             * @returns the value, parsed
+             */
+            parseValue: null,
+            /**
+             *  The character sequence to use to separate lines.
+             */
+            lineSeparator: "\n",
+            /** A RegExp to recognize line separators */
+            lineSplitter: /\r?\n/,
+            /** The character sequence to use to separate values. */
+            valueSeparator: "\t",
+            /** A RegExp to recognize value separators. */
+            valueSplitter: /\t/,
+            /**
+             * If supplied, a function of one argument to convert a row to an object.
+             *
+             * @param row an array of values, e.g. ["1", "2", "3.14"]
+             * @param options { columns: ["id", "count", "price"] }
+             * @returns e.g. {id: "1", count: "2", price: "3.14"}
+             */
+            arrayToObject: null,
+            /**
+             * If supplied, a function of one argument to convert an object to a row. Typically, this will implement a variant
+             * of the contract for $.tsv.objectToArray.
+             *
+             * @param object an object to be converted to a row, e.g. {id: "1", count: "2", price: "3.14"}
+             * @param options { columns: ["id", "count", "price"] }
+             * @returns an array of values, e.g. ["1", "2", "3.14"]. Typically these would be ordered by options.column
+             */
+            objectToArray: null,
+            /**
+             * If true, when converting from an array of objects to a TSV string, include the column names as the
+             * first line. For most purposes, you won't want to override this, but if you're working with tables in sections,
+             * for example, you'd want to suppress this for the latter segments.
+             *
+             * But you are strongly encouraged to use column names whenever possible, especially if you work with objects.
+             */
+            includeHeader: true,
+            /**
+             * The starting row number, not counting the header, if any (which is always numbered -1).
+             * This can be useful for computing subranges of a table, or appending to a table.
+             */
+            startRownum: 0,
+            // An internal flag, to avoid multiple defaulting steps.
+            // values are true, if it is this default, or 'copy'.
+            ___defaults_applied: true,
+            extend: $.extend
+        },
+
+        /**
+         * Parse one value. This can be overridden in the options.
+         * @param value the string to parse
+         * @param options optional: { parseValue: <substitute function> }
+         * @param colnum the column number
+         * @param colname the column name, if known, or the column number as a string.
+         * @param rownum the row number
+         * @returns the string
+         */
+        parseValue: function parseValue(value, options, colnum, colname, rownum) {
+            var opts = tsvOptions(options);
+            if (opts.parseValue) {
+                // We have an override; use that instead.
+                return options.parseValue(value, opts, colnum, colname, rownum);
+            }
+            return value;
+        },
+
+        /**
+         * Format one value. This can be overridden in the options.
+         * @param value the value to format
+         * @param options optional: { formatValue: <substitute function> }
+         * @param colnum the column number
+         * @param colname the column name, if known, or the column number as a string.
+         * @param rownum the row number
+         */
+        formatValue: function formatValue(value, options, rownum, colnum, colname, rownum) {
+            var opts = tsvOptions(options);
+            if (opts.formatValue) {
+                // We have an override; use that instead.
+                return options.formatValue(value, opts, colnum, colname, rownum);
+            }
+            return String(value);
+        },
+
+        /**
+         * $.tsv.toArray(line, options) parses one line of TSV input into an array of values.
+         * @param line A line with values separated by single tab characters, e.g. "11\t12\t13"
+         * @param options optional: { valueSplitter: /\t/, parseValue: <a function to parse each value>}
+         * @param rownum optional: the row number (defaults to 0);
+         * @returns an array of values, e.g. ["11" "12", "13"]
+         */
+        toArray: function toArray(line, options, rownum) {
+            var opts = tsvOptions(options);
+            var valueSplitter = opts.valueSplitter;
+            rownum = rownum || 0;
+            var colnum = 0;
+            function doValue(val) {
+                var c = colnum++;
+                return $.tsv.parseValue(val, opts, c, tsvColumn(opts, c), rownum);
+            }
+            return line.split(valueSplitter).map(doValue);
+        },
+
+        /**
+        * $.tsv.fromArray(row, options) returns one line of TSV input from an array of values.
+        * @param array an array of values, e.g. ["11" "12", "13"]
+        * @param options optional: { valueSeparator: "\t", formatValue: <a function to format each value>}
+        * @param rownum optional: the row number (defaults to 0);
+        * @returns A line with values separated by single tab characters, e.g. "11\t12\t13"
+        */
+        fromArray: function fromArray(array, options, rownum) {
+            var opts = tsvOptions(options);
+            var valueSeparator = opts.valueSeparator;
+            var colnum = 0;
+            function doValue(val) {
+                var c = colnum++;
+                return $.tsv.formatValue(val, opts, c, tsvColumn(c), rownum);
+            }
+            return array.map(doValue).join(valueSeparator);
+        },
+
+        /**
+         * $.tsv.toArrays(tsv, options) returns an array of arrays, one per line, each containing values from one row.
+         * @param tsv a tab-separated-values input, e.g. "11\t\12\t13\n21\t22\t23"
+         * @param options optional: { valueSplitter: /\t/, lineSplitter: /\r?\n/, parseValue: <a function to parse each value> }
+         * @returns an array of arrays, e.g. [["11", "12", "13"], ["21", "22", "23"]]
+         */
+        toArrays: function toArrays(tsv, options) {
+            var opts = tsvOptions(options);
+            var lines = tsv.split(opts.lineSplitter);
+            var rownum = opts.startRownum || 0;
+            return lines.map(function doLine(line) {
+                return $.tsv.toArray(line, opts, rownum++);
+            });
+        },
+
+        /**
+         * $.tsv.fromArrays(array, options) returns a TSV string representing the array of row arrays.
+         * @param array an array of arrays of values. To produce valid TSV, all the arrays should be of the same length.
+         * @param options optional: { valueSeparator: "\t", lineSeparator: "\n", columns: ["c1", "c2", "c3"], formatValue: <a function to format each value> }
+         * @returns An tsv string, e.g. "c1\tc2\tc3\n11\t\12\t13\n21\t22\t23"
+         */
+        fromArrays: function fromArrays(array, options) {
+            var opts = tsvOptions(options);
+            var first = array.length ? array[0] : [];
+            var cols = tsvColumns(opts, first);
+            var rownum = opts.startRownum || 0;
+            var header = opts.includeHeader ? $.tsv.fromArray(cols, opts, -1) : undefined;
+            function doRow(row) {
+                return $.tsv.fromArray(row, opts, rownum++);
+            }
+            var rtemp = array.map(doRow);
+            if (header) {
+                rtemp.unshift(header);
+            }
+            return rtemp.join(opts.lineSeparator);
+        },
+
+        /**
+         * $.tsv.arrayToObject(row, options) returns an object whose fields are named in options.columns, and
+         * whose values come from the corresponding position in row (an array of values in the same order).
+         *
+         * If the columns are not supplied, "0", "1", etc. will be used.
+         * @param row the values, e.g. ["v1", "v2"]
+         * @param options optional: { columns: ["name1", "name2"], rowToObject: <optional conversion function to call instead> }
+         * @param rownum optional: the row number
+         * @returns an object derived from the elements of the row.
+         */
+        arrayToObject: function arrayToObject(row, options, rownum) {
+            var opts = tsvOptions(options);
+            rownum = rownum || 0;
+            var columns = tsvColumns(opts, row);
+            if (opts.arrayToObject) {
+                // We have an override; use that instead.
+                return opts.arrayToObject(row, opts, rownum);
+            }
+            var dict = {};
+            for (var j = 0; j < columns.length; j++) {
+                dict[columns[j]] = row[j];
+            }
+            return dict;
+        },
+
+        /**
+         * $.tsv.arraysToObjects(array, options) returns an array of objects, derived from the array.
+         * The array must either have the first row be column names, or columns: ["name1", "name2", ...] must be supplied
+         * in the options.
+         * @param array an array of arrays of values. [ ["name1", "name2" ...],? ["val1", "val2" ...] ...]
+         * @param options optional: { columns: ["name1", "name2", ...] }
+         * @returns An array of objects, [ { name1: val1, name2: val2 ... } ... ]
+         */
+        arraysToObjects: function arraysToObjects(array, options) {
+            var opts = tsvOptions(options);
+            if (! opts.columns) {
+                opts.columns = array.shift();
+            }
+            var rownum = opts.startRownum || 0;
+            return array.map(function convert(row) {
+                return $.tsv.arrayToObject(row, opts, rownum++);
+            });
+        },
+
+        /**
+         * $.tsv.toObjects(tsv, options) returns an array of objects from a tsv string.
+         * The string must either have the first row be column names, or columns: ["name1", "name2", ...] must be supplied
+         * in the options.
+         *
+         * @param A TSV string, e.g. "val1\tval2..." or "name1\tname2...\n\val1\val2..."
+         * @param options optional: { columns ["name1", "name2" ...] }
+         * @returns an array of objects, e.g. [ {name1: val1, name2: val2 ...} ...]
+         */
+        toObjects: function toObjects(tsv, options) {
+            var opts = tsvOptions(options);
+            return $.tsv.arraysToObjects($.tsv.toArrays(tsv, opts), opts);
+        },
+
+        /**
+         * $.tsv.objectToArray(obj, options) Convert one object to an array representation for storing as a TSV line.
+         *
+         * @param obj an object to convert to an array representations, e.g. { name1: "val1", name2: "val2" ... }
+         * @param options optional: { columns: ["name1", "name2"], objectToArray: <a function to use instead> }
+         * @param rownum optional: the row number
+         * @result an array, e.g. ["val1", "val2"]
+         */
+        objectToArray: function objectToArray(obj, options, rownum) {
+            var opts = tsvOptions(options);
+            var columns = tsvColumns(opts, obj);
+            rownum = rownum || 0;
+            if (opts.objectToArray) {
+                // We have an override; use that instead.
+                return opts.objectToArray(obj, opts, rownum);
+            }
+            var row = [];
+            for (var j = 0; j < columns.length; j++) {
+                row.push(obj[columns[j]]);
+            }
+            return row;
+        },
+
+        /**
+         * $.tsv.objectsToArrays(array, options) converts an array of objects into an array of row arrays.
+         *
+         * @param array An array of objects, e.g. [ { name1: "val1", name2: "val2", ...} ...]
+         * @param options { columns: ["name1", "name2"...], includeHeaders: true, objectToArray: <optional function to convert each object> }
+         */
+        objectsToArrays: function objectsToArrays(array, options) {
+            var opts = tsvOptions(options);
+            var rownum = options.startRownum;
+            var result = array.map(function convert(obj) {
+                return $.tsv.objectToArray(obj, opts, rownum++);
+            });
+            return result;
+        },
+
+        fromObject: function fromObject(array, options) {
+            var opts = tsvOptions(options);
+            return $.tsv.fromArray($.tsv.objectToArray(array, opts), opts);
+        },
+
+        /**
+         * $.tsv.fromObjects(array, options) converts an array of objects into a tsv string.
+         *
+         * @param array An array of objects, e.g. [ { name1: "val1", name2: "val2", ...} ...]
+         * @param options { columns: ["name1", "name2"...], includeHeaders: true, objectToArray: <optional function to convert each object> }
+         */
+        fromObjects: function fromObjects(array, options) {
+            var opts = tsvOptions(options);
+            var first = array.length ? array[0] : {};
+            // Calculate the columns while we still have the original objects.  This is being called for side-effect!
+            tsvColumns(opts, first);
+            return $.tsv.fromArrays($.tsv.objectsToArrays(array, opts), opts);
+        },
+
+        extend: $.extend
+    };
+    // Compatibility with initial release.
+    $.tsv.parseRow = $.tsv.toArray;
+    $.tsv.parseRows = $.tsv.toArrays;
+    $.tsv.parseObject = $.tsv.toObject;
+    $.tsv.parseObjects = $.tsv.toObjects;
+    $.tsv.formatValue = $.tsv.formatValue;
+    $.tsv.formatRow = $.tsv.fromArray;
+    $.tsv.formatRows = $.tsv.fromArrays;
+    $.tsv.formatObject = $.tsv.fromObject;
+    $.tsv.formatObjects = $.tsv.fromObjects;
+
+})(jQuery);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/jstat-1.0.0.js	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,2576 @@
+function jstat(){}
+j = jstat;
+/* Simple JavaScript Inheritance
+ * By John Resig http://ejohn.org/
+ * MIT Licensed.
+ */
+// Inspired by base2 and Prototype
+(function(){
+    var initializing = false, fnTest = /xyz/.test(function(){
+        xyz;
+    }) ? /\b_super\b/ : /.*/;
+    // The base Class implementation (does nothing)
+    this.Class = function(){};
+
+    // Create a new Class that inherits from this class
+    Class.extend = function(prop) {
+        var _super = this.prototype;
+
+        // Instantiate a base class (but only create the instance,
+        // don't run the init constructor)
+        initializing = true;
+        var prototype = new this();
+        initializing = false;
+
+        // Copy the properties over onto the new prototype
+        for (var name in prop) {
+            // Check if we're overwriting an existing function
+            prototype[name] = typeof prop[name] == "function" &&
+            typeof _super[name] == "function" && fnTest.test(prop[name]) ?
+            (function(name, fn){
+                return function() {
+                    var tmp = this._super;
+
+                    // Add a new ._super() method that is the same method
+                    // but on the super-class
+                    this._super = _super[name];
+
+                    // The method only need to be bound temporarily, so we
+                    // remove it when we're done executing
+                    var ret = fn.apply(this, arguments);
+                    this._super = tmp;
+
+                    return ret;
+                };
+            })(name, prop[name]) :
+            prop[name];
+        }
+
+        // The dummy class constructor
+        function Class() {
+            // All construction is actually done in the init method
+            if ( !initializing && this.init )
+                this.init.apply(this, arguments);
+        }
+
+        // Populate our constructed prototype object
+        Class.prototype = prototype;
+
+        // Enforce the constructor to be what we expect
+        Class.constructor = Class;
+
+        // And make this class extendable
+        Class.extend = arguments.callee;
+
+        return Class;
+    };
+})();
+
+/******************************************************************************/
+/*                           Constants                                        */
+/******************************************************************************/
+jstat.ONE_SQRT_2PI  =   0.3989422804014327;
+jstat.LN_SQRT_2PI   =   0.9189385332046727417803297;
+jstat.LN_SQRT_PId2  =   0.225791352644727432363097614947;
+jstat.DBL_MIN       =   2.22507e-308;
+jstat.DBL_EPSILON   =   2.220446049250313e-16;
+jstat.SQRT_32       =   5.656854249492380195206754896838;
+jstat.TWO_PI        =   6.283185307179586;
+jstat.DBL_MIN_EXP   =   -999;
+jstat.SQRT_2dPI     =   0.79788456080287;
+jstat.LN_SQRT_PI    =   0.5723649429247;
+/******************************************************************************/
+/*                          jstat   Functions                                 */
+/******************************************************************************/
+jstat.seq = function(min, max, length) {
+    var r = new Range(min, max, length);
+    return r.getPoints();
+}
+
+jstat.dnorm = function(x, mean, sd, log) {
+    if(mean == null) mean = 0;
+    if(sd == null) sd = 1;
+    if(log == null) log = false;
+    var n = new NormalDistribution(mean, sd);
+    if(!isNaN(x)) {
+        // is a number
+        return n._pdf(x, log);
+    } else if(x.length) {
+        var res = [];
+        for(var i = 0; i < x.length; i++) {
+            res.push(n._pdf(x[i], log));
+        }
+        return res;
+    } else {
+        throw "Illegal argument: x";
+    }
+}
+
+jstat.pnorm = function(q, mean, sd, lower_tail, log) {
+    if(mean == null) mean = 0;
+    if(sd == null) sd = 1;
+    if(lower_tail == null) lower_tail = true;
+    if(log == null) log = false;
+
+    var n = new NormalDistribution(mean, sd);
+    if(!isNaN(q)) {
+        // is a number
+        return n._cdf(q, lower_tail, log);
+    } else if(q.length) {
+        var res = [];
+        for(var i = 0; i < q.length; i++) {
+            res.push(n._cdf(q[i], lower_tail, log));
+        }
+        return res;
+    } else {
+        throw "Illegal argument: x";
+    }
+}
+
+jstat.dlnorm = function(x, meanlog, sdlog, log) {
+    if(meanlog == null) meanlog = 0;
+    if(sdlog == null) sdlog = 1;
+    if(log == null) log = false;
+    var n = new LogNormalDistribution(meanlog, sdlog);
+    if(!isNaN(x)) {
+        // is a number
+        return n._pdf(x, log);
+    } else if(x.length) {
+        var res = [];
+        for(var i = 0; i < x.length; i++) {
+            res.push(n._pdf(x[i], log));
+        }
+        return res;
+    } else {
+        throw "Illegal argument: x";
+    }
+}
+
+jstat.plnorm = function(q, meanlog, sdlog, lower_tail, log) {
+    if(meanlog == null) meanlog = 0;
+    if(sdlog == null) sdlog = 1;
+    if(lower_tail == null) lower_tail = true;
+    if(log == null) log = false;
+
+    var n = new LogNormalDistribution(meanlog, sdlog);
+    if(!isNaN(q)) {
+        // is a number
+        return n._cdf(q, lower_tail, log);
+    }
+    else if(q.length) {
+        var res = [];
+        for(var i = 0; i < q.length; i++) {
+            res.push(n._cdf(q[i], lower_tail, log));
+        }
+        return res;
+    } else {
+        throw "Illegal argument: x";
+    }
+}
+
+jstat.dbeta = function(x, alpha, beta, ncp, log) {
+    if(ncp == null) ncp = 0;
+    if(log == null) log = false;
+    var b = new BetaDistribution(alpha, beta);
+    if(!isNaN(x)) {
+        // is a number
+        return b._pdf(x, log);
+    }
+    else if(x.length) {
+        var res = [];
+        for(var i = 0; i < x.length; i++) {
+            res.push(b._pdf(x[i], log));
+        }
+        return res;
+    } else {
+        throw "Illegal argument: x";
+    }
+}
+
+jstat.pbeta = function(q, alpha, beta, ncp, lower_tail, log) {
+    if(ncp == null) ncp = 0;
+    if(log == null) log = false;
+    if(lower_tail == null) lower_tail = true;
+
+    var b = new BetaDistribution(alpha, beta);
+    if(!isNaN(q)) {
+        // is a number
+        return b._cdf(q, lower_tail, log);
+    } else if(q.length) {
+        var res = [];
+        for(var i = 0; i < q.length; i++) {
+            res.push(b._cdf(q[i], lower_tail, log));
+        }
+        return res;
+    }
+    else {
+        throw "Illegal argument: x";
+    }
+}
+
+jstat.dgamma = function(x, shape, rate, scale, log) {
+    if(rate == null) rate = 1;
+    if(scale == null) scale = 1/rate;
+    if(log == null) log = false;
+
+    var g = new GammaDistribution(shape, scale);
+    if(!isNaN(x)) {
+        // is a number
+        return g._pdf(x, log);
+    } else if(x.length) {
+        var res = [];
+        for(var i = 0; i < x.length; i++) {
+            res.push(g._pdf(x[i], log));
+        }
+        return res;
+    } else {
+        throw "Illegal argument: x";
+    }
+}
+
+jstat.pgamma = function(q, shape, rate, scale, lower_tail, log) {
+    if(rate == null) rate = 1;
+    if(scale == null) scale = 1/rate;
+    if(lower_tail == null) lower_tail = true;
+    if(log == null) log = false;
+
+    var g = new GammaDistribution(shape, scale);
+    if(!isNaN(q)) {
+        // is a number
+        return g._cdf(q, lower_tail, log);
+    } else if(q.length) {
+        var res = [];
+        for(var i = 0; i < q.length; i++) {
+            res.push(g._cdf(q[i], lower_tail, log));
+        }
+        return res;
+    } else {
+        throw "Illegal argument: x";
+    }
+
+}
+
+jstat.dt = function(x, df, ncp, log) {
+    if(log == null) log = false;
+
+    var t = new StudentTDistribution(df, ncp);
+    if(!isNaN(x)) {
+        // is a number
+        return t._pdf(x, log);
+    } else if(x.length) {
+        var res = [];
+        for(var i = 0; i < x.length; i++) {
+            res.push(t._pdf(x[i], log));
+        }
+        return res;
+    } else {
+        throw "Illegal argument: x";
+    }
+
+}
+
+jstat.pt = function(q, df, ncp, lower_tail, log) {
+    if(lower_tail == null) lower_tail = true;
+    if(log == null) log = false;
+
+    var t = new StudentTDistribution(df, ncp);
+    if(!isNaN(q)) {
+        // is a number
+        return t._cdf(q, lower_tail, log);
+    } else if(q.length) {
+        var res = [];
+        for(var i = 0; i < q.length; i++) {
+            res.push(t._cdf(q[i], lower_tail, log));
+        }
+        return res;
+    } else {
+        throw "Illegal argument: x";
+    }
+
+}
+
+jstat.plot = function(x, y, options) {
+    if(x == null) {
+        throw "x is undefined in jstat.plot";
+    }
+    if(y == null) {
+        throw "y is undefined in jstat.plot";
+    }
+    if(x.length != y.length) {
+        throw "x and y lengths differ in jstat.plot";
+    }
+
+    var flotOpt = {
+        series: {
+            lines: {
+
+            },
+            points: {
+
+        }
+        }
+    };
+
+    // combine x & y
+    var series = [];
+    if(x.length == undefined) {
+        // single point
+        series.push([x, y]);
+        flotOpt.series.points.show = true;
+    } else {
+        // array
+        for(var i = 0; i < x.length; i++) {
+            series.push([x[i], y[i]]);
+        }
+    }
+
+    var title = 'jstat graph';
+
+    // configure Flot options
+    if(options != null) {
+        // options = JSON.parse(String(options));
+        if(options.type != null) {
+            if(options.type == 'l') {
+                flotOpt.series.lines.show =  true;
+            } else if (options.type == 'p') {
+                flotOpt.series.lines.show = false;
+                flotOpt.series.points.show = true;
+            }
+        }
+        if(options.hover != null) {
+            flotOpt.grid = {
+                hoverable: options.hover
+            }
+        }
+
+        if(options.main != null) {
+            title = options.main;
+        }
+    }
+    var now = new Date();
+    var hash = now.getMilliseconds() * now.getMinutes() + now.getSeconds();
+    $('body').append('<div title="' + title + '" style="display: none;" id="'+ hash +'"><div id="graph-' + hash + '" style="width:95%; height: 95%"></div></div>');
+
+    $('#' + hash).dialog({
+        modal: false,
+        width: 475,
+        height: 475,
+        resizable: true,
+        resize: function() {
+            $.plot($('#graph-' + hash), [series], flotOpt);
+        },
+        open: function(event, ui) {
+            var id = '#graph-' + hash;
+            $.plot($('#graph-' + hash), [series], flotOpt);
+        }
+    })
+}
+
+/******************************************************************************/
+/*                          Special Functions                                 */
+/******************************************************************************/
+
+jstat.log10 = function(arg) {
+    return Math.log(arg) / Math.LN10;
+}
+
+/*
+ *
+ */
+jstat.toSigFig = function(num, n) {
+    if(num == 0) {
+        return 0;
+    }
+    var d = Math.ceil(jstat.log10(num < 0 ? -num: num));
+    var power = n - parseInt(d);
+    var magnitude = Math.pow(10,power);
+    var shifted = Math.round(num*magnitude);
+    return shifted/magnitude;
+}
+
+jstat.trunc = function(x) {
+    return (x > 0) ? Math.floor(x) : Math.ceil(x);
+}
+
+/**
+ *  Tests whether x is a finite number
+ */
+jstat.isFinite = function(x) {
+    return (!isNaN(x) && (x != Number.POSITIVE_INFINITY) && (x != Number.NEGATIVE_INFINITY));
+}
+
+/**
+ *      dopois_raw() computes the Poisson probability  lb^x exp(-lb) / x!.
+ *      This does not check that x is an integer, since dgamma() may
+ *      call this with a fractional x argument. Any necessary argument
+ *      checks should be done in the calling function.
+ */
+jstat.dopois_raw = function(x, lambda, give_log) {
+    /*       x >= 0 ; integer for dpois(), but not e.g. for pgamma()!
+        lambda >= 0
+     */
+    if (lambda == 0) {
+        if(x == 0) {
+            return(give_log) ? 0.0 : 1.0; //R_D__1
+        }
+        return (give_log) ? Number.NEGATIVE_INFINITY : 0.0; // R_D__0
+    }
+    if (!jstat.isFinite(lambda)) return (give_log) ? Number.NEGATIVE_INFINITY : 0.0; //R_D__0;
+    if (x < 0) return(give_log) ? Number.NEGATIVE_INFINITY : 0.0; //R_D__0
+    if (x <= lambda * jstat.DBL_MIN) {
+        return (give_log) ? -lambda : Math.exp(-lambda);    // R_D_exp(-lambda)
+    }
+    if (lambda < x * jstat.DBL_MIN) {
+        var param = -lambda + x*Math.log(lambda) -jstat.lgamma(x+1);
+        return (give_log) ? param : Math.exp(param);    // R_D_exp(-lambda + x*log(lambda) -lgammafn(x+1))
+    }
+    var param1 = jstat.TWO_PI * x;  // f
+    var param2 = -jstat.stirlerr(x)-jstat.bd0(x,lambda);    // x
+    return (give_log) ? -0.5*Math.log(param1)+param2 : Math.exp(param2)/Math.sqrt(param1);  // R_D_fexp(M_2PI*x, -stirlerr(x)-bd0(x,lambda))
+//return(R_D_fexp( , -stirlerr(x)-bd0(x,lambda) ));
+}
+
+/**	Evaluates the "deviance part"
+ *	bd0(x,M) :=  M * D0(x/M) = M*[ x/M * log(x/M) + 1 - (x/M) ] =
+ *		  =  x * log(x/M) + M - x
+ *	where M = E[X] = n*p (or = lambda), for	  x, M > 0
+ *
+ *	in a manner that should be stable (with small relative error)
+ *	for all x and M=np. In particular for x/np close to 1, direct
+ *	evaluation fails, and evaluation is based on the Taylor series
+ *	of log((1+v)/(1-v)) with v = (x-np)/(x+np).
+ */
+jstat.bd0 = function(x, np) {
+    var ej, s, s1, v, j;
+    if(!jstat.isFinite(x) || !jstat.isFinite(np) || np == 0.0) throw "illegal parameter in jstat.bd0";
+
+    if(Math.abs(x-np) > 0.1*(x+np)) {
+        v = (x-np)/(x+np);
+        s = (x-np)*v;/* s using v -- change by MM */
+        ej = 2*x*v;
+        v = v*v;
+        for (j=1; ; j++) { /* Taylor series */
+            ej *= v;
+            s1 = s+ej/((j<<1)+1);
+            if (s1==s) /* last term was effectively 0 */
+                return(s1);
+            s = s1;
+        }
+    }
+    /* else:  | x - np |  is not too small */
+    return(x*Math.log(x/np)+np-x);
+}
+
+/**    Computes the log of the error term in Stirling's formula.
+ *      For n > 15, uses the series 1/12n - 1/360n^3 + ...
+ *      For n <=15, integers or half-integers, uses stored values.
+ *      For other n < 15, uses lgamma directly (don't use this to
+ *        write lgamma!)
+ */
+jstat.stirlerr= function(n) {
+    var S0 = 0.083333333333333333333;
+    var S1 = 0.00277777777777777777778;
+    var S2 = 0.00079365079365079365079365;
+    var S3 = 0.000595238095238095238095238;
+    var S4 = 0.0008417508417508417508417508;
+
+    var sferr_halves = [
+    0.0, /* n=0 - wrong, place holder only */
+    0.1534264097200273452913848,  /* 0.5 */
+    0.0810614667953272582196702,  /* 1.0 */
+    0.0548141210519176538961390,  /* 1.5 */
+    0.0413406959554092940938221,  /* 2.0 */
+    0.03316287351993628748511048, /* 2.5 */
+    0.02767792568499833914878929, /* 3.0 */
+    0.02374616365629749597132920, /* 3.5 */
+    0.02079067210376509311152277, /* 4.0 */
+    0.01848845053267318523077934, /* 4.5 */
+    0.01664469118982119216319487, /* 5.0 */
+    0.01513497322191737887351255, /* 5.5 */
+    0.01387612882307074799874573, /* 6.0 */
+    0.01281046524292022692424986, /* 6.5 */
+    0.01189670994589177009505572, /* 7.0 */
+    0.01110455975820691732662991, /* 7.5 */
+    0.010411265261972096497478567, /* 8.0 */
+    0.009799416126158803298389475, /* 8.5 */
+    0.009255462182712732917728637, /* 9.0 */
+    0.008768700134139385462952823, /* 9.5 */
+    0.008330563433362871256469318, /* 10.0 */
+    0.007934114564314020547248100, /* 10.5 */
+    0.007573675487951840794972024, /* 11.0 */
+    0.007244554301320383179543912, /* 11.5 */
+    0.006942840107209529865664152, /* 12.0 */
+    0.006665247032707682442354394, /* 12.5 */
+    0.006408994188004207068439631, /* 13.0 */
+    0.006171712263039457647532867, /* 13.5 */
+    0.005951370112758847735624416, /* 14.0 */
+    0.005746216513010115682023589, /* 14.5 */
+    0.005554733551962801371038690  /* 15.0 */
+    ];
+
+    var nn;
+
+    if (n <= 15.0) {
+        nn = n + n;
+        if (nn == parseInt(nn)) return(sferr_halves[parseInt(nn)]);
+        return(jstat.lgamma(n + 1.0) - (n + 0.5)*Math.log(n) + n - jstat.LN_SQRT_2PI);
+    }
+
+    nn = n*n;
+    if (n>500) return((S0-S1/nn)/n);
+    if (n> 80) return((S0-(S1-S2/nn)/nn)/n);
+    if (n> 35) return((S0-(S1-(S2-S3/nn)/nn)/nn)/n);
+    /* 15 < n <= 35 : */
+    return((S0-(S1-(S2-(S3-S4/nn)/nn)/nn)/nn)/n);
+}
+
+
+
+/**    The function lgamma computes log|gamma(x)|.  The function
+ *    lgammafn_sign in addition assigns the sign of the gamma function
+ *    to the address in the second argument if this is not null.
+ */
+jstat.lgamma = function(x) {
+    function lgammafn_sign(x, sgn) {
+        var ans, y, sinpiy;
+        var xmax = 2.5327372760800758e+305;
+        var dxrel = 1.490116119384765696e-8;
+
+        // if (xmax == 0) {/* initialize machine dependent constants _ONCE_ */
+        //     xmax = jstat.DBL_MAX/Math.log(jstat.DBL_MAX);/* = 2.533 e305	 for IEEE double */
+        //     dxrel = Math.sqrt(jstat.DBL_EPSILON);/* sqrt(Eps) ~ 1.49 e-8  for IEEE double */
+        // }
+
+        /* For IEEE double precision DBL_EPSILON = 2^-52 = 2.220446049250313e-16 :
+           xmax  = DBL_MAX / log(DBL_MAX) = 2^1024 / (1024 * log(2)) = 2^1014 / log(2)
+           dxrel = sqrt(DBL_EPSILON) = 2^-26 = 5^26 * 1e-26 (is *exact* below !)
+         */
+
+        if (sgn != null) sgn = 1;
+
+        if(isNaN(x)) return x;
+
+        if (x < 0 && (Math.floor(-x) % 2.0) == 0)
+            if (sgn != null) sgn = -1;
+
+        if (x <= 0 && x == jstat.trunc(x)) { /* Negative integer argument */
+            console.warn("Negative integer argument in lgammafn_sign");
+            return Number.POSITIVE_INFINITY;/* +Inf, since lgamma(x) = log|gamma(x)| */
+        }
+
+        y = Math.abs(x);
+
+        if(y <= 10) return Math.log(Math.abs(jstat.gamma(x)));  // TODO: implement jstat.gamma
+
+        if(y > xmax) {
+            console.warn("Illegal arguement passed to lgammafn_sign");
+            return Number.POSITIVE_INFINITY;
+        }
+
+        if(x > 0) {
+            if(x > 1e17) {
+                return (x*(Math.log(x)-1.0));
+            } else if(x > 4934720.0) {
+                return (jstat.LN_SQRT_2PI + (x-0.5) * Math.log(x) - x);
+            } else {
+                return jstat.LN_SQRT_2PI + (x-0.5) * Math.log(x) - x + jstat.lgammacor(x);  // TODO: implement lgammacor
+            }
+        }
+
+        sinpiy = Math.abs(Math.sin(Math.PI * y));
+
+        if(sinpiy == 0) {
+            throw "Should never happen!!";
+        }
+
+        ans = jstat.LN_SQRT_PId2 + (x - 0.5) * Math.log(y) - x - Math.log(sinpiy) - jstat.lgammacor(y);
+
+        if(Math.abs((x-jstat.trunc(x-0.5))* ans / x) < dxrel) {
+            throw "The answer is less than half the precision argument too close to a negative integer";
+        }
+        return ans;
+    }
+
+    return lgammafn_sign(x, null);
+}
+
+jstat.gamma = function(x) {
+    var xbig = 171.624;
+    var p = [
+    -1.71618513886549492533811,
+    24.7656508055759199108314,-379.804256470945635097577,
+    629.331155312818442661052,866.966202790413211295064,
+    -31451.2729688483675254357,-36144.4134186911729807069,
+    66456.1438202405440627855
+    ];
+    var q = [
+    -30.8402300119738975254353,
+    315.350626979604161529144,-1015.15636749021914166146,
+    -3107.77167157231109440444,22538.1184209801510330112,
+    4755.84627752788110767815,-134659.959864969306392456,
+    -115132.259675553483497211
+    ];
+    var c = [
+    -.001910444077728,8.4171387781295e-4,
+    -5.952379913043012e-4,7.93650793500350248e-4,
+    -.002777777777777681622553,.08333333333333333331554247,
+    .0057083835261
+    ];
+
+    var i,n,parity,fact,xden,xnum,y,z,yi,res,sum,ysq;
+
+    parity = (0);
+    fact = 1.0;
+    n = 0;
+    y=x;
+    if(y <= 0.0) {
+        /* -------------------------------------------------------------
+	   Argument is negative
+	   ------------------------------------------------------------- */
+        y = -x;
+        yi = jstat.trunc(y);
+        res = y - yi;
+        if (res != 0.0) {
+            if (yi != jstat.trunc(yi * 0.5) * 2.0)
+                parity = (1);
+            fact = -Math.PI / Math.sin(Math.PI * res);
+            y += 1.0;
+        } else {
+            return(Number.POSITIVE_INFINITY);
+        }
+    }
+    /* -----------------------------------------------------------------
+       Argument is positive
+       -----------------------------------------------------------------*/
+    if (y < jstat.DBL_EPSILON) {
+        /* --------------------------------------------------------------
+	   Argument < EPS
+	   -------------------------------------------------------------- */
+        if (y >= jstat.DBL_MIN) {
+            res = 1.0 / y;
+        } else {
+            return(Number.POSITIVE_INFINITY);
+        }
+    } else if (y < 12.0) {
+        yi = y;
+        if (y < 1.0) {
+            /* ---------------------------------------------------------
+	       EPS < argument < 1
+	       --------------------------------------------------------- */
+            z = y;
+            y += 1.0;
+        } else {
+            /* -----------------------------------------------------------
+	       1 <= argument < 12, reduce argument if necessary
+	       ----------------------------------------------------------- */
+            n = parseInt(y) - 1;
+            y -= parseFloat(n);
+            z = y - 1.0;
+        }
+        /* ---------------------------------------------------------
+	   Evaluate approximation for 1. < argument < 2.
+	   ---------------------------------------------------------*/
+        xnum = 0.0;
+        xden = 1.0;
+        for (i = 0; i < 8; ++i) {
+            xnum = (xnum + p[i]) * z;
+            xden = xden * z + q[i];
+        }
+        res = xnum / xden + 1.0;
+        if (yi < y) {
+            /* --------------------------------------------------------
+	       Adjust result for case  0. < argument < 1.
+	       -------------------------------------------------------- */
+            res /= yi;
+        } else if (yi > y) {
+            /* ----------------------------------------------------------
+	       Adjust result for case  2. < argument < 12.
+	       ---------------------------------------------------------- */
+            for (i = 0; i < n; ++i) {
+                res *= y;
+                y += 1.0;
+            }
+        }
+    } else {
+        /* -------------------------------------------------------------
+	   Evaluate for argument >= 12.,
+	   ------------------------------------------------------------- */
+        if (y <= xbig) {
+            ysq = y * y;
+            sum = c[6];
+            for (i = 0; i < 6; ++i) {
+                sum = sum / ysq + c[i];
+            }
+            sum = sum / y - y + jstat.LN_SQRT_2PI;
+            sum += (y - 0.5) * Math.log(y);
+            res = Math.exp(sum);
+        } else {
+            return(Number.POSITIVE_INFINITY);
+        }
+    }
+    /* ----------------------------------------------------------------------
+       Final adjustments and return
+       ----------------------------------------------------------------------*/
+    if (parity)
+        res = -res;
+    if (fact != 1.0)
+        res = fact / res;
+    return res;
+}
+
+/**    Compute the log gamma correction factor for x >= 10 so that
+ *
+ *    log(gamma(x)) = .5*log(2*pi) + (x-.5)*log(x) -x + lgammacor(x)
+ *
+ *    [ lgammacor(x) is called	Del(x)	in other contexts (e.g. dcdflib)]
+ */
+jstat.lgammacor = function(x) {
+    var algmcs = [
+    +.1666389480451863247205729650822e+0,
+    -.1384948176067563840732986059135e-4,
+    +.9810825646924729426157171547487e-8,
+    -.1809129475572494194263306266719e-10,
+    +.6221098041892605227126015543416e-13,
+    -.3399615005417721944303330599666e-15,
+    +.2683181998482698748957538846666e-17,
+    -.2868042435334643284144622399999e-19,
+    +.3962837061046434803679306666666e-21,
+    -.6831888753985766870111999999999e-23,
+    +.1429227355942498147573333333333e-24,
+    -.3547598158101070547199999999999e-26,
+    +.1025680058010470912000000000000e-27,
+    -.3401102254316748799999999999999e-29,
+    +.1276642195630062933333333333333e-30
+    ];
+
+    var tmp;
+    var nalgm = 5;
+    var xbig = 94906265.62425156;
+    var xmax = 3.745194030963158e306;
+
+    if(x < 10) {
+        return Number.NaN;
+    } else if (x >= xmax) {
+        throw "Underflow error in lgammacor";
+    } else if (x < xbig) {
+        tmp = 10 / x;
+        return jstat.chebyshev(tmp*tmp*2-1,algmcs,nalgm) / x;
+    }
+    return 1 / (x*12);
+}
+
+/*
+ * Incomplete Beta function
+ */
+jstat.incompleteBeta = function(a, b, x) {
+    /*
+     * Used by incompleteBeta: Evaluates continued fraction for incomplete
+     * beta function by modified Lentz's method.
+     */
+    function betacf(a, b, x) {
+        var MAXIT = 100;
+        var EPS = 3.0e-12;
+        var FPMIN = 1.0e-30;
+
+        var m,m2,aa,c,d,del,h,qab,qam,qap;
+
+        qab=a+b;
+        qap=a+1.0;
+        qam=a-1.0;
+        c=1.0;
+        d=1.0-qab*x/qap;
+
+        if(Math.abs(d) < FPMIN) {
+            d=FPMIN;
+        }
+
+        d = 1.0/d;
+        h=d;
+        for(m = 1; m <= MAXIT; m++) {
+            m2=2*m;
+            aa=m*(b-m)*x/((qam+m2)*(a+m2));
+            d=1.0+aa*d;
+            if(Math.abs(d) < FPMIN) {
+                d = FPMIN;
+            }
+            c=1.0+aa/c;
+            if(Math.abs(c) < FPMIN) {
+                c = FPMIN;
+            }
+            d=1.0/d;
+            h *= d*c;
+            aa = -(a+m)*(qab+m)*x/((a+m2) * (qap+m2));
+            d=1.0+aa*d;
+            if(Math.abs(d) < FPMIN) {
+                d = FPMIN;
+            }
+            c=1.0+aa/c;
+            if(Math.abs(c) < FPMIN) {
+                c=FPMIN;
+            }
+            d=1.0/d;
+            del=d*c;
+            h *= del;
+            if(Math.abs(del-1.0) < EPS) {
+                // are we done?
+                break;
+            }
+        }
+        if(m > MAXIT) {
+            console.warn("a or b too big, or MAXIT too small in betacf: " + a + ", " + b + ", " + x + ", " + h);
+            return h;
+        }
+        if(isNaN(h)) {
+            console.warn(a + ", " + b + ", " + x);
+        }
+        return h;
+    }
+
+    var bt;
+
+    if(x < 0.0 || x > 1.0) {
+        throw "bad x in routine incompleteBeta";
+    }
+    if(x == 0.0 || x == 1.0) {
+        bt = 0.0;
+    } else {
+        bt = Math.exp(jstat.lgamma(a+b) - jstat.lgamma(a) - jstat.lgamma(b) + a * Math.log(x)+ b * Math.log(1.0-x));
+    }
+    if(x < (a + 1.0)/(a+b+2.0)) {
+        return bt * betacf(a,b,x)/a;
+    } else {
+        return 1.0-bt*betacf(b,a,1.0-x)/b;
+    }
+}
+
+/**   Evaluates the n-term Chebyshev series
+ *    "a" at "x".
+ */
+jstat.chebyshev = function(x, a, n) {
+    var b0, b1, b2, twox;
+    var i;
+
+    if (n < 1 || n > 1000) return Number.NaN;
+
+    if (x < -1.1 || x > 1.1) return Number.NaN;
+
+    twox = x * 2;
+    b2 = b1 = 0;
+    b0 = 0;
+    for (i = 1; i <= n; i++) {
+        b2 = b1;
+        b1 = b0;
+        b0 = twox * b1 - b2 + a[n - i];
+    }
+    return (b0 - b2) * 0.5;
+}
+
+jstat.fmin2 = function(x, y) {
+    return (x < y) ? x : y;
+}
+
+jstat.log1p = function(x) {
+    // http://kevin.vanzonneveld.net
+    // +   original by: Brett Zamir (http://brett-zamir.me)
+    // %          note 1: Precision 'n' can be adjusted as desired
+    // *     example 1: log1p(1e-15);
+    // *     returns 1: 9.999999999999995e-16
+
+    var ret = 0,
+    n = 50; // degree of precision
+    if (x <= -1) {
+        return Number.NEGATIVE_INFINITY; // JavaScript style would be to return Number.NEGATIVE_INFINITY
+    }
+    if (x < 0 || x > 1) {
+        return Math.log(1 + x);
+    }
+    for (var i = 1; i < n; i++) {
+        if ((i % 2) === 0) {
+            ret -= Math.pow(x, i) / i;
+        } else {
+            ret += Math.pow(x, i) / i;
+        }
+    }
+    return ret;
+}
+
+jstat.expm1 = function(x) {
+    var y, a  = Math.abs(x);
+    if(a < jstat.DBL_EPSILON) return x;
+    if(a > 0.697) return Math.exp(x) - 1; /* negligable cancellation */
+    if(a > 1e-8) {
+        y = Math.exp(x) - 1;
+    } else {
+        y = (x / 2 + 1) * x;
+    }
+
+    /* Newton step for solving   log(1 + y) = x   for y : */
+    /* WARNING: does not work for y ~ -1: bug in 1.5.0 */
+    y -= (1 + y) * (jstat.log1p(y) - x);
+    return y;
+}
+
+jstat.logBeta = function(a, b) {
+    var corr, p, q;
+    p = q = a;
+    if(b < p) p = b;/* := min(a,b) */
+    if(b > q) q = b;/* := max(a,b) */
+
+    /* both arguments must be >= 0 */
+    if (p < 0) {
+        console.warn('Both arguements must be >= 0');
+        return Number.NaN;
+    }
+    else if (p == 0) {
+        return Number.POSITIVE_INFINITY;
+    }
+    else if (!jstat.isFinite(q)) { /* q == +Inf */
+        return Number.NEGATIVE_INFINITY;
+    }
+
+    if (p >= 10) {
+        /* p and q are big. */
+        corr = jstat.lgammacor(p) + jstat.lgammacor(q) - jstat.lgammacor(p + q);
+        return Math.log(q) * -0.5 + jstat.LN_SQRT_2PI + corr
+        + (p - 0.5) * Math.log(p / (p + q)) + q * jstat.log1p(-p / (p + q));
+    }
+    else if (q >= 10) {
+        /* p is small, but q is big. */
+        corr = jstat.lgammacor(q) - jstat.lgammacor(p + q);
+        return jstat.lgamma(p) + corr + p - p * Math.log(p + q)
+        + (q - 0.5) * jstat.log1p(-p / (p + q));
+    }
+    else
+        /* p and q are small: p <= q < 10. */
+        return Math.log(jstat.gamma(p) * (jstat.gamma(q) / jstat.gamma(p + q)));
+}
+
+jstat.dbinom_raw = function(x, n, p, q, give_log) {
+    if(give_log == null) give_log = false;
+    var lf, lc;
+
+    if(p == 0) {
+        if(x == 0) {
+            // R_D__1
+            return (give_log) ? 0.0 : 1.0;
+        } else {
+            // R_D__0
+            return (give_log) ? Number.NEGATIVE_INFINITY : 0.0;
+        }
+    }
+    if(q == 0) {
+        if(x == n) {
+            // R_D__1
+            return (give_log) ? 0.0 : 1.0;
+        } else {
+            // R_D__0
+            return (give_log) ? Number.NEGATIVE_INFINITY : 0.0;
+        }
+    }
+
+    if (x == 0) {
+        if(n == 0) return (give_log) ? 0.0 : 1.0;   //R_D__1;
+        lc = (p < 0.1) ? -jstat.bd0(n,n*q) - n*p : n*Math.log(q);
+        return ( give_log ) ? lc : Math.exp(lc); //R_D_exp(lc)
+    }
+
+    if (x == n) {
+        lc = (q < 0.1) ? -jstat.bd0(n,n*p) - n*q : n*Math.log(p);
+        return ( give_log ) ? lc : Math.exp(lc); //R_D_exp(lc)
+    }
+
+    if (x < 0 || x > n) return (give_log) ? Number.NEGATIVE_INFINITY : 0.0; // R_D__0;
+
+    /* n*p or n*q can underflow to zero if n and p or q are small.  This
+       used to occur in dbeta, and gives NaN as from R 2.3.0.  */
+    lc = jstat.stirlerr(n) - jstat.stirlerr(x) - jstat.stirlerr(n-x) - jstat.bd0(x,n*p) - jstat.bd0(n-x,n*q);
+
+    /* f = (M_2PI*x*(n-x))/n; could overflow or underflow */
+    /* Upto R 2.7.1:
+     * lf = log(M_2PI) + log(x) + log(n-x) - log(n);
+     * -- following is much better for  x << n : */
+    lf = Math.log(jstat.TWO_PI) + Math.log(x) + jstat.log1p(- x/n);
+
+    return (give_log) ? lc - 0.5*lf : Math.exp(lc - 0.5*lf); // R_D_exp(lc - 0.5*lf);
+}
+
+jstat.max = function(values) {
+    var max = Number.NEGATIVE_INFINITY;
+    for(var i = 0; i < values.length; i++) {
+        if(values[i] > max) {
+            max = values[i];
+        }
+    }
+    return max;
+}
+
+/******************************************************************************/
+/*                      Probability Distributions                             */
+/******************************************************************************/
+
+/**
+ * Range class
+ */
+var Range = Class.extend({
+    init: function(min, max, numPoints) {
+        this._minimum = parseFloat(min);
+        this._maximum = parseFloat(max);
+        this._numPoints = parseFloat(numPoints);
+    },
+    getMinimum: function() {
+        return this._minimum;
+    },
+    getMaximum: function() {
+        return this._maximum;
+    },
+    getNumPoints: function() {
+        return this._numPoints;
+    },
+    getPoints: function() {
+        var results = [];
+        var x = this._minimum;
+        var step = (this._maximum-this._minimum)/(this._numPoints-1);
+        for(var i = 0; i < this._numPoints; i++) {
+            results[i] = parseFloat(x.toFixed(6));
+            x += step;
+        }
+        return results;
+    }
+});
+
+Range.validate = function(range) {
+    if( ! range instanceof Range) {
+        return false;
+    }
+    if(isNaN(range.getMinimum()) || isNaN(range.getMaximum()) || isNaN(range.getNumPoints()) || range.getMaximum() < range.getMinimum() || range.getNumPoints() <= 0) {
+        return false;
+    }
+    return true;
+}
+
+var ContinuousDistribution = Class.extend({
+    init: function(name) {
+        this._name = name;
+    },
+    toString: function() {
+        return this._string;
+    },
+    getName: function() {
+        return this._name;
+    },
+    getClassName: function() {
+        return this._name + 'Distribution';
+    },
+    density: function(valueOrRange) {
+        if(!isNaN(valueOrRange)) {
+            // single value
+            return parseFloat(this._pdf(valueOrRange).toFixed(15));
+        } else if (Range.validate(valueOrRange)) {
+            // multiple values
+            var points = valueOrRange.getPoints();
+
+            var result = [];
+            // For each point in the range
+            for(var i = 0; i < points.length; i++) {
+                result[i] = parseFloat(this._pdf(points[i]));
+            }
+            return result;
+        } else {
+            // neither value or range
+            throw "Invalid parameter supplied to " + this.getClassName() + ".density()";
+        }
+    },
+    cumulativeDensity: function(valueOrRange) {
+        if(!isNaN(valueOrRange)) {
+            // single value
+            return parseFloat(this._cdf(valueOrRange).toFixed(15));
+        } else if (Range.validate(valueOrRange)) {
+            // multiple values
+            var points = valueOrRange.getPoints();
+            var result = [];
+            // For each point in the range
+            for(var i = 0; i < points.length; i++) {
+                result[i] = parseFloat(this._cdf(points[i]));
+            }
+            return result;
+        } else {
+            // neither value or range
+            throw "Invalid parameter supplied to " + this.getClassName() + ".cumulativeDensity()";
+        }
+    },
+    getRange: function(standardDeviations, numPoints) {
+        if(standardDeviations == null) {
+            standardDeviations = 5;
+        }
+        if(numPoints == null) {
+            numPoints = 100;
+        }
+        var min = this.getMean() - standardDeviations * Math.sqrt(this.getVariance());
+        var max = this.getMean() + standardDeviations * Math.sqrt(this.getVariance());
+
+        if(this.getClassName() == 'GammaDistribution' || this.getClassName() == 'LogNormalDistribution') {
+            min = 0.0;
+            max = this.getMean() + standardDeviations * Math.sqrt(this.getVariance());
+        } else if(this.getClassName() == 'BetaDistribution') {
+            min = 0.0;
+            max = 1.0;
+        }
+
+
+        var range = new Range(min, max, numPoints);
+        return range;
+    },
+    getVariance: function(){},
+    getMean: function(){},
+    getQuantile: function(p) {
+        var self = this;
+        /*
+         *  Recursive function to find the closest match
+         */
+        function findClosestMatch(range, p) {
+            var ERR = 1.0e-5;
+            var xs = range.getPoints();
+            var closestIndex = 0;
+            var closestDistance = 999;
+
+            for(var i=0; i<xs.length; i++) {
+                var pp = self.cumulativeDensity(xs[i]);
+                var distance = Math.abs(pp - p);
+                if(distance < closestDistance) {
+                    // closer value found
+                    closestIndex = i;
+                    closestDistance = distance;
+                }
+            }
+            if(closestDistance <= ERR) {
+                // Acceptable - return value;
+                return xs[closestIndex];
+            } else {
+                // Calculate the new range
+                var newRange = new Range(xs[closestIndex-1], xs[closestIndex+1],20);
+                return findClosestMatch(newRange, p);
+            }
+        }
+        var range = this.getRange(5, 20);
+        return findClosestMatch(range, p);
+    }
+});
+
+/**
+ * A normal distribution object
+ */
+var NormalDistribution = ContinuousDistribution.extend({
+    init: function(mean, sigma) {
+        this._super('Normal');
+        this._mean = parseFloat(mean);
+        this._sigma = parseFloat(sigma);
+        this._string = "Normal ("+this._mean.toFixed(2)+", " + this._sigma.toFixed(2) + ")";
+    },
+    _pdf: function(x, give_log) {
+        if(give_log == null) {
+            give_log=false;
+        }  // default is false;
+        var sigma = this._sigma;
+        var mu = this._mean;
+        if(!jstat.isFinite(sigma)) {
+            return (give_log) ? Number.NEGATIVE_INFINITY : 0.0
+        }
+        if(!jstat.isFinite(x) && mu == x) {
+            return Number.NaN;
+        }
+        if(sigma<=0) {
+            if(sigma < 0) {
+                throw "invalid sigma in _pdf";
+            }
+            return (x==mu)?Number.POSITIVE_INFINITY:(give_log)?Number.NEGATIVE_INFINITY:0.0;
+        }
+        x=(x-mu)/sigma;
+        if(!jstat.isFinite(x)){
+            return (give_log)?Number.NEGATIVE_INFINITY:0.0;
+        }
+        return (give_log ? -(jstat.LN_SQRT_2PI + 0.5 * x * x + Math.log(sigma)) :
+            jstat.ONE_SQRT_2PI * Math.exp(-0.5 * x * x) / sigma);
+    },
+    _cdf: function(x, lower_tail, log_p) {
+
+        if(lower_tail == null) lower_tail = true;
+        if(log_p == null) log_p = false;
+
+        function pnorm_both(x, cum, ccum, i_tail, log_p) {
+            /*  i_tail in {0,1,2} means: "lower", "upper", or "both" :
+                if(lower) return  *cum := P[X <= x]
+                if(upper) return *ccum := P[X >  x] = 1 - P[X <= x]
+             */
+
+            var a = [
+            2.2352520354606839287,
+            161.02823106855587881,
+            1067.6894854603709582,
+            18154.981253343561249,
+            0.065682337918207449113
+            ];
+            var b = [
+            47.20258190468824187,
+            976.09855173777669322,
+            10260.932208618978205,
+            45507.789335026729956
+            ];
+            var c = [
+            0.39894151208813466764,
+            8.8831497943883759412,
+            93.506656132177855979,
+            597.27027639480026226,
+            2494.5375852903726711,
+            6848.1904505362823326,
+            11602.651437647350124,
+            9842.7148383839780218,
+            1.0765576773720192317e-8
+            ];
+            var d = [
+            22.266688044328115691,
+            235.38790178262499861,
+            1519.377599407554805,
+            6485.558298266760755,
+            18615.571640885098091,
+            34900.952721145977266,
+            38912.003286093271411,
+            19685.429676859990727
+            ];
+            var p = [
+            0.21589853405795699,
+            0.1274011611602473639,
+            0.022235277870649807,
+            0.001421619193227893466,
+            2.9112874951168792e-5,
+            0.02307344176494017303
+            ];
+            var q = [
+            1.28426009614491121,
+            0.468238212480865118,
+            0.0659881378689285515,
+            0.00378239633202758244,
+            7.29751555083966205e-5
+            ];
+
+            var xden, xnum, temp, del, eps, xsq, y, i, lower, upper;
+
+            /* Consider changing these : */
+            eps = jstat.DBL_EPSILON * 0.5;
+
+            /* i_tail in {0,1,2} =^= {lower, upper, both} */
+            lower = i_tail != 1;
+            upper = i_tail != 0;
+
+            y = Math.abs(x);
+
+            if (y <= 0.67448975) { /* qnorm(3/4) = .6744.... -- earlier had 0.66291 */
+                if (y > eps) {
+                    xsq = x * x;
+                    xnum = a[4] * xsq;
+                    xden = xsq;
+                    for (i = 0; i < 3; ++i) {
+                        xnum = (xnum + a[i]) * xsq;
+                        xden = (xden + b[i]) * xsq;
+                    }
+                } else {
+                    xnum = xden = 0.0;
+                }
+                temp = x * (xnum + a[3]) / (xden + b[3]);
+                if(lower)  cum = 0.5 + temp;
+                if(upper) ccum = 0.5 - temp;
+                if(log_p) {
+                    if(lower)  cum = Math.log(cum);
+                    if(upper) ccum = Math.log(ccum);
+                }
+
+            } else if (y <= jstat.SQRT_32) {
+                /* Evaluate pnorm for 0.674.. = qnorm(3/4) < |x| <= sqrt(32) ~= 5.657 */
+
+                xnum = c[8] * y;
+                xden = y;
+                for (i = 0; i < 7; ++i) {
+                    xnum = (xnum + c[i]) * y;
+                    xden = (xden + d[i]) * y;
+                }
+                temp = (xnum + c[7]) / (xden + d[7]);
+
+                /* do_del */
+                xsq = jstat.trunc(x * 16) / 16;
+                del = (x - xsq) * (x + xsq);
+                if(log_p) {
+                    cum = (-xsq * xsq * 0.5) + (-del * 0.5) + Math.log(temp);
+                    if((lower && x > 0.) || (upper && x <= 0.))
+                        ccum = jstat.log1p(-Math.exp(-xsq * xsq * 0.5) *
+                            Math.exp(-del * 0.5) * temp);
+                }
+                else {
+                    cum = Math.exp(-xsq * xsq * 0.5) * Math.exp(-del * 0.5) * temp;
+                    ccum = 1.0 - cum;
+                }
+                /* end do_del */
+
+                /* swap_tail */
+                if (x > 0.0) {/* swap  ccum <--> cum */
+                    temp = cum;
+                    if(lower) {
+                        cum = ccum;
+
+                    }
+                    ccum = temp;
+                }
+            /* end swap_tail */
+
+            }
+            /* else	  |x| > sqrt(32) = 5.657 :
+             * the next two case differentiations were really for lower=T, log=F
+             * Particularly	 *not*	for  log_p !
+
+             * Cody had (-37.5193 < x  &&  x < 8.2924) ; R originally had y < 50
+             *
+             * Note that we do want symmetry(0), lower/upper -> hence use y
+             */
+
+            else if((log_p && y < 1e170)|| (lower && -37.5193 < x  &&  x < 8.2924)
+                || (upper && -8.2924  < x  &&  x < 37.5193)) {
+                /* Evaluate pnorm for x in (-37.5, -5.657) union (5.657, 37.5) */
+                xsq = 1.0 / (x * x); /* (1./x)*(1./x) might be better */
+                xnum = p[5] * xsq;
+                xden = xsq;
+                for (i = 0; i < 4; ++i) {
+                    xnum = (xnum + p[i]) * xsq;
+                    xden = (xden + q[i]) * xsq;
+                }
+                temp = xsq * (xnum + p[4]) / (xden + q[4]);
+                temp = (jstat.ONE_SQRT_2PI - temp) / y;
+
+                /* do_del */
+                xsq = jstat.trunc(x * 16) / 16;
+                del = (x - xsq) * (x + xsq);
+                if(log_p) {
+                    cum = (-xsq * xsq * 0.5) + (-del * 0.5) + Math.log(temp);
+                    if((lower && x > 0.) || (upper && x <= 0.))
+                        ccum = jstat.log1p(-Math.exp(-xsq * xsq * 0.5) *
+                            Math.exp(-del * 0.5) * temp);
+                }
+                else {
+                    cum = Math.exp(-xsq * xsq * 0.5) * Math.exp(-del * 0.5) * temp;
+                    ccum = 1.0 - cum;
+                }
+                /* end do_del */
+
+                /* swap_tail */
+                if (x > 0.0) {/* swap  ccum <--> cum */
+                    temp = cum;
+                    if(lower) {
+                        cum = ccum;
+
+                    }
+                    ccum = temp;
+                }
+            /* end swap_tail */
+
+            } else { /* large x such that probs are 0 or 1 */
+                if(x > 0) {
+                    cum = (log_p) ? 0.0 : 1.0;  // R_D__1
+                    ccum = (log_p) ? Number.NEGATIVE_INFINITY : 0.0;  //R_D__0;
+                } else {
+                    cum = (log_p) ? Number.NEGATIVE_INFINITY : 0.0;  //R_D__0;
+                    ccum = (log_p) ? 0.0 : 1.0;  // R_D__1
+                }
+            }
+
+            return [cum, ccum];
+        }
+
+        var p, cp;
+        var mu = this._mean;
+        var sigma = this._sigma;
+        var R_DT_0, R_DT_1;
+
+        if(lower_tail) {
+            if(log_p) {
+                R_DT_0 = Number.NEGATIVE_INFINITY;
+                R_DT_1 = 0.0;
+            } else {
+                R_DT_0 = 0.0;
+                R_DT_1 = 1.0;
+            }
+        } else {
+            if(log_p) {
+                R_DT_0 = 0.0;
+                R_DT_1 = Number.NEGATIVE_INFINITY;
+            } else {
+                R_DT_0 = 1.0;
+                R_DT_1 = 0.0;
+            }
+        }
+
+        if(!jstat.isFinite(x) && mu == x) return Number.NaN;
+        if(sigma <= 0) {
+            if(sigma < 0) {
+                console.warn("Sigma is less than 0");
+                return Number.NaN;
+            }
+            return (x < mu) ? R_DT_0 : R_DT_1;
+        }
+
+        p = (x - mu) / sigma;
+
+        if(!jstat.isFinite(p)) {
+            return (x < mu) ? R_DT_0 : R_DT_1;
+        }
+
+        x = p;
+
+        // pnorm_both(x, &p, &cp, (lower_tail ? 0 : 1), log_p);
+        // result[0] == &p
+        // result[1] == &cp
+
+        var result = pnorm_both(x, p, cp, (lower_tail ? false : true), log_p);
+
+        return (lower_tail ? result[0] : result[1]);
+
+    },
+    getMean: function() {
+        return this._mean;
+    },
+    getSigma: function() {
+        return this._sigma;
+    },
+    getVariance: function() {
+        return this._sigma*this._sigma;
+    }
+});
+
+/**
+ *  A Log-normal distribution object
+ */
+var LogNormalDistribution = ContinuousDistribution.extend({
+    init: function(location, scale) {
+        this._super('LogNormal')
+        this._location = parseFloat(location);
+        this._scale = parseFloat(scale);
+        this._string = "LogNormal ("+this._location.toFixed(2)+", " + this._scale.toFixed(2) + ")";
+    },
+    _pdf: function(x, give_log) {
+        var y;
+        var sdlog = this._scale;
+        var meanlog = this._location;
+        if(give_log == null) {
+            give_log = false;
+        }
+
+        if(sdlog <= 0) throw "Illegal parameter in _pdf";
+
+        if(x <= 0) {
+            return (give_log) ? Number.NEGATIVE_INFINITY : 0.0;
+        }
+
+        y = (Math.log(x) - meanlog) / sdlog;
+
+        return (give_log ? -(jstat.LN_SQRT_2PI + 0.5 * y * y + Math.log(x * sdlog)) :
+            jstat.ONE_SQRT_2PI * Math.exp(-0.5 * y * y) / (x * sdlog));
+
+    },
+    _cdf: function(x, lower_tail, log_p) {
+        var sdlog = this._scale;
+        var meanlog = this._location;
+        if(lower_tail == null) {
+            lower_tail = true;
+        }
+        if(log_p == null) {
+            log_p = false;
+        }
+
+
+        if(sdlog <= 0) {
+            throw "illegal std in _cdf";
+        }
+
+        if(x > 0) {
+            var nd = new NormalDistribution(meanlog, sdlog);
+            return nd._cdf(Math.log(x), lower_tail, log_p);
+        }
+        if(lower_tail) {
+            return (log_p) ? Number.NEGATIVE_INFINITY : 0.0;    // R_D__0
+        } else {
+            return (log_p) ? 0.0 : 1.0;                         // R_D__1
+        }
+    },
+    getLocation: function() {
+        return this._location;
+    },
+    getScale: function() {
+        return this._scale;
+    },
+    getMean: function() {
+        return Math.exp((this._location + this._scale) /  2);
+    },
+    getVariance: function() {
+        var ans = (Math.exp(this._scale)-1)*Math.exp(2*this._location+this._scale);
+        return ans;
+    }
+});
+
+
+/**
+ *  Gamma distribution object
+ */
+var GammaDistribution = ContinuousDistribution.extend({
+    init: function(shape, scale) {
+        this._super('Gamma');
+        this._shape = parseFloat(shape);
+        this._scale = parseFloat(scale);
+        this._string = "Gamma ("+this._shape.toFixed(2)+", " + this._scale.toFixed(2) + ")";
+    },
+    _pdf: function(x, give_log) {
+        var pr;
+        var shape = this._shape;
+        var scale = this._scale;
+        if(give_log == null) {
+            give_log = false;    // default value
+        }
+
+        if(shape < 0 || scale <= 0) {
+            throw "Illegal argument in _pdf";
+        }
+
+        if(x < 0) {
+            return (give_log) ? Number.NEGATIVE_INFINITY : 0.0; // R_D__0
+        }
+        if(shape == 0) { /* point mass at 0 */
+            return (x == 0) ? Number.POSITIVE_INFINITY : (give_log) ? Number.NEGATIVE_INFINITY : 0.0;   // R_D__0
+        }
+        if(x == 0) {
+            if(shape < 1) return Number.POSITIVE_INFINITY;
+            if(shape > 1) return (give_log) ? Number.NEGATIVE_INFINITY : 0.0; // R_D__0
+            /* else */
+            return (give_log) ? -Math.log(scale) : 1/scale;
+        }
+
+        if(shape < 1) {
+            pr = jstat.dopois_raw(shape, x/scale, give_log);
+            return give_log ? pr + Math.log(shape/x) : pr*shape/x;
+        }
+        /* else shape >= 1 */
+        pr = jstat.dopois_raw(shape-1, x/scale, give_log);
+        return give_log ? pr - Math.log(scale) : pr/scale;
+
+    },
+    /**
+     *	This function computes the distribution function for the
+     *	gamma distribution with shape parameter alph and scale parameter
+     *	scale.	This is also known as the incomplete gamma function.
+     *	See Abramowitz and Stegun (6.5.1) for example.
+     */
+    _cdf: function(x, lower_tail, log_p) {
+        /* define USE_PNORM */
+        function USE_PNORM() {
+            pn1 = Math.sqrt(alph) * 3.0 * (Math.pow(x/alph,1.0/3.0) + 1.0 / (9.0 * alph) - 1.0);
+            var norm_dist = new NormalDistribution(0.0, 1.0);
+            return norm_dist._cdf(pn1, lower_tail, log_p);
+        }
+
+        /* Defaults */
+        if(lower_tail == null) lower_tail = true;
+        if(log_p == null) log_p = false;
+        var alph = this._shape;
+        var scale = this._scale;
+        var xbig = 1.0e+8;
+        var xlarge = 1.0e+37;
+        var alphlimit = 1e5;
+        var pn1,pn2,pn3,pn4,pn5,pn6,arg,a,b,c,an,osum,sum,n,pearson;
+
+        if(alph <= 0. || scale <= 0.) {
+            console.warn('Invalid gamma params in _cdf');
+            return Number.NaN;
+        }
+
+        x/=scale;
+        if(isNaN(x)) return x;
+        if(x <= 0.0) {
+            // R_DT_0
+            if(lower_tail) {
+                // R_D__0
+                return (log_p) ? Number.NEGATIVE_INFINITY : 0.0;
+            } else {
+                // R_D__1
+                return (log_p) ? 0.0 : 1.0;
+            }
+        }
+
+        if(alph > alphlimit) {
+            return USE_PNORM();
+        }
+
+        if(x > xbig * alph) {
+            if(x > jstat.DBL_MAX * alph) {
+                // R_DT_1
+                if(lower_tail) {
+                    // R_D__1
+                    return (log_p) ? 0.0 : 1.0;
+                } else {
+                    // R_D__0
+                    return (log_p) ? Number.NEGATIVE_INFINITY : 0.0;
+                }
+            } else {
+                return USE_PNORM();
+            }
+        }
+
+        if(x <= 1.0 || x < alph) {
+            pearson = 1; /* use pearson's series expansion */
+            arg = alph * Math.log(x) - x - jstat.lgamma(alph + 1.0);
+
+            c = 1.0;
+            sum = 1.0;
+            a = alph;
+            do {
+                a += 1.0;
+                c *= x / a;
+                sum += c;
+            } while(c > jstat.DBL_EPSILON * sum);
+        } else { /* x >= max( 1, alph) */
+            pearson = 0;/* use a continued fraction expansion */
+            arg = alph * Math.log(x) - x - jstat.lgamma(alph);
+
+            a = 1. - alph;
+            b = a + x + 1.;
+            pn1 = 1.;
+            pn2 = x;
+            pn3 = x + 1.;
+            pn4 = x * b;
+            sum = pn3 / pn4;
+
+            for (n = 1; ; n++) {
+                a += 1.;/* =   n+1 -alph */
+                b += 2.;/* = 2(n+1)-alph+x */
+                an = a * n;
+                pn5 = b * pn3 - an * pn1;
+                pn6 = b * pn4 - an * pn2;
+                if (Math.abs(pn6) > 0.) {
+                    osum = sum;
+                    sum = pn5 / pn6;
+                    if (Math.abs(osum - sum) <= jstat.DBL_EPSILON * jstat.fmin2(1.0, sum))
+                        break;
+                }
+                pn1 = pn3;
+                pn2 = pn4;
+                pn3 = pn5;
+                pn4 = pn6;
+
+                if (Math.abs(pn5) >= xlarge) {
+                    pn1 /= xlarge;
+                    pn2 /= xlarge;
+                    pn3 /= xlarge;
+                    pn4 /= xlarge;
+                }
+            }
+        }
+        arg += Math.log(sum);
+        lower_tail = (lower_tail == pearson);
+
+        if (log_p && lower_tail)
+            return(arg);
+        /* else */
+        /* sum = exp(arg); and return   if(lower_tail) sum	else 1-sum : */
+
+        if(lower_tail) {
+            return Math.exp(arg);
+        } else {
+            if(log_p) {
+                // R_Log1_Exp(arg);
+                return (arg > -Math.LN2)  ? Math.log(-jstat.expm1(arg)) : jstat.log1p(-Math.exp(arg));
+            } else {
+                return -jstat.expm1(arg);
+            }
+        }
+    },
+    getShape: function() {
+        return this._shape;
+    },
+    getScale: function() {
+        return this._scale;
+    },
+    getMean: function() {
+        return this._shape * this._scale;
+    },
+    getVariance: function() {
+        return this._shape*Math.pow(this._scale,2);
+    }
+});
+
+/**
+ *  A Beta distribution object
+ */
+var BetaDistribution = ContinuousDistribution.extend({
+    init: function(alpha, beta) {
+        this._super('Beta');
+        this._alpha = parseFloat(alpha);
+        this._beta = parseFloat(beta);
+        this._string = "Beta ("+this._alpha.toFixed(2)+", " + this._beta.toFixed(2) + ")";
+    },
+    _pdf: function(x, give_log) {
+        if(give_log == null) give_log = false; // default;
+        var a = this._alpha;
+        var b = this._beta;
+        var lval;
+        if(a <= 0 || b <= 0) {
+            console.warn('Illegal arguments in _pdf');
+            return Number.NaN;
+        }
+        if(x < 0 || x > 1) {
+            // R_D__0
+            return (give_log) ? Number.NEGATIVE_INFINITY : 0.0;
+        }
+        if(x == 0) {
+            if(a > 1) {
+                // R_D__0
+                return (give_log) ? Number.NEGATIVE_INFINITY : 0.0;
+            }
+            if(a < 1) {
+                return Number.POSITIVE_INFINITY;
+            }
+            /*a == 1 */ return (give_log) ? Math.log(b) : b;    // R_D_val(b)
+        }
+        if(x == 1) {
+            if(b > 1) {
+                // R_D__0
+                return (give_log) ? Number.NEGATIVE_INFINITY : 0.0;
+            }
+            if(b < 1) {
+                return Number.POSITIVE_INFINITY;
+            }
+            /* b == 1 */ return (give_log) ? Math.log(a) : a;   // R_D_val(a)
+        }
+        if(a<=2||b<=2) {
+            lval = (a-1)*Math.log(x) + (b-1)*jstat.log1p(-x) - jstat.logBeta(a, b);
+        } else {
+            lval = Math.log(a+b-1) + jstat.dbinom_raw(a-1, a+b-2, x, 1-x, true);
+        }
+        //R_D_exp(lval)
+        return (give_log) ? lval : Math.exp(lval);
+    },
+    _cdf: function(x, lower_tail, log_p) {
+        if(lower_tail == null) lower_tail = true;
+        if(log_p == null) log_p = false;
+
+        var pin = this._alpha;
+        var qin = this._beta;
+
+        if(pin <= 0 || qin <= 0) {
+            console.warn('Invalid argument in _cdf');
+            return Number.NaN;
+        }
+
+        if(x <= 0) {
+            //R_DT_0;
+            if(lower_tail) {
+                // R_D__0
+                return (log_p) ? Number.NEGATIVE_INFINITY : 0.0;
+            } else {
+                // R_D__1
+                return (log_p) ? 0.1 : 1.0;
+            }
+        }
+
+        if(x >= 1){
+            // R_DT_1
+            if(lower_tail) {
+                // R_D__1
+                return (log_p) ? 0.1 : 1.0;
+            } else {
+                // R_D__0
+                return (log_p) ? Number.NEGATIVE_INFINITY : 0.0;
+            }
+        }
+
+        /* else */
+        return jstat.incompleteBeta(pin, qin, x);
+    },
+    getAlpha: function() {
+        return this._alpha;
+    },
+    getBeta: function() {
+        return this._beta;
+    },
+    getMean: function() {
+        return this._alpha / (this._alpha+ this._beta);
+    },
+    getVariance: function() {
+        var ans = (this._alpha * this._beta) / (Math.pow(this._alpha+this._beta,2)*(this._alpha+this._beta+1));
+        return ans;
+    }
+});
+
+var StudentTDistribution = ContinuousDistribution.extend({
+    init: function(degreesOfFreedom, mu) {
+        this._super('StudentT');
+        this._dof = parseFloat(degreesOfFreedom);
+
+        if(mu != null) {
+            this._mu = parseFloat(mu);
+            this._string = "StudentT ("+this._dof.toFixed(2)+", " + this._mu.toFixed(2)+ ")";
+        } else {
+            this._mu = 0.0;
+            this._string = "StudentT ("+this._dof.toFixed(2)+")";
+        }
+
+    },
+    _pdf: function(x, give_log) {
+        if(give_log == null) give_log = false;
+        if(this._mu == null) {
+            return this._dt(x, give_log);
+        } else {
+            var y = this._dnt(x, give_log);
+            if(y > 1){
+                console.warn('x:' + x + ', y: ' + y);
+            }
+            return y;
+        }
+    },
+    _cdf: function(x, lower_tail, give_log) {
+        if(lower_tail == null) lower_tail = true;
+        if(give_log == null) give_log = false;
+        if(this._mu == null) {
+            return this._pt(x, lower_tail, give_log);
+        } else {
+            return this._pnt(x, lower_tail, give_log);
+        }
+    },
+    _dt: function(x, give_log) {
+        var t,u;
+        var n = this._dof;
+        if (n <= 0){
+            console.warn('Invalid parameters in _dt');
+            return Number.NaN;
+        }
+        if(!jstat.isFinite(x)) {
+            return (give_log) ? Number.NEGATIVE_INFINITY : 0.0; // R_D__0;
+        }
+
+        if(!jstat.isFinite(n)) {
+            var norm = new NormalDistribution(0.0, 1.0);
+            return norm.density(x, give_log);
+        }
+
+
+        t = -jstat.bd0(n/2.0,(n+1)/2.0) + jstat.stirlerr((n+1)/2.0) - jstat.stirlerr(n/2.0);
+        if ( x*x > 0.2*n )
+            u = Math.log( 1+ x*x/n ) * n/2;
+        else
+            u = -jstat.bd0(n/2.0,(n+x*x)/2.0) + x*x/2.0;
+
+        var p1 = jstat.TWO_PI *(1+x*x/n);
+        var p2 = t-u;
+
+        return (give_log) ? -0.5*Math.log(p1) + p2 : Math.exp(p2)/Math.sqrt(p1);       // R_D_fexp(M_2PI*(1+x*x/n), t-u);
+    },
+    _dnt: function(x, give_log) {
+        if(give_log == null) give_log = false;
+        var df = this._dof;
+        var ncp = this._mu;
+        var u;
+
+        if(df <= 0.0) {
+            console.warn("Illegal arguments _dnf");
+            return Number.NaN;
+        }
+        if(ncp == 0.0) {
+            return this._dt(x, give_log);
+        }
+
+        if(!jstat.isFinite(x)) {
+            // R_D__0
+            if(give_log) {
+                return Number.NEGATIVE_INFINITY;
+            } else {
+                return 0.0;
+            }
+        }
+
+        /* If infinite df then the density is identical to a
+           normal distribution with mean = ncp.  However, the formula
+           loses a lot of accuracy around df=1e9
+         */
+        if(!isFinite(df) || df > 1e8) {
+            var dist = new NormalDistribution(ncp, 1.);
+            return dist.density(x, give_log);
+        }
+
+        /* Do calculations on log scale to stabilize */
+
+        /* Consider two cases: x ~= 0 or not */
+        if (Math.abs(x) > Math.sqrt(df * jstat.DBL_EPSILON)) {
+            var newT = new StudentTDistribution(df+2, ncp);
+            u = Math.log(df) - Math.log(Math.abs(x)) +
+            Math.log(Math.abs(newT._pnt(x*Math.sqrt((df+2)/df), true, false) -
+                this._pnt(x, true, false)));
+        /* FIXME: the above still suffers from cancellation (but not horribly) */
+        }
+        else {  /* x ~= 0 : -> same value as for  x = 0 */
+            u = jstat.lgamma((df+1)/2) - jstat.lgamma(df/2)
+            - .5*(Math.log(Math.PI) + Math.log(df) + ncp*ncp);
+        }
+
+        return (give_log ? u : Math.exp(u));
+    },
+    _pt: function(x, lower_tail, log_p) {
+        if(lower_tail == null) lower_tail = true;
+        if(log_p == null) log_p = false;
+        var val, nx;
+        var n = this._dof;
+        var DT_0, DT_1;
+
+        if(lower_tail) {
+            if(log_p) {
+                DT_0 = Number.NEGATIVE_INFINITY;
+                DT_1 = 1.;
+            } else {
+                DT_0 = 0.;
+                DT_1 = 1.;
+            }
+        } else {
+            if(log_p) {
+                // not lower_tail but log_p
+                DT_0 = 0.;
+                DT_1 = Number.NEGATIVE_INFINITY;
+            } else {
+                // not lower_tail and not log_p
+                DT_0 = 1.;
+                DT_1 = 0.;
+            }
+        }
+
+        if(n <= 0.0) {
+            console.warn("Invalid T distribution _pt");
+            return Number.NaN;
+        }
+        var norm = new NormalDistribution(0,1);
+        if(!jstat.isFinite(x)) {
+            return (x < 0) ? DT_0 : DT_1;
+        }
+        if(!jstat.isFinite(n)) {
+            return norm._cdf(x, lower_tail, log_p);
+        }
+
+        if (n > 4e5) { /*-- Fixme(?): test should depend on `n' AND `x' ! */
+            /* Approx. from	 Abramowitz & Stegun 26.7.8 (p.949) */
+            val = 1./(4.*n);
+            return norm._cdf(x*(1. - val)/sqrt(1. + x*x*2.*val), lower_tail, log_p);
+        }
+
+        nx = 1 + (x/n)*x;
+        /* FIXME: This test is probably losing rather than gaining precision,
+         * now that pbeta(*, log_p = TRUE) is much better.
+         * Note however that a version of this test *is* needed for x*x > D_MAX */
+        if(nx > 1e100) { /* <==>  x*x > 1e100 * n  */
+            /* Danger of underflow. So use Abramowitz & Stegun 26.5.4
+	   pbeta(z, a, b) ~ z^a(1-z)^b / aB(a,b) ~ z^a / aB(a,b),
+	   with z = 1/nx,  a = n/2,  b= 1/2 :
+             */
+            var lval;
+            lval = -0.5*n*(2*Math.log(Math.abs(x)) - Math.log(n))
+            - jstat.logBeta(0.5*n, 0.5) - Math.log(0.5*n);
+            val = log_p ? lval : Math.exp(lval);
+        } else {
+            /*
+            val = (n > x * x)
+            //    ? pbeta (x * x / (n + x * x), 0.5, n / 2., 0, log_p)
+           // : pbeta (1. / nx,             n / 2., 0.5, 1, log_p);
+             */
+            if(n > x * x) {
+                var beta = new BetaDistribution(0.5, n/2.);
+                return beta._cdf(x*x/ (n + x * x), false, log_p);
+            } else {
+                beta = new BetaDistribution(n / 2., 0.5);
+                return beta._cdf(1. / nx, true, log_p);
+            }
+
+
+        }
+
+        /* Use "1 - v"  if	lower_tail  and	 x > 0 (but not both):*/
+        if(x <= 0.)
+            lower_tail = !lower_tail;
+
+        if(log_p) {
+            if(lower_tail) return jstat.log1p(-0.5*Math.exp(val));
+            else return val - M_LN2; /* = log(.5* pbeta(....)) */
+        }
+        else {
+            val /= 2.;
+            if(lower_tail) {
+                return (0.5 - val + 0.5);
+            } else {
+                return val;
+            }
+        }
+    },
+    _pnt: function(t, lower_tail, log_p) {
+
+        var dof = this._dof;
+        var ncp = this._mu;
+        var DT_0, DT_1;
+
+        if(lower_tail) {
+            if(log_p) {
+                DT_0 = Number.NEGATIVE_INFINITY;
+                DT_1 = 1.;
+            } else {
+                DT_0 = 0.;
+                DT_1 = 1.;
+            }
+        } else {
+            if(log_p) {
+                // not lower_tail but log_p
+                DT_0 = 0.;
+                DT_1 = Number.NEGATIVE_INFINITY;
+            } else {
+                // not lower_tail and not log_p
+                DT_0 = 1.;
+                DT_1 = 0.;
+            }
+        }
+
+        var albeta, a, b, del, errbd, lambda, rxb, tt, x;
+        var geven, godd, p, q, s, tnc, xeven, xodd;
+        var it, negdel;
+
+        /* note - itrmax and errmax may be changed to suit one's needs. */
+        var ITRMAX = 1000;
+        var ERRMAX = 1.e-7;
+
+        if(dof <= 0.0) {
+            return Number.NaN;
+        } else if (dof == 0.0) {
+            return this._pt(t);
+        }
+
+        if(!jstat.isFinite(t)) {
+            return (t < 0) ? DT_0 : DT_1;
+        }
+        if(t >= 0.) {
+            negdel = false;
+            tt = t;
+            del = ncp;
+        } else {
+            /* We deal quickly with left tail if extreme,
+	   since pt(q, df, ncp) <= pt(0, df, ncp) = \Phi(-ncp) */
+            if(ncp >= 40 && (!log_p || !lower_tail)) {
+                return DT_0;
+            }
+            negdel = true;
+            tt = -t;
+            del = -ncp;
+        }
+
+        if(dof > 4e5 || del*del > 2* Math.LN2 * (-(jstat.DBL_MIN_EXP))) {
+            /*-- 2nd part: if del > 37.62, then p=0 below
+	    FIXME: test should depend on `df', `tt' AND `del' ! */
+            /* Approx. from	 Abramowitz & Stegun 26.7.10 (p.949) */
+            s=1./(4.*dof);
+            var norm = new NormalDistribution(del, Math.sqrt(1. + tt*tt*2.*s));
+            var result = norm._cdf(tt*(1.-s), lower_tail != negdel, log_p);
+            return result;
+        }
+
+        /* initialize twin series */
+        /* Guenther, J. (1978). Statist. Computn. Simuln. vol.6, 199. */
+        x = t * t;
+        rxb = dof/(x + dof);/* := (1 - x) {x below} -- but more accurately */
+        x = x / (x + dof);/* in [0,1) */
+        if (x > 0.) {/* <==>  t != 0 */
+            lambda = del * del;
+            p = .5 * Math.exp(-.5 * lambda);
+            if(p == 0.) {   // underflow!
+                console.warn("underflow in _pnt");
+                return DT_0;
+            }
+            q = jstat.SQRT_2dPI * p * del;
+            s = .5 - p;
+            if(s < 1e-7) {
+                s = -0.5 * jstat.expm1(-0.5 * lambda);
+            }
+            a = .5;
+            b = .5 * dof;
+            /* rxb = (1 - x) ^ b   [ ~= 1 - b*x for tiny x --> see 'xeven' below]
+             * where '(1 - x)' =: rxb {accurately!} above */
+            rxb = Math.pow(rxb, b);
+            albeta = jstat.LN_SQRT_PI + jstat.lgamma(b) - jstat.lgamma(.5 + b);
+            /* TODO: change incompleteBeta function to accept lower_tail and p_log */
+            xodd = jstat.incompleteBeta(a, b, x);
+            godd = 2. * rxb * Math.exp(a * Math.log(x) - albeta);
+            tnc = b * x;
+            xeven = (tnc < jstat.DBL_EPSILON) ? tnc : 1. - rxb;
+            geven = tnc * rxb;
+            tnc = p * xodd + q * xeven;
+
+            /* repeat until convergence or iteration limit */
+            for(it = 1; it <= ITRMAX; it++) {
+                a += 1.;
+                xodd  -= godd;
+                xeven -= geven;
+                godd  *= x * (a + b - 1.) / a;
+                geven *= x * (a + b - .5) / (a + .5);
+                p *= lambda / (2 * it);
+                q *= lambda / (2 * it + 1);
+                tnc += p * xodd + q * xeven;
+                s -= p;
+                /* R 2.4.0 added test for rounding error here. */
+                if(s < -1.e-10) { /* happens e.g. for (t,df,ncp)=(40,10,38.5), after 799 it.*/
+                    //console.write("precision error _pnt");
+                    break;
+                }
+                if(s <= 0 && it > 1) break;
+
+                errbd = 2. * s * (xodd - godd);
+
+                if(Math.abs(errbd) < ERRMAX) break;/*convergence*/
+            }
+
+            if(it == ITRMAX) {
+                throw "Non-convergence _pnt";
+            }
+        } else {
+            tnc = 0.;
+        }
+        norm = new NormalDistribution(0,1);
+        tnc += norm._cdf(-del, /*lower*/true, /*log_p*/ false);
+
+        lower_tail = lower_tail != negdel; /* xor */
+        if(tnc > 1 - 1e-10 && lower_tail) {
+            //console.warn("precision error _pnt");
+        }
+        var res = jstat.fmin2(tnc, 1.);
+        if(lower_tail) {
+            if(log_p) {
+                return Math.log(res);
+            } else {
+                return res;
+            }
+        } else {
+            if(log_p) {
+                return jstat.log1p(-(res));
+            } else {
+                return (0.5 - (res) + 0.5);
+            }
+        }
+    },
+    getDegreesOfFreedom: function() {
+        return this._dof;
+    },
+    getNonCentralityParameter: function() {
+        return this._mu;
+    },
+    getMean: function() {
+        if(this._dof > 1) {
+            var ans = (1/2)*Math.log(this._dof/2) + jstat.lgamma((this._dof-1)/2) - jstat.lgamma(this._dof/2)
+            return Math.exp(ans) * this._mu;
+        } else {
+            return Number.NaN;
+        }
+    },
+    getVariance: function() {
+        if(this._dof > 2) {
+            var ans = this._dof * (1 + this._mu*this._mu)/(this._dof-2) - (((this._mu*this._mu * this._dof) / 2) * Math.pow(Math.exp(jstat.lgamma((this._dof - 1)/2)-jstat.lgamma(this._dof/2)), 2));
+            return ans;
+        } else {
+            return Number.NaN;
+        }
+    }
+});
+
+
+/******************************************************************************/
+/*                      jQuery Flot graph objects                             */
+/******************************************************************************/
+
+var Plot = Class.extend({
+    init: function(id, options) {
+        this._container = '#' + String(id);
+        this._plots = [];
+        this._flotObj = null;
+        this._locked = false;
+
+        if(options != null) {
+            this._options = options;
+        } else {
+            this._options = {
+            };
+        }
+
+    },
+    getContainer: function() {
+        return this._container;
+    },
+    getGraph: function() {
+        return this._flotObj;
+    },
+    setData: function(data) {
+        this._plots = data;
+    },
+    clear: function() {
+        this._plots = [];
+    //this.render();
+    },
+    showLegend: function() {
+        this._options.legend = {
+            show: true
+        }
+        this.render();
+    },
+    hideLegend: function() {
+        this._options.legend = {
+            show: false
+        }
+        this.render();
+    },
+    render: function() {
+        this._flotObj = null;
+        this._flotObj = $.plot($(this._container), this._plots, this._options);
+    }
+});
+
+var DistributionPlot = Plot.extend({
+    init: function(id, distribution, range, options) {
+        this._super(id, options);
+        this._showPDF = true;
+        this._showCDF = false;
+        this._pdfValues = [];     // raw values for pdf
+        this._cdfValues = [];     // raw values for cdf
+        this._maxY = 1;
+        this._plotType = 'line';    // line, point, both
+        this._fill = false;
+        this._distribution = distribution;    // underlying PDF
+        // Range object for the plot
+        if(range != null && Range.validate(range)) {
+            this._range = range;
+        } else {
+            this._range = this._distribution.getRange(); // no range supplied, use distribution default
+        }
+
+        // render
+        if(this._distribution != null) {
+            this._maxY = this._generateValues();   // create the pdf/cdf values in the ctor
+        } else {
+            this._options.xaxis = {
+                min: range.getMinimum(),
+                max: range.getMaximum()
+            }
+            this._options.yaxis = {
+                max: 1
+            }
+        }
+
+
+
+        this.render();
+    },
+    setHover: function(bool) {
+        if(bool) {
+            if(this._options.grid == null) {
+                this._options.grid = {
+                    hoverable: true,
+                    mouseActiveRadius: 25
+                }
+            } else {
+                this._options.grid.hoverable = true,
+                this._options.grid.mouseActiveRadius = 25
+            }
+            function showTooltip(x, y, contents, color) {
+                $('<div id="jstat_tooltip">' + contents + '</div>').css( {
+                    position: 'absolute',
+                    display: 'none',
+                    top: y + 15,
+                    'font-size': 'small',
+                    left: x + 5,
+                    border: '1px solid ' + color[1],
+                    color: color[2],
+                    padding: '5px',
+                    'background-color': color[0],
+                    opacity: 0.80
+                }).appendTo("body").show();
+            }
+            var previousPoint = null;
+            $(this._container).bind("plothover", function(event, pos, item) {
+                $("#x").text(pos.x.toFixed(2));
+                $("#y").text(pos.y.toFixed(2));
+                if (item) {
+                    if (previousPoint != item.datapoint) {
+                        previousPoint = item.datapoint;
+                        $("#jstat_tooltip").remove();
+                        var x = jstat.toSigFig(item.datapoint[0],2), y = jstat.toSigFig(item.datapoint[1], 2);
+                        var text = null;
+                        var color = item.series.color;
+                        if(item.series.label == 'PDF') {
+                            text = "P(" + x + ") = " + y;
+                            color = ["#fee", "#fdd", "#C05F5F"];
+                        } else {
+                            // cdf
+                            text = "F(" + x + ") = " + y;
+                            color = ["#eef", "#ddf", "#4A4AC0"];
+                        }
+                        showTooltip(item.pageX, item.pageY, text, color);
+                    }
+                }
+                else {
+                    $("#jstat_tooltip").remove();
+                    previousPoint = null;
+                }
+            });
+            $(this._container).bind("mouseleave", function() {
+                if($('#jstat_tooltip').is(':visible')) {
+                    $('#jstat_tooltip').remove();
+                    previousPoint = null;
+                }
+            });
+        } else {
+            // unbind
+            if(this._options.grid == null) {
+                this._options.grid = {
+                    hoverable: false
+                }
+            } else {
+                this._options.grid.hoverable = false
+            }
+            $(this._container).unbind("plothover");
+        }
+
+        this.render();
+    },
+    setType: function(type) {
+        this._plotType = type;
+        var lines = {};
+        var points = {};
+        if(this._plotType == 'line') {
+            lines.show = true;
+            points.show = false;
+        } else if(this._plotType == 'points') {
+            lines.show = false;
+            points.show = true;
+        } else if(this._plotType == 'both') {
+            lines.show = true;
+            points.show = true;
+        }
+        if(this._options.series == null) {
+            this._options.series = {
+                lines: lines,
+                points: points
+            }
+        } else {
+            if(this._options.series.lines == null) {
+                this._options.series.lines = lines;
+            } else {
+                this._options.series.lines.show = lines.show;
+            }
+            if(this._options.series.points == null) {
+                this._options.series.points = points;
+            } else {
+                this._options.series.points.show = points.show;
+            }
+        }
+
+        this.render();
+    },
+    setFill: function(bool) {
+        this._fill = bool;
+        if(this._options.series == null) {
+            this._options.series = {
+                lines: {
+                    fill: bool
+                }
+            }
+        } else {
+            if(this._options.series.lines == null) {
+                this._options.series.lines = {
+                    fill: bool
+                }
+            } else {
+                this._options.series.lines.fill = bool;
+            }
+        }
+        this.render();
+    },
+    clear: function() {
+        this._super();
+        this._distribution = null;
+        this._pdfValues = [];
+        this._cdfValues = [];
+        this.render();
+    },
+    _generateValues: function() {
+        this._cdfValues = [];     // reinitialize the arrays.
+        this._pdfValues = [];
+
+        var xs = this._range.getPoints();
+
+        this._options.xaxis = {
+            min: xs[0],
+            max: xs[xs.length-1]
+        }
+        var pdfs = this._distribution.density(this._range);
+        var cdfs = this._distribution.cumulativeDensity(this._range);
+        for(var i = 0; i < xs.length; i++) {
+            if(pdfs[i] == Number.POSITIVE_INFINITY || pdfs[i] == Number.NEGATIVE_INFINITY) {
+                pdfs[i] = null;
+            }
+            if(cdfs[i] == Number.POSITIVE_INFINITY || cdfs[i] == Number.NEGATIVE_INFINITY) {
+                cdfs[i] = null;
+            }
+            this._pdfValues.push([xs[i], pdfs[i]]);
+            this._cdfValues.push([xs[i], cdfs[i]]);
+        }
+        return jstat.max(pdfs);
+    },
+    showPDF: function() {
+        this._showPDF = true;
+        this.render();
+    },
+    hidePDF: function() {
+        this._showPDF = false;
+        this.render();
+    },
+    showCDF: function() {
+        this._showCDF = true;
+        this.render();
+    },
+    hideCDF: function() {
+        this._showCDF = false;
+        this.render();
+    },
+    setDistribution: function(distribution, range) {
+        this._distribution = distribution;
+        if(range != null) {
+            this._range = range;
+        } else {
+            this._range = distribution.getRange();
+        }
+        this._maxY = this._generateValues();
+        this._options.yaxis = {
+            max: this._maxY*1.1
+        }
+
+        this.render();
+    },
+    getDistribution: function() {
+        return this._distribution;
+    },
+    getRange: function() {
+        return this._range;
+    },
+    setRange: function(range) {
+        this._range = range;
+        this._generateValues();
+        this.render();
+    },
+    render: function() {
+        if(this._distribution != null) {
+            if(this._showPDF && this._showCDF) {
+                this.setData([{
+                    yaxis: 1,
+                    data:this._pdfValues,
+                    color: 'rgb(237,194,64)',
+                    clickable: false,
+                    hoverable: true,
+                    label: "PDF"
+                }, {
+                    yaxis: 2,
+                    data:this._cdfValues,
+                    clickable: false,
+                    color: 'rgb(175,216,248)',
+                    hoverable: true,
+                    label: "CDF"
+                }]);
+                this._options.yaxis = {
+                    max: this._maxY*1.1
+                }
+            } else if(this._showPDF) {
+                this.setData([{
+                    data:this._pdfValues,
+                    hoverable: true,
+                    color: 'rgb(237,194,64)',
+                    clickable: false,
+                    label: "PDF"
+                }]);
+                this._options.yaxis = {
+                    max: this._maxY*1.1
+                }
+            } else if(this._showCDF) {
+                this.setData([{
+                    data:this._cdfValues,
+                    hoverable: true,
+                    color: 'rgb(175,216,248)',
+                    clickable: false,
+                    label: "CDF"
+                }]);
+                this._options.yaxis = {
+                    max: 1.1
+                }
+            }
+        } else {
+            this.setData([]);
+        }
+        this._super();  // Call the parent plot method
+    }
+});
+
+var DistributionFactory = {};
+DistributionFactory.build = function(json) {
+    /*
+    if(json.name == null) {
+        try{
+            json = JSON.parse(json);
+        }
+        catch(err) {
+            throw "invalid JSON";
+        }
+
+    // try to parse it
+    }*/
+
+    /*
+    if(json.name != null) {
+        var name = json.name;
+    } else {
+        throw "Malformed JSON provided to DistributionBuilder " + json;
+    }
+     */
+    if(json.NormalDistribution) {
+        if(json.NormalDistribution.mean != null && json.NormalDistribution.standardDeviation != null) {
+            return new NormalDistribution(json.NormalDistribution.mean[0], json.NormalDistribution.standardDeviation[0]);
+        } else {
+            throw "Malformed JSON provided to DistributionBuilder " + json;
+        }
+    } else if (json.LogNormalDistribution) {
+        if(json.LogNormalDistribution.location != null && json.LogNormalDistribution.scale != null) {
+            return new LogNormalDistribution(json.LogNormalDistribution.location[0], json.LogNormalDistribution.scale[0]);
+        } else {
+            throw "Malformed JSON provided to DistributionBuilder " + json;
+        }
+    } else if (json.BetaDistribution) {
+        if(json.BetaDistribution.alpha != null && json.BetaDistribution.beta != null) {
+            return new BetaDistribution(json.BetaDistribution.alpha[0], json.BetaDistribution.beta[0]);
+        } else {
+            throw "Malformed JSON provided to DistributionBuilder " + json;
+        }
+    } else if (json.GammaDistribution) {
+        if(json.GammaDistribution.shape != null && json.GammaDistribution.scale != null) {
+            return new GammaDistribution(json.GammaDistribution.shape[0], json.GammaDistribution.scale[0]);
+        } else {
+            throw "Malformed JSON provided to DistributionBuilder " + json;
+        }
+    } else if (json.StudentTDistribution) {
+        if(json.StudentTDistribution.degreesOfFreedom != null && json.StudentTDistribution.nonCentralityParameter != null) {
+            return new StudentTDistribution(json.StudentTDistribution.degreesOfFreedom[0], json.StudentTDistribution.nonCentralityParameter[0]);
+        } else if(json.StudentTDistribution.degreesOfFreedom != null) {
+            return new StudentTDistribution(json.StudentTDistribution.degreesOfFreedom[0]);
+        } else {
+            throw "Malformed JSON provided to DistributionBuilder " + json;
+        }
+    } else {
+        throw "Malformed JSON provided to DistributionBuilder " + json;
+    }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/layers.tab	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,6 @@
+metformin_0.00001	layer_0.tab	10	86	
+a_very long_long_long_name_with_no convenient_text_wrapping_breakpoints <br/> <br/> <br/>now with some extra spaced text that should already wrap fine	layer_1.tab	11	86	
+tissue	layer_2.tab	1	20	
+iCluster.k25	layer_3.tab	2	20	
+what 1	layer_4.tab	2	20	12
+zap 2	layer_5.tab	2	20	11
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/maplabel-compiled.js	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,21 @@
+(function(){/*
+
+
+ Copyright 2011 Google Inc.
+
+ 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.
+*/
+var d="prototype";function e(a){this.set("fontFamily","sans-serif");this.set("fontSize",12);this.set("fontColor","#000000");this.set("strokeWeight",4);this.set("strokeColor","#ffffff");this.set("align","center");this.set("zIndex",1E3);this.setValues(a)}e.prototype=new google.maps.OverlayView;window.MapLabel=e;e[d].changed=function(a){switch(a){case "fontFamily":case "fontSize":case "fontColor":case "strokeWeight":case "strokeColor":case "align":case "text":return h(this);case "maxZoom":case "minZoom":case "position":return this.draw()}};
+function h(a){var b=a.a;if(b){var f=b.style;f.zIndex=a.get("zIndex");var c=b.getContext("2d");c.clearRect(0,0,b.width,b.height);c.strokeStyle=a.get("strokeColor");c.fillStyle=a.get("fontColor");c.font=a.get("fontSize")+"px "+a.get("fontFamily");var b=Number(a.get("strokeWeight")),g=a.get("text");if(g){if(b)c.lineWidth=b,c.strokeText(g,b,b);c.fillText(g,b,b);a:{c=c.measureText(g).width+b;switch(a.get("align")){case "left":a=0;break a;case "right":a=-c;break a}a=c/-2}f.marginLeft=a+"px";f.marginTop=
+"-0.4em"}}}e[d].onAdd=function(){var a=this.a=document.createElement("canvas");a.style.position="absolute";var b=a.getContext("2d");b.lineJoin="round";b.textBaseline="top";h(this);(b=this.getPanes())&&b.mapPane.appendChild(a)};e[d].onAdd=e[d].onAdd;
+e[d].draw=function(){var a=this.getProjection();if(a&&this.a){var b=this.get("position");if(b){b=a.fromLatLngToDivPixel(b);a=this.a.style;a.top=b.y+"px";a.left=b.x+"px";var b=this.get("minZoom"),f=this.get("maxZoom");if(b===void 0&&f===void 0)b="";else{var c=this.getMap();c?(c=c.getZoom(),b=c<b||c>f?"hidden":""):b=""}a.visibility=b}}};e[d].draw=e[d].draw;e[d].onRemove=function(){var a=this.a;a&&a.parentNode&&a.parentNode.removeChild(a)};e[d].onRemove=e[d].onRemove;})()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/matrices.tab	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,1 @@
+matrix_0.tab
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/matrix_0.tab	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,21 @@
+id	metformin_0.00001	a_very long_long_long_name_with_no convenient_text_wrapping_breakpoints <br/> <br/> <br/>now with some extra spaced text that should already wrap fine	tissue	iCluster.k25	binary 1	binary 2
+GSE14206_GPL8_stage=T3A_PHENOTYPE.tab	0.103656268715241	0.0662108455001999	0	1	1	0
+GSE18655_GPL5858_recurrence=No_Rec_PHENOTYPE.tab	0.00736496699455639	-0.0030872481570366	2	2	0	0
+GSE21034_GPL10264_pathological_stage=T4_PHENOTYPE.tab	0.212395889719257	0.104946746175943	3	3	1	1
+TCGA_tumor_level=Middle_PHENOTYPE.tab	-0.0750289367770828	-0.0160926008102573	4	4	0	1
+GSE14206_GPL8_stage=T2A_PHENOTYPE.tab	0.101591559700421	0.0581690019909618	5	5	1	1
+TCGA_clinical_spread_ct2=Induration_and+or_Nodularity_Involves___or_=__?_of_one_lobe__cT2a__PHENOTYPE.tab	-0.0448303097941127	-0.0555992149962499	6	6	1	0
+GSE21034_GPL10264_ERG-fusion_gex=True_PHENOTYPE.tab	0.0292437277400893	-0.0210673135177115	7	7	1	1
+GSE14206_GPL8_stage=T2C_PHENOTYPE.tab	-0.0942458205785588	-0.0632178924636636	8	8	1	0
+GSE18655_GPL5858_age_MEAN.tab	-0.0398766576526588	0.00359207190540213	9	9	0	1
+GSE18655_GPL5858_psa_MEAN.tab	-0.0959320347649498	-0.00882728114771138	11	10	0	1
+GSE21034_GPL10264_Gene_fusion=True_PHENOTYPE.tab	0.0292437277400893	-0.0210673135177115	1	11	0	0
+GSE14206_GPL8_stage=T3B_PHENOTYPE.tab	-0.105814702233279	-0.0740118918016848	2	12	0	0
+GSE21034_GPL10264_pathological_stage=NA_PHENOTYPE.tab	0.112017018347965	0.0251898331610073	3	13	1	0
+TCGA_zone_of_origin=Peripheral_Zone_PHENOTYPE.tab	-0.00304197273959563	-0.0207284395193551	4	14	1	1
+TCGA_diagnostic_mri_results=Extraprostatic_Extension_Localized__e.g._seminal_vesicles__PHENOTYPE.tab	0.00993944807969242	-0.0317703371649353	5	15	0	0
+TCGA_pathologic_spread_pt4=YES_PHENOTYPE.tab	-0.0724829088312745	-0.0274093321577233	6	16	0	1
+TCGA_shortest_dimension_MEAN.tab	-0.0777725626701397	-0.0166257461335536	7	17	1	1
+GSE21034_GPL10264_clint_stage=T2C_PHENOTYPE.tab	0.192148495282519	0.077324537538078	8	18	1	1
+TCGA_diagnostic_ct_abd_pelvis_performed=YES_PHENOTYPE.tab	0.098060120151555	0.0451944074068774	9	19	1	1
+GSE14206_GPL887_ets_group=ESE3_Low_PHENOTYPE.tab	-0.0777826036647061	-0.028060859132074	10	20	1	1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/right.svg	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="20"
+   height="20"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.3.1 r9886"
+   sodipodi:docname="right.svg">
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient3837">
+      <stop
+         style="stop-color:#000000;stop-opacity:0;"
+         offset="0"
+         id="stop3839" />
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="1"
+         id="stop3841" />
+    </linearGradient>
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3837"
+       id="radialGradient3845"
+       gradientUnits="userSpaceOnUse"
+       cx="7.1428571"
+       cy="7.3214283"
+       fx="7.1428571"
+       fy="7.3214283"
+       r="4.875" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3837"
+       id="radialGradient2989"
+       gradientUnits="userSpaceOnUse"
+       cx="7.1428571"
+       cy="7.3214283"
+       fx="7.1428571"
+       fy="7.3214283"
+       r="4.875" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3837"
+       id="radialGradient2993"
+       gradientUnits="userSpaceOnUse"
+       cx="7.1428571"
+       cy="7.3214283"
+       fx="7.1428571"
+       fy="7.3214283"
+       r="4.875" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="11.2"
+     inkscape:cx="-17.99843"
+     inkscape:cy="7.8642309"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1215"
+     inkscape:window-height="1000"
+     inkscape:window-x="65"
+     inkscape:window-y="24"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1032.3622)">
+    <path
+       style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 1.5,1034.3622 5,8 -5,8"
+       id="path2997"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccc" />
+    <path
+       sodipodi:nodetypes="ccc"
+       inkscape:connector-curvature="0"
+       id="path2999"
+       d="m 13.5,1034.3622 5,8 -5,8"
+       style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 7.5,1034.3622 5,8 -5,8"
+       id="path3001"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccc" />
+  </g>
+</svg>
Binary file hexagram/select2-spinner.gif has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/select2.css	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,652 @@
+/*
+Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
+*/
+.select2-container {
+    margin: 0;
+    position: relative;
+    display: inline-block;
+    /* inline-block for ie7 */
+    zoom: 1;
+    *display: inline;
+    vertical-align: middle;
+}
+
+.select2-container,
+.select2-drop,
+.select2-search,
+.select2-search input{
+  /*
+    Force border-box so that % widths fit the parent
+    container without overlap because of margin/padding.
+
+    More Info : http://www.quirksmode.org/css/box.html
+  */
+  -webkit-box-sizing: border-box; /* webkit */
+   -khtml-box-sizing: border-box; /* konqueror */
+     -moz-box-sizing: border-box; /* firefox */
+      -ms-box-sizing: border-box; /* ie */
+          box-sizing: border-box; /* css3 */
+}
+
+.select2-container .select2-choice {
+    display: block;
+    height: 26px;
+    padding: 0 0 0 8px;
+    overflow: hidden;
+    position: relative;
+
+    border: 1px solid #aaa;
+    white-space: nowrap;
+    line-height: 26px;
+    color: #444;
+    text-decoration: none;
+
+    -webkit-border-radius: 4px;
+       -moz-border-radius: 4px;
+            border-radius: 4px;
+
+    -webkit-background-clip: padding-box;
+       -moz-background-clip: padding;
+            background-clip: padding-box;
+
+    -webkit-touch-callout: none;
+      -webkit-user-select: none;
+       -khtml-user-select: none;
+         -moz-user-select: none;
+          -ms-user-select: none;
+              user-select: none;
+
+    background-color: #fff;
+    background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.5, white));
+    background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 50%);
+    background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 50%);
+    background-image: -o-linear-gradient(bottom, #eeeeee 0%, #ffffff 50%);
+    background-image: -ms-linear-gradient(top, #ffffff 0%, #eeeeee 50%);
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0);
+    background-image: linear-gradient(top, #ffffff 0%, #eeeeee 50%);
+}
+
+.select2-container.select2-drop-above .select2-choice {
+    border-bottom-color: #aaa;
+
+    -webkit-border-radius:0 0 4px 4px;
+       -moz-border-radius:0 0 4px 4px;
+            border-radius:0 0 4px 4px;
+
+    background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eeeeee), color-stop(0.9, white));
+    background-image: -webkit-linear-gradient(center bottom, #eeeeee 0%, white 90%);
+    background-image: -moz-linear-gradient(center bottom, #eeeeee 0%, white 90%);
+    background-image: -o-linear-gradient(bottom, #eeeeee 0%, white 90%);
+    background-image: -ms-linear-gradient(top, #eeeeee 0%,#ffffff 90%);
+    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee',GradientType=0 );
+    background-image: linear-gradient(top, #eeeeee 0%,#ffffff 90%);
+}
+
+.select2-container.select2-allowclear .select2-choice span {
+    margin-right: 42px;
+}
+
+.select2-container .select2-choice span {
+    margin-right: 26px;
+    display: block;
+    overflow: hidden;
+
+    white-space: nowrap;
+
+    -ms-text-overflow: ellipsis;
+     -o-text-overflow: ellipsis;
+        text-overflow: ellipsis;
+}
+
+.select2-container .select2-choice abbr {
+    display: none;
+    width: 12px;
+    height: 12px;
+    position: absolute;
+    right: 24px;
+    top: 8px;
+
+    font-size: 1px;
+    text-decoration: none;
+
+    border: 0;
+    background: url('select2.png') right top no-repeat;
+    cursor: pointer;
+    outline: 0;
+}
+
+.select2-container.select2-allowclear .select2-choice abbr {
+    display: inline-block;
+}
+
+.select2-container .select2-choice abbr:hover {
+    background-position: right -11px;
+    cursor: pointer;
+}
+
+.select2-drop-mask {
+    position: absolute;
+    left: 0;
+    top: 0;
+    z-index: 9998;
+}
+
+.select2-drop {
+    width: 100%;
+    margin-top:-1px;
+    position: absolute;
+    z-index: 9999;
+    top: 100%;
+
+    background: #fff;
+    color: #000;
+    border: 1px solid #aaa;
+    border-top: 0;
+
+    -webkit-border-radius: 0 0 4px 4px;
+       -moz-border-radius: 0 0 4px 4px;
+            border-radius: 0 0 4px 4px;
+
+    -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+       -moz-box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+            box-shadow: 0 4px 5px rgba(0, 0, 0, .15);
+}
+
+.select2-drop-auto-width {
+    border-top: 1px solid #aaa;
+    width: auto;
+}
+
+.select2-drop-auto-width .select2-search {
+    padding-top: 4px;
+}
+
+.select2-drop.select2-drop-above {
+    margin-top: 1px;
+    border-top: 1px solid #aaa;
+    border-bottom: 0;
+
+    -webkit-border-radius: 4px 4px 0 0;
+       -moz-border-radius: 4px 4px 0 0;
+            border-radius: 4px 4px 0 0;
+
+    -webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
+       -moz-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
+            box-shadow: 0 -4px 5px rgba(0, 0, 0, .15);
+}
+
+.select2-container .select2-choice div {
+    display: inline-block;
+    width: 18px;
+    height: 100%;
+    position: absolute;
+    right: 0;
+    top: 0;
+
+    border-left: 1px solid #aaa;
+    -webkit-border-radius: 0 4px 4px 0;
+       -moz-border-radius: 0 4px 4px 0;
+            border-radius: 0 4px 4px 0;
+
+    -webkit-background-clip: padding-box;
+       -moz-background-clip: padding;
+            background-clip: padding-box;
+
+    background: #ccc;
+    background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee));
+    background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%);
+    background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%);
+    background-image: -o-linear-gradient(bottom, #ccc 0%, #eee 60%);
+    background-image: -ms-linear-gradient(top, #cccccc 0%, #eeeeee 60%);
+    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#cccccc', GradientType = 0);
+    background-image: linear-gradient(top, #cccccc 0%, #eeeeee 60%);
+}
+
+.select2-container .select2-choice div b {
+    display: block;
+    width: 100%;
+    height: 100%;
+    background: url('select2.png') no-repeat 0 1px;
+}
+
+.select2-search {
+    display: inline-block;
+    width: 100%;
+    min-height: 26px;
+    margin: 0;
+    padding-left: 4px;
+    padding-right: 4px;
+
+    position: relative;
+    z-index: 10000;
+
+    white-space: nowrap;
+}
+
+.select2-search input {
+    width: 100%;
+    height: auto !important;
+    min-height: 26px;
+    padding: 4px 20px 4px 5px;
+    margin: 0;
+
+    outline: 0;
+    font-family: sans-serif;
+    font-size: 1em;
+
+    border: 1px solid #aaa;
+    -webkit-border-radius: 0;
+       -moz-border-radius: 0;
+            border-radius: 0;
+
+    -webkit-box-shadow: none;
+       -moz-box-shadow: none;
+            box-shadow: none;
+
+    background: #fff url('select2.png') no-repeat 100% -22px;
+    background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee));
+    background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%);
+    background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%);
+    background: url('select2.png') no-repeat 100% -22px, -o-linear-gradient(bottom, white 85%, #eeeeee 99%);
+    background: url('select2.png') no-repeat 100% -22px, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%);
+    background: url('select2.png') no-repeat 100% -22px, linear-gradient(top, #ffffff 85%, #eeeeee 99%);
+}
+
+.select2-drop.select2-drop-above .select2-search input {
+    margin-top: 4px;
+}
+
+.select2-search input.select2-active {
+    background: #fff url('select2-spinner.gif') no-repeat 100%;
+    background: url('select2-spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, white), color-stop(0.99, #eeeeee));
+    background: url('select2-spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, white 85%, #eeeeee 99%);
+    background: url('select2-spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, white 85%, #eeeeee 99%);
+    background: url('select2-spinner.gif') no-repeat 100%, -o-linear-gradient(bottom, white 85%, #eeeeee 99%);
+    background: url('select2-spinner.gif') no-repeat 100%, -ms-linear-gradient(top, #ffffff 85%, #eeeeee 99%);
+    background: url('select2-spinner.gif') no-repeat 100%, linear-gradient(top, #ffffff 85%, #eeeeee 99%);
+}
+
+.select2-container-active .select2-choice,
+.select2-container-active .select2-choices {
+    border: 1px solid #5897fb;
+    outline: none;
+
+    -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3);
+       -moz-box-shadow: 0 0 5px rgba(0,0,0,.3);
+            box-shadow: 0 0 5px rgba(0,0,0,.3);
+}
+
+.select2-dropdown-open .select2-choice {
+    border-bottom-color: transparent;
+    -webkit-box-shadow: 0 1px 0 #fff inset;
+       -moz-box-shadow: 0 1px 0 #fff inset;
+            box-shadow: 0 1px 0 #fff inset;
+
+    -webkit-border-bottom-left-radius: 0;
+        -moz-border-radius-bottomleft: 0;
+            border-bottom-left-radius: 0;
+
+    -webkit-border-bottom-right-radius: 0;
+        -moz-border-radius-bottomright: 0;
+            border-bottom-right-radius: 0;
+
+    background-color: #eee;
+    background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, white), color-stop(0.5, #eeeeee));
+    background-image: -webkit-linear-gradient(center bottom, white 0%, #eeeeee 50%);
+    background-image: -moz-linear-gradient(center bottom, white 0%, #eeeeee 50%);
+    background-image: -o-linear-gradient(bottom, white 0%, #eeeeee 50%);
+    background-image: -ms-linear-gradient(top, #ffffff 0%,#eeeeee 50%);
+    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 );
+    background-image: linear-gradient(top, #ffffff 0%,#eeeeee 50%);
+}
+
+.select2-dropdown-open.select2-drop-above .select2-choice,
+.select2-dropdown-open.select2-drop-above .select2-choices {
+    border: 1px solid #5897fb;
+    border-top-color: transparent;
+
+    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, white), color-stop(0.5, #eeeeee));
+    background-image: -webkit-linear-gradient(center top, white 0%, #eeeeee 50%);
+    background-image: -moz-linear-gradient(center top, white 0%, #eeeeee 50%);
+    background-image: -o-linear-gradient(top, white 0%, #eeeeee 50%);
+    background-image: -ms-linear-gradient(bottom, #ffffff 0%,#eeeeee 50%);
+    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff',GradientType=0 );
+    background-image: linear-gradient(bottom, #ffffff 0%,#eeeeee 50%);
+}
+
+.select2-dropdown-open .select2-choice div {
+    background: transparent;
+    border-left: none;
+    filter: none;
+}
+.select2-dropdown-open .select2-choice div b {
+    background-position: -18px 1px;
+}
+
+/* results */
+.select2-results {
+    max-height: 200px;
+    padding: 0 0 0 4px;
+    margin: 4px 4px 4px 0;
+    position: relative;
+    overflow-x: hidden;
+    overflow-y: auto;
+    -webkit-tap-highlight-color: rgba(0,0,0,0);
+}
+
+.select2-results ul.select2-result-sub {
+    margin: 0;
+    padding-left: 0;
+}
+
+.select2-results ul.select2-result-sub > li .select2-result-label { padding-left: 20px }
+.select2-results ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 40px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 60px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 80px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 100px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 110px }
+.select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 120px }
+
+.select2-results li {
+    list-style: none;
+    display: list-item;
+    background-image: none;
+}
+
+.select2-results li.select2-result-with-children > .select2-result-label {
+    font-weight: bold;
+}
+
+.select2-results .select2-result-label {
+    padding: 3px 7px 4px;
+    margin: 0;
+    cursor: pointer;
+
+    min-height: 1em;
+
+    -webkit-touch-callout: none;
+      -webkit-user-select: none;
+       -khtml-user-select: none;
+         -moz-user-select: none;
+          -ms-user-select: none;
+              user-select: none;
+}
+
+.select2-results .select2-highlighted {
+    background: #3875d7;
+    color: #fff;
+}
+
+.select2-results li em {
+    background: #feffde;
+    font-style: normal;
+}
+
+.select2-results .select2-highlighted em {
+    background: transparent;
+}
+
+.select2-results .select2-highlighted ul {
+    background: white;
+    color: #000;
+}
+
+
+.select2-results .select2-no-results,
+.select2-results .select2-searching,
+.select2-results .select2-selection-limit {
+    background: #f4f4f4;
+    display: list-item;
+}
+
+/*
+disabled look for disabled choices in the results dropdown
+*/
+.select2-results .select2-disabled.select2-highlighted {
+    color: #666;
+    background: #f4f4f4;
+    display: list-item;
+    cursor: default;
+}
+.select2-results .select2-disabled {
+  background: #f4f4f4;
+  display: list-item;
+  cursor: default;
+}
+
+.select2-results .select2-selected {
+    display: none;
+}
+
+.select2-more-results.select2-active {
+    background: #f4f4f4 url('select2-spinner.gif') no-repeat 100%;
+}
+
+.select2-more-results {
+    background: #f4f4f4;
+    display: list-item;
+}
+
+/* disabled styles */
+
+.select2-container.select2-container-disabled .select2-choice {
+    background-color: #f4f4f4;
+    background-image: none;
+    border: 1px solid #ddd;
+    cursor: default;
+}
+
+.select2-container.select2-container-disabled .select2-choice div {
+    background-color: #f4f4f4;
+    background-image: none;
+    border-left: 0;
+}
+
+.select2-container.select2-container-disabled .select2-choice abbr {
+    display: none;
+}
+
+
+/* multiselect */
+
+.select2-container-multi .select2-choices {
+    height: auto !important;
+    height: 1%;
+    margin: 0;
+    padding: 0;
+    position: relative;
+
+    border: 1px solid #aaa;
+    cursor: text;
+    overflow: hidden;
+
+    background-color: #fff;
+    background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff));
+    background-image: -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+    background-image: -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+    background-image: -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+    background-image: -ms-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+    background-image: linear-gradient(top, #eeeeee 1%, #ffffff 15%);
+}
+
+.select2-locked {
+  padding: 3px 5px 3px 5px !important;
+}
+
+.select2-container-multi .select2-choices {
+    min-height: 26px;
+}
+
+.select2-container-multi.select2-container-active .select2-choices {
+    border: 1px solid #5897fb;
+    outline: none;
+
+    -webkit-box-shadow: 0 0 5px rgba(0,0,0,.3);
+       -moz-box-shadow: 0 0 5px rgba(0,0,0,.3);
+            box-shadow: 0 0 5px rgba(0,0,0,.3);
+}
+.select2-container-multi .select2-choices li {
+    float: left;
+    list-style: none;
+}
+.select2-container-multi .select2-choices .select2-search-field {
+    margin: 0;
+    padding: 0;
+    white-space: nowrap;
+}
+
+.select2-container-multi .select2-choices .select2-search-field input {
+    padding: 5px;
+    margin: 1px 0;
+
+    font-family: sans-serif;
+    font-size: 100%;
+    color: #666;
+    outline: 0;
+    border: 0;
+    -webkit-box-shadow: none;
+       -moz-box-shadow: none;
+            box-shadow: none;
+    background: transparent !important;
+}
+
+.select2-container-multi .select2-choices .select2-search-field input.select2-active {
+    background: #fff url('select2-spinner.gif') no-repeat 100% !important;
+}
+
+.select2-default {
+    color: #999 !important;
+}
+
+.select2-container-multi .select2-choices .select2-search-choice {
+    padding: 3px 5px 3px 18px;
+    margin: 3px 0 3px 5px;
+    position: relative;
+
+    line-height: 13px;
+    color: #333;
+    cursor: default;
+    border: 1px solid #aaaaaa;
+
+    -webkit-border-radius: 3px;
+       -moz-border-radius: 3px;
+            border-radius: 3px;
+
+    -webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
+       -moz-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
+            box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
+
+    -webkit-background-clip: padding-box;
+       -moz-background-clip: padding;
+            background-clip: padding-box;
+
+    -webkit-touch-callout: none;
+      -webkit-user-select: none;
+       -khtml-user-select: none;
+         -moz-user-select: none;
+          -ms-user-select: none;
+              user-select: none;
+
+    background-color: #e4e4e4;
+    filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#f4f4f4', GradientType=0 );
+    background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
+    background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+    background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+    background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+    background-image: -ms-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+    background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
+}
+.select2-container-multi .select2-choices .select2-search-choice span {
+    cursor: default;
+}
+.select2-container-multi .select2-choices .select2-search-choice-focus {
+    background: #d4d4d4;
+}
+
+.select2-search-choice-close {
+    display: block;
+    width: 12px;
+    height: 13px;
+    position: absolute;
+    right: 3px;
+    top: 4px;
+
+    font-size: 1px;
+    outline: none;
+    background: url('select2.png') right top no-repeat;
+}
+
+.select2-container-multi .select2-search-choice-close {
+    left: 3px;
+}
+
+.select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover {
+  background-position: right -11px;
+}
+.select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close {
+    background-position: right -11px;
+}
+
+/* disabled styles */
+.select2-container-multi.select2-container-disabled .select2-choices{
+    background-color: #f4f4f4;
+    background-image: none;
+    border: 1px solid #ddd;
+    cursor: default;
+}
+
+.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice {
+    padding: 3px 5px 3px 5px;
+    border: 1px solid #ddd;
+    background-image: none;
+    background-color: #f4f4f4;
+}
+
+.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close {    display: none;
+    background:none;
+}
+/* end multiselect */
+
+
+.select2-result-selectable .select2-match,
+.select2-result-unselectable .select2-match {
+    text-decoration: underline;
+}
+
+.select2-offscreen, .select2-offscreen:focus {
+    clip: rect(0 0 0 0);
+    width: 1px;
+    height: 1px;
+    border: 0;
+    margin: 0;
+    padding: 0;
+    overflow: hidden;
+    position: absolute;
+    outline: 0;
+    left: 0px;
+}
+
+.select2-display-none {
+    display: none;
+}
+
+.select2-measure-scrollbar {
+    position: absolute;
+    top: -10000px;
+    left: -10000px;
+    width: 100px;
+    height: 100px;
+    overflow: scroll;
+}
+/* Retina-ize icons */
+
+@media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi)  {
+  .select2-search input, .select2-search-choice-close, .select2-container .select2-choice abbr, .select2-container .select2-choice div b {
+      background-image: url('select2x2.png') !important;
+      background-repeat: no-repeat !important;
+      background-size: 60px 40px !important;
+  }
+  .select2-search input {
+      background-position: 100% -21px !important;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/select2.js	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,3054 @@
+/*
+Copyright 2012 Igor Vaynberg
+
+Version: 3.4.0 Timestamp: Tue May 14 08:27:33 PDT 2013
+
+This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU
+General Public License version 2 (the "GPL License"). You may choose either license to govern your
+use of this software only upon the condition that you accept all of the terms of either the Apache
+License or the GPL License.
+
+You may obtain a copy of the Apache License and the GPL License at:
+
+    http://www.apache.org/licenses/LICENSE-2.0
+    http://www.gnu.org/licenses/gpl-2.0.html
+
+Unless required by applicable law or agreed to in writing, software distributed under the
+Apache License or the GPL Licesnse is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
+CONDITIONS OF ANY KIND, either express or implied. See the Apache License and the GPL License for
+the specific language governing permissions and limitations under the Apache License and the GPL License.
+*/
+ (function ($) {
+ 	if(typeof $.fn.each2 == "undefined"){
+ 		$.fn.extend({
+ 			/*
+			* 4-10 times faster .each replacement
+			* use it carefully, as it overrides jQuery context of element on each iteration
+			*/
+			each2 : function (c) {
+				var j = $([0]), i = -1, l = this.length;
+				while (
+					++i < l
+					&& (j.context = j[0] = this[i])
+					&& c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object
+				);
+				return this;
+			}
+ 		});
+ 	}
+})(jQuery);
+
+(function ($, undefined) {
+    "use strict";
+    /*global document, window, jQuery, console */
+
+    if (window.Select2 !== undefined) {
+        return;
+    }
+
+    var KEY, AbstractSelect2, SingleSelect2, MultiSelect2, nextUid, sizer,
+        lastMousePosition, $document, scrollBarDimensions,
+
+    KEY = {
+        TAB: 9,
+        ENTER: 13,
+        ESC: 27,
+        SPACE: 32,
+        LEFT: 37,
+        UP: 38,
+        RIGHT: 39,
+        DOWN: 40,
+        SHIFT: 16,
+        CTRL: 17,
+        ALT: 18,
+        PAGE_UP: 33,
+        PAGE_DOWN: 34,
+        HOME: 36,
+        END: 35,
+        BACKSPACE: 8,
+        DELETE: 46,
+        isArrow: function (k) {
+            k = k.which ? k.which : k;
+            switch (k) {
+            case KEY.LEFT:
+            case KEY.RIGHT:
+            case KEY.UP:
+            case KEY.DOWN:
+                return true;
+            }
+            return false;
+        },
+        isControl: function (e) {
+            var k = e.which;
+            switch (k) {
+            case KEY.SHIFT:
+            case KEY.CTRL:
+            case KEY.ALT:
+                return true;
+            }
+
+            if (e.metaKey) return true;
+
+            return false;
+        },
+        isFunctionKey: function (k) {
+            k = k.which ? k.which : k;
+            return k >= 112 && k <= 123;
+        }
+    },
+    MEASURE_SCROLLBAR_TEMPLATE = "<div class='select2-measure-scrollbar'></div>";
+
+    $document = $(document);
+
+    nextUid=(function() { var counter=1; return function() { return counter++; }; }());
+
+    function indexOf(value, array) {
+        var i = 0, l = array.length;
+        for (; i < l; i = i + 1) {
+            if (equal(value, array[i])) return i;
+        }
+        return -1;
+    }
+
+    function measureScrollbar () {
+        var $template = $( MEASURE_SCROLLBAR_TEMPLATE );
+        $template.appendTo('body');
+
+        var dim = {
+            width: $template.width() - $template[0].clientWidth,
+            height: $template.height() - $template[0].clientHeight
+        };
+        $template.remove();
+
+        return dim;
+    }
+
+    /**
+     * Compares equality of a and b
+     * @param a
+     * @param b
+     */
+    function equal(a, b) {
+        if (a === b) return true;
+        if (a === undefined || b === undefined) return false;
+        if (a === null || b === null) return false;
+        if (a.constructor === String) return a+'' === b+''; // IE requires a+'' instead of just a
+        if (b.constructor === String) return b+'' === a+''; // IE requires b+'' instead of just b
+        return false;
+    }
+
+    /**
+     * Splits the string into an array of values, trimming each value. An empty array is returned for nulls or empty
+     * strings
+     * @param string
+     * @param separator
+     */
+    function splitVal(string, separator) {
+        var val, i, l;
+        if (string === null || string.length < 1) return [];
+        val = string.split(separator);
+        for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
+        return val;
+    }
+
+    function getSideBorderPadding(element) {
+        return element.outerWidth(false) - element.width();
+    }
+
+    function installKeyUpChangeEvent(element) {
+        var key="keyup-change-value";
+        element.on("keydown", function () {
+            if ($.data(element, key) === undefined) {
+                $.data(element, key, element.val());
+            }
+        });
+        element.on("keyup", function () {
+            var val= $.data(element, key);
+            if (val !== undefined && element.val() !== val) {
+                $.removeData(element, key);
+                element.trigger("keyup-change");
+            }
+        });
+    }
+
+    $document.on("mousemove", function (e) {
+        lastMousePosition = {x: e.pageX, y: e.pageY};
+    });
+
+    /**
+     * filters mouse events so an event is fired only if the mouse moved.
+     *
+     * filters out mouse events that occur when mouse is stationary but
+     * the elements under the pointer are scrolled.
+     */
+    function installFilteredMouseMove(element) {
+	    element.on("mousemove", function (e) {
+            var lastpos = lastMousePosition;
+            if (lastpos === undefined || lastpos.x !== e.pageX || lastpos.y !== e.pageY) {
+                $(e.target).trigger("mousemove-filtered", e);
+            }
+        });
+    }
+
+    /**
+     * Debounces a function. Returns a function that calls the original fn function only if no invocations have been made
+     * within the last quietMillis milliseconds.
+     *
+     * @param quietMillis number of milliseconds to wait before invoking fn
+     * @param fn function to be debounced
+     * @param ctx object to be used as this reference within fn
+     * @return debounced version of fn
+     */
+    function debounce(quietMillis, fn, ctx) {
+        ctx = ctx || undefined;
+        var timeout;
+        return function () {
+            var args = arguments;
+            window.clearTimeout(timeout);
+            timeout = window.setTimeout(function() {
+                fn.apply(ctx, args);
+            }, quietMillis);
+        };
+    }
+
+    /**
+     * A simple implementation of a thunk
+     * @param formula function used to lazily initialize the thunk
+     * @return {Function}
+     */
+    function thunk(formula) {
+        var evaluated = false,
+            value;
+        return function() {
+            if (evaluated === false) { value = formula(); evaluated = true; }
+            return value;
+        };
+    };
+
+    function installDebouncedScroll(threshold, element) {
+        var notify = debounce(threshold, function (e) { element.trigger("scroll-debounced", e);});
+        element.on("scroll", function (e) {
+            if (indexOf(e.target, element.get()) >= 0) notify(e);
+        });
+    }
+
+    function focus($el) {
+        if ($el[0] === document.activeElement) return;
+
+        /* set the focus in a 0 timeout - that way the focus is set after the processing
+            of the current event has finished - which seems like the only reliable way
+            to set focus */
+        window.setTimeout(function() {
+            var el=$el[0], pos=$el.val().length, range;
+
+            $el.focus();
+
+            /* make sure el received focus so we do not error out when trying to manipulate the caret.
+                sometimes modals or others listeners may steal it after its set */
+            if ($el.is(":visible") && el === document.activeElement) {
+
+                /* after the focus is set move the caret to the end, necessary when we val()
+                    just before setting focus */
+                if(el.setSelectionRange)
+                {
+                    el.setSelectionRange(pos, pos);
+                }
+                else if (el.createTextRange) {
+                    range = el.createTextRange();
+                    range.collapse(false);
+                    range.select();
+                }
+            }
+        }, 0);
+    }
+
+    function getCursorInfo(el) {
+        el = $(el)[0];
+        var offset = 0;
+        var length = 0;
+        if ('selectionStart' in el) {
+            offset = el.selectionStart;
+            length = el.selectionEnd - offset;
+        } else if ('selection' in document) {
+            el.focus();
+            var sel = document.selection.createRange();
+            length = document.selection.createRange().text.length;
+            sel.moveStart('character', -el.value.length);
+            offset = sel.text.length - length;
+        }
+        return { offset: offset, length: length };
+    }
+
+    function killEvent(event) {
+        event.preventDefault();
+        event.stopPropagation();
+    }
+    function killEventImmediately(event) {
+        event.preventDefault();
+        event.stopImmediatePropagation();
+    }
+
+    function measureTextWidth(e) {
+        if (!sizer){
+        	var style = e[0].currentStyle || window.getComputedStyle(e[0], null);
+        	sizer = $(document.createElement("div")).css({
+	            position: "absolute",
+	            left: "-10000px",
+	            top: "-10000px",
+	            display: "none",
+	            fontSize: style.fontSize,
+	            fontFamily: style.fontFamily,
+	            fontStyle: style.fontStyle,
+	            fontWeight: style.fontWeight,
+	            letterSpacing: style.letterSpacing,
+	            textTransform: style.textTransform,
+	            whiteSpace: "nowrap"
+	        });
+            sizer.attr("class","select2-sizer");
+        	$("body").append(sizer);
+        }
+        sizer.text(e.val());
+        return sizer.width();
+    }
+
+    function syncCssClasses(dest, src, adapter) {
+        var classes, replacements = [], adapted;
+
+        classes = dest.attr("class");
+        if (classes) {
+            classes = '' + classes; // for IE which returns object
+            $(classes.split(" ")).each2(function() {
+                if (this.indexOf("select2-") === 0) {
+                    replacements.push(this);
+                }
+            });
+        }
+        classes = src.attr("class");
+        if (classes) {
+            classes = '' + classes; // for IE which returns object
+            $(classes.split(" ")).each2(function() {
+                if (this.indexOf("select2-") !== 0) {
+                    adapted = adapter(this);
+                    if (adapted) {
+                        replacements.push(this);
+                    }
+                }
+            });
+        }
+        dest.attr("class", replacements.join(" "));
+    }
+
+
+    function markMatch(text, term, markup, escapeMarkup) {
+        var match=text.toUpperCase().indexOf(term.toUpperCase()),
+            tl=term.length;
+
+        if (match<0) {
+            markup.push(escapeMarkup(text));
+            return;
+        }
+
+        markup.push(escapeMarkup(text.substring(0, match)));
+        markup.push("<span class='select2-match'>");
+        markup.push(escapeMarkup(text.substring(match, match + tl)));
+        markup.push("</span>");
+        markup.push(escapeMarkup(text.substring(match + tl, text.length)));
+    }
+
+    /**
+     * Produces an ajax-based query function
+     *
+     * @param options object containing configuration paramters
+     * @param options.params parameter map for the transport ajax call, can contain such options as cache, jsonpCallback, etc. see $.ajax
+     * @param options.transport function that will be used to execute the ajax request. must be compatible with parameters supported by $.ajax
+     * @param options.url url for the data
+     * @param options.data a function(searchTerm, pageNumber, context) that should return an object containing query string parameters for the above url.
+     * @param options.dataType request data type: ajax, jsonp, other datatatypes supported by jQuery's $.ajax function or the transport function if specified
+     * @param options.quietMillis (optional) milliseconds to wait before making the ajaxRequest, helps debounce the ajax function if invoked too often
+     * @param options.results a function(remoteData, pageNumber) that converts data returned form the remote request to the format expected by Select2.
+     *      The expected format is an object containing the following keys:
+     *      results array of objects that will be used as choices
+     *      more (optional) boolean indicating whether there are more results available
+     *      Example: {results:[{id:1, text:'Red'},{id:2, text:'Blue'}], more:true}
+     */
+    function ajax(options) {
+        var timeout, // current scheduled but not yet executed request
+            requestSequence = 0, // sequence used to drop out-of-order responses
+            handler = null,
+            quietMillis = options.quietMillis || 100,
+            ajaxUrl = options.url,
+            self = this;
+
+        return function (query) {
+            window.clearTimeout(timeout);
+            timeout = window.setTimeout(function () {
+                requestSequence += 1; // increment the sequence
+                var requestNumber = requestSequence, // this request's sequence number
+                    data = options.data, // ajax data function
+                    url = ajaxUrl, // ajax url string or function
+                    transport = options.transport || $.fn.select2.ajaxDefaults.transport,
+                    // deprecated - to be removed in 4.0  - use params instead
+                    deprecated = {
+                        type: options.type || 'GET', // set type of request (GET or POST)
+                        cache: options.cache || false,
+                        jsonpCallback: options.jsonpCallback||undefined,
+                        dataType: options.dataType||"json"
+                    },
+                    params = $.extend({}, $.fn.select2.ajaxDefaults.params, deprecated);
+
+                data = data ? data.call(self, query.term, query.page, query.context) : null;
+                url = (typeof url === 'function') ? url.call(self, query.term, query.page, query.context) : url;
+
+                if( null !== handler) { handler.abort(); }
+
+                if (options.params) {
+                    if ($.isFunction(options.params)) {
+                        $.extend(params, options.params.call(self));
+                    } else {
+                        $.extend(params, options.params);
+                    }
+                }
+
+                $.extend(params, {
+                    url: url,
+                    dataType: options.dataType,
+                    data: data,
+                    success: function (data) {
+                        if (requestNumber < requestSequence) {
+                            return;
+                        }
+                        // TODO - replace query.page with query so users have access to term, page, etc.
+                        var results = options.results(data, query.page);
+                        query.callback(results);
+                    }
+                });
+                handler = transport.call(self, params);
+            }, quietMillis);
+        };
+    }
+
+    /**
+     * Produces a query function that works with a local array
+     *
+     * @param options object containing configuration parameters. The options parameter can either be an array or an
+     * object.
+     *
+     * If the array form is used it is assumed that it contains objects with 'id' and 'text' keys.
+     *
+     * If the object form is used ti is assumed that it contains 'data' and 'text' keys. The 'data' key should contain
+     * an array of objects that will be used as choices. These objects must contain at least an 'id' key. The 'text'
+     * key can either be a String in which case it is expected that each element in the 'data' array has a key with the
+     * value of 'text' which will be used to match choices. Alternatively, text can be a function(item) that can extract
+     * the text.
+     */
+    function local(options) {
+        var data = options, // data elements
+            dataText,
+            tmp,
+            text = function (item) { return ""+item.text; }; // function used to retrieve the text portion of a data item that is matched against the search
+
+		 if ($.isArray(data)) {
+            tmp = data;
+            data = { results: tmp };
+        }
+
+		 if ($.isFunction(data) === false) {
+            tmp = data;
+            data = function() { return tmp; };
+        }
+
+        var dataItem = data();
+        if (dataItem.text) {
+            text = dataItem.text;
+            // if text is not a function we assume it to be a key name
+            if (!$.isFunction(text)) {
+                dataText = dataItem.text; // we need to store this in a separate variable because in the next step data gets reset and data.text is no longer available
+                text = function (item) { return item[dataText]; };
+            }
+        }
+
+        return function (query) {
+            var t = query.term, filtered = { results: [] }, process;
+            if (t === "") {
+                query.callback(data());
+                return;
+            }
+
+            process = function(datum, collection) {
+                var group, attr;
+                datum = datum[0];
+                if (datum.children) {
+                    group = {};
+                    for (attr in datum) {
+                        if (datum.hasOwnProperty(attr)) group[attr]=datum[attr];
+                    }
+                    group.children=[];
+                    $(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); });
+                    if (group.children.length || query.matcher(t, text(group), datum)) {
+                        collection.push(group);
+                    }
+                } else {
+                    if (query.matcher(t, text(datum), datum)) {
+                        collection.push(datum);
+                    }
+                }
+            };
+
+            $(data().results).each2(function(i, datum) { process(datum, filtered.results); });
+            query.callback(filtered);
+        };
+    }
+
+    // TODO javadoc
+    function tags(data) {
+        var isFunc = $.isFunction(data);
+        return function (query) {
+            var t = query.term, filtered = {results: []};
+            $(isFunc ? data() : data).each(function () {
+                var isObject = this.text !== undefined,
+                    text = isObject ? this.text : this;
+                if (t === "" || query.matcher(t, text)) {
+                    filtered.results.push(isObject ? this : {id: this, text: this});
+                }
+            });
+            query.callback(filtered);
+        };
+    }
+
+    /**
+     * Checks if the formatter function should be used.
+     *
+     * Throws an error if it is not a function. Returns true if it should be used,
+     * false if no formatting should be performed.
+     *
+     * @param formatter
+     */
+    function checkFormatter(formatter, formatterName) {
+        if ($.isFunction(formatter)) return true;
+        if (!formatter) return false;
+        throw new Error("formatterName must be a function or a falsy value");
+    }
+
+    function evaluate(val) {
+        return $.isFunction(val) ? val() : val;
+    }
+
+    function countResults(results) {
+        var count = 0;
+        $.each(results, function(i, item) {
+            if (item.children) {
+                count += countResults(item.children);
+            } else {
+                count++;
+            }
+        });
+        return count;
+    }
+
+    /**
+     * Default tokenizer. This function uses breaks the input on substring match of any string from the
+     * opts.tokenSeparators array and uses opts.createSearchChoice to create the choice object. Both of those
+     * two options have to be defined in order for the tokenizer to work.
+     *
+     * @param input text user has typed so far or pasted into the search field
+     * @param selection currently selected choices
+     * @param selectCallback function(choice) callback tho add the choice to selection
+     * @param opts select2's opts
+     * @return undefined/null to leave the current input unchanged, or a string to change the input to the returned value
+     */
+    function defaultTokenizer(input, selection, selectCallback, opts) {
+        var original = input, // store the original so we can compare and know if we need to tell the search to update its text
+            dupe = false, // check for whether a token we extracted represents a duplicate selected choice
+            token, // token
+            index, // position at which the separator was found
+            i, l, // looping variables
+            separator; // the matched separator
+
+        if (!opts.createSearchChoice || !opts.tokenSeparators || opts.tokenSeparators.length < 1) return undefined;
+
+        while (true) {
+            index = -1;
+
+            for (i = 0, l = opts.tokenSeparators.length; i < l; i++) {
+                separator = opts.tokenSeparators[i];
+                index = input.indexOf(separator);
+                if (index >= 0) break;
+            }
+
+            if (index < 0) break; // did not find any token separator in the input string, bail
+
+            token = input.substring(0, index);
+            input = input.substring(index + separator.length);
+
+            if (token.length > 0) {
+                token = opts.createSearchChoice(token, selection);
+                if (token !== undefined && token !== null && opts.id(token) !== undefined && opts.id(token) !== null) {
+                    dupe = false;
+                    for (i = 0, l = selection.length; i < l; i++) {
+                        if (equal(opts.id(token), opts.id(selection[i]))) {
+                            dupe = true; break;
+                        }
+                    }
+
+                    if (!dupe) selectCallback(token);
+                }
+            }
+        }
+
+        if (original!==input) return input;
+    }
+
+    /**
+     * Creates a new class
+     *
+     * @param superClass
+     * @param methods
+     */
+    function clazz(SuperClass, methods) {
+        var constructor = function () {};
+        constructor.prototype = new SuperClass;
+        constructor.prototype.constructor = constructor;
+        constructor.prototype.parent = SuperClass.prototype;
+        constructor.prototype = $.extend(constructor.prototype, methods);
+        return constructor;
+    }
+
+    AbstractSelect2 = clazz(Object, {
+
+        // abstract
+        bind: function (func) {
+            var self = this;
+            return function () {
+                func.apply(self, arguments);
+            };
+        },
+
+        // abstract
+        init: function (opts) {
+            var results, search, resultsSelector = ".select2-results", disabled, readonly;
+
+            // prepare options
+            this.opts = opts = this.prepareOpts(opts);
+
+            this.id=opts.id;
+
+            // destroy if called on an existing component
+            if (opts.element.data("select2") !== undefined &&
+                opts.element.data("select2") !== null) {
+                this.destroy();
+            }
+
+            this.container = this.createContainer();
+
+            this.containerId="s2id_"+(opts.element.attr("id") || "autogen"+nextUid());
+            this.containerSelector="#"+this.containerId.replace(/([;&,\.\+\*\~':"\!\^#$%@\[\]\(\)=>\|])/g, '\\$1');
+            this.container.attr("id", this.containerId);
+
+            // cache the body so future lookups are cheap
+            this.body = thunk(function() { return opts.element.closest("body"); });
+
+            syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
+
+            this.container.css(evaluate(opts.containerCss));
+            this.container.addClass(evaluate(opts.containerCssClass));
+
+            this.elementTabIndex = this.opts.element.attr("tabindex");
+
+            // swap container for the element
+            this.opts.element
+                .data("select2", this)
+                .attr("tabindex", "-1")
+                .before(this.container);
+            this.container.data("select2", this);
+
+            this.dropdown = this.container.find(".select2-drop");
+            this.dropdown.addClass(evaluate(opts.dropdownCssClass));
+            this.dropdown.data("select2", this);
+
+            this.results = results = this.container.find(resultsSelector);
+            this.search = search = this.container.find("input.select2-input");
+
+            this.resultsPage = 0;
+            this.context = null;
+
+            // initialize the container
+            this.initContainer();
+
+            installFilteredMouseMove(this.results);
+            this.dropdown.on("mousemove-filtered touchstart touchmove touchend", resultsSelector, this.bind(this.highlightUnderEvent));
+
+            installDebouncedScroll(80, this.results);
+            this.dropdown.on("scroll-debounced", resultsSelector, this.bind(this.loadMoreIfNeeded));
+
+            // do not propagate change event from the search field out of the component
+            $(this.container).on("change", ".select2-input", function(e) {e.stopPropagation();});
+            $(this.dropdown).on("change", ".select2-input", function(e) {e.stopPropagation();});
+
+            // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel
+            if ($.fn.mousewheel) {
+                results.mousewheel(function (e, delta, deltaX, deltaY) {
+                    var top = results.scrollTop(), height;
+                    if (deltaY > 0 && top - deltaY <= 0) {
+                        results.scrollTop(0);
+                        killEvent(e);
+                    } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) {
+                        results.scrollTop(results.get(0).scrollHeight - results.height());
+                        killEvent(e);
+                    }
+                });
+            }
+
+            installKeyUpChangeEvent(search);
+            search.on("keyup-change input paste", this.bind(this.updateResults));
+            search.on("focus", function () { search.addClass("select2-focused"); });
+            search.on("blur", function () { search.removeClass("select2-focused");});
+
+            this.dropdown.on("mouseup", resultsSelector, this.bind(function (e) {
+                if ($(e.target).closest(".select2-result-selectable").length > 0) {
+                    this.highlightUnderEvent(e);
+                    this.selectHighlighted(e);
+                }
+            }));
+
+            // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening
+            // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's
+            // dom it will trigger the popup close, which is not what we want
+            this.dropdown.on("click mouseup mousedown", function (e) { e.stopPropagation(); });
+
+            if ($.isFunction(this.opts.initSelection)) {
+                // initialize selection based on the current value of the source element
+                this.initSelection();
+
+                // if the user has provided a function that can set selection based on the value of the source element
+                // we monitor the change event on the element and trigger it, allowing for two way synchronization
+                this.monitorSource();
+            }
+
+            if (opts.maximumInputLength !== null) {
+                this.search.attr("maxlength", opts.maximumInputLength);
+            }
+
+            var disabled = opts.element.prop("disabled");
+            if (disabled === undefined) disabled = false;
+            this.enable(!disabled);
+
+            var readonly = opts.element.prop("readonly");
+            if (readonly === undefined) readonly = false;
+            this.readonly(readonly);
+
+            // Calculate size of scrollbar
+            scrollBarDimensions = scrollBarDimensions || measureScrollbar();
+
+            this.autofocus = opts.element.prop("autofocus")
+            opts.element.prop("autofocus", false);
+            if (this.autofocus) this.focus();
+        },
+
+        // abstract
+        destroy: function () {
+            var select2 = this.opts.element.data("select2");
+
+            if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; }
+
+            if (select2 !== undefined) {
+
+                select2.container.remove();
+                select2.dropdown.remove();
+                select2.opts.element
+                    .removeClass("select2-offscreen")
+                    .removeData("select2")
+                    .off(".select2")
+                    .attr({"tabindex": this.elementTabIndex})
+                    .prop("autofocus", this.autofocus||false)
+                    .show();
+            }
+        },
+
+        // abstract
+        optionToData: function(element) {
+            if (element.is("option")) {
+                return {
+                    id:element.prop("value"),
+                    text:element.text(),
+                    element: element.get(),
+                    css: element.attr("class"),
+                    disabled: element.prop("disabled"),
+                    locked: equal(element.attr("locked"), "locked")
+                };
+            } else if (element.is("optgroup")) {
+                return {
+                    text:element.attr("label"),
+                    children:[],
+                    element: element.get(),
+                    css: element.attr("class")
+                };
+            }
+        },
+
+        // abstract
+        prepareOpts: function (opts) {
+            var element, select, idKey, ajaxUrl, self = this;
+
+            element = opts.element;
+
+            if (element.get(0).tagName.toLowerCase() === "select") {
+                this.select = select = opts.element;
+            }
+
+            if (select) {
+                // these options are not allowed when attached to a select because they are picked up off the element itself
+                $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
+                    if (this in opts) {
+                        throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a <select> element.");
+                    }
+                });
+            }
+
+            opts = $.extend({}, {
+                populateResults: function(container, results, query) {
+                    var populate,  data, result, children, id=this.opts.id;
+
+                    populate=function(results, container, depth) {
+
+                        var i, l, result, selectable, disabled, compound, node, label, innerContainer, formatted;
+
+                        results = opts.sortResults(results, container, query);
+
+                        for (i = 0, l = results.length; i < l; i = i + 1) {
+
+                            result=results[i];
+
+                            disabled = (result.disabled === true);
+                            selectable = (!disabled) && (id(result) !== undefined);
+
+                            compound=result.children && result.children.length > 0;
+
+                            node=$("<li></li>");
+                            node.addClass("select2-results-dept-"+depth);
+                            node.addClass("select2-result");
+                            node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable");
+                            if (disabled) { node.addClass("select2-disabled"); }
+                            if (compound) { node.addClass("select2-result-with-children"); }
+                            node.addClass(self.opts.formatResultCssClass(result));
+
+                            label=$(document.createElement("div"));
+                            label.addClass("select2-result-label");
+
+                            formatted=opts.formatResult(result, label, query, self.opts.escapeMarkup);
+                            if (formatted!==undefined) {
+                                label.html(formatted);
+                            }
+
+                            node.append(label);
+
+                            if (compound) {
+
+                                innerContainer=$("<ul></ul>");
+                                innerContainer.addClass("select2-result-sub");
+                                populate(result.children, innerContainer, depth+1);
+                                node.append(innerContainer);
+                            }
+
+                            node.data("select2-data", result);
+                            container.append(node);
+                        }
+                    };
+
+                    populate(results, container, 0);
+                }
+            }, $.fn.select2.defaults, opts);
+
+            if (typeof(opts.id) !== "function") {
+                idKey = opts.id;
+                opts.id = function (e) { return e[idKey]; };
+            }
+
+            if ($.isArray(opts.element.data("select2Tags"))) {
+                if ("tags" in opts) {
+                    throw "tags specified as both an attribute 'data-select2-tags' and in options of Select2 " + opts.element.attr("id");
+                }
+                opts.tags=opts.element.data("select2Tags");
+            }
+
+            if (select) {
+                opts.query = this.bind(function (query) {
+                    var data = { results: [], more: false },
+                        term = query.term,
+                        children, firstChild, process;
+
+                    process=function(element, collection) {
+                        var group;
+                        if (element.is("option")) {
+                            if (query.matcher(term, element.text(), element)) {
+                                collection.push(self.optionToData(element));
+                            }
+                        } else if (element.is("optgroup")) {
+                            group=self.optionToData(element);
+                            element.children().each2(function(i, elm) { process(elm, group.children); });
+                            if (group.children.length>0) {
+                                collection.push(group);
+                            }
+                        }
+                    };
+
+                    children=element.children();
+
+                    // ignore the placeholder option if there is one
+                    if (this.getPlaceholder() !== undefined && children.length > 0) {
+                        firstChild = children[0];
+                        if ($(firstChild).text() === "") {
+                            children=children.not(firstChild);
+                        }
+                    }
+
+                    children.each2(function(i, elm) { process(elm, data.results); });
+
+                    query.callback(data);
+                });
+                // this is needed because inside val() we construct choices from options and there id is hardcoded
+                opts.id=function(e) { return e.id; };
+                opts.formatResultCssClass = function(data) { return data.css; };
+            } else {
+                if (!("query" in opts)) {
+
+                    if ("ajax" in opts) {
+                        ajaxUrl = opts.element.data("ajax-url");
+                        if (ajaxUrl && ajaxUrl.length > 0) {
+                            opts.ajax.url = ajaxUrl;
+                        }
+                        opts.query = ajax.call(opts.element, opts.ajax);
+                    } else if ("data" in opts) {
+                        opts.query = local(opts.data);
+                    } else if ("tags" in opts) {
+                        opts.query = tags(opts.tags);
+                        if (opts.createSearchChoice === undefined) {
+                            opts.createSearchChoice = function (term) { return {id: term, text: term}; };
+                        }
+                        if (opts.initSelection === undefined) {
+                            opts.initSelection = function (element, callback) {
+                                var data = [];
+                                $(splitVal(element.val(), opts.separator)).each(function () {
+                                    var id = this, text = this, tags=opts.tags;
+                                    if ($.isFunction(tags)) tags=tags();
+                                    $(tags).each(function() { if (equal(this.id, id)) { text = this.text; return false; } });
+                                    data.push({id: id, text: text});
+                                });
+
+                                callback(data);
+                            };
+                        }
+                    }
+                }
+            }
+            if (typeof(opts.query) !== "function") {
+                throw "query function not defined for Select2 " + opts.element.attr("id");
+            }
+
+            return opts;
+        },
+
+        /**
+         * Monitor the original element for changes and update select2 accordingly
+         */
+        // abstract
+        monitorSource: function () {
+            var el = this.opts.element, sync;
+
+            el.on("change.select2", this.bind(function (e) {
+                if (this.opts.element.data("select2-change-triggered") !== true) {
+                    this.initSelection();
+                }
+            }));
+
+            sync = this.bind(function () {
+
+                var enabled, readonly, self = this;
+
+                // sync enabled state
+
+                var disabled = el.prop("disabled");
+                if (disabled === undefined) disabled = false;
+                this.enable(!disabled);
+
+                var readonly = el.prop("readonly");
+                if (readonly === undefined) readonly = false;
+                this.readonly(readonly);
+
+                syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
+                this.container.addClass(evaluate(this.opts.containerCssClass));
+
+                syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass);
+                this.dropdown.addClass(evaluate(this.opts.dropdownCssClass));
+
+            });
+
+            // mozilla and IE
+            el.on("propertychange.select2 DOMAttrModified.select2", sync);
+
+
+            // hold onto a reference of the callback to work around a chromium bug
+            if (this.mutationCallback === undefined) {
+                this.mutationCallback = function (mutations) {
+                    mutations.forEach(sync);
+                }
+            }
+
+            // safari and chrome
+            if (typeof WebKitMutationObserver !== "undefined") {
+                if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; }
+                this.propertyObserver = new WebKitMutationObserver(this.mutationCallback);
+                this.propertyObserver.observe(el.get(0), { attributes:true, subtree:false });
+            }
+        },
+
+        // abstract
+        triggerSelect: function(data) {
+            var evt = $.Event("select2-selecting", { val: this.id(data), object: data });
+            this.opts.element.trigger(evt);
+            return !evt.isDefaultPrevented();
+        },
+
+        /**
+         * Triggers the change event on the source element
+         */
+        // abstract
+        triggerChange: function (details) {
+
+            details = details || {};
+            details= $.extend({}, details, { type: "change", val: this.val() });
+            // prevents recursive triggering
+            this.opts.element.data("select2-change-triggered", true);
+            this.opts.element.trigger(details);
+            this.opts.element.data("select2-change-triggered", false);
+
+            // some validation frameworks ignore the change event and listen instead to keyup, click for selects
+            // so here we trigger the click event manually
+            this.opts.element.click();
+
+            // ValidationEngine ignorea the change event and listens instead to blur
+            // so here we trigger the blur event manually if so desired
+            if (this.opts.blurOnChange)
+                this.opts.element.blur();
+        },
+
+        //abstract
+        isInterfaceEnabled: function()
+        {
+            return this.enabledInterface === true;
+        },
+
+        // abstract
+        enableInterface: function() {
+            var enabled = this._enabled && !this._readonly,
+                disabled = !enabled;
+
+            if (enabled === this.enabledInterface) return false;
+
+            this.container.toggleClass("select2-container-disabled", disabled);
+            this.close();
+            this.enabledInterface = enabled;
+
+            return true;
+        },
+
+        // abstract
+        enable: function(enabled) {
+            if (enabled === undefined) enabled = true;
+            if (this._enabled === enabled) return false;
+            this._enabled = enabled;
+
+            this.opts.element.prop("disabled", !enabled);
+            this.enableInterface();
+            return true;
+        },
+
+        // abstract
+        readonly: function(enabled) {
+            if (enabled === undefined) enabled = false;
+            if (this._readonly === enabled) return false;
+            this._readonly = enabled;
+
+            this.opts.element.prop("readonly", enabled);
+            this.enableInterface();
+            return true;
+        },
+
+        // abstract
+        opened: function () {
+            return this.container.hasClass("select2-dropdown-open");
+        },
+
+        // abstract
+        positionDropdown: function() {
+            var $dropdown = this.dropdown,
+                offset = this.container.offset(),
+                height = this.container.outerHeight(false),
+                width = this.container.outerWidth(false),
+                dropHeight = $dropdown.outerHeight(false),
+	            viewPortRight = $(window).scrollLeft() + $(window).width(),
+                viewportBottom = $(window).scrollTop() + $(window).height(),
+                dropTop = offset.top + height,
+                dropLeft = offset.left,
+                enoughRoomBelow = dropTop + dropHeight <= viewportBottom,
+                enoughRoomAbove = (offset.top - dropHeight) >= this.body().scrollTop(),
+	            dropWidth = $dropdown.outerWidth(false),
+	            enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight,
+                aboveNow = $dropdown.hasClass("select2-drop-above"),
+                bodyOffset,
+                above,
+                css,
+                resultsListNode;
+
+            if (this.opts.dropdownAutoWidth) {
+                resultsListNode = $('.select2-results', $dropdown)[0];
+                $dropdown.addClass('select2-drop-auto-width');
+                $dropdown.css('width', '');
+                // Add scrollbar width to dropdown if vertical scrollbar is present
+                dropWidth = $dropdown.outerWidth(false) + (resultsListNode.scrollHeight === resultsListNode.clientHeight ? 0 : scrollBarDimensions.width);
+                dropWidth > width ? width = dropWidth : dropWidth = width;
+                enoughRoomOnRight = dropLeft + dropWidth <= viewPortRight;
+            }
+            else {
+                this.container.removeClass('select2-drop-auto-width');
+            }
+
+            //console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow);
+            //console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body().scrollTop(), "enough?", enoughRoomAbove);
+
+            // fix positioning when body has an offset and is not position: static
+
+            if (this.body().css('position') !== 'static') {
+                bodyOffset = this.body().offset();
+                dropTop -= bodyOffset.top;
+                dropLeft -= bodyOffset.left;
+            }
+
+            // always prefer the current above/below alignment, unless there is not enough room
+
+            if (aboveNow) {
+                above = true;
+                if (!enoughRoomAbove && enoughRoomBelow) above = false;
+            } else {
+                above = false;
+                if (!enoughRoomBelow && enoughRoomAbove) above = true;
+            }
+
+            if (!enoughRoomOnRight) {
+               dropLeft = offset.left + width - dropWidth;
+            }
+
+            if (above) {
+                dropTop = offset.top - dropHeight;
+                this.container.addClass("select2-drop-above");
+                $dropdown.addClass("select2-drop-above");
+            }
+            else {
+                this.container.removeClass("select2-drop-above");
+                $dropdown.removeClass("select2-drop-above");
+            }
+
+            css = $.extend({
+                top: dropTop,
+                left: dropLeft,
+                width: width
+            }, evaluate(this.opts.dropdownCss));
+
+            $dropdown.css(css);
+        },
+
+        // abstract
+        shouldOpen: function() {
+            var event;
+
+            if (this.opened()) return false;
+
+            if (this._enabled === false || this._readonly === true) return false;
+
+            event = $.Event("select2-opening");
+            this.opts.element.trigger(event);
+            return !event.isDefaultPrevented();
+        },
+
+        // abstract
+        clearDropdownAlignmentPreference: function() {
+            // clear the classes used to figure out the preference of where the dropdown should be opened
+            this.container.removeClass("select2-drop-above");
+            this.dropdown.removeClass("select2-drop-above");
+        },
+
+        /**
+         * Opens the dropdown
+         *
+         * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example,
+         * the dropdown is already open, or if the 'open' event listener on the element called preventDefault().
+         */
+        // abstract
+        open: function () {
+
+            if (!this.shouldOpen()) return false;
+
+            this.opening();
+
+            return true;
+        },
+
+        /**
+         * Performs the opening of the dropdown
+         */
+        // abstract
+        opening: function() {
+            var cid = this.containerId,
+                scroll = "scroll." + cid,
+                resize = "resize."+cid,
+                orient = "orientationchange."+cid,
+                mask;
+
+            this.container.addClass("select2-dropdown-open").addClass("select2-container-active");
+
+            this.clearDropdownAlignmentPreference();
+
+            if(this.dropdown[0] !== this.body().children().last()[0]) {
+                this.dropdown.detach().appendTo(this.body());
+            }
+
+            // create the dropdown mask if doesnt already exist
+            mask = $("#select2-drop-mask");
+            if (mask.length == 0) {
+                mask = $(document.createElement("div"));
+                mask.attr("id","select2-drop-mask").attr("class","select2-drop-mask");
+                mask.hide();
+                mask.appendTo(this.body());
+                mask.on("mousedown touchstart", function (e) {
+                    var dropdown = $("#select2-drop"), self;
+                    if (dropdown.length > 0) {
+                        self=dropdown.data("select2");
+                        if (self.opts.selectOnBlur) {
+                            self.selectHighlighted({noFocus: true});
+                        }
+                        self.close();
+                        e.preventDefault();
+                        e.stopPropagation();
+                    }
+                });
+            }
+
+            // ensure the mask is always right before the dropdown
+            if (this.dropdown.prev()[0] !== mask[0]) {
+                this.dropdown.before(mask);
+            }
+
+            // move the global id to the correct dropdown
+            $("#select2-drop").removeAttr("id");
+            this.dropdown.attr("id", "select2-drop");
+
+            // show the elements
+            mask.css(_makeMaskCss());
+            mask.show();
+            this.dropdown.show();
+            this.positionDropdown();
+
+            this.dropdown.addClass("select2-drop-active");
+            this.ensureHighlightVisible();
+
+            // attach listeners to events that can change the position of the container and thus require
+            // the position of the dropdown to be updated as well so it does not come unglued from the container
+            var that = this;
+            this.container.parents().add(window).each(function () {
+                $(this).on(resize+" "+scroll+" "+orient, function (e) {
+                    $("#select2-drop-mask").css(_makeMaskCss());
+                    that.positionDropdown();
+                });
+            });
+
+            function _makeMaskCss() {
+                return {
+                    width  : Math.max(document.documentElement.scrollWidth,  $(window).width()),
+                    height : Math.max(document.documentElement.scrollHeight, $(window).height())
+                }
+            }
+        },
+
+        // abstract
+        close: function () {
+            if (!this.opened()) return;
+
+            var cid = this.containerId,
+                scroll = "scroll." + cid,
+                resize = "resize."+cid,
+                orient = "orientationchange."+cid;
+
+            // unbind event listeners
+            this.container.parents().add(window).each(function () { $(this).off(scroll).off(resize).off(orient); });
+
+            this.clearDropdownAlignmentPreference();
+
+            $("#select2-drop-mask").hide();
+            this.dropdown.removeAttr("id"); // only the active dropdown has the select2-drop id
+            this.dropdown.hide();
+            this.container.removeClass("select2-dropdown-open");
+            this.results.empty();
+
+
+            this.clearSearch();
+            this.search.removeClass("select2-active");
+            this.opts.element.trigger($.Event("select2-close"));
+        },
+
+        // abstract
+        clearSearch: function () {
+
+        },
+
+        //abstract
+        getMaximumSelectionSize: function() {
+            return evaluate(this.opts.maximumSelectionSize);
+        },
+
+        // abstract
+        ensureHighlightVisible: function () {
+            var results = this.results, children, index, child, hb, rb, y, more;
+
+            index = this.highlight();
+
+            if (index < 0) return;
+
+            if (index == 0) {
+
+                // if the first element is highlighted scroll all the way to the top,
+                // that way any unselectable headers above it will also be scrolled
+                // into view
+
+                results.scrollTop(0);
+                return;
+            }
+
+            children = this.findHighlightableChoices().find('.select2-result-label');
+
+            child = $(children[index]);
+
+            hb = child.offset().top + child.outerHeight(true);
+
+            // if this is the last child lets also make sure select2-more-results is visible
+            if (index === children.length - 1) {
+                more = results.find("li.select2-more-results");
+                if (more.length > 0) {
+                    hb = more.offset().top + more.outerHeight(true);
+                }
+            }
+
+            rb = results.offset().top + results.outerHeight(true);
+            if (hb > rb) {
+                results.scrollTop(results.scrollTop() + (hb - rb));
+            }
+            y = child.offset().top - results.offset().top;
+
+            // make sure the top of the element is visible
+            if (y < 0 && child.css('display') != 'none' ) {
+                results.scrollTop(results.scrollTop() + y); // y is negative
+            }
+        },
+
+        // abstract
+        findHighlightableChoices: function() {
+            return this.results.find(".select2-result-selectable:not(.select2-selected):not(.select2-disabled)");
+        },
+
+        // abstract
+        moveHighlight: function (delta) {
+            var choices = this.findHighlightableChoices(),
+                index = this.highlight();
+
+            while (index > -1 && index < choices.length) {
+                index += delta;
+                var choice = $(choices[index]);
+                if (choice.hasClass("select2-result-selectable") && !choice.hasClass("select2-disabled") && !choice.hasClass("select2-selected")) {
+                    this.highlight(index);
+                    break;
+                }
+            }
+        },
+
+        // abstract
+        highlight: function (index) {
+            var choices = this.findHighlightableChoices(),
+                choice,
+                data;
+
+            if (arguments.length === 0) {
+                return indexOf(choices.filter(".select2-highlighted")[0], choices.get());
+            }
+
+            if (index >= choices.length) index = choices.length - 1;
+            if (index < 0) index = 0;
+
+            this.results.find(".select2-highlighted").removeClass("select2-highlighted");
+
+            choice = $(choices[index]);
+            choice.addClass("select2-highlighted");
+
+            this.ensureHighlightVisible();
+
+            data = choice.data("select2-data");
+            if (data) {
+                this.opts.element.trigger({ type: "select2-highlight", val: this.id(data), choice: data });
+            }
+        },
+
+        // abstract
+        countSelectableResults: function() {
+            return this.findHighlightableChoices().length;
+        },
+
+        // abstract
+        highlightUnderEvent: function (event) {
+            var el = $(event.target).closest(".select2-result-selectable");
+            if (el.length > 0 && !el.is(".select2-highlighted")) {
+        		var choices = this.findHighlightableChoices();
+                this.highlight(choices.index(el));
+            } else if (el.length == 0) {
+                // if we are over an unselectable item remove al highlights
+                this.results.find(".select2-highlighted").removeClass("select2-highlighted");
+            }
+        },
+
+        // abstract
+        loadMoreIfNeeded: function () {
+            var results = this.results,
+                more = results.find("li.select2-more-results"),
+                below, // pixels the element is below the scroll fold, below==0 is when the element is starting to be visible
+                offset = -1, // index of first element without data
+                page = this.resultsPage + 1,
+                self=this,
+                term=this.search.val(),
+                context=this.context;
+
+            if (more.length === 0) return;
+            below = more.offset().top - results.offset().top - results.height();
+
+            if (below <= this.opts.loadMorePadding) {
+                more.addClass("select2-active");
+                this.opts.query({
+                        element: this.opts.element,
+                        term: term,
+                        page: page,
+                        context: context,
+                        matcher: this.opts.matcher,
+                        callback: this.bind(function (data) {
+
+                    // ignore a response if the select2 has been closed before it was received
+                    if (!self.opened()) return;
+
+
+                    self.opts.populateResults.call(this, results, data.results, {term: term, page: page, context:context});
+                    self.postprocessResults(data, false, false);
+
+                    if (data.more===true) {
+                        more.detach().appendTo(results).text(self.opts.formatLoadMore(page+1));
+                        window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
+                    } else {
+                        more.remove();
+                    }
+                    self.positionDropdown();
+                    self.resultsPage = page;
+                    self.context = data.context;
+                })});
+            }
+        },
+
+        /**
+         * Default tokenizer function which does nothing
+         */
+        tokenize: function() {
+
+        },
+
+        /**
+         * @param initial whether or not this is the call to this method right after the dropdown has been opened
+         */
+        // abstract
+        updateResults: function (initial) {
+            var search = this.search,
+                results = this.results,
+                opts = this.opts,
+                data,
+                self = this,
+                input,
+                term = search.val(),
+                lastTerm=$.data(this.container, "select2-last-term");
+
+            // prevent duplicate queries against the same term
+            if (initial !== true && lastTerm && equal(term, lastTerm)) return;
+
+            $.data(this.container, "select2-last-term", term);
+
+            // if the search is currently hidden we do not alter the results
+            if (initial !== true && (this.showSearchInput === false || !this.opened())) {
+                return;
+            }
+
+            function postRender() {
+                results.scrollTop(0);
+                search.removeClass("select2-active");
+                self.positionDropdown();
+            }
+
+            function render(html) {
+                results.html(html);
+                postRender();
+            }
+
+            var maxSelSize = this.getMaximumSelectionSize();
+            if (maxSelSize >=1) {
+                data = this.data();
+                if ($.isArray(data) && data.length >= maxSelSize && checkFormatter(opts.formatSelectionTooBig, "formatSelectionTooBig")) {
+            	    render("<li class='select2-selection-limit'>" + opts.formatSelectionTooBig(maxSelSize) + "</li>");
+            	    return;
+                }
+            }
+
+            if (search.val().length < opts.minimumInputLength) {
+                if (checkFormatter(opts.formatInputTooShort, "formatInputTooShort")) {
+                    render("<li class='select2-no-results'>" + opts.formatInputTooShort(search.val(), opts.minimumInputLength) + "</li>");
+                } else {
+                    render("");
+                }
+                if (initial) this.showSearch(true);
+                return;
+            }
+
+            if (opts.maximumInputLength && search.val().length > opts.maximumInputLength) {
+                if (checkFormatter(opts.formatInputTooLong, "formatInputTooLong")) {
+                    render("<li class='select2-no-results'>" + opts.formatInputTooLong(search.val(), opts.maximumInputLength) + "</li>");
+                } else {
+                    render("");
+                }
+                return;
+            }
+
+            if (opts.formatSearching && this.findHighlightableChoices().length === 0) {
+                render("<li class='select2-searching'>" + opts.formatSearching() + "</li>");
+            }
+
+            search.addClass("select2-active");
+
+            // give the tokenizer a chance to pre-process the input
+            input = this.tokenize();
+            if (input != undefined && input != null) {
+                search.val(input);
+            }
+
+            this.resultsPage = 1;
+
+            opts.query({
+                element: opts.element,
+                    term: search.val(),
+                    page: this.resultsPage,
+                    context: null,
+                    matcher: opts.matcher,
+                    callback: this.bind(function (data) {
+                var def; // default choice
+
+                // ignore a response if the select2 has been closed before it was received
+                if (!this.opened()) {
+                    this.search.removeClass("select2-active");
+                    return;
+                }
+
+                // save context, if any
+                this.context = (data.context===undefined) ? null : data.context;
+                // create a default choice and prepend it to the list
+                if (this.opts.createSearchChoice && search.val() !== "") {
+                    def = this.opts.createSearchChoice.call(null, search.val(), data.results);
+                    if (def !== undefined && def !== null && self.id(def) !== undefined && self.id(def) !== null) {
+                        if ($(data.results).filter(
+                            function () {
+                                return equal(self.id(this), self.id(def));
+                            }).length === 0) {
+                            data.results.unshift(def);
+                        }
+                    }
+                }
+
+                if (data.results.length === 0 && checkFormatter(opts.formatNoMatches, "formatNoMatches")) {
+                    render("<li class='select2-no-results'>" + opts.formatNoMatches(search.val()) + "</li>");
+                    return;
+                }
+
+                results.empty();
+                self.opts.populateResults.call(this, results, data.results, {term: search.val(), page: this.resultsPage, context:null});
+
+                if (data.more === true && checkFormatter(opts.formatLoadMore, "formatLoadMore")) {
+                    results.append("<li class='select2-more-results'>" + self.opts.escapeMarkup(opts.formatLoadMore(this.resultsPage)) + "</li>");
+                    window.setTimeout(function() { self.loadMoreIfNeeded(); }, 10);
+                }
+
+                this.postprocessResults(data, initial);
+
+                postRender();
+
+                this.opts.element.trigger({ type: "select2-loaded", data:data });
+            })});
+        },
+
+        // abstract
+        cancel: function () {
+            this.close();
+        },
+
+        // abstract
+        blur: function () {
+            // if selectOnBlur == true, select the currently highlighted option
+            if (this.opts.selectOnBlur)
+                this.selectHighlighted({noFocus: true});
+
+            this.close();
+            this.container.removeClass("select2-container-active");
+            // synonymous to .is(':focus'), which is available in jquery >= 1.6
+            if (this.search[0] === document.activeElement) { this.search.blur(); }
+            this.clearSearch();
+            this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
+        },
+
+        // abstract
+        focusSearch: function () {
+            focus(this.search);
+        },
+
+        // abstract
+        selectHighlighted: function (options) {
+            var index=this.highlight(),
+                highlighted=this.results.find(".select2-highlighted"),
+                data = highlighted.closest('.select2-result').data("select2-data");
+
+            if (data) {
+                this.highlight(index);
+                this.onSelect(data, options);
+            }
+        },
+
+        // abstract
+        getPlaceholder: function () {
+            return this.opts.element.attr("placeholder") ||
+                this.opts.element.attr("data-placeholder") || // jquery 1.4 compat
+                this.opts.element.data("placeholder") ||
+                this.opts.placeholder;
+        },
+
+        /**
+         * Get the desired width for the container element.  This is
+         * derived first from option `width` passed to select2, then
+         * the inline 'style' on the original element, and finally
+         * falls back to the jQuery calculated element width.
+         */
+        // abstract
+        initContainerWidth: function () {
+            function resolveContainerWidth() {
+                var style, attrs, matches, i, l;
+
+                if (this.opts.width === "off") {
+                    return null;
+                } else if (this.opts.width === "element"){
+                    return this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px';
+                } else if (this.opts.width === "copy" || this.opts.width === "resolve") {
+                    // check if there is inline style on the element that contains width
+                    style = this.opts.element.attr('style');
+                    if (style !== undefined) {
+                        attrs = style.split(';');
+                        for (i = 0, l = attrs.length; i < l; i = i + 1) {
+                            matches = attrs[i].replace(/\s/g, '')
+                                .match(/width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i);
+                            if (matches !== null && matches.length >= 1)
+                                return matches[1];
+                        }
+                    }
+
+                    // next check if css('width') can resolve a width that is percent based, this is sometimes possible
+                    // when attached to input type=hidden or elements hidden via css
+                    style = this.opts.element.css('width');
+                    if (style && style.length > 0) return style;
+
+                    if (this.opts.width === "resolve") {
+                        // finally, fallback on the calculated width of the element
+                        return (this.opts.element.outerWidth(false) === 0 ? 'auto' : this.opts.element.outerWidth(false) + 'px');
+                    }
+
+                    return null;
+                } else if ($.isFunction(this.opts.width)) {
+                    return this.opts.width();
+                } else {
+                    return this.opts.width;
+               }
+            };
+
+            var width = resolveContainerWidth.call(this);
+            if (width !== null) {
+                this.container.css("width", width);
+            }
+        }
+    });
+
+    SingleSelect2 = clazz(AbstractSelect2, {
+
+        // single
+
+		createContainer: function () {
+            var container = $(document.createElement("div")).attr({
+                "class": "select2-container"
+            }).html([
+                "<a href='javascript:void(0)' onclick='return false;' class='select2-choice' tabindex='-1'>",
+                "   <span>&nbsp;</span><abbr class='select2-search-choice-close'></abbr>",
+                "   <div><b></b></div>" ,
+                "</a>",
+                "<input class='select2-focusser select2-offscreen' type='text'/>",
+                "<div class='select2-drop select2-display-none'>" ,
+                "   <div class='select2-search'>" ,
+                "       <input type='text' autocomplete='off' autocorrect='off' autocapitilize='off' spellcheck='false' class='select2-input'/>" ,
+                "   </div>" ,
+                "   <ul class='select2-results'>" ,
+                "   </ul>" ,
+                "</div>"].join(""));
+            return container;
+        },
+
+        // single
+        enableInterface: function() {
+            if (this.parent.enableInterface.apply(this, arguments)) {
+                this.focusser.prop("disabled", !this.isInterfaceEnabled());
+            }
+        },
+
+        // single
+        opening: function () {
+            var el, range;
+            this.parent.opening.apply(this, arguments);
+            if (this.showSearchInput !== false) {
+                // IE appends focusser.val() at the end of field :/ so we manually insert it at the beginning using a range
+                // all other browsers handle this just fine
+
+                this.search.val(this.focusser.val());
+            }
+            this.search.focus();
+            // in IE we have to move the cursor to the end after focussing, otherwise it will be at the beginning and
+            // new text will appear *before* focusser.val()
+            el = this.search.get(0);
+            if (el.createTextRange) {
+                range = el.createTextRange();
+                range.collapse(false);
+                range.select();
+            }
+
+            this.focusser.prop("disabled", true).val("");
+            this.updateResults(true);
+            this.opts.element.trigger($.Event("select2-open"));
+        },
+
+        // single
+        close: function () {
+            if (!this.opened()) return;
+            this.parent.close.apply(this, arguments);
+            this.focusser.removeAttr("disabled");
+            this.focusser.focus();
+        },
+
+        // single
+        focus: function () {
+            if (this.opened()) {
+                this.close();
+            } else {
+                this.focusser.removeAttr("disabled");
+                this.focusser.focus();
+            }
+        },
+
+        // single
+        isFocused: function () {
+            return this.container.hasClass("select2-container-active");
+        },
+
+        // single
+        cancel: function () {
+            this.parent.cancel.apply(this, arguments);
+            this.focusser.removeAttr("disabled");
+            this.focusser.focus();
+        },
+
+        // single
+        initContainer: function () {
+
+            var selection,
+                container = this.container,
+                dropdown = this.dropdown;
+
+            this.showSearch(false);
+
+            this.selection = selection = container.find(".select2-choice");
+
+            this.focusser = container.find(".select2-focusser");
+
+            // rewrite labels from original element to focusser
+            this.focusser.attr("id", "s2id_autogen"+nextUid());
+
+            $("label[for='" + this.opts.element.attr("id") + "']")
+                .attr('for', this.focusser.attr('id'));
+
+            this.focusser.attr("tabindex", this.elementTabIndex);
+
+            this.search.on("keydown", this.bind(function (e) {
+                if (!this.isInterfaceEnabled()) return;
+
+                if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
+                    // prevent the page from scrolling
+                    killEvent(e);
+                    return;
+                }
+
+                switch (e.which) {
+                    case KEY.UP:
+                    case KEY.DOWN:
+                        this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
+                        killEvent(e);
+                        return;
+                    case KEY.ENTER:
+                        this.selectHighlighted();
+                        killEvent(e);
+                        return;
+                    case KEY.TAB:
+                        this.selectHighlighted({noFocus: true});
+                        return;
+                    case KEY.ESC:
+                        this.cancel(e);
+                        killEvent(e);
+                        return;
+                }
+            }));
+
+            this.search.on("blur", this.bind(function(e) {
+                // a workaround for chrome to keep the search field focussed when the scroll bar is used to scroll the dropdown.
+                // without this the search field loses focus which is annoying
+                if (document.activeElement === this.body().get(0)) {
+                    window.setTimeout(this.bind(function() {
+                        this.search.focus();
+                    }), 0);
+                }
+            }));
+
+            this.focusser.on("keydown", this.bind(function (e) {
+                if (!this.isInterfaceEnabled()) return;
+
+                if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e) || e.which === KEY.ESC) {
+                    return;
+                }
+
+                if (this.opts.openOnEnter === false && e.which === KEY.ENTER) {
+                    killEvent(e);
+                    return;
+                }
+
+                if (e.which == KEY.DOWN || e.which == KEY.UP
+                    || (e.which == KEY.ENTER && this.opts.openOnEnter)) {
+                    this.open();
+                    killEvent(e);
+                    return;
+                }
+
+                if (e.which == KEY.DELETE || e.which == KEY.BACKSPACE) {
+                    if (this.opts.allowClear) {
+                        this.clear();
+                    }
+                    killEvent(e);
+                    return;
+                }
+            }));
+
+
+            installKeyUpChangeEvent(this.focusser);
+            this.focusser.on("keyup-change input", this.bind(function(e) {
+                e.stopPropagation();
+                if (this.opened()) return;
+                this.open();
+            }));
+
+            selection.on("mousedown", "abbr", this.bind(function (e) {
+                if (!this.isInterfaceEnabled()) return;
+                this.clear();
+                killEventImmediately(e);
+                this.close();
+                this.selection.focus();
+            }));
+
+            selection.on("mousedown", this.bind(function (e) {
+
+                if (!this.container.hasClass("select2-container-active")) {
+                    this.opts.element.trigger($.Event("select2-focus"));
+                }
+
+                if (this.opened()) {
+                    this.close();
+                } else if (this.isInterfaceEnabled()) {
+                    this.open();
+                }
+
+                killEvent(e);
+            }));
+
+            dropdown.on("mousedown", this.bind(function() { this.search.focus(); }));
+
+            selection.on("focus", this.bind(function(e) {
+                killEvent(e);
+            }));
+
+            this.focusser.on("focus", this.bind(function(){
+                if (!this.container.hasClass("select2-container-active")) {
+                    this.opts.element.trigger($.Event("select2-focus"));
+                }
+                this.container.addClass("select2-container-active");
+            })).on("blur", this.bind(function() {
+                if (!this.opened()) {
+                    this.container.removeClass("select2-container-active");
+                    this.opts.element.trigger($.Event("select2-blur"));
+                }
+            }));
+            this.search.on("focus", this.bind(function(){
+                if (!this.container.hasClass("select2-container-active")) {
+                    this.opts.element.trigger($.Event("select2-focus"));
+                }
+                this.container.addClass("select2-container-active");
+            }));
+
+            this.initContainerWidth();
+            this.opts.element.addClass("select2-offscreen");
+            this.setPlaceholder();
+
+        },
+
+        // single
+        clear: function(triggerChange) {
+            var data=this.selection.data("select2-data");
+            if (data) { // guard against queued quick consecutive clicks
+                this.opts.element.val("");
+                this.selection.find("span").empty();
+                this.selection.removeData("select2-data");
+                this.setPlaceholder();
+
+                if (triggerChange !== false){
+                    this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data });
+                    this.triggerChange({removed:data});
+                }
+            }
+        },
+
+        /**
+         * Sets selection based on source element's value
+         */
+        // single
+        initSelection: function () {
+            var selected;
+            if (this.opts.element.val() === "" && this.opts.element.text() === "") {
+                this.updateSelection([]);
+                this.close();
+                this.setPlaceholder();
+            } else {
+                var self = this;
+                this.opts.initSelection.call(null, this.opts.element, function(selected){
+                    if (selected !== undefined && selected !== null) {
+                        self.updateSelection(selected);
+                        self.close();
+                        self.setPlaceholder();
+                    }
+                });
+            }
+        },
+
+        // single
+        prepareOpts: function () {
+            var opts = this.parent.prepareOpts.apply(this, arguments),
+                self=this;
+
+            if (opts.element.get(0).tagName.toLowerCase() === "select") {
+                // install the selection initializer
+                opts.initSelection = function (element, callback) {
+                    var selected = element.find(":selected");
+                    // a single select box always has a value, no need to null check 'selected'
+                    callback(self.optionToData(selected));
+                };
+            } else if ("data" in opts) {
+                // install default initSelection when applied to hidden input and data is local
+                opts.initSelection = opts.initSelection || function (element, callback) {
+                    var id = element.val();
+                    //search in data by id, storing the actual matching item
+                    var match = null;
+                    opts.query({
+                        matcher: function(term, text, el){
+                            var is_match = equal(id, opts.id(el));
+                            if (is_match) {
+                                match = el;
+                            }
+                            return is_match;
+                        },
+                        callback: !$.isFunction(callback) ? $.noop : function() {
+                            callback(match);
+                        }
+                    });
+                };
+            }
+
+            return opts;
+        },
+
+        // single
+        getPlaceholder: function() {
+            // if a placeholder is specified on a single select without the first empty option ignore it
+            if (this.select) {
+                if (this.select.find("option").first().text() !== "") {
+                    return undefined;
+                }
+            }
+
+            return this.parent.getPlaceholder.apply(this, arguments);
+        },
+
+        // single
+        setPlaceholder: function () {
+            var placeholder = this.getPlaceholder();
+
+            if (this.opts.element.val() === "" && placeholder !== undefined) {
+
+                // check for a first blank option if attached to a select
+                if (this.select && this.select.find("option:first").text() !== "") return;
+
+                this.selection.find("span").html(this.opts.escapeMarkup(placeholder));
+
+                this.selection.addClass("select2-default");
+
+                this.container.removeClass("select2-allowclear");
+            }
+        },
+
+        // single
+        postprocessResults: function (data, initial, noHighlightUpdate) {
+            var selected = 0, self = this, showSearchInput = true;
+
+            // find the selected element in the result list
+
+            this.findHighlightableChoices().each2(function (i, elm) {
+                if (equal(self.id(elm.data("select2-data")), self.opts.element.val())) {
+                    selected = i;
+                    return false;
+                }
+            });
+
+            // and highlight it
+            if (noHighlightUpdate !== false) {
+                this.highlight(selected);
+            }
+
+            // show the search box if this is the first we got the results and there are enough of them for search
+
+            if (initial === true && this.showSearchInput === false) {
+                var min=this.opts.minimumResultsForSearch;
+                if (min>=0) {
+                    this.showSearch(countResults(data.results)>=min);
+                }
+            }
+
+        },
+
+        // single
+        showSearch: function(showSearchInput) {
+            this.showSearchInput = showSearchInput;
+
+            this.dropdown.find(".select2-search").toggleClass("select2-search-hidden", !showSearchInput);
+            this.dropdown.find(".select2-search").toggleClass("select2-offscreen", !showSearchInput);
+            //add "select2-with-searchbox" to the container if search box is shown
+            $(this.dropdown, this.container).toggleClass("select2-with-searchbox", showSearchInput);
+        },
+
+        // single
+        onSelect: function (data, options) {
+
+            if (!this.triggerSelect(data)) { return; }
+
+            var old = this.opts.element.val(),
+                oldData = this.data();
+
+            this.opts.element.val(this.id(data));
+            this.updateSelection(data);
+
+            this.opts.element.trigger({ type: "select2-selected", val: this.id(data), choice: data });
+
+            this.close();
+
+            if (!options || !options.noFocus)
+                this.selection.focus();
+
+            if (!equal(old, this.id(data))) { this.triggerChange({added:data,removed:oldData}); }
+        },
+
+        // single
+        updateSelection: function (data) {
+
+            var container=this.selection.find("span"), formatted;
+
+            this.selection.data("select2-data", data);
+
+            container.empty();
+            formatted=this.opts.formatSelection(data, container);
+            if (formatted !== undefined) {
+                container.append(this.opts.escapeMarkup(formatted));
+            }
+
+            this.selection.removeClass("select2-default");
+
+            if (this.opts.allowClear && this.getPlaceholder() !== undefined) {
+                this.container.addClass("select2-allowclear");
+            }
+        },
+
+        // single
+        val: function () {
+            var val,
+                triggerChange = false,
+                data = null,
+                self = this,
+                oldData = this.data();
+
+            if (arguments.length === 0) {
+                return this.opts.element.val();
+            }
+
+            val = arguments[0];
+
+            if (arguments.length > 1) {
+                triggerChange = arguments[1];
+            }
+
+            if (this.select) {
+                this.select
+                    .val(val)
+                    .find(":selected").each2(function (i, elm) {
+                        data = self.optionToData(elm);
+                        return false;
+                    });
+                this.updateSelection(data);
+                this.setPlaceholder();
+                if (triggerChange) {
+                    this.triggerChange({added: data, removed:oldData});
+                }
+            } else {
+                if (this.opts.initSelection === undefined) {
+                    throw new Error("cannot call val() if initSelection() is not defined");
+                }
+                // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
+                if (!val && val !== 0) {
+                    this.clear(triggerChange);
+                    return;
+                }
+                this.opts.element.val(val);
+                this.opts.initSelection(this.opts.element, function(data){
+                    self.opts.element.val(!data ? "" : self.id(data));
+                    self.updateSelection(data);
+                    self.setPlaceholder();
+                    if (triggerChange) {
+                        self.triggerChange({added: data, removed:oldData});
+                    }
+                });
+            }
+        },
+
+        // single
+        clearSearch: function () {
+            this.search.val("");
+            this.focusser.val("");
+        },
+
+        // single
+        data: function(value, triggerChange) {
+            var data;
+
+            if (arguments.length === 0) {
+                data = this.selection.data("select2-data");
+                if (data == undefined) data = null;
+                return data;
+            } else {
+                if (!value || value === "") {
+                    this.clear(triggerChange);
+                } else {
+                    data = this.data();
+                    this.opts.element.val(!value ? "" : this.id(value));
+                    this.updateSelection(value);
+                    if (triggerChange) {
+                        this.triggerChange({added: value, removed:data});
+                    }
+                }
+            }
+        }
+    });
+
+    MultiSelect2 = clazz(AbstractSelect2, {
+
+        // multi
+        createContainer: function () {
+            var container = $(document.createElement("div")).attr({
+                "class": "select2-container select2-container-multi"
+            }).html([
+                "    <ul class='select2-choices'>",
+                //"<li class='select2-search-choice'><span>California</span><a href="javascript:void(0)" class="select2-search-choice-close"></a></li>" ,
+                "  <li class='select2-search-field'>" ,
+                "    <input type='text' autocomplete='off' autocorrect='off' autocapitilize='off' spellcheck='false' class='select2-input'>" ,
+                "  </li>" ,
+                "</ul>" ,
+                "<div class='select2-drop select2-drop-multi select2-display-none'>" ,
+                "   <ul class='select2-results'>" ,
+                "   </ul>" ,
+                "</div>"].join(""));
+			return container;
+        },
+
+        // multi
+        prepareOpts: function () {
+            var opts = this.parent.prepareOpts.apply(this, arguments),
+                self=this;
+
+            // TODO validate placeholder is a string if specified
+
+            if (opts.element.get(0).tagName.toLowerCase() === "select") {
+                // install sthe selection initializer
+                opts.initSelection = function (element, callback) {
+
+                    var data = [];
+
+                    element.find(":selected").each2(function (i, elm) {
+                        data.push(self.optionToData(elm));
+                    });
+                    callback(data);
+                };
+            } else if ("data" in opts) {
+                // install default initSelection when applied to hidden input and data is local
+                opts.initSelection = opts.initSelection || function (element, callback) {
+                    var ids = splitVal(element.val(), opts.separator);
+                    //search in data by array of ids, storing matching items in a list
+                    var matches = [];
+                    opts.query({
+                        matcher: function(term, text, el){
+                            var is_match = $.grep(ids, function(id) {
+                                return equal(id, opts.id(el));
+                            }).length;
+                            if (is_match) {
+                                matches.push(el);
+                            }
+                            return is_match;
+                        },
+                        callback: !$.isFunction(callback) ? $.noop : function() {
+                            // reorder matches based on the order they appear in the ids array because right now
+                            // they are in the order in which they appear in data array
+                            var ordered = [];
+                            for (var i = 0; i < ids.length; i++) {
+                                var id = ids[i];
+                                for (var j = 0; j < matches.length; j++) {
+                                    var match = matches[j];
+                                    if (equal(id, opts.id(match))) {
+                                        ordered.push(match);
+                                        matches.splice(j, 1);
+                                        break;
+                                    }
+                                }
+                            }
+                            callback(ordered);
+                        }
+                    });
+                };
+            }
+
+            return opts;
+        },
+
+        selectChoice: function (choice) {
+
+            var selected = this.container.find(".select2-search-choice-focus");
+            if (selected.length && choice && choice[0] == selected[0]) {
+
+            } else {
+                if (selected.length) {
+                    this.opts.element.trigger("choice-deselected", selected);
+                }
+                selected.removeClass("select2-search-choice-focus");
+                if (choice && choice.length) {
+                    this.close();
+                    choice.addClass("select2-search-choice-focus");
+                    this.opts.element.trigger("choice-selected", choice);
+                }
+            }
+        },
+
+        // multi
+        initContainer: function () {
+
+            var selector = ".select2-choices", selection;
+
+            this.searchContainer = this.container.find(".select2-search-field");
+            this.selection = selection = this.container.find(selector);
+
+            var _this = this;
+            this.selection.on("mousedown", ".select2-search-choice", function (e) {
+                //killEvent(e);
+                _this.search[0].focus();
+                _this.selectChoice($(this));
+            })
+            //.sortable({
+            //    items: " > li",
+            //    tolerance: "pointer",
+            //    revert: 100
+            //});
+
+            // rewrite labels from original element to focusser
+            this.search.attr("id", "s2id_autogen"+nextUid());
+            $("label[for='" + this.opts.element.attr("id") + "']")
+                .attr('for', this.search.attr('id'));
+
+            this.search.on("input paste", this.bind(function() {
+                if (!this.isInterfaceEnabled()) return;
+                if (!this.opened()) {
+                    this.open();
+                }
+            }));
+
+            this.search.attr("tabindex", this.elementTabIndex);
+
+            this.keydowns = 0;
+            this.search.on("keydown", this.bind(function (e) {
+                if (!this.isInterfaceEnabled()) return;
+
+                ++this.keydowns;
+                var selected = selection.find(".select2-search-choice-focus");
+                var prev = selected.prev(".select2-search-choice:not(.select2-locked)");
+                var next = selected.next(".select2-search-choice:not(.select2-locked)");
+                var pos = getCursorInfo(this.search);
+
+                if (selected.length &&
+                    (e.which == KEY.LEFT || e.which == KEY.RIGHT || e.which == KEY.BACKSPACE || e.which == KEY.DELETE || e.which == KEY.ENTER)) {
+                    var selectedChoice = selected;
+                    if (e.which == KEY.LEFT && prev.length) {
+                        selectedChoice = prev;
+                    }
+                    else if (e.which == KEY.RIGHT) {
+                        selectedChoice = next.length ? next : null;
+                    }
+                    else if (e.which === KEY.BACKSPACE) {
+                        this.unselect(selected.first());
+                        this.search.width(10);
+                        selectedChoice = prev.length ? prev : next;
+                    } else if (e.which == KEY.DELETE) {
+                        this.unselect(selected.first());
+                        this.search.width(10);
+                        selectedChoice = next.length ? next : null;
+                    } else if (e.which == KEY.ENTER) {
+                        selectedChoice = null;
+                    }
+
+                    this.selectChoice(selectedChoice);
+                    killEvent(e);
+                    if (!selectedChoice || !selectedChoice.length) {
+                        this.open();
+                    }
+                    return;
+                } else if (((e.which === KEY.BACKSPACE && this.keydowns == 1)
+                    || e.which == KEY.LEFT) && (pos.offset == 0 && !pos.length)) {
+
+                    this.selectChoice(selection.find(".select2-search-choice:not(.select2-locked)").last());
+                    killEvent(e);
+                    return;
+                } else {
+                    this.selectChoice(null);
+                }
+
+                if (this.opened()) {
+                    switch (e.which) {
+                    case KEY.UP:
+                    case KEY.DOWN:
+                        this.moveHighlight((e.which === KEY.UP) ? -1 : 1);
+                        killEvent(e);
+                        return;
+                    case KEY.ENTER:
+                        this.selectHighlighted();
+                        killEvent(e);
+                        return;
+                    case KEY.TAB:
+                        this.selectHighlighted({noFocus:true});
+                        return;
+                    case KEY.ESC:
+                        this.cancel(e);
+                        killEvent(e);
+                        return;
+                    }
+                }
+
+                if (e.which === KEY.TAB || KEY.isControl(e) || KEY.isFunctionKey(e)
+                 || e.which === KEY.BACKSPACE || e.which === KEY.ESC) {
+                    return;
+                }
+
+                if (e.which === KEY.ENTER) {
+                    if (this.opts.openOnEnter === false) {
+                        return;
+                    } else if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
+                        return;
+                    }
+                }
+
+                this.open();
+
+                if (e.which === KEY.PAGE_UP || e.which === KEY.PAGE_DOWN) {
+                    // prevent the page from scrolling
+                    killEvent(e);
+                }
+
+                if (e.which === KEY.ENTER) {
+                    // prevent form from being submitted
+                    killEvent(e);
+                }
+
+            }));
+
+            this.search.on("keyup", this.bind(function (e) {
+                this.keydowns = 0;
+                this.resizeSearch();
+            })
+            );
+
+            this.search.on("blur", this.bind(function(e) {
+                this.container.removeClass("select2-container-active");
+                this.search.removeClass("select2-focused");
+                this.selectChoice(null);
+                if (!this.opened()) this.clearSearch();
+                e.stopImmediatePropagation();
+                this.opts.element.trigger($.Event("select2-blur"));
+            }));
+
+            this.container.on("mousedown", selector, this.bind(function (e) {
+                if (!this.isInterfaceEnabled()) return;
+                if ($(e.target).closest(".select2-search-choice").length > 0) {
+                    // clicked inside a select2 search choice, do not open
+                    return;
+                }
+                this.selectChoice(null);
+                this.clearPlaceholder();
+                if (!this.container.hasClass("select2-container-active")) {
+                    this.opts.element.trigger($.Event("select2-focus"));
+                }
+                this.open();
+                this.focusSearch();
+                e.preventDefault();
+            }));
+
+            this.container.on("focus", selector, this.bind(function () {
+                if (!this.isInterfaceEnabled()) return;
+                if (!this.container.hasClass("select2-container-active")) {
+                    this.opts.element.trigger($.Event("select2-focus"));
+                }
+                this.container.addClass("select2-container-active");
+                this.dropdown.addClass("select2-drop-active");
+                this.clearPlaceholder();
+            }));
+
+            this.initContainerWidth();
+            this.opts.element.addClass("select2-offscreen");
+
+            // set the placeholder if necessary
+            this.clearSearch();
+        },
+
+        // multi
+        enableInterface: function() {
+            if (this.parent.enableInterface.apply(this, arguments)) {
+                this.search.prop("disabled", !this.isInterfaceEnabled());
+            }
+        },
+
+        // multi
+        initSelection: function () {
+            var data;
+            if (this.opts.element.val() === "" && this.opts.element.text() === "") {
+                this.updateSelection([]);
+                this.close();
+                // set the placeholder if necessary
+                this.clearSearch();
+            }
+            if (this.select || this.opts.element.val() !== "") {
+                var self = this;
+                this.opts.initSelection.call(null, this.opts.element, function(data){
+                    if (data !== undefined && data !== null) {
+                        self.updateSelection(data);
+                        self.close();
+                        // set the placeholder if necessary
+                        self.clearSearch();
+                    }
+                });
+            }
+        },
+
+        // multi
+        clearSearch: function () {
+            var placeholder = this.getPlaceholder(),
+                maxWidth = this.getMaxSearchWidth();
+
+            if (placeholder !== undefined  && this.getVal().length === 0 && this.search.hasClass("select2-focused") === false) {
+                this.search.val(placeholder).addClass("select2-default");
+                // stretch the search box to full width of the container so as much of the placeholder is visible as possible
+                // we could call this.resizeSearch(), but we do not because that requires a sizer and we do not want to create one so early because of a firefox bug, see #944
+                this.search.width(maxWidth > 0 ? maxWidth : this.container.css("width"));
+            } else {
+                this.search.val("").width(10);
+            }
+        },
+
+        // multi
+        clearPlaceholder: function () {
+            if (this.search.hasClass("select2-default")) {
+                this.search.val("").removeClass("select2-default");
+            }
+        },
+
+        // multi
+        opening: function () {
+            this.clearPlaceholder(); // should be done before super so placeholder is not used to search
+            this.resizeSearch();
+
+            this.parent.opening.apply(this, arguments);
+
+            this.focusSearch();
+
+            this.updateResults(true);
+            this.search.focus();
+            this.opts.element.trigger($.Event("select2-open"));
+        },
+
+        // multi
+        close: function () {
+            if (!this.opened()) return;
+            this.parent.close.apply(this, arguments);
+        },
+
+        // multi
+        focus: function () {
+            this.close();
+            this.search.focus();
+            //this.opts.element.triggerHandler("focus");
+        },
+
+        // multi
+        isFocused: function () {
+            return this.search.hasClass("select2-focused");
+        },
+
+        // multi
+        updateSelection: function (data) {
+            var ids = [], filtered = [], self = this;
+
+            // filter out duplicates
+            $(data).each(function () {
+                if (indexOf(self.id(this), ids) < 0) {
+                    ids.push(self.id(this));
+                    filtered.push(this);
+                }
+            });
+            data = filtered;
+
+            this.selection.find(".select2-search-choice").remove();
+            $(data).each(function () {
+                self.addSelectedChoice(this);
+            });
+            self.postprocessResults();
+        },
+
+        // multi
+        tokenize: function() {
+            var input = this.search.val();
+            input = this.opts.tokenizer(input, this.data(), this.bind(this.onSelect), this.opts);
+            if (input != null && input != undefined) {
+                this.search.val(input);
+                if (input.length > 0) {
+                    this.open();
+                }
+            }
+
+        },
+
+        // multi
+        onSelect: function (data, options) {
+
+            if (!this.triggerSelect(data)) { return; }
+
+            this.addSelectedChoice(data);
+
+            this.opts.element.trigger({ type: "selected", val: this.id(data), choice: data });
+
+            if (this.select || !this.opts.closeOnSelect) this.postprocessResults();
+
+            if (this.opts.closeOnSelect) {
+                this.close();
+                this.search.width(10);
+            } else {
+                if (this.countSelectableResults()>0) {
+                    this.search.width(10);
+                    this.resizeSearch();
+                    if (this.getMaximumSelectionSize() > 0 && this.val().length >= this.getMaximumSelectionSize()) {
+                        // if we reached max selection size repaint the results so choices
+                        // are replaced with the max selection reached message
+                        this.updateResults(true);
+                    }
+                    this.positionDropdown();
+                } else {
+                    // if nothing left to select close
+                    this.close();
+                    this.search.width(10);
+                }
+            }
+
+            // since its not possible to select an element that has already been
+            // added we do not need to check if this is a new element before firing change
+            this.triggerChange({ added: data });
+
+            if (!options || !options.noFocus)
+                this.focusSearch();
+        },
+
+        // multi
+        cancel: function () {
+            this.close();
+            this.focusSearch();
+        },
+
+        addSelectedChoice: function (data) {
+            var enableChoice = !data.locked,
+                enabledItem = $(
+                    "<li class='select2-search-choice'>" +
+                    "    <div></div>" +
+                    "    <a href='#' onclick='return false;' class='select2-search-choice-close' tabindex='-1'></a>" +
+                    "</li>"),
+                disabledItem = $(
+                    "<li class='select2-search-choice select2-locked'>" +
+                    "<div></div>" +
+                    "</li>");
+            var choice = enableChoice ? enabledItem : disabledItem,
+                id = this.id(data),
+                val = this.getVal(),
+                formatted;
+
+            formatted=this.opts.formatSelection(data, choice.find("div"));
+            if (formatted != undefined) {
+                choice.find("div").replaceWith("<div title='"+this.opts.escapeMarkup(formatted)+"'>"+this.opts.escapeMarkup(formatted)+"</div>");
+            }
+
+            if(enableChoice){
+              choice.find(".select2-search-choice-close")
+                  .on("mousedown", killEvent)
+                  .on("click dblclick", this.bind(function (e) {
+                  if (!this.isInterfaceEnabled()) return;
+
+                  $(e.target).closest(".select2-search-choice").fadeOut('fast', this.bind(function(){
+                      this.unselect($(e.target));
+                      this.selection.find(".select2-search-choice-focus").removeClass("select2-search-choice-focus");
+                      this.close();
+                      this.focusSearch();
+                  })).dequeue();
+                  killEvent(e);
+              })).on("focus", this.bind(function () {
+                  if (!this.isInterfaceEnabled()) return;
+                  this.container.addClass("select2-container-active");
+                  this.dropdown.addClass("select2-drop-active");
+              }));
+            }
+
+            choice.data("select2-data", data);
+            choice.insertBefore(this.searchContainer);
+
+            val.push(id);
+            this.setVal(val);
+        },
+
+        // multi
+        unselect: function (selected) {
+            var val = this.getVal(),
+                data,
+                index;
+
+            selected = selected.closest(".select2-search-choice");
+
+            if (selected.length === 0) {
+                throw "Invalid argument: " + selected + ". Must be .select2-search-choice";
+            }
+
+            data = selected.data("select2-data");
+
+            if (!data) {
+                // prevent a race condition when the 'x' is clicked really fast repeatedly the event can be queued
+                // and invoked on an element already removed
+                return;
+            }
+
+            index = indexOf(this.id(data), val);
+
+            if (index >= 0) {
+                val.splice(index, 1);
+                this.setVal(val);
+                if (this.select) this.postprocessResults();
+            }
+            selected.remove();
+
+            this.opts.element.trigger({ type: "removed", val: this.id(data), choice: data });
+            this.triggerChange({ removed: data });
+        },
+
+        // multi
+        postprocessResults: function (data, initial, noHighlightUpdate) {
+            var val = this.getVal(),
+                choices = this.results.find(".select2-result"),
+                compound = this.results.find(".select2-result-with-children"),
+                self = this;
+
+            choices.each2(function (i, choice) {
+                var id = self.id(choice.data("select2-data"));
+                if (indexOf(id, val) >= 0) {
+                    choice.addClass("select2-selected");
+                    // mark all children of the selected parent as selected
+                    choice.find(".select2-result-selectable").addClass("select2-selected");
+                }
+            });
+
+            compound.each2(function(i, choice) {
+                // hide an optgroup if it doesnt have any selectable children
+                if (!choice.is('.select2-result-selectable')
+                    && choice.find(".select2-result-selectable:not(.select2-selected)").length === 0) {
+                    choice.addClass("select2-selected");
+                }
+            });
+
+            if (this.highlight() == -1 && noHighlightUpdate !== false){
+                self.highlight(0);
+            }
+
+            //If all results are chosen render formatNoMAtches
+            if(!this.opts.createSearchChoice && !choices.filter('.select2-result:not(.select2-selected)').length > 0){
+                this.results.append("<li class='select2-no-results'>" + self.opts.formatNoMatches(self.search.val()) + "</li>");
+            }
+
+        },
+
+        // multi
+        getMaxSearchWidth: function() {
+            return this.selection.width() - getSideBorderPadding(this.search);
+        },
+
+        // multi
+        resizeSearch: function () {
+            var minimumWidth, left, maxWidth, containerLeft, searchWidth,
+            	sideBorderPadding = getSideBorderPadding(this.search);
+
+            minimumWidth = measureTextWidth(this.search) + 10;
+
+            left = this.search.offset().left;
+
+            maxWidth = this.selection.width();
+            containerLeft = this.selection.offset().left;
+
+            searchWidth = maxWidth - (left - containerLeft) - sideBorderPadding;
+
+            if (searchWidth < minimumWidth) {
+                searchWidth = maxWidth - sideBorderPadding;
+            }
+
+            if (searchWidth < 40) {
+                searchWidth = maxWidth - sideBorderPadding;
+            }
+
+            if (searchWidth <= 0) {
+              searchWidth = minimumWidth;
+            }
+
+            this.search.width(searchWidth);
+        },
+
+        // multi
+        getVal: function () {
+            var val;
+            if (this.select) {
+                val = this.select.val();
+                return val === null ? [] : val;
+            } else {
+                val = this.opts.element.val();
+                return splitVal(val, this.opts.separator);
+            }
+        },
+
+        // multi
+        setVal: function (val) {
+            var unique;
+            if (this.select) {
+                this.select.val(val);
+            } else {
+                unique = [];
+                // filter out duplicates
+                $(val).each(function () {
+                    if (indexOf(this, unique) < 0) unique.push(this);
+                });
+                this.opts.element.val(unique.length === 0 ? "" : unique.join(this.opts.separator));
+            }
+        },
+
+        // multi
+        buildChangeDetails: function (old, current) {
+            var current = current.slice(0),
+                old = old.slice(0);
+
+            // remove intersection from each array
+            for (var i = 0; i < current.length; i++) {
+                for (var j = 0; j < old.length; j++) {
+                    if (equal(this.opts.id(current[i]), this.opts.id(old[j]))) {
+                        current.splice(i, 1);
+                        i--;
+                        old.splice(j, 1);
+                        j--;
+                    }
+                }
+            }
+
+            return {added: current, removed: old};
+        },
+
+
+        // multi
+        val: function (val, triggerChange) {
+            var oldData, self=this, changeDetails;
+
+            if (arguments.length === 0) {
+                return this.getVal();
+            }
+
+            oldData=this.data();
+            if (!oldData.length) oldData=[];
+
+            // val is an id. !val is true for [undefined,null,'',0] - 0 is legal
+            if (!val && val !== 0) {
+                this.opts.element.val("");
+                this.updateSelection([]);
+                this.clearSearch();
+                if (triggerChange) {
+                    this.triggerChange({added: this.data(), removed: oldData});
+                }
+                return;
+            }
+
+            // val is a list of ids
+            this.setVal(val);
+
+            if (this.select) {
+                this.opts.initSelection(this.select, this.bind(this.updateSelection));
+                if (triggerChange) {
+                    this.triggerChange(this.buildChangeDetails(oldData, this.data()));
+                }
+            } else {
+                if (this.opts.initSelection === undefined) {
+                    throw new Error("val() cannot be called if initSelection() is not defined");
+                }
+
+                this.opts.initSelection(this.opts.element, function(data){
+                    var ids=$(data).map(self.id);
+                    self.setVal(ids);
+                    self.updateSelection(data);
+                    self.clearSearch();
+                    if (triggerChange) {
+                        self.triggerChange(this.buildChangeDetails(oldData, this.data()));
+                    }
+                });
+            }
+            this.clearSearch();
+        },
+
+        // multi
+        onSortStart: function() {
+            if (this.select) {
+                throw new Error("Sorting of elements is not supported when attached to <select>. Attach to <input type='hidden'/> instead.");
+            }
+
+            // collapse search field into 0 width so its container can be collapsed as well
+            this.search.width(0);
+            // hide the container
+            this.searchContainer.hide();
+        },
+
+        // multi
+        onSortEnd:function() {
+
+            var val=[], self=this;
+
+            // show search and move it to the end of the list
+            this.searchContainer.show();
+            // make sure the search container is the last item in the list
+            this.searchContainer.appendTo(this.searchContainer.parent());
+            // since we collapsed the width in dragStarted, we resize it here
+            this.resizeSearch();
+
+            // update selection
+
+            this.selection.find(".select2-search-choice").each(function() {
+                val.push(self.opts.id($(this).data("select2-data")));
+            });
+            this.setVal(val);
+            this.triggerChange();
+        },
+
+        // multi
+        data: function(values, triggerChange) {
+            var self=this, ids, old;
+            if (arguments.length === 0) {
+                 return this.selection
+                     .find(".select2-search-choice")
+                     .map(function() { return $(this).data("select2-data"); })
+                     .get();
+            } else {
+                old = this.data();
+                if (!values) { values = []; }
+                ids = $.map(values, function(e) { return self.opts.id(e); });
+                this.setVal(ids);
+                this.updateSelection(values);
+                this.clearSearch();
+                if (triggerChange) {
+                    this.triggerChange(this.buildChangeDetails(old, this.data()));
+                }
+            }
+        }
+    });
+
+    $.fn.select2 = function () {
+
+        var args = Array.prototype.slice.call(arguments, 0),
+            opts,
+            select2,
+            value, multiple,
+            allowedMethods = ["val", "destroy", "opened", "open", "close", "focus", "isFocused", "container", "onSortStart", "onSortEnd", "enable", "readonly", "positionDropdown", "data"],
+            valueMethods = ["val", "opened", "isFocused", "container", "data"];
+
+        this.each(function () {
+            if (args.length === 0 || typeof(args[0]) === "object") {
+                opts = args.length === 0 ? {} : $.extend({}, args[0]);
+                opts.element = $(this);
+
+                if (opts.element.get(0).tagName.toLowerCase() === "select") {
+                    multiple = opts.element.prop("multiple");
+                } else {
+                    multiple = opts.multiple || false;
+                    if ("tags" in opts) {opts.multiple = multiple = true;}
+                }
+
+                select2 = multiple ? new MultiSelect2() : new SingleSelect2();
+                select2.init(opts);
+            } else if (typeof(args[0]) === "string") {
+
+                if (indexOf(args[0], allowedMethods) < 0) {
+                    throw "Unknown method: " + args[0];
+                }
+
+                value = undefined;
+                select2 = $(this).data("select2");
+                if (select2 === undefined) return;
+                if (args[0] === "container") {
+                    value=select2.container;
+                } else {
+                    value = select2[args[0]].apply(select2, args.slice(1));
+                }
+                if (indexOf(args[0], valueMethods) >= 0) {
+                    return false;
+                }
+            } else {
+                throw "Invalid arguments to select2 plugin: " + args;
+            }
+        });
+        return (value === undefined) ? this : value;
+    };
+
+    // plugin defaults, accessible to users
+    $.fn.select2.defaults = {
+        width: "copy",
+        loadMorePadding: 0,
+        closeOnSelect: true,
+        openOnEnter: true,
+        containerCss: {},
+        dropdownCss: {},
+        containerCssClass: "",
+        dropdownCssClass: "",
+        formatResult: function(result, container, query, escapeMarkup) {
+            var markup=[];
+            markMatch(result.text, query.term, markup, escapeMarkup);
+            return markup.join("");
+        },
+        formatSelection: function (data, container) {
+            return data ? data.text : undefined;
+        },
+        sortResults: function (results, container, query) {
+            return results;
+        },
+        formatResultCssClass: function(data) {return undefined;},
+        formatNoMatches: function () { return "No matches found"; },
+        formatInputTooShort: function (input, min) { var n = min - input.length; return "Please enter " + n + " more character" + (n == 1? "" : "s"); },
+        formatInputTooLong: function (input, max) { var n = input.length - max; return "Please delete " + n + " character" + (n == 1? "" : "s"); },
+        formatSelectionTooBig: function (limit) { return "You can only select " + limit + " item" + (limit == 1 ? "" : "s"); },
+        formatLoadMore: function (pageNumber) { return "Loading more results..."; },
+        formatSearching: function () { return "Searching..."; },
+        minimumResultsForSearch: 0,
+        minimumInputLength: 0,
+        maximumInputLength: null,
+        maximumSelectionSize: 0,
+        id: function (e) { return e.id; },
+        matcher: function(term, text) {
+            return (''+text).toUpperCase().indexOf((''+term).toUpperCase()) >= 0;
+        },
+        separator: ",",
+        tokenSeparators: [],
+        tokenizer: defaultTokenizer,
+        escapeMarkup: function (markup) {
+            var replace_map = {
+                '\\': '&#92;',
+                '&': '&amp;',
+                '<': '&lt;',
+                '>': '&gt;',
+                '"': '&quot;',
+                "'": '&#39;',
+                "/": '&#47;'
+            };
+
+            return String(markup).replace(/[&<>"'\/\\]/g, function (match) {
+                    return replace_map[match];
+            });
+        },
+        blurOnChange: false,
+        selectOnBlur: false,
+        adaptContainerCssClass: function(c) { return c; },
+        adaptDropdownCssClass: function(c) { return null; }
+    };
+
+    $.fn.select2.ajaxDefaults = {
+        transport: $.ajax,
+        params: {
+            type: "GET",
+            cache: false,
+            dataType: "json"
+        }
+    };
+
+    // exports
+    window.Select2 = {
+        query: {
+            ajax: ajax,
+            local: local,
+            tags: tags
+        }, util: {
+            debounce: debounce,
+            markMatch: markMatch
+        }, "class": {
+            "abstract": AbstractSelect2,
+            "single": SingleSelect2,
+            "multi": MultiSelect2
+        }
+    };
+
+}(jQuery));
Binary file hexagram/select2.png has changed
Binary file hexagram/select2x2.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/statistics.js	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,563 @@
+// statistics.js: Web Worker file to run statistical tests in the background.
+
+// Constants:
+// How many pseudocount trials should we use for the binomial test?
+var BINOMIAL_PSEUDOCOUNTS = 5;
+
+// Should we log information about suspicious p values to the console for manual
+// spot checking?
+var LOG_SUSPICIOUS = false;
+
+// Go get jStat. Hope it's happy in Worker-land.
+importScripts("jstat-1.0.0.js");
+
+// Make a fake console to catch jstat warnings, so they don't crash the script.
+console = {
+    warn: print
+}
+
+onmessage = function(message) {
+    // Handle incoming messages from the page. Each message's data is an RPC
+    // request, with "name" set to a function name, "args" set to an array of
+    // arguments, and "id" set to an ID that should be returned with the return
+    // value in a reply message. If the function call fails, an error is sent
+    // back.
+    
+    
+    try {
+        // Go get the specified global function, and apply it on the given
+        // arguments. Use the global scope ("self") as its "this".
+        var return_value = self[message.data.name].apply(self, 
+            message.data.args);
+        
+    } catch(exception) {
+    
+        // Send the error back to the page instead of a return value.
+        // Unfortunately, errors themselves can't be cloned, so we do all the
+        // message making here and send back a string.
+        
+        // First we build a string with all the parts of the error we can get.
+        var error_message = "Error in web worker doing job " + message.data.id;
+        error_message += "\n";
+        error_message += exception.name + ": " + exception.message;
+        error_message += "\n";
+        error_message += "Full details:\n";
+        for(field in exception) {
+            if(field == "name" || field == "message") {
+                // Already got these.
+                continue;
+            }
+            
+            // Copy the field into the message as a string.
+            error_message += field + ": " + exception[field] + "\n";
+        }
+        error_message += "Call: " + message.data.name + "(";
+        for(var i = 0; i < message.data.args.length; i++) {
+            error_message += message.data.args[i];
+            if(i + 1 < message.data.args.length) {
+                // Have an argument after this.
+                error_message += ", ";
+            }
+        }
+        error_message += ")";
+
+        postMessage({
+            id: message.data.id,
+            error: error_message
+        });
+        
+        return;
+    }
+    
+    
+    // Send the return value back with the id.
+    postMessage({
+        id: message.data.id,
+        return_value: return_value
+    });
+}
+
+function print(message) {
+    // Print a message to the console of the parent page.
+    postMessage({
+        log: message
+    });
+}
+
+function statistics_for_matrix(matrix_url, in_list, out_list, all_list) {
+    // Download the given score matrix, do stats between in_list and out_list
+    // for each layer in it, and return an object from layer name to p value.
+    // all_list specifies the names of all signatures that figure into the
+    // analysis at all.
+    
+    // Download the matrix synchronously. 
+    // See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synch
+    // ronous_and_Asynchronous_Requests
+    // A side effect of this is that we won't have more simultaneous downloads 
+    // than workers, which is probably good.
+    // This holds the request.
+    var request = new XMLHttpRequest();
+    // Get the layer data by GET. The false makes it synchronous.
+    request.open("GET", matrix_url, false);
+    request.send(null);
+    
+    // Now we have the layer TSV
+    // But we don't have our fancy jQuery TSV parser. Parse it manually.
+    
+    // This holds an object of layer data objects (from signature to float) by
+    // layer name.
+    layers = {};
+
+    // This holds the array of lines
+    // Split on newlines (as seen in jQuery.tsv.js)
+    var lines = request.responseText.split(/\r?\n/);
+    
+    // Line 0 gives all the layer names, but the first thing isn't a layer name
+    // (since it's above the signature column).
+    var layer_names = lines[0].split(/\t/);
+    for(var i = 1; i < layer_names.length; i++) {
+        // Make sure we have an object for this layer
+        layers[layer_names[i]] = {};
+    }
+    
+    // The rest give values per layer for the hex in column 1.
+    for(var i = 1; i < lines.length; i++) {
+        // This holds the parts of each line
+        var parts = lines[i].split(/\t/);
+        
+        if(parts[0]) {
+            // We actually have data
+            
+            // Get the singature
+            var signature = parts[0];
+            
+            for(var j = 1; j < parts.length; j++) {
+                // Go through each non-signature entry and set the appropriate
+                // layer's value for this signature.
+                layers[layer_names[j]][signature] = parseFloat(parts[j]);
+            }
+        }
+    }
+    
+    // Now we've parsed the matrix.
+    // Go do stats for each layer.
+    // This holds our calculated p valued by layer name.
+    var p_values = {};
+    
+    print("Running statistics for (up to) " + layer_names.length + 
+        " layers from matrix " + matrix_url);
+    
+    for(var i = 1; i < layer_names.length; i++) {
+        // Pass the layer data to the per-layer statistics, and get the p value
+        // back. It's probably easier to do this in this worker than to go
+        // invoke more workers.
+        p_values[layer_names[i]] = statistics_for_layer(layers[layer_names[i]],
+            in_list, out_list, all_list);
+    }
+    
+    // We've now calculated a p value for every layer in the matrix. Return the
+    // calculated p values labeled by layer.
+    return p_values;
+    
+}
+
+function statistics_for_layer(layer_data, in_list, out_list, all_list) {
+    // Run the appropriate statistical test for the passed layer data, between
+    // the given in and out arrays of signatures. all_list specifies the names
+    // of all signatures that figure into the analysis at all. Return the p
+    // value for the layer, or NaN if no p value could be calculated.
+
+    // This holds whether the layer is discrete
+    var is_discrete = true;
+    
+    // This holds whether the layer is binary
+    var is_binary = true;
+    
+    for(var signature in layer_data) {
+        if(layer_data[signature] > 1 || layer_data[signature] < 0) {
+            // Not a binary layer
+            is_binary = false;
+        }
+        
+        if(layer_data[signature] % 1 !== 0) {
+            // It's a float
+            is_binary = false;
+            is_discrete = false;
+        }
+    }
+    
+    if(is_binary) {
+        // This is a binary/dichotomous layer, so run a binomial test.
+        return binomial_compare(layer_data, in_list, out_list, all_list);
+    } else {
+        // TODO: statistics that aren't on binary layers
+        return NaN;
+    }
+
+}
+
+function statistics_for_url(layer_url, in_list, out_list, all_list) {
+    // Run the stats for the layer with the given url, between the given in and
+    // out arrays of signatures. all_list specifies the names of all signatures
+    // that figure into the analysis at all. Return the p value for the layer,
+    // or NaN if no p value could be calculated.
+    
+    print("Running statistics for individual layer " + layer_url);
+    
+    // Download the layer data synchronously. 
+    // See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synch
+    // ronous_and_Asynchronous_Requests
+    // A side effect of this is that we won't have more simultaneous downloads 
+    // than workers, which is probably good.
+    // This holds the request.
+    var request = new XMLHttpRequest();
+    // Get the layer data by GET. The false makes it synchronous.
+    request.open("GET", layer_url, false);
+    request.send(null);
+    
+    // Now we have the layer TSV
+    // But we don't have our fancy jQuery TSV parser. Parse it manually.
+    
+    // This holds the layer data (signature to float)
+    var layer_data = {}
+
+    // This holds the array of lines
+    // Split on newlines (as seen in jQuery.tsv.js)
+    var lines = request.responseText.split(/\r?\n/);
+    
+    for(var i = 0; i < lines.length; i++) {
+        // This holds the parts of each line
+        var parts = lines[i].split(/\t/);
+        
+        if(parts[0]) {
+            // We actually have data
+            // Parse the layer value for this signature
+            var value = parseFloat(parts[1]);
+            
+            // Store the value in the layer data
+            layer_data[parts[0]] = value;
+        }
+    }
+    
+    // Run stats on the downloaded data
+    return statistics_for_layer(layer_data, in_list, out_list, all_list);
+}
+
+
+function binomial_compare(layer_data, in_list, out_list, all_list) {
+    // Given the data of a binary layer object (an object from signature name to
+    // 0 or 1 (or undefined)), and arrays of the names of "in" and "out"
+    // signatures, do a binomial test for whether the in signatures differ from
+    // the out signatures. Uses a number of pseudocount trials as specified in
+    // the global constant BINOMIAL_PSEUDOCOUNTS Return the p value, or NaN if
+    // it cannot be calculated. all_list specifies the names of all signatures
+    // that figure into the analysis at all (i.e. those which the user hasn't
+    // filtered out), which we use when calculating how many of our pseudocounts
+    // should be successes. Signature names appearing in all_list but with no
+    // data in layer_data are not counted.
+    
+    
+    // Work out the distribution from the out list
+    // How many out signatures are 1?
+    var outside_yes = 0;
+    // And are 0?
+    var outside_no = 0;
+    
+    for(var i = 0; i < out_list.length; i++) {
+        if(layer_data[out_list[i]] === 1) {
+            // This is a yes and it's outside.
+            outside_yes++;
+        } else if(layer_data[out_list[i]] === 0) {
+            // A no and outside
+            outside_no++;
+        }
+    }
+    
+    // It's OK for all the outside hexes to be 0 now. Pseudocounts can give us a
+    // p value.
+    
+    // Now work out our pseudocounts.
+    // How many signatures in all_list are successes?
+    var all_yes = 0;
+    // And how many are failures (as opposed to undef)
+    var all_no = 0;
+    
+    for(var i = 0; i < all_list.length; i++) {
+        if(layer_data[all_list[i]] === 1) {
+            // A yes anywhere
+            all_yes++;
+        } else if(layer_data[all_list[i]] === 0) {
+            // A real no (not a no-data) anywhere
+            all_no++;
+        }
+    }
+    
+    // It't not OK for there to be no hexes in the all set. Maybe they filtered
+    // out all the ones with any data?
+    if(all_yes + all_no == 0) {
+        // TODO: Sure wish we had layer names here.
+        print("No signatures were available with data for this layer.");
+        return NaN;
+    }
+    
+    // Calculate how many pseudo-yeses we should have.
+    // Match the frequency in all signatures.
+    var pseudo_yes = BINOMIAL_PSEUDOCOUNTS * (all_yes / (all_yes + all_no));
+    
+    // pseudo-trials is just BINOMIAL_PSEUDOCOUNTS
+    
+    // This holds the probability of being a 1 for the out list.
+    // We want to test if the in list differs significantly from this.
+    var background_probability = (outside_yes + pseudo_yes) / (outside_yes + 
+        outside_no + BINOMIAL_PSEUDOCOUNTS);
+
+    if(background_probability == 0) {
+        // Can't do the binomial test in this case. Somehow there were no yeses
+        // anywhere.
+        return NaN;
+    }
+    
+    // How many 1s are in the in list?
+    var inside_yes = 0;
+    // And how many 0s?
+    var inside_no = 0;
+    
+    for(var i = 0; i < in_list.length; i++) {
+        if(layer_data[in_list[i]] === 1) {
+            // This is a yes and it's inside.
+            inside_yes++;
+        } else if(layer_data[in_list[i]] === 0) {
+            // A no and it's inside
+            inside_no++;
+        }
+    }
+
+    // Return the p value for rejecting the null hypothesis that the in
+    // signatures follow the background distribution.
+    var p = binomial_test(inside_yes + inside_no, inside_yes,
+        background_probability);
+        
+    if(LOG_SUSPICIOUS && (p == 0 || p == 1)) {
+        // We got an odd p value. Complain about it.
+        print("Got suspicious p value " + p);
+        print("Was binomial test for " + inside_yes + " successes in " + 
+            (inside_yes + inside_no) + " trials at probability " + 
+            background_probability);
+        print("Background was " + outside_yes + " out of " + (outside_yes + 
+            outside_no) + " with " + pseudo_yes + " out of " + 
+            BINOMIAL_PSEUDOCOUNTS + " pseudocounts.");
+    }
+        
+    return p;
+}    
+    
+function binomial_test(trials, successes, success_probability) {
+    if(trials < successes) {
+        print("Trying to test " + trials + " trials with " + successes + 
+            " successes!");
+    }
+
+    // Return the p value for rejecting the null hypothesis that the observed
+    // number of successes happened in the observed number of trials when the
+    // probability of success was success_probability. Does a Binomial
+    // test.
+    
+    // Calculate the P value
+    // This must be terribly complicated since nobody seems to have written up 
+    // how to do it as anything other than an arcane stats ritual.
+    // Something close: http://www.johnmyleswhite.com/notebook/2012/04/14/implem
+    // enting-the-exact-binomial-test-in-julia/
+    // How scipy.stats does it (x = successes, n = trials, p = supposed 
+    // probability):
+    // SourceForge says Scipy is BSD licensed, so we can steal this code for our
+    // comments.
+    /*
+        d = distributions.binom.pmf(x,n,p)
+        rerr = 1+1e-7
+        if (x < p*n):
+            i = np.arange(np.ceil(p*n),n+1)
+            y = np.sum(distributions.binom.pmf(i,n,p) <= d*rerr,axis=0)
+            pval = distributions.binom.cdf(x,n,p) + distributions.binom.sf(n-y,
+                n,p)
+        else:
+            i = np.arange(np.floor(p*n))
+            y = np.sum(distributions.binom.pmf(i,n,p) <= d*rerr,axis=0)
+            pval = distributions.binom.cdf(y-1,n,p) + distributions.binom.sf(
+                x-1,n,p)
+    */
+    // There is of course no justification for why this would work.
+    // What it's actually doing is a complicated Numpy vectorized operation to 
+    // find the boundary of the tail we don't have, and then adding the CDF of 
+    // the lower tail boundary and (1-CDF) of the upper tail boundary (which is 
+    // the P value by definition).
+    
+    // This holds the probability of exactly what we've observed under the null
+    // hypothesis.
+    var observed_probability = binomial_pmf(trials, successes, 
+        success_probability);
+    
+    if(successes < trials * success_probability) {
+        // We know anything with fewer successes than this is more extreme. But
+        // how many successes would we need to have an equally extreme but
+        // higher than expected number of successes?
+        // We should sum down from all successes. (We'll sum from small to large
+        // so it's OK numerically.)
+        
+        // This holds the total probability of everything more extremely
+        // successful than what we've observed.
+        var other_tail_total_probability = 0;
+        
+        // TODO: implement some better sort of search thing and use CDF
+        for(var other_tail_start = trials; other_tail_start >= 
+            Math.ceil(trials * success_probability); other_tail_start--) {
+            
+            // Get the probability for this particular case
+            var case_probability = binomial_pmf(trials, other_tail_start, 
+                success_probability);
+            
+            if(case_probability > observed_probability) {
+                // This case is actually less extreme than what we've observed, 
+                // so our summation is complete.
+                
+                break;
+            } else {
+                // This case is more extreme than what we've observed, so use it
+                other_tail_total_probability += case_probability;
+            }
+        }
+        
+        // This holds the probability in this tail
+        var this_tail_probability = binomial_cdf(trials, successes, 
+            success_probability)
+        
+        
+        // Return the total probability from both tails, clamped to 1.
+        return Math.min(this_tail_probability + other_tail_total_probability, 
+            1.0);
+    } else {
+        // We know anything with more successes than this is more extreme. But
+        // how few successes would we need to have an equally extreme but lower
+        // than expected number of successes?
+        // We will sum up from 0 successes. We really ought to use the CDF 
+        // somehow, but I can't think of how we would do it.
+        
+        // This holds the total probability of everything more extremely
+        // failureful than what we've observed.
+        var other_tail_total_probability = 0;
+        
+        for(var other_tail_end = 0; other_tail_end < 
+            Math.floor(trials * success_probability); other_tail_end++) {
+            // We only have to iterate up to the peak (most likely) value.
+        
+            // Get the probability for this particular case
+            var case_probability = binomial_pmf(trials, other_tail_end, 
+                success_probability);
+            
+            if(case_probability > observed_probability) {
+                // This case is actually less extreme than what we've observed, 
+                // so our summation is complete.
+                break;
+            } else {
+                // This case is more extreme than what we've observed, so use it
+                other_tail_total_probability += case_probability;
+            }     
+            
+        }
+        
+        // This holds the probability in this tail. It is equal to the
+        // probability up to, but not including, where this tail starts. So even
+        // if the tail starts at the highest possible number of successes, it
+        // has some probability. successes can't be 0 here (since then we'd be
+        // below any nonzero expected probability and take the other branch.
+        // Since it's a positive integer, it must be 1 or more, so we can
+        // subtract 1 safely.
+        var this_tail_probability = 1 - binomial_cdf(trials, successes - 1, 
+            success_probability);
+        
+        // Return the total probability from both tails, clamped to 1
+        return Math.min(this_tail_probability + other_tail_total_probability, 
+            1.0);
+    }
+        
+    
+}
+
+function binomial_cdf(trials, successes, success_probability) {
+    // The Binomial distribution's cumulative distribution function. Given a 
+    // number of trials, a number of successes, and a success probability, 
+    // return the probability of having observed that many successes or fewer.
+    
+    // We compute this efficiently using the "regularized incomplete beta 
+    // function", AKA the beta distribution cdf, which we get from jstat.
+    // See http://en.wikipedia.org/wiki/Binomial_distribution#Cumulative_distrib
+    // ution_function and http://en.wikipedia.org/wiki/Regularized_incomplete_be
+    // ta_function#Incomplete_beta_function
+    
+    if(trials == successes) {
+        // jStat doesn't want a 0 alpha for its beta distribution (no failures)
+        // Calculate this one by hand (it's easy)
+        return 1;
+    }
+    
+    if(trials < successes) {
+        // This should never happen. TODO: Debug when it happens.
+        print("Error: trials (" + trials + ") < successes (" + successes + 
+            ")!");
+        return NaN;
+    }
+    
+    // This is the observation that we want the beta distribution CDF before
+    var beta_observation = 1 - success_probability;
+    
+    // These are the parameters of the relavent beta distribution
+    var beta_alpha = trials - successes;
+    var beta_beta = successes + 1;
+    
+    // Return the beta distribution CDF value, which happens to also be our CDF.
+    return jstat.pbeta(beta_observation, beta_alpha, beta_beta);
+}
+
+function binomial_pmf(trials, successes, success_probability) {
+    // The Binomial distribution's probability mass function. Given a number of
+    // trials, a number of successes, and the probability of success on each
+    // trial, calculate the probability of observing that many successes in that
+    // many trials with the given success rate.
+    
+    // The probability of this many successes in this many trials at this
+    // success rate is the probability of succeeding so many times and failing
+    // so many times, summed over all the mutually exclusive arrangements of
+    // successes and failures.
+    return (choose(trials, successes) * 
+        Math.pow(success_probability, successes) * 
+        Math.pow(1 - success_probability, trials - successes));
+    
+}
+
+function choose(available, selected) {
+    // The choose function: from available distinct objects, how many ways are 
+    // there to select selected of them. Returns "available choose selected". 
+    // Works with large input numbers that are too big to take the factorials 
+    // of.
+    
+    // We use a neat overflow-robust algorithm that eliminates the factorials 
+    // and makes the computation a multiplication of numbers greater than one.
+    // So, no overflow unless the result itself is too big.
+    // See http://arantxa.ii.uam.es/~ssantini/writing/notes/s667_binomial.pdf
+    
+    if(selected < available - selected) {
+        // It would be faster to think about choosing what we don't include. So
+        // do that instead.
+        return choose(available, available - selected);
+    }
+    
+    // This holds the result we are accumulating. Initialize to the 
+    // multiplicative identity.
+    var result = 1;
+    
+    for(var i = 1; i < available - selected + 1; i++) {
+        result *= (1 + (selected / i));
+    }
+    
+    // TODO: The result ought always to be an integer. Ensure this.
+    return result;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/statistics.svg	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,129 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="20"
+   height="20"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.48.3.1 r9886"
+   sodipodi:docname="statistics.svg">
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient3837">
+      <stop
+         style="stop-color:#000000;stop-opacity:0;"
+         offset="0"
+         id="stop3839" />
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="1"
+         id="stop3841" />
+    </linearGradient>
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3837"
+       id="radialGradient3845"
+       gradientUnits="userSpaceOnUse"
+       cx="7.1428571"
+       cy="7.3214283"
+       fx="7.1428571"
+       fy="7.3214283"
+       r="4.875" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3837"
+       id="radialGradient2989"
+       gradientUnits="userSpaceOnUse"
+       cx="7.1428571"
+       cy="7.3214283"
+       fx="7.1428571"
+       fy="7.3214283"
+       r="4.875" />
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3837"
+       id="radialGradient2993"
+       gradientUnits="userSpaceOnUse"
+       cx="7.1428571"
+       cy="7.3214283"
+       fx="7.1428571"
+       fy="7.3214283"
+       r="4.875" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="11.2"
+     inkscape:cx="-14.107143"
+     inkscape:cy="15.007088"
+     inkscape:document-units="px"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     inkscape:window-width="1215"
+     inkscape:window-height="1000"
+     inkscape:window-x="65"
+     inkscape:window-y="24"
+     inkscape:window-maximized="1" />
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(0,-1032.3622)">
+    <path
+       style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 1.1607142,1034.014 0,16.6965 17.6785718,0"
+       id="path2997"
+       inkscape:connector-curvature="0" />
+    <g
+       id="g3005"
+       transform="translate(-0.30733603,0)">
+      <rect
+         y="1047.3176"
+         x="3.0357144"
+         height="2.8571429"
+         width="2.8571429"
+         id="rect2999"
+         style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-opacity:1" />
+      <rect
+         style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1.61723435;stroke-opacity:1"
+         id="rect3001"
+         width="2.8571429"
+         height="7.4727058"
+         x="8.5714293"
+         y="1042.6127" />
+      <rect
+         y="1035.6177"
+         x="14.107143"
+         height="14.199912"
+         width="2.8571429"
+         id="rect3003"
+         style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:2.2293427;stroke-opacity:1" />
+    </g>
+  </g>
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/throbber.svg	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   version="1.1"
+   width="20"
+   height="20"
+   id="svg2">
+  <g
+     id="layer1">
+    <path
+       d="m 5.6023796,8.4792487 -8.3476172,-4e-7 -4.1738082,-7.2292487 4.1738089,-7.2292483 8.3476172,4e-7 4.1738082,7.2292487 z"
+       transform="translate(8.5714287,8.75)"
+       id="path2993"
+       style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-opacity:1" />
+    <!-- 
+        Animation based on the Chromiam browser's throbber SVG
+        http://commons.wikimedia.org/wiki/File:Chromiumthrobber.svg
+        CC Attribution license.
+    -->
+    <animateTransform
+        attributeName="transform" 
+        attributeType="XML" 
+        type="rotate" 
+        from="0 10 10" 
+        to="360 10 10" 
+        begin="0s" 
+        dur="3s" 
+        fill="freeze" 
+        repeatCount="indefinite"/>
+  </g>
+</svg>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/tool_dependency.xml	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+<!--
+Defines how to install the binaries that this tool depends on (in this case, DrL).
+Based on the examples at http://wiki.galaxyproject.org/ToolShedToolFeatures
+and http://toolshed.g2.bx.psu.edu/repos/jjohnson/defuse/file/f65857c1b92e/tool_dependencies.xml
+-->
+<tool_dependency>
+    <package name="drl-graph-layout" version="1.1">
+        <install version="1.0"><!-- This is the install tag version, not the package version -->
+            <actions>
+                <action type="shell_command">hg clone https://bitbucket.org/adam_novak/drl-graph-layout</action>
+                <!-- 
+                TODO: We're supposed to copy the right Configuration.mk file. 
+                Not doing so assumes our system is GNU 
+                -->
+                <action type="shell_command">hg up -r drl-graph-layout-1.1</action>
+                <action type="shell_command">make</action>
+                <action type="move_directory_files">
+                    <source_directory>bin</source_directory>
+                    <destination_directory>$INSTALL_DIR/bin</destination_directory>
+                </action>
+                <action type="set_environment">
+                    <environment_variable name="PATH" action="prepend_to">$INSTALL_DIR/bin</environment_variable>
+                    <!-- Now we can access DrL tools like truncate (at the expense of GNU truncate) -->
+                </action>
+            </actions>
+        </install>
+        <readme>
+        This installs the latest DrL Graph Layout tool from Adam Novak's Bitbucket, because Shawn Martin has stopped maintaining it.
+        </readme>
+    </package>
+</tool_dependency>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/tools.js	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,372 @@
+// tools.js: Code to run all the tools in the menu bar.
+// References globals in hexagram.js to actually do the tools' work.
+
+// To add a tool:
+// * Make a $(function() {...}); block to hold your code.
+// * Add a tool with add_tool with your tool code as the callback.
+// * Add at least one tool listener with add_tool_listener. Give it cleanup code
+//   if necessary to remove temporary UI elements.
+// * Make sure to set selected_tool to undefined when your tool's normal 
+//   workflow completes, so that the infowindow can use click events again.
+//   (it got set to your tool's name by the code prepended to your callback).
+
+$(function() {
+    // Set up the add text control
+    add_tool("add-text", "Add Text...", function() {
+        
+        // We'll prompt the user for some text, and then put a label where they 
+        // next click.
+        
+        var text = prompt("Enter some text, and click anywhere on the " +
+            "visualization to place it there", "Label Text");
+            
+        if(!text) {
+            // They don't want to put a label
+            selected_tool = undefined;
+            return;
+        }
+        
+        // Add a tool listenerr that places the label. It fires on a click 
+        // anywhere on anything on the map, including the background. We keep a 
+        // handle to it so we can remove it when it fires, ensuring we get just 
+        // one label. See http://stackoverflow.com/a/1544185
+        var handle = add_tool_listener("click", function(event) {
+            
+            // Make a new MapLabel at the click position
+            // See http://bit.ly/18MbLhR (the MapLabel library example page)
+            var map_label = new MapLabel({
+                text: text,
+                position: event.latLng,
+                map: googlemap,
+                fontSize: 10,
+                align: "left"
+            });
+            
+            // Subscribe tool listeners to the label
+            subscribe_tool_listeners(map_label);
+            
+            // Don't trigger again
+            remove_tool_listener(handle);
+        }, function() {
+            // Cleanup: de-select ourselves.
+            selected_tool = undefined;
+        });
+    });
+});
+
+$(function() {
+    // Set up the selection tool
+    add_tool("select", "Select", function() {
+    
+        // Turn on a crosshair cursor
+        googlemap.setOptions({
+            draggableCursor:"crosshair"
+        });
+    
+        // Add a listener to start the selection where the user clicks
+        var start_handle = add_tool_listener("click",
+            function(event) {
+            
+            // Don't trigger again
+            remove_tool_listener(start_handle);
+            
+            // Turn on a crosshair cursor again
+            googlemap.setOptions({
+                draggableCursor:"crosshair"
+            });
+            
+            // Store the start of the selection
+            var selection_start = event.latLng;
+            
+            print("Selection started at " + selection_start);
+            
+            // Make a rectangle for the selection
+            var rectangle = new google.maps.Rectangle({
+                fillColor: "#FFFFFF",
+                strokeColor: "#FFFFFF",
+                strokeWeight: 2,
+                strokeOpacity: 1.0,
+                fillOpacity: 0.5,
+                // Don't give us a clickable cursor, or take mouse events.
+                clickable: false, 
+                map: googlemap,
+                bounds: new google.maps.LatLngBounds(selection_start, 
+                    selection_start)
+            });
+            
+            // This holds a selection preview event handler that should happen
+            // when we mouse over the map or the rectangle.
+            var preview = function(event) {
+                
+                // Store the end of the selection (provisionally)
+                var selection_end = event.latLng;
+                
+                
+                if(selection_end.lng() < selection_start.lng()) {
+                    // The user has selected a backwards rectangle, which wraps
+                    // across the place where the globe is cut. None of our 
+                    // selections ever need to do this.
+                    
+                    // Make the rectangle backwards
+                    rectangle.setBounds(new google.maps.LatLngBounds(
+                        selection_end, selection_start));    
+                    
+                } else {
+                    // Make the rectangle forwards
+                    rectangle.setBounds(new google.maps.LatLngBounds(
+                        selection_start, selection_end));    
+                }
+            }
+            
+            // This holds a cleanup function to get rid of the rectangle when 
+            // the resizing listener goes away.
+            var preview_cleanup = function() {
+                // Remove the rectangle
+                rectangle.setMap(undefined);
+                
+                // Remove the crosshair cursor
+                googlemap.setOptions({
+                    draggableCursor: undefined
+                });
+            };
+            
+            // Add a mouse move listener for interactivity
+            // Works over the map, hexes, or the rectangle.
+            var move_handle = add_tool_listener("mousemove", preview, 
+                preview_cleanup);
+            
+            // We need a listener to finish the selection
+            var finish = function(event) {
+                // Don't trigger again
+                remove_tool_listener(stop_handle);
+                
+                // Also stop the dynamic updates. This removes the rectangle.
+                remove_tool_listener(move_handle);
+
+                // Store the end of the selection
+                var selection_end = event.latLng;
+                
+                print("Selection ended at " + selection_end);
+                    
+                // Select the rectangle by arbitrary corners.
+                select_rectangle(selection_start, selection_end);    
+            };
+            
+            // Attach the listener.
+            // The listener can still use its own handle because variable 
+            // references are resolved at runtime.
+            var stop_handle = add_tool_listener("click", finish, function() {
+                // Cleanup: say this tool is no longer selected
+                selected_tool = undefined;
+            });
+            
+        }, function() {
+            // Remove the crosshair cursor
+            googlemap.setOptions({
+                draggableCursor: undefined
+            });
+        });
+    });
+});
+
+// A tool for importing a list of hexes as a selection
+$(function() {
+    add_tool("import", "Import...", function() {
+        // Make the import form
+        var import_form = $("<form/>").attr("title", 
+            "Import List As Selection");
+        
+        import_form.append($("<div/>").text("Input names, one per line:"));
+        
+        // A big text box
+        var text_area = $("<textarea/>").addClass("import");
+        import_form.append(text_area);
+        
+        import_form.append($("<div/>").text(
+            "Open a file:"));
+            
+        // This holds a file form element
+        var file_picker = $("<input/>").attr("type", "file").addClass("import");
+        
+        import_form.append(file_picker);
+        
+        file_picker.change(function(event) {
+            // When a file is selected, read it in and populate the text box.
+            
+            // What file do we really want to read?
+            var file = event.target.files[0];
+            
+            // Make a FileReader to read the file
+            var reader = new FileReader();
+            
+            reader.onload = function(read_event) {  
+                // When we read with readAsText, we get a string. Just stuff it
+                // in the text box for the user to see.
+                text_area.text(reader.result);
+            };
+            
+            // Read the file, and, when it comes in, stick it in the textbox.
+            reader.readAsText(file);
+        });
+        
+        import_form.dialog({
+            modal: true,
+            buttons: {
+                "Import": function() {
+                    // Do the import of the data. The data in question is always
+                    // in the textbox.
+                    
+                    // Select all the entered hexes
+                    select_string(text_area.val());
+                    
+                    // Finally, close the dialog
+                    $(this).dialog("close");
+                    
+                    // Done with the tool
+                    selected_tool = undefined;
+                }   
+            },
+            close: function() {
+                // They didn't want to use this tool.
+                selected_tool = undefined;
+            }
+        });
+    });
+});
+
+// The actual text to selection import function used by that tool
+function select_string(string) {
+    // Given a string of hex names, one per line, make a selection of all those
+    // hexes.
+    
+    // This is an array of signature names entered.
+    var to_select = [];
+    
+    // This holds the array of lines. Split on newlines (as seen in
+    // jQuery.tsv.js)
+    var lines = string.split(/\r?\n/);
+    
+    for(var i = 0; i < lines.length; i++) {
+        // Trim and add to our requested selection
+        to_select.push(lines[i].trim());
+    }
+    
+    // Add a selection with as many of the requested hexes as actually exist and
+    // pass the current filters.
+    select_list(to_select);
+}
+
+// And a tool for exporting selections as lists of hexes
+$(function() {
+    add_tool("export", "Export...", function() {
+        // Make the export form
+        var export_form = $("<form/>").attr("title", 
+            "Export Selection As List");
+        
+        export_form.append($("<div/>").text("Select a selection to export:"));
+        
+        // Make a select box for picking from all selections.
+        var select_box = $("<select/>");
+        
+        // Populate it with all existing selections
+        for(var layer_name in layers) {
+            if(layers[layer_name].selection) {
+                // This is a selection, so add it to the dropdown.
+                select_box.append($("<option/>").text(layer_name).attr("value",
+                    layer_name));
+            }
+        }
+        
+        export_form.append(select_box);
+        
+        export_form.append($("<div/>").text("Exported data:"));
+        
+        // A big text box
+        var text_area = $("<textarea/>").addClass("export");
+        text_area.prop("readonly", true);
+        export_form.append(text_area);
+        
+        // Add a download as file link. The "download" attribute makes the
+        // browser save it, and the href data URI holds the data.
+        var download_link = $("<a/>").attr("download", "selection.txt");
+        download_link.attr("href", "data:text/plain;base64,");
+        download_link.text("Download As Text");
+        
+        export_form.append(download_link);
+        
+        text_area.focus(function() {
+            // Select all on focus.
+            
+            $(this).select();
+        });
+        
+        text_area.mouseup(function(event) {
+            // Don't change selection on mouseup. See
+            // http://stackoverflow.com/a/5797700/402891 and
+            // http://stackoverflow.com/q/3380458/402891
+            event.preventDefault();
+        });
+        
+        select_box.change(function() {
+            // Update the text area with the list of hexes in the selected
+            // layer.
+            
+            // Get the layer name.
+            var layer_name = select_box.val();
+            if(!have_layer(layer_name)) {
+                // Not a real layer.
+                // Probably just an empty select or something
+                return;
+            }
+            
+            // This holds our list. We build it in a string so we can escape it
+            // with one .text() call when adding it to the page.
+            var exported = "";
+            
+            // Get the layer data to export
+            var layer_data = layers[layer_name].data;
+            for(var signature in layer_data) {
+                if(layer_data[signature]) {
+                    // It's selected, put it in
+                    
+                    if(exported != "") {
+                        // If there's already text, put a newline first.
+                        exported += "\n";
+                    }
+                    
+                    exported += signature;
+                }
+            }
+            
+            // Now we know all the signatures from the selection, so tell the
+            // page.
+            text_area.text(exported);
+            
+            // Also fill in the data URI for saving. We use the handy
+            // window.bota encoding function.
+            download_link.attr("href", "data:text/plain;base64," + 
+                window.btoa(exported));
+        });
+        
+        // Trigger the change event on the select box for the first selected
+        // thing, if any.
+        select_box.change();
+        
+        export_form.dialog({
+            modal: true,
+            buttons: {
+                "Done": function() {
+                    // First, close the dialog
+                    $(this).dialog("close");
+                    
+                    // Done with the tool
+                    selected_tool = undefined;
+                }   
+            },
+            close: function() {
+                // They didn't want to use this tool.
+                selected_tool = undefined;
+            }
+        });
+    });
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/tsv.py	Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,109 @@
+#!/usr/bin/env python2.7
+# tsv.py: a module for writing TSV (tab-separated value) files
+"""
+This module defines two classes: a TsvWriter, which can be constructed on a
+stream to allow writing TSV data lines and #-delimited comments to that stream,
+and a TsvReader, which can be constructed on a stream and iterated over to
+obtain lists of the values from each non-comment line in the stream.
+
+TSV is most useful as the basis for other, more tightly specified file formats.
+
+"""
+
+class TsvWriter(object):
+    """
+    Represents a writer for tab-separated value files containing  #-delimited
+    comments.
+    
+    """
+    def __init__(self, stream):
+        """
+        Make a new TsvWriter for writing TSV data to the given stream.
+        """
+        
+        # This holds the stream
+        self.stream = stream
+    
+    
+    def line(self, *args):
+        """
+        Write the given values to the file, as a TSV line. Args holds a list of
+        all arguments passed. Any argument that stringifies to a string legal as
+        a TSV data item can be written.
+        
+        """
+        
+        self.list_line(args)
+        
+    
+    def list_line(self, line):
+        """
+        Write the given iterable of values (line) to the file as items on the 
+        same line. Any argument that stringifies to a string legal as a TSV data
+        item can be written.
+        
+        Does not copy the line or build a big string in memory.
+        """
+        
+        if len(line) == 0:
+            return
+        
+        self.stream.write(str(line[0]))
+        
+        for item in line[1:]:
+            self.stream.write("\t")
+            self.stream.write(str(item))
+        
+        self.stream.write("\n")
+        
+    def comment(self, text):
+        """
+        Write the given text as a TSV comment. text must be a string containing
+        no newlines.
+        
+        """
+        
+        self.stream.write("# {}\n".format(text))
+        
+    def close(self):
+        """
+        Close the underlying stream.
+        """
+        
+        self.stream.close()
+        
+class TsvReader(object):
+    """
+    Represents a reader for tab-separated value files. Skips over comments 
+    starting with #. Can be iterated over.
+    
+    Field values consisting of only whitespace are not allowed.
+    """
+    
+    def __init__(self, stream):
+        """
+        Make a new TsvReader to read from the given stream.
+        """
+        
+        self.stream = stream
+        
+    def __iter__(self):
+        """
+        Yields lists of all fields on each line, as strings, until all lines are
+        exhausted. Strips whitespace around field contents. 
+        """
+        
+        for line in self.stream:
+            line = line.strip()
+            if line == "" or line[0] == "#":
+                # Skip comments and empty lines
+                continue
+            
+            yield map(str.strip, line.split("\t"))
+            
+    def close(self):
+        """
+        Close the underlying stream.
+        """
+        
+        self.stream.close()
Binary file hexagram/tsv.pyc has changed