Mercurial > repos > adam-novak > hexagram
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>
--- /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> </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 = { + '\\': '\', + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + "/": '/' + }; + + 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));
--- /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()